在Android中使用ColorMatrixFilter减去混合模式?

Zen*_*Zen 54 java android opengl-es colormatrix

我有以下ColorMatrixFilter.但我想将它用作Subtract-Blend模式的掩码,而不是直接使用它.我该如何实现这一目标?

嘉洛斯:

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
    ];
Run Code Online (Sandbox Code Playgroud)

The*_*ind 10

长话短说

在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);
Run Code Online (Sandbox Code Playgroud)

理论

坦率地说,这个问题对我来说有点让人困惑.为了理清这些东西,让我们定义两组不同的功能:Android中的颜色混合颜色过滤.

颜色混合

颜色混合在设计师和使用图形的人中是众所周知的.如标题所示,它使用其通道值(称为红色,绿色,蓝色和Alpha)和混合功能混合两种颜色.这些功能称为混合模式.其中一种模式称为Subtract.减去混合模式使用以下公式来获取输出颜色:

减去混合模式

其中Cout是产生的颜色,Cdst是"当前"颜色,Csrc是用于改变原始颜色的颜色值.如果任何通道差异为负,则应用0值.正如人们可能猜到的,减法混合的结果往往比原始图像更暗,因为通道接近于零.我从这个页面中找到了很清楚的例子来演示Subtract效果:

目的地

目的地

资源

资源

减去输出

产量

颜色过滤

对于Android,与Color混合相比,Color过滤是一种超级操作.有关它们的完整列表,您可以参考ColorFilter子类描述.正如您从文档中看到的,有三种可用的实现ColorFilter:

  • PorterDuffColorFilter 基本上是上面讨论的混合模式;
  • LightingColorFilter很简单.它由两个参数组成,其中一个用作因子,另一个用作红色,绿色和蓝色通道的附加参数.Alpha通道保持不变.因此,您可以使某些图像看起来更亮(或者更暗,如果因子介于0和1之间,或者加法为负).
  • ColorMatrixColorFilter是一个更奇特的事情.该过滤器由a构成ColorMatrix.在某种程度上a ColorMatrixColorFilter类似于a LightingColorFilter,它还对原始颜色执行一些数学运算并构成其中使用的参数,但它更强大.让我们参考ColorMatrix文档来了解它的实际工作原理:

    4x5矩阵,用于转换位图的颜色和alpha分量.矩阵可以作为单个数组传递,并按如下方式处理:

    [ a, b, c, d, e,
      f, g, h, i, j,
      k, l, m, n, o,
      p, q, r, s, t ]
    Run Code Online (Sandbox Code Playgroud)

    当应用于颜色[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;
    Run Code Online (Sandbox Code Playgroud)

以下是OP的帖子中指定的过滤器样本图像的样子:

过滤后的图像

目标

现在,我们需要确定我们的实际目标.我认为他的问题中的OP正在讲述ColorMatrixColorFilter(因为没有其他方法可以利用这个矩阵).从上面的描述中可以看出,"减去混合模式"采用两种颜色,"颜色矩阵"颜色过滤器采用颜色和更改该颜色的矩阵.这是两个不同的函数,它们采用不同类型的参数.我能想到它们如何组合的唯一方法是采用原始颜色(Cdst),ColorMatrix首先应用它(滤镜功能),并从原始颜色中减去此操作的结果,所以我们应该得到这个公式:

减去过滤器公式

问题

上面的任务并不困难,我们可以使用a ColorMatrixColorFilter然后使用后续PorterDuffColorFilter的减法模式,使用过滤结果作为源图像.但是,如果您仔细查看PorterDuff.Mode参考,您会注意到Android在其工具中没有Subtract Blend Mode.Android操作系统使用谷歌下面的Skia库进行画布绘制,出于某种原因,它确实缺少Subtract模式,因此我们不得不采用另一种方式进行减法.在Open GL中这样的事情比较简单,主要的挑战是建立一个Open GL环境,这样我们就可以按照我们需要的方式绘制我们需要的东西.


I don't want to make us to do all the hard work ourselves. Android already has GLSurfaceView, that set up Open GL context under the hood and give us all needed power, but it won't work until we add this view to the View hierarchy, so my plan is to instantiate a GLSurfaceView, attach it our application window, give it a bitmap that we want to apply our effects to and perform all the fancy stuff there. I won't go in too many details about OpenGL itself since it's not directly related to the question, however if you need anything to clarify, feel free to ask in comments.

Adding GLSurfaceView

First let's make an instance of GLSurfaceView and set all required for our goal parameters:

GLSurfaceView hostView = new GLSurfaceView(activityContext);
hostView.setEGLContextClientVersion(2);
hostView.setEGLConfigChooser(8, 8, 8, 8, 0, 0);
Run Code Online (Sandbox Code Playgroud)

Then you need to add this view on View hierarchy to make it run its drawing cycle:

// 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);
Run Code Online (Sandbox Code Playgroud)

I added this GL view on our root window, so this can be called from any activity in our app. The width and height params of the layout should match width and height of the bitmap we want to process.

Adding Renderer

GLSurfaceView draws nothing itself. This work is to be done by the Renderer class. Let's define a class with a few fields:

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) {}
}
Run Code Online (Sandbox Code Playgroud)

The renderer should retain the Bitmap it will to change. Instead of actual ColorMatrix instance we will use plain float[] java array, since eventually we won't use Android facilities to apply this effect and don't need this class. We also need to keep a reference to our GLSurfaceView, so we can remove it from the application window when the work is done. The last, but not least is the callback. All the drawing in a GLSurfaceView happens in a separate thread, so we cannot perform this work synchronously and need a callback to return the result. I defined callback interface as follow:

interface Callback {
    void onSuccess(@NonNull Bitmap blendedImage);
    void onFailure(@Nullable Exception error);
}
Run Code Online (Sandbox Code Playgroud)

So it either returns successful result or an optional error. mFinished flag will be needed at the very end, when posting result, to prevent any further operations. After the renderer is defined, get back to the GLSurfaceView settings and set our renderer instance. I also recommend set rendering mode to RENDERMODE_WHEN_DIRTY to prevent a 60-times-per-second drawing:

hostView.setRenderer(new BlendingFilterRenderer(hostView, image, filterValues, callback));
hostView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
Run Code Online (Sandbox Code Playgroud)

Draw meshes

We cannot draw our bitmap on OpenGL surface just yet. First we need to draw meshes that be the surface for the texture. In order to do that we will have to define shaders - small programs that execute on a GPU, one program to define meshes form and position (Vertex shader) and another to determine output color (Fragment shader). When both shaders are compiled they must be linked into a program. Well, enough theory. First define the following method in the renderer class, we will use it to create our shader programs:

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;
}
Run Code Online (Sandbox Code Playgroud)

First attribute in this method defines the shader type (Vertex or Fragment), second defined the actual code. Our Vertex shader will look as follow:

attribute vec2 aPosition;
void main() {
  gl_Position = vec4(aPosition.x, aPosition.y, 0.0, 1.0);
}
Run Code Online (Sandbox Code Playgroud)

aPosition attribute will take x and y coordinates in normalized coordinate system (x and y coordinates are from -1 to 1) and pass them into the global gl_Position variable.

And here our fragment shader:

precision mediump float;
void main() {
  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
Run Code Online (Sandbox Code Playgroud)

In OpenGL version 2 we have to specify float precision explicitly, otherwise this program wont compile. This shader also write to global variable gl_FragColor, that defines the output color (this is where the actual magic will take place). Now we need to compile these shaders and link into a program:

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;
}
Run Code Online (Sandbox Code Playgroud)

Now this program is ready to take our vertices. In order to pass them, we will use the following helper method:

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);
}
Run Code Online (Sandbox Code Playgroud)

We need our meshes to cover all the surface, so it matches the GLSurfaceSize, in the normalized device coordinate system (NDCS) it's quite simple, the whole surface coordinates can be referred to by range from -1 to 1 for both x and y coordinates, so here are our coordinates:

new float[] {
  -1, 1,
  -1, -1,
  1,  1,
  1,  -1,
}
Run Code Online (Sandbox Code Playgroud)

Unfortunately it's not possible to just draw a box as only three types of primitives exist in OpenGL: triangles, lines and dots. A couple of right triangles will be enough to make a rectangle that covers the whole surface. Let's load our vertices into the array buffer first, so they are accessible for the shaders:

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);
}
Run Code Online (Sandbox Code Playgroud)

Let's put everything together in our Renderer interface functions:

@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);
}
Run Code Online (Sandbox Code Playgroud)

If you run the program now, you should see white surface instead of black. We're almost at the halfway point now.

Draw the bitmap

Now we need to pass our into the shader programs and draw over the meshes (triangles). Apart from the texture (bitmap in our case) itself, we need to pass texture coordinates, so the texture can be interpolated across the surface. Here are is our new vertex shader:

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

The good news, this shader won't change anymore. Vertex shader in it's final stage now. Let's take a look at the fragment shader:

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);
}
Run Code Online (Sandbox Code Playgroud)

So, what is happening here? Roughly speaking we pass coordinates for texture into vertex (into the aTextureCoord attribute), after that the vertex shader pass these coordinates into sort of special variable vTextureCoord of type varying, that interpolates these coordinates between vertices and pass intrpolated value to the fragment shader. Fragment shader takes our texture via the uSampler uniform parameter and takes required color for the current pixel from texture2D function and texture coordinates passed from the vertex shader. Apart from vertices position we now need to pass texture coordinates. Texture coordinates vary from 0.0 to 1.0 for x and y, with the beginning (0.0, 0.0) at the bottom left corner. It may sound uncommon for those who get used to Android coordinate system where 0,0 is always at top left corner. Lucky us, we don't have to bother about it too much, let's just flip our texture vertically in OpenGL so in the end we will be able to get correctly positioned image. Change you initVertices to look as follow:

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);
}
Run Code Online (Sandbox Code Playgroud)

Now let's pass actual Bitmap to the fragment shader. Here is the method that does it for us:

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);
}
Run Code Online (Sandbox Code Playgroud)

Don't forget to call this method in the onSurfaceChanged method:

@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);
}
Run Code Online (Sandbox Code Playgroud)

Apply color filter

Now we are all set to apply the color filter. Again let's start with shaders. For the vertex shader nothing changes, only fragment buffer is interested in color calculation. The color filter is a 4x5 matrix, and the problem is that OpenGL has only matrices up to 4 in rows or columns. To get it round we will define new structure, that will consist of a 4x4 matrix and a 4x vector. After color filter is passed we have all required stuff to perform color transformation and blending. You already know the formula, so i won't describe it any further, here is our almost final fragment shader:

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;
}
Run Code Online (Sandbox Code Playgroud)

And here is how we pass the color filter to the shader:

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);
}
Run Code Online (Sandbox Code Playgroud)

You also need to call this method in onSurfaceChanged method:

@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);
}
Run Code Online (Sandbox Code Playgroud)

Alpha channel blending

When setting this parameter at the very beginning: hostView.setEGLConfigChooser(8, 8, 8, 8, 0, 0); we actually added buffer for Alpha channel in the OpenGL context. Otherwise we would always get some background for the output image (that is not correct, taking into account that png images tend to have different alpha channels for some pixels). The bad news is that it broke alpha blending mechanism, and for some corner-cases you will get unexpected colors. The good news - we can easily fix it. First we need to apply alpha blending ourselves in our fragment shader:

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);
}
Run Code Online (Sandbox Code Playgroud)

I also recommend to set the blend function to the following, so our output is not any affected by whatever is currently in the color buffer and behavior is closer to the Android's ImageView. However we didn't set color for clear color and it doesn't seem to change anything:

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

Post the result

We almost made it. The only remaining thing is to return the result to the caller side. First let's get bitmap from the GLSurfaceView, there is one brilliant solution that I borrowed from another stackoverflow answer:

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;
}
Run Code Online (Sandbox Code Playgroud)

Now just grab the bitmap, check for errors and return the result:

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);
}
Run Code Online (Sandbox Code Playgroud)

And call this from the onDrawFrame method:

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

Result

Now let's play around with the utility we just made. Let's start with the 0 filter, so it won't affect our original image at any channel:

Code

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);
Run Code Online (Sandbox Code Playgroud)

Output

原始图像

The original image is on the left and filter-subtracted image is on the right. They are the same, as expected. Now let's do something more exciting, e.g. remove red and green channels completely:

Code

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);
Run Code Online (Sandbox Code Playgroud)

Output

蓝色图像

输出现在只有蓝色通道,两个休息完全被减去.让我们试一下OP在他的问题中给出的过滤器:

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);
Run Code Online (Sandbox Code Playgroud)

产量

减去的fiter图像

要旨

如果您在任何步骤中挣扎,请随意使用上述实用程序的完整代码来引用该要点.


希望你们这篇长篇文章不会太无聊.我试着简单地解释它是如何工作的,所以可能有些东西太模糊了.如果出现错误或不一致的情况,请告诉我.


小智 0

我不是计算机图形学专家,但我假设您想要迭代要混合的图像的每个像素,将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
Run Code Online (Sandbox Code Playgroud)

(1) 获取位置处的像素(3,3)并应用变换矩阵 - 即将图像像素乘以(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
Run Code Online (Sandbox Code Playgroud)

(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
Run Code Online (Sandbox Code Playgroud)

要获得完整的减法混合,请对每个像素执行此操作。

编辑:实际上我认为上面可能是高斯模糊。