Подтвердить что ты не робот

Вычесть режим смешивания с помощью ColorMatrixFilter в Android?

У меня есть следующий ColorMatrixFilter. Но я хочу использовать его в качестве маски для режима Subtract-Blend, вместо того, чтобы использовать его напрямую. Как мне добиться этого?

ColorMatrix:

colorMatrix[
        0.393, 0.7689999, 0.18899999, 0, 0,
        0.349, 0.6859999, 0.16799999, 0, 0,
        0.272, 0.5339999, 0.13099999, 0, 0,
        0,     0,         0,          1, 0
    ];
4b9b3361

Ответ 1

Короче

В Android нет вычитания, смешанного из коробки. Однако вы можете добиться желаемого смешения цветов, используя OpenGL. Вот суть, которую вы можете использовать следующим образом:

BlendingFilterUtil.subtractMatrixColorFilter(bitmap, new float[]{
   0.393f, 0.7689999f, 0.18899999f, 0, 0,
   0.349f, 0.6859999f, 0.16799999f, 0, 0,
   0.272f, 0.5339999f, 0.13099999f, 0, 0,
   0,      0,          0,           1, 0
}, activity, callback);

теория

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

Смешивание цветов

Смешивание цветов - довольно известная вещь среди дизайнеров и людей, работающих с графикой. Как указано в его заголовке, он смешивает два цвета, используя значения их каналов (известные как красный, зеленый, синий и альфа-канал) и функции смешивания. Эти функции называются режимами смешивания. Один из этих режимов называется вычитанием. Режим Subtract Blend использует следующую формулу для получения выходного цвета:

Subtract blend mode

Где Cout - итоговый цвет, Cdst - "текущий" цвет, а Csrc - значение цвета, используемое для изменения исходного цвета. Если для какого-либо канала разность отрицательна, применяется значение 0. Как можно догадаться, результат смешивания Subtract, как правило, темнее, чем исходное изображение, так как каналы становятся ближе к нулю. Я нахожу пример с этой страницы достаточно ясным, чтобы продемонстрировать эффект вычитания:

Место назначения

Destination

Источник

Source

Вычесть вывод

Output

Цветовая фильтрация

Для Android цветовая фильтрация является своего рода супер-набором операций по сравнению с смешиванием цветов. Полный список из них вы можете найти в ColorFilter подклассов ColorFilter. Как вы можете видеть из документации, есть три доступных реализации ColorFilter:

  • PorterDuffColorFilter - это, по сути, режимы PorterDuffColorFilter описанные выше;
  • LightingColorFilter очень прост. Он состоит из двух параметров, один из которых используется как фактор, а другой - как дополнение для красного, зеленого и синего каналов. Альфа-канал остается нетронутым. Таким образом, вы можете сделать изображение более ярким (или темным, если коэффициент от 0 до 1 или сложение отрицательное).
  • ColorMatrixColorFilter - более ColorMatrixColorFilter вещь. Этот фильтр построен из ColorMatrix. В некоторой степени ColorMatrixColorFilter похож на LightingColorFilter, он также выполняет некоторые математические ColorMatrixColorFilter с исходным цветом и составляет параметры, которые в нем используются, но он гораздо более мощный. Давайте ColorMatrix документации ColorMatrix чтобы узнать больше о том, как она на самом деле работает:

    Матрица 4х5 для преобразования цветовых и альфа-компонентов растрового изображения. Матрица может быть передана как один массив и обрабатывается следующим образом:

    [ a, b, c, d, e,
      f, g, h, i, j,
      k, l, m, n, o,
      p, q, r, s, t ]

    При применении к цвету [R, G, B, A] результирующий цвет вычисляется как:

    R = a*R + b*G + c*B + d*A + e;
    G = f*R + g*G + h*B + i*A + j; 
    B = k*R + l*G + m*B + n*A + o;
    A = p*R + q*G + r*B + s*A + t;

Вот как выглядит пример изображения с фильтром, указанным в сообщении OP:

Filtered Image

Цель

Теперь мы подошли к моменту, когда мне нужно определить нашу реальную цель. Я полагаю, что OP в своем вопросе рассказывает именно о ColorMatrixColorFilter (поскольку других способов использовать эту матрицу нет). Как видно из приведенного выше описания, режим вычитания смешивания принимает два цвета, а цветной фильтр цветовой матрицы принимает цвет и матрицу, которая изменяет этот цвет. Это две разные функции, и они принимают разные типы аргументов. Единственный способ, которым я могу думать о том, как их можно комбинировать, - это взять оригинальный цвет (Cdst), ColorMatrix применить к нему ColorMatrix (функция фильтра) и вычесть результат этой операции из исходного цвета, поэтому мы должны получить ColorMatrix формулу:

Subtract filter formula

Эта проблема

Вышеуказанная задача не так сложна, мы могли бы использовать ColorMatrixColorFilter а затем использовать последующий PorterDuffColorFilter с режимом вычитания, используя отфильтрованный результат в качестве исходного изображения. Однако, если вы внимательно посмотрите на ссылку PorterDuff.Mode, вы заметите, что в Android нет режима вычитания. Android OS использует библиотеку Google Skia для рисования на холсте, и по какой-то причине в ней действительно отсутствует режим вычитания, поэтому нам придется выполнить наше вычитание другим способом. В Open GL это сравнительно просто, основная задача - настроить среду Open GL, чтобы она позволяла нам рисовать то, что нам нужно, так, как нам нужно.


Решение

Я не хочу заставлять нас делать всю тяжелую работу самим. Android уже имеет GLSurfaceView, который настраивает контекст Open GL под капотом и дает нам всю необходимую мощность, но он не будет работать, пока мы не добавим это представление в иерархию View, поэтому я планирую создать экземпляр GLSurfaceView, прикрепить его к нашему приложению окно, дайте ему растровое изображение, к которому мы хотим применить наши эффекты и выполнить все причудливые вещи там. Я не буду вдаваться в подробности о самом OpenGL, поскольку он не имеет прямого отношения к вопросу, однако, если вам нужно что-то прояснить, не стесняйтесь спрашивать в комментариях.

Добавление GLSurfaceView

Сначала давайте GLSurfaceView экземпляр GLSurfaceView и установим все необходимые для нашей цели параметры:

GLSurfaceView hostView = new GLSurfaceView(activityContext);
hostView.setEGLContextClientVersion(2);
hostView.setEGLConfigChooser(8, 8, 8, 8, 0, 0);

Затем вам нужно добавить это представление в иерархию представлений, чтобы запустить цикл рисования:

// View should be of bitmap size
final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(width, height, TYPE_APPLICATION, 0, PixelFormat.OPAQUE);
view.setLayoutParams(layoutParams);
final WindowManager windowManager = (WindowManager) view.getContext().getSystemService(Context.WINDOW_SERVICE);
Objects.requireNonNull(windowManager).addView(view, layoutParams);

Я добавил это представление GL в наше корневое окно, чтобы его можно было вызывать из любого действия в нашем приложении. Параметры width и height макета должны соответствовать width и height растрового изображения, которое мы хотим обработать.

Добавление рендерера

GLSurfaceView ничего не рисует сам. Эта работа должна быть выполнена классом Renderer. Давайте определим класс с несколькими полями:

class BlendingFilterRenderer implements GLSurfaceView.Renderer {
    private final Bitmap mBitmap;
    private final WeakReference<GLSurfaceView> mHostViewReference;
    private final float[] mColorFilter;
    private final BlendingFilterUtil.Callback mCallback;
    private boolean mFinished = false;

    BlendingFilterRenderer(@NonNull GLSurfaceView hostView, @NonNull Bitmap bitmap,
                           @NonNull float[] colorFilter,
                           @NonNull BlendingFilterUtil.Callback callback)
            throws IllegalArgumentException {
        if (colorFilter.length != 4 * 5) {
            throw new IllegalArgumentException("Color filter should be a 4 x 5 matrix");
        }
        mBitmap = bitmap;
        mHostViewReference = new WeakReference<>(hostView);
        mColorFilter = colorFilter;
        mCallback = callback;
    }

    // ========================================== //
    // GLSurfaceView.Renderer
    // ========================================== //

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {}

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {}

    @Override
    public void onDrawFrame(GL10 gl) {}
}

Рендерер должен сохранить Bitmap он хочет изменить. Вместо реального экземпляра ColorMatrix мы будем использовать Java-массив plain float[], поскольку в конечном итоге мы не будем использовать средства Android для применения этого эффекта и не будем нуждаться в этом классе. Нам также нужно сохранить ссылку на наш GLSurfaceView, чтобы мы могли удалить его из окна приложения после завершения работы. Последнее, но не менее важное, это обратный вызов. Все рисование в GLSurfaceView происходит в отдельном потоке, поэтому мы не можем выполнять эту работу синхронно и нам необходим обратный вызов для возврата результата. Я определил интерфейс обратного вызова следующим образом:

interface Callback {
    void onSuccess(@NonNull Bitmap blendedImage);
    void onFailure(@Nullable Exception error);
}

Таким образом, он либо возвращает успешный результат, либо необязательную ошибку. Флаг mFinished понадобится в самом конце, при публикации результата, чтобы предотвратить дальнейшие операции. После определения средства визуализации вернитесь к настройкам GLSurfaceView и установите наш экземпляр средства визуализации. Я также рекомендую установить режим рендеринга на RENDERMODE_WHEN_DIRTY чтобы предотвратить рисование 60 раз в секунду:

hostView.setRenderer(new BlendingFilterRenderer(hostView, image, filterValues, callback));
hostView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

Рисовать сетки

Мы пока не можем нарисовать наше растровое изображение на поверхности OpenGL. Сначала нам нужно нарисовать сетки, которые будут поверхностью для текстуры. Для этого нам потребуется определить шейдеры - небольшие программы, которые выполняются на графическом процессоре, одна программа для определения формы и положения сетки (вершинный шейдер) и другая для определения выходного цвета (фрагментный шейдер). Когда оба шейдера скомпилированы, они должны быть связаны в программу. Ну, хватит теории. Сначала определите следующий метод в классе рендерера, мы будем использовать его для создания наших шейдерных программ:

private int loadShader(int type, String shaderCode) throws GLException {
    int reference = GLES20.glCreateShader(type);
    GLES20.glShaderSource(reference, shaderCode);
    GLES20.glCompileShader(reference);
    int[] compileStatus = new int[1];
    GLES20.glGetShaderiv(reference, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
    if (compileStatus[0] != GLES20.GL_TRUE) {
        GLES20.glDeleteShader(reference);
        final String message = GLES20.glGetShaderInfoLog(reference);
        throw new GLException(compileStatus[0], message);
    }

    return reference;
}

Первый атрибут в этом методе определяет тип шейдера (вершина или фрагмент), второй определяет фактический код. Наш вершинный шейдер будет выглядеть следующим образом:

attribute vec2 aPosition;
void main() {
  gl_Position = vec4(aPosition.x, aPosition.y, 0.0, 1.0);
}

aPosition примет координаты x и y в нормализованной системе координат (координаты x и y находятся в диапазоне от -1 до 1) и передаст их в глобальную переменную gl_Position.

И вот наш фрагментный шейдер:

precision mediump float;
void main() {
  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}

В OpenGL версии 2 мы должны явно указывать точность с плавающей точкой, иначе эта программа не будет компилироваться. Этот шейдер также записывает в глобальную переменную gl_FragColor, которая определяет выходной цвет (именно здесь происходит настоящее волшебство). Теперь нам нужно скомпилировать эти шейдеры и связать их в программу:

private int loadProgram() {
    int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, "precision mediump float;" +
            "void main() {" +
            "  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);" +
            "}");
    int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, "attribute vec2 aPosition;" +
            "void main() {" +
            "  gl_Position = vec4(aPosition.x, aPosition.y, 0.0, 1.0);" +
            "}");
    int programReference = GLES20.glCreateProgram();
    GLES20.glAttachShader(programReference, vertexShader);
    GLES20.glAttachShader(programReference, fragmentShader);
    GLES20.glLinkProgram(programReference);
    return programReference;
}

Теперь эта программа готова принять наши вершины. Чтобы передать их, мы будем использовать следующий вспомогательный метод:

private void enableVertexAttribute(int program, String attributeName, int size, int stride, int offset) {
    final int attributeLocation = GLES20.glGetAttribLocation(program, attributeName);
    GLES20.glVertexAttribPointer(attributeLocation, size, GLES20.GL_FLOAT, false, stride, offset);
    GLES20.glEnableVertexAttribArray(attributeLocation);
}

Нам нужны наши сетки, чтобы покрыть всю поверхность, чтобы она соответствовала GLSurfaceSize, в нормализованной системе координат устройства (NDCS) она довольно проста, координаты всей поверхности можно ссылаться в диапазоне от -1 до 1 для x и y. координаты, так вот наши координаты:

new float[] {
  -1, 1,
  -1, -1,
  1,  1,
  1,  -1,
}

К сожалению, просто невозможно нарисовать прямоугольник, поскольку в OpenGL существует только три типа примитивов: треугольники, линии и точки. Пара прямоугольных треугольников будет достаточно, чтобы сделать прямоугольник, который покрывает всю поверхность. Давайте сначала загрузим наши вершины в буфер массива, чтобы они были доступны для шейдеров:

private FloatBuffer convertToBuffer(float[] array) {
    final ByteBuffer buffer = ByteBuffer.allocateDirect(array.length * PrimitiveSizes.FLOAT);
    FloatBuffer output = buffer.order(ByteOrder.nativeOrder()).asFloatBuffer();
    output.put(array);
    output.position(0);
    return output;
}

private void initVertices(int programReference) {
    final float[] verticesData = new float[] {
            -1, 1,
            -1, -1,
            1,  1,
            1,  -1,
    }
    int buffers[] = new int[1];
    GLES20.glGenBuffers(1, buffers, 0);
    GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[0]);
    GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, verticesData.length * 4, convertToBuffer(verticesData), GLES20.GL_STREAM_DRAW);
    enableVertexAttribute(programReference, "aPosition", 2, 0, 0);
}

Давайте соединим все вместе в наших интерфейсных функциях Renderer:

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {}

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    GLES20.glViewport(0, 0, width, height);
    final int program = loadProgram();
    GLES20.glUseProgram(program);
    initVertices(program);
}

@Override
public void onDrawFrame(GL10 gl) {
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
}

Если вы запустите программу сейчас, вы должны увидеть белую поверхность вместо черной. Мы почти на полпути сейчас.

Нарисуйте растровое изображение

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

attribute vec2 aPosition;
attribute vec2 aTextureCoord;
varying vec2 vTextureCoord;
void main() {
  gl_Position = vec4(aPosition.x, aPosition.y, 0.0, 1.0);
  vTextureCoord = aTextureCoord;
}

Хорошая новость, этот шейдер больше не изменится. Вершинный шейдер в этом завершающий этап. Давайте посмотрим на фрагмент шейдера:

precision mediump float;
uniform sampler2D uSampler;
varying vec2 vTextureCoord;
void main() {
  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
  gl_FragColor = texture2D(uSampler, vTextureCoord);
}

Итак, что здесь происходит? Грубо говоря, мы передаем координаты текстуры в вершину (в атрибут aTextureCoord), после чего вершинный шейдер передает эти координаты в виде специальной переменной vTextureCoord с изменяющимся типом, которая интерполирует эти координаты между вершинами и передает введенное значение фрагментному шейдеру. Фрагментный шейдер получает нашу текстуру через универсальный параметр uSampler и получает требуемый цвет для текущего пикселя из функции texture2D и координаты текстуры, передаваемые из вершинного шейдера. Помимо положения вершин нам теперь нужно передать текстурные координаты. Координаты текстуры варьируются от 0,0 до 1,0 для x и y, с началом (0.0, 0.0) в нижнем левом углу. Это может показаться необычным для тех, кто привык к системе координат Android, где 0,0 всегда в левом верхнем углу. К счастью, нам не нужно об этом беспокоиться, давайте просто перевернем нашу текстуру по вертикали в OpenGL, чтобы в итоге мы смогли получить правильно расположенное изображение. Измените ваши initVertices чтобы они выглядели следующим образом:

private void initVertices(int programReference) {
    final float[] verticesData = new float[] {
           //NDCS coords   //UV map
            -1, 1,          0, 1,
            -1, -1,         0, 0,
            1,  1,          1, 1,
            1,  -1,         1, 0
    }
    int buffers[] = new int[1];
    GLES20.glGenBuffers(1, buffers, 0);
    GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[0]);
    GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, verticesData.length * 4, convertToBuffer(verticesData), GLES20.GL_STREAM_DRAW);
    final int stride = 4 * 4;
    enableVertexAttribute(programReference, "aPosition", 2, stride, 0);
    enableVertexAttribute(programReference, "aTextureCoord", 2, stride, 2 * 4);
}

Теперь давайте передадим фактический Bitmap фрагментному шейдеру. Вот метод, который делает это для нас:

private void attachTexture(int programReference) {
    final int[] textures = new int[1];
    GLES20.glGenTextures(1, textures, 0);
    final int textureId = textures[0];
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
    GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
    GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0);
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
    final int samplerLocation = GLES20.glGetUniformLocation(programReference, "uSampler");
    GLES20.glUniform1i(samplerLocation, 0);
}

Не забудьте вызвать этот метод в методе onSurfaceChanged:

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    GLES20.glViewport(0, 0, width, height);
    final int program = loadProgram();
    GLES20.glUseProgram(program);
    initVertices(program);
    attachTexture(program);
}

Применить цветной фильтр

Теперь все готово для применения цветового фильтра. Опять давайте начнем с шейдеров. Для вершинного шейдера ничего не меняется, только фрагментный буфер заинтересован в расчете цвета. Цветовой фильтр представляет собой матрицу 4x5, и проблема в том, что OpenGL имеет только матрицы до 4 в строках или столбцах. Чтобы обойти это, мы определим новую структуру, которая будет состоять из матрицы 4x4 и вектора 4x. После прохождения цветового фильтра у нас есть все необходимое для преобразования цвета и смешивания. Вы уже знаете формулу, поэтому я не буду ее описывать, вот наш почти последний фрагментный шейдер:

precision mediump float;
struct ColorFilter {
  mat4 factor;
  vec4 shift;
};
uniform sampler2D uSampler;
uniform ColorFilter uColorFilter;
varying vec2 vTextureCoord;
void main() {
  gl_FragColor = texture2D(uSampler, vTextureCoord);
  vec4 originalColor = texture2D(uSampler, vTextureCoord);
  vec4 filteredColor = (originalColor * uColorFilter.factor) + uColorFilter.shift;
  gl_FragColor = originalColor - filteredColor;
}

А вот как мы передаем цветовой фильтр шейдеру:

private void attachColorFilter(int program) {
    final float[] colorFilterFactor = new float[4 * 4];
    final float[] colorFilterShift = new float[4];
    for (int i = 0; i < mColorFilter.length; i++) {
        final float value = mColorFilter[i];
        final int calculateIndex = i + 1;
        if (calculateIndex % 5 == 0) {
            colorFilterShift[calculateIndex / 5 - 1] = value / 255;
        } else {
            colorFilterFactor[i - calculateIndex / 5] = value;
        }
    }
    final int colorFactorLocation = GLES20.glGetUniformLocation(program, "uColorFilter.factor");
    GLES20.glUniformMatrix4fv(colorFactorLocation, 1, false, colorFilterFactor, 0);
    final int colorShiftLocation = GLES20.glGetUniformLocation(program, "uColorFilter.shift");
    GLES20.glUniform4fv(colorShiftLocation, 1, colorFilterShift, 0);
}

Вам также необходимо вызвать этот метод в методе onSurfaceChanged:

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    GLES20.glViewport(0, 0, width, height);
    final int program = loadProgram();
    GLES20.glUseProgram(program);
    initVertices(program);
    attachTexture(program);
    attachColorFilter(program);
}

Альфа-канал смешивания

При установке этого параметра в самом начале: hostView.setEGLConfigChooser(8, 8, 8, 8, 0, 0); мы фактически добавили буфер для альфа-канала в контексте OpenGL. В противном случае мы всегда получали бы некоторый фон для выходного изображения (это неверно, принимая во внимание, что у изображений PNG, как правило, разные альфа-каналы для некоторых пикселей). Плохая новость заключается в том, что он сломал механизм альфа-смешивания, и для некоторых угловых случаев вы получите неожиданные цвета. Хорошая новость - мы можем легко это исправить. Сначала нам нужно применить альфа-смешение в нашем фрагментном шейдере:

precision mediump float;
struct ColorFilter {
  mat4 factor;
  vec4 shift;
};
uniform sampler2D uSampler;
uniform ColorFilter uColorFilter;
varying vec2 vTextureCoord;
void main() {
  vec4 originalColor = texture2D(uSampler, vTextureCoord);
  originalColor.rgb *= originalColor.a;
  vec4 filteredColor = (originalColor * uColorFilter.factor) + uColorFilter.shift;
  filteredColor.rgb *= filteredColor.a;
  gl_FragColor = originalColor - filteredColor
  gl_FragColor = vec4(originalColor.rgb - filteredColor.rgb, originalColor.a);
}

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

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    GLES20.glEnable(GLES20.GL_BLEND);
    GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO);
}

Опубликовать результат

Мы почти сделали это. Осталось только вернуть результат вызывающей стороне. Сначала давайте получим растровое изображение из GLSurfaceView, есть одно блестящее решение, которое я позаимствовал из fooobar.com/questions/4380760/...:

private Bitmap retrieveBitmapFromGl(int width, int height) {
    final ByteBuffer pixelBuffer = ByteBuffer.allocateDirect(width * height * PrimitiveSizes.FLOAT);
    pixelBuffer.order(ByteOrder.LITTLE_ENDIAN);
    GLES20.glReadPixels(0,0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixelBuffer);
    final Bitmap image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    image.copyPixelsFromBuffer(pixelBuffer);
    return image;
}

Теперь просто возьмите растровое изображение, проверьте ошибки и верните результат:

private GLException getGlError() {
    int errorValue = GLES20.glGetError();
    switch (errorValue) {
        case GLES20.GL_NO_ERROR:
            return null;
        default:
            return new GLException(errorValue);
    }
}

private void postResult() {
    if (mFinished) {
        return;
    }
    final GLSurfaceView hostView = mHostViewReference.get();
    if (hostView == null) {
        return;
    }
    GLException glError = getGlError();
    if (glError != null) {
        hostView.post(() -> {
            mCallback.onFailure(glError);
            removeHostView(hostView);
        });
    } else {
        final Bitmap result = retrieveBitmapFromGl(mBitmap.getWidth(), mBitmap.getHeight());
        hostView.post(() -> {
            mCallback.onSuccess(result);
            removeHostView(hostView);
        });
    }
    mFinished = true;
}

private void removeHostView(@NonNull GLSurfaceView hostView) {
    if (hostView.getParent() == null) {
        return;
    }
    final WindowManager windowManager = (WindowManager) hostView.getContext().getSystemService(Context.WINDOW_SERVICE);
    Objects.requireNonNull(windowManager).removeView(hostView);
}

И вызвать это из метода onDrawFrame:

@Override
public void onDrawFrame(GL10 gl) {
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    postResult();
}

Результат

Теперь давайте поиграем с утилитой, которую мы только что сделали. Начнем с фильтра 0, чтобы он не влиял на наше исходное изображение ни на одном канале:

Код

BlendingFilterUtil.subtractMatrixColorFilter(bitmap, new float[]{
    0,      0,      0,      0,      0,
    0,      0,      0,      0,      0,
    0,      0,      0,      0,      0,
    0,      0,      0,      0,      0
}, activity, callback);

Выход

Original image

Исходное изображение находится слева, а изображение с вычитанным фильтром - справа. Они такие же, как и ожидалось. Теперь давайте сделаем что-то более захватывающее, например, полностью удалим красный и зеленый каналы:

Код

BlendingFilterUtil.subtractMatrixColorFilter(bitmap, new float[]{
    1,      0,      0,      0,      0,
    0,      1,      0,      0,      0,
    0,      0,      0,      0,      0,
    0,      0,      0,      1,      0
}, activity, callback);

Выход

Blue image

Вывод теперь имеет только синий канал, два остатка были полностью вычтены. Давай попробуем фильтр ОП дал в своем вопросе:

Код

BlendingFilterUtil.subtractMatrixColorFilter(bitmap, new float[]{
   0.393f, 0.7689999f, 0.18899999f, 0, 0,
   0.349f, 0.6859999f, 0.16799999f, 0, 0,
   0.272f, 0.5339999f, 0.13099999f, 0, 0,
   0,      0,          0,           1, 0
}, activity, callback);

Выход

Subtracted fiter image

Суть

Если вы боретесь на любом этапе, не стесняйтесь обращаться к сути с полным кодом утилиты, описанной выше.


Надеюсь, что вы, ребята, не слишком соскучились по этому длинному посту. Я попытался лишь кратко объяснить, как это работает, поэтому, возможно, что-то слишком расплывчато. Дайте мне знать, если что-то выглядит неправильно или противоречиво.

Ответ 2

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

Пример: предположим, что у вас есть изображение 5x4 с пиксельными значениями, поэтому

    1     2    3    4    5
 1 1000 1000 1000 1000 1000
 2 1000 1000 1000 1000 1000
 3 1000 1000 1000 1000 1000
 4 1000 1000 1000 1000 1000

(1) Взяв пиксель в позиции (3,3) и применив вашу матрицу преобразования - i.e умножая пиксель изображения (i,j) на позицию матрицы (i,j) - получим

     1     2    3    4    5
 1  393  769  189    0    0
 2  349  686  168    0    0
 3  272  534  131    0    0
 4    0    0    0 1000    0

(2) Теперь, взяв среднее значение этого преобразования - добавим все числа и разделим на 20 - получим 224,5 или приблизительно 225. Таким образом, наше новообразованное изображение будет выглядеть как

    1     2    3    4    5
 1 1000 1000 1000 1000 1000
 2 1000 1000 1000 1000 1000
 3 1000 1000  225 1000 1000
 4 1000 1000 1000 1000 1000

Чтобы получить полную композицию вычитания, сделайте это для каждого пикселя.

EDIT: на самом деле я думаю, что вышеупомянутое может быть размытием по Гауссу.