Хотите сделать редактирование изображений быстрым с помощью нескольких строк кода? Хотите использовать вычислительную мощность графического процессора вашего телефона без сложности OpenCL? Что ж, Renderscript создан для вас.

Все еще не уверены в полезности этого мощного инструмента? Давайте говорить цифрами:

Я сравнил размытие рендеринга с fastblur на основе java, которое вы можете найти здесь. Изображение горы имеет разрешение 4806x3604 пикселей. При обработке размытия на моем Nexus 6P скрипт рендеринга занял 738 мс. Fastblur даже не работал (нехватка памяти)! Итак, я попробовал изображение меньшего размера (1944 x 1944), и быстрое размытие сработало за 1354 мс, поэтому я попробовал еще раз с Renderscript, и это заняло 160 мс, это более чем в 8 раз быстрее.

Ниже вы можете найти сравнение производительности java-сценария рендеринга на Gaussian Blur:

Я не буду здесь говорить о NDK, так как у меня недостаточно знаний, но вы можете найти сравнение визуализации и NDK здесь. Я не стал продвигаться дальше в этом направлении, так как его было сложно настроить и он работал не на всех телефонах, в отличие от Renderscript.

Renderscript основан на C99 (язык ред. C), поэтому вам необходимо знать этот язык. Знать основы не составит труда, если вы уже знаете Java.

Прежде всего, вам нужно добавить эти две жирные строки в файл build.gradle:

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"

    defaultConfig {
        minSdkVersion 8
        targetSdkVersion 19

        renderscriptTargetApi 18
        renderscriptSupportModeEnabled true

    }
}

Если ваше приложение minSDK 16 и ниже, вам следует использовать режим поддержки, так как после API 17 было добавлено множество методов.

RenderscriptTargetApi увеличивается до 23, но вы должны установить его на самый низкий уровень API, способный обеспечить все функции, которые вы используете в скриптах. Если вы хотите настроить таргетинг на API 21+ в режиме поддержки, вам необходимо использовать gradle-plugin 2.1.0 и buildToolsVersion «23.0.3» или выше.

Renderscript будет использовать скрипты, написанные на C, которые будут распараллеливать вычисления для каждого пикселя вашего изображения. Сценарий - это файл с расширением «.rs», который необходимо поместить в app / src / main / rs. Android Studio не создаст для вас эту папку или какой-либо файл сценария.

Чтобы проиллюстрировать эту статью, я сделал образец приложения, которое вычисляет выравнивание гистограммы на канале Y цветового пространства YUV (см. Рисунок ниже) и размывает изображение. Он доступен на гитхабе.

Первый пример: размытие изображения

Начнем с простой задачи: размыть изображение. В этом примере код визуализации не требуется, поскольку API предоставляет класс: ScriptIntrinsicBlur.

public static Bitmap blurBitmap(Bitmap bitmap, float radius, Context context) {
    //Create renderscript
    RenderScript rs = RenderScript.create(context);

    //Create allocation from Bitmap
    Allocation allocation = Allocation.createFromBitmap(rs, bitmap);
    
    Type t = allocation.getType();

    //Create allocation with the same type
    Allocation blurredAllocation = Allocation.createTyped(rs, t);

    //Create script
    ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
    //Set blur radius (maximum 25.0)
    blurScript.setRadius(radius);
    //Set input for script
    blurScript.setInput(allocation);
    //Call script for output allocation
    blurScript.forEach(blurredAllocation);

    //Copy script result into bitmap
    blurredAllocation.copyTo(bitmap);

    //Destroy everything to free memory
    allocation.destroy();
    blurredAllocation.destroy();
    blurScript.destroy();
    t.destroy();
    rs.destroy();
    return bitmap;
}

Как вы уже поняли, этот метод возвращает размытое растровое изображение. Позвольте представить вам 3 важных объекта, использованных в приведенных выше строках:

  1. Распределение: распределение памяти выполняется на стороне Java, поэтому вы не должны выполнять malloc в функции, которая вызывается для каждого пикселя (OOM - это заноза в заднице). Первое созданное мной выделение заполнено данными, содержащимися в растровом изображении. Второй не инициализируется, он содержит 2D-массив того же размера и того же типа, что и первое выделение.
  2. Тип: Тип описывает элемент и измерения, используемые для выделения или параллельной операции. (взято с developer.android.com)
  3. Элемент: Элемент представляет один элемент в распределении. Элемент примерно эквивалентен типу C в ядре RenderScript. Элементы могут быть простыми или сложными. (взято с сайта developer.android.com )

Второй пример: выравнивание гистограммы

Теперь, когда вы понимаете основы, мы можем приступить к программированию наших собственных скриптов.

Алгоритм выравнивания гистограммы Y прост:

  1. Преобразование RGB в цветовое пространство YUV.
  2. Вычислите гистограмму Y-канала.
  3. Измените отображение канала Y в соответствии с гистограммой.
  4. Преобразование обратно из цветового пространства YUV в RGB.

Примечание. Я редактировал код благодаря Стивену Акриджу (см. Комментарии). Теперь он на 20% быстрее. Большое ему спасибо!

Теперь мы готовы создать наш rs-файл: histEq.rs, расположенный в папке rs.

#pragma version(1)
#pragma rs_fp_relaxed
#pragma rs java_package_name(com.example.q.renderscriptexample)

#include "rs_debug.rsh"

int32_t histo[256];
float remapArray[256];
int size;

//Method to keep the result between 0 and 1
static float bound (float val) {
    float m = fmax(0.0f, val);
    return fmin(1.0f, m);
}

uchar4 __attribute__((kernel)) root(uchar4 in, uint32_t x, uint32_t y) {
    //Convert input uchar4 to float4
    float4 f4 = rsUnpackColor8888(in);

    //Get YUV channels values
    float Y = 0.299f * f4.r + 0.587f * f4.g + 0.114f * f4.b;
    float U = ((0.492f * (f4.b - Y))+1)/2;
    float V = ((0.877f * (f4.r - Y))+1)/2;

    //Get Y value between 0 and 255 (included)
    int32_t val = Y * 255;
    //Increment histogram for that value
    rsAtomicInc(&histo[val]);

    //Put the values in the output uchar4, note that we keep the alpha value
    return rsPackColorTo8888(Y, U, V, f4.a);
}

uchar4 __attribute__((kernel)) remaptoRGB(uchar4 in, uint32_t x, uint32_t y) {
    //Convert input uchar4 to float4
    float4 f4 = rsUnpackColor8888(in);

    //Get Y value
    float Y = f4.r;
    //Get Y value between 0 and 255 (included)
    int32_t val = Y * 255;
    //Get Y new value in the map array
    Y = remapArray[val];

    //Get value for U and V channel (back to their original values)
    float U = (2*f4.g)-1;
    float V = (2*f4.b)-1;

    //Compute values for red, green and blue channels
    float red = bound(Y + 1.14f * V);
    float green = bound(Y - 0.395f * U - 0.581f * V);
    float blue = bound(Y + 2.033f * U);

    //Put the values in the output uchar4
    return rsPackColorTo8888(red, green, blue, f4.a);
}

void init() {
    //init the array with zeros
    for (int i = 0; i < 256; i++) {
        histo[i] = 0;
        remapArray[i] = 0.0f;
    }
}

void createRemapArray() {
    //create map for y
    float sum = 0;
    for (int i = 0; i < 256; i++) {
        sum += histo[i];
        remapArray[i] = sum / (size);
    }
}

Здесь у нас разные методы:

  • bound (float val): этот метод используется для сохранения результата между 0 и 1.
  • root (): этот метод вызывается для каждого пикселя входного выделения (он называется ядром). Он преобразует пиксель из RGBA в YUVA, он помещает результат в распределение вывода. Он также увеличивает значение гистограммы Y.
  • remaptoRGB (): этот метод также является ядром. Он переназначает значение Y, а затем преобразует обратно из YUVA в RGBA.
  • init (): этот метод автоматически вызывается при создании скрипта на java. Он инициализирует массивы нулями.
  • createRemapArray (): он создает массив переназначения для канала Y.

Обратите внимание, что вы можете создавать методы, аналогичные тем, к которым вы привыкли в C. Но здесь, если вам нужно вернуть что-то, как я это делаю в bound (), метод должен быть статическим.

Вызов скриптов из Java-кода

Теперь, когда ваш сценарий готов, вы должны вызвать его из кода Java.

Классы Java будут сгенерированы для сценариев при сборке проекта (поэтому не забудьте выполнить сборку перед использованием сценариев на java). Если у вас есть сценарий с именем foo.rs, будет создан класс с именем ScriptC_foo. Вы можете создать его экземпляр, передав объект RenderScript в конструктор.

Вы можете вызвать методы ядра, вызвав метод forEach_root () с выделением входных и выходных данных в качестве параметров, и он будет вычислять корневой метод для каждого пикселя.

Когда ваш rs-скрипт использует глобальную переменную, установщик и получатель генерируются в java-коде, поэтому, если вы используете глобальную переменную с именем value, вы можете установить ее с помощью script.set_value (yourValue) и получить его с помощью script.get_value ().

Если вы хотите использовать массив в качестве глобальной переменной, вы можете либо объявить тип распределения и использовать метод set или get, либо объявить массив типа и связать его с script.bind_variableName (yourAllocation) . Затем вы получите доступ к нему в своем сценарии с помощью rsGetElementAt_type (variableName, x, y) и установите значения с помощью rsSetElementAt_type (variableName, element, x, y).

Вот мой пример кода Java для выравнивания гистограммы Y:

public static Bitmap histogramEqualization(Bitmap image, Context context) {
    //Get image size
    int width = image.getWidth();
    int height = image.getHeight();

    //Create new bitmap
    Bitmap res = image.copy(image.getConfig(), true);

    //Create renderscript
    RenderScript rs = RenderScript.create(context);

    //Create allocation from Bitmap
    Allocation allocationA = Allocation.createFromBitmap(rs, res);

    //Create allocation with same type
    Allocation allocationB = Allocation.createTyped(rs, allocationA.getType());

    //Create script from rs file.
    ScriptC_histEq histEqScript = new ScriptC_histEq(rs);
    
    //Set size in script
    histEqScript.set_size(width*height);

    //Call the first kernel.
    histEqScript.forEach_root(allocationA, allocationB);

    //Call the rs method to compute the remap array
    histEqScript.invoke_createRemapArray();

    //Call the second kernel
    histEqScript.forEach_remaptoRGB(allocationB, allocationA);

    //Copy script result into bitmap
    allocationA.copyTo(res);

    //Destroy everything to free memory
    allocationA.destroy();
    allocationB.destroy();
    histEqScript.destroy();
    rs.destroy();

    return res;
}

Отладка RenderScript

Вы не можете использовать отладчик для анализа вашего renderScript пока (см. Комментарий Стивена Хайнса к его ответу здесь), но вы можете использовать журналы.

Чтобы использовать журналы в файле rs, вам необходимо включить «rs_debug.sh». Затем вы можете использовать метод rsDebug, взяв сообщение журнала и одну или несколько переменных в качестве параметров.

#pragma version(1)
#pragma rs java_package_name(com.example.q.renderscriptexample)
#include "rs_debug.rsh"

void root(const uchar4 *v_in, uchar4 *v_out, const void *usrData, uint32_t x, uint32_t y) {
    float4 f4 = rsUnpackColor8888(*v_in);

    rsDebug("Red", f4.r);
    *v_out = rsPackColorTo8888(f4.r,f4.g,f4.b,f4.a);
}

Я инженер по обработке изображений Android в компании Pictarine, и мы ежедневно вычисляем сложный сценарий автоматического улучшения для более чем 100 000 изображений. Он использует множество скриптов, таких как выравнивание гистограммы (дважды), и его вычисления обычно занимают менее одной секунды. Мы более чем довольны этим мощным инструментом, и если вы планируете обрабатывать изображения, я настоятельно рекомендую вам его использовать.

Спасибо Батисту (моему ведущему разработчику), который посоветовал мне изучить Renderscript и написать об этом статью.