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

Posted

技术标签:

【中文标题】在 Android 中使用 ColorMatrixFilter 减去混合模式?【英文标题】:Subtract Blend Mode using ColorMatrixFilter in Android? 【发布时间】:2016-01-26 06:39:21 【问题描述】:

我有以下 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
    ];

【问题讨论】:

你说的是“ColorMatrix”,比如developer.android.com/reference/android/graphics/ColorMatrix ? 【参考方案1】:

长话短说

Android 中没有开箱即用的减法混合。但是,您可以使用 OpenGL 实现所需的颜色混合。 Here 是要点,您可以这样使用:

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 中的颜色混合颜色过滤

颜色混合

颜色混合在设计师和图形工作人员中是众所周知的事情。如其标题所示,它使用它们的通道值(称为红色、绿色、蓝色和 Alpha)和混合函数混合两种颜色。这些功能称为混合模式。其中一种模式称为减法。减混合模式使用以下公式来获得输出颜色:

Cout 是结果颜色,Cdst 是“当前”颜色,Csrc 是用于改变原始颜色的颜色值。如果任何通道差异为负,则应用 0 值。 正如人们可能猜到的那样,减去混合的结果往往比原始图像更暗,因为通道更接近于零。我发现来自this page 的示例非常清楚地展示了减法效果:

目的地

来源

减去输出

颜色过滤

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

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

4x5 矩阵,用于转换 a 的颜色和 alpha 分量 位图。矩阵可以作为单个数组传递,并被视为 如下:

[ 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 帖子中指定的过滤器后的样子:

目标

现在我们需要定义我们的实际目标。我假设 OP 在他的问题中准确地讲述了ColorMatrixColorFilter(因为没有其他方法可以利用这个矩阵)。从上面的描述可以看出,Subtract Blend Mode 采用两种颜色,Color Matrix 滤色器采用一种颜色和一个改变该颜色的矩阵。这是两个不同的函数,它们采用不同类型的参数。我能想到如何组合它们的唯一方法是获取原始颜色(Cdst),首先将ColorMatrix 应用于它(filter 函数),然后减去结果这个操作从原始颜色开始,所以我们应该得到这个公式:

问题

上面的任务并不难,我们可以使用ColorMatrixColorFilter,然后使用带有减法模式的后续PorterDuffColorFilter,使用过滤后的结果作为源图像。但是,如果您仔细查看 PorterDuff.Mode 参考资料,您会注意到 Android 在其工具中没有减法混合模式。 Android 操作系统使用Google's Skia 库来绘制画布,出于某种原因它确实是lacks Subtract mode,所以我们将不得不以另一种方式进行减法。 这样的事情在 Open GL 中相对很简单,主要挑战是设置一个 Open GL 环境,以便它允许我们以我们需要的方式绘制我们需要的东西。


解决方案

我不想让我们自己做所有艰苦的工作。 Android 已经有GLSurfaceView,它在底层设置了 Open GL 上下文并为我们提供了所有需要的功能,但是在我们将此视图添加到视图层次结构之前它不会起作用,所以我的计划是实例化一个GLSurfaceView,将它附加到我们的应用程序窗口,给它一个我们想要应用我们的效果的位图并在那里执行所有花哨的东西。我不会过多介绍 OpenGL 本身的细节,因为它与问题没有直接关系,但是如果您需要任何澄清,请随时在 cmets 中提问。

添加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 视图,因此可以从我们应用中的任何活动中调用它。布局的widthheight 参数应该与我们要处理的位图的widthheight 匹配。

添加渲染器

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 它将改变。我们将使用普通的float[] java 数组代替实际的ColorMatrix 实例,因为最终我们不会使用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 表面上绘制位图。首先,我们需要绘制作为纹理表面的网格。为了做到这一点,我们必须定义着色器——在 GPU 上执行的小程序,一个用于定义网格形状和位置的程序(顶点着色器),另一个用于确定输出颜色(片段着色器)。当两个着色器都被编译时,它们必须链接到一个程序中。好吧,足够的理论。首先在渲染器类中定义如下方法,我们将使用它来创建我们的着色器程序:

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 函数获取当前像素所需的颜色,并从顶点着色器传递纹理坐标。 除了顶点位置,我们现在需要传递纹理坐标。 x 和 y 的纹理坐标在 0.0 到 1.0 之间变化,起点 (0.0, 0.0) 位于左下角。对于那些习惯了 0,0 始终位于左上角的 Android 坐标系的人来说,这听起来可能并不常见。幸运的是,我们不必太在意它,让我们在 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);

现在让我们将实际的位图传递给片段着色器。这是为我们做这件事的方法:

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);

Alpha 通道混合

一开始设置这个参数时:hostView.setEGLConfigChooser(8, 8, 8, 8, 0, 0); 我们实际上在 OpenGL 上下文中为 Alpha 通道添加了缓冲区。否则,我们总是会得到输出图像的一些背景(这是不正确的,考虑到 png 图像对于某些像素往往具有不同的 alpha 通道)。坏消息是它破坏了 alpha 混合机制,并且对于某些极端情况,您会得到意想不到的颜色。好消息 - 我们可以轻松修复它。首先,我们需要在片段着色器中应用 alpha 混合:

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 获取位图,我从another *** 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;

现在只需抓取位图,检查错误并返回结果:

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);

输出

原始图像在左侧,过滤后的图像在右侧。正如预期的那样,它们是相同的。现在让我们做一些更令人兴奋的事情,例如完全去除红色和绿色通道:

代码

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);

输出

输出现在只有蓝色通道,两个休止符被完全减去。让我们试试 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);

输出

要点

如果您在任何步骤中遇到困难,请随时参考the gist 以及上述实用程序的完整代码。


希望你们不会对这篇长篇文章感到厌烦。我试图只简要解释它是如何工作的,所以可能有些东西太模糊了。如果有问题或不一致,请告诉我。

【讨论】:

【参考方案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,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

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

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

【讨论】:

我不认为你对矩阵有正确的想法。要了解更多信息,请查看this 答案。

以上是关于在 Android 中使用 ColorMatrixFilter 减去混合模式?的主要内容,如果未能解决你的问题,请参考以下文章

Android的矩阵:ColorMatrix

Android 颜色矩阵——ColorMatrix

Android - ColorMatrix 处理图像对比度

Android Matrix矩阵详解

如何在 .NET 中使用 ColorMatrix 更改亮度、颜色、饱和度、色调

Android图像处理之图形特效处理