java - ¿Resta el modo de fusión con ColorMatrixFilter en Android?
opengl-es (2)
Tengo el siguiente ColorMatrixFilter. Pero quiero usarlo como una máscara para el modo Subtract-Blend, en lugar de usarlo directamente. ¿Cómo hago para lograr esto?
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
];
Larga historia corta
No hay una resta de mezcla fuera de la caja en Android. Sin embargo, puede lograr la combinación de colores deseada con OpenGL. Here está la esencia, que puedes usar así:
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);
Teoría
Francamente, esta pregunta me parece algo confusa. Para ordenar las cosas, definamos dos conjuntos distintos de características: combinación de colores y filtrado de colores en Android.
Mezcla de colores
La combinación de colores es algo bastante conocido entre los diseñadores y las personas que trabajan con gráficos. Como lo indica su título, combina dos colores usando sus valores de canal (conocidos como Rojo, Verde, Azul y Alfa) y funciones de fusión. Estas funciones se denominan modos de fusión. Uno de estos modos se llama Restar . El modo Subtract Blend usa la siguiente fórmula para obtener el color de salida:
Donde Cout es el color resultante, Cdst es el color "actual" y Csrc es un valor de color utilizado para cambiar el color original. Si para cualquier canal la diferencia es negativa, se aplica el valor 0. Como se podría adivinar, el resultado de la combinación Restar tiende a ser más oscuro que la imagen original, ya que los canales se acercan a cero. Encuentro el ejemplo de esta página bastante claro para demostrar el efecto Restar:
Destino
Fuente
Restar salida
Filtrado de color
Para Android, el filtrado de color es una especie de superconjunto de operaciones en comparación con la combinación de colores.
Para obtener una lista completa de ellos, puede
ColorFilter
descripción de las subclases de
ColorFilter
.
Como puede ver en los documentos, hay tres implementaciones disponibles de
ColorFilter
:
-
PorterDuffColorFilter
es esencialmente los modos de mezcla discutidos anteriormente; -
LightingColorFilter
es muy sencillo. Se compone de dos parámetros, uno de ellos se utiliza como factor y otro como una adición para los canales rojo, verde y azul. El canal alfa permanece intacto. Por lo tanto, puede hacer que algunas imágenes se vean más brillantes (u oscuras, si el factor está entre 0 y 1, o la suma es negativa) -
ColorMatrixColorFilter
es algo más elegante. Este filtro está construido a partir de una developer.android.com/reference/android/graphics/ColorMatrix . Hasta cierto punto, unColorMatrixColorFilter
es similar a unLightingColorFilter
, también realiza algunas matemáticas en un color original y constituye los parámetros que se utilizan en él, pero es mucho más poderoso. Consulte la documentación deColorMatrix
para obtener más información sobre cómo funciona realmente:Matriz 4x5 para transformar el color y los componentes alfa de un mapa de bits. La matriz se puede pasar como una matriz única y se trata de la siguiente manera:
[ a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t ]
Cuando se aplica a un color [R, G, B, A], el color resultante se calcula como:
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;
Así es como se ve la imagen de muestra con el filtro especificado en la publicación de OP:
La meta
Ahora llegamos a un punto en el que necesito definir nuestro objetivo real.
Supongo que
OP en su pregunta dice exactamente sobre
ColorMatrixColorFilter
(ya que no hay otras formas de aprovechar esta matriz).
Como puede ver en la descripción anterior, el modo de fusión de sustracción toma dos colores y el filtro de color Matriz de color toma un color y una matriz que cambia ese color.
Estas son dos funciones diferentes, y toman diferentes tipos de argumentos.
La única forma en que puedo pensar en cómo se pueden combinar es tomar el color original (
Cdst
), aplicar
ColorMatrix
primero (función de
filtro
) y restar el resultado de esta operación del color original, por lo que deberíamos terminar con esta fórmula:
El problema
La tarea anterior no es tan difícil, podríamos usar un
ColorMatrixColorFilter
y luego usar el posterior
PorterDuffColorFilter
con el modo de sustracción, usando el resultado filtrado como la imagen de origen.
Sin embargo, si observa más de
PorterDuff.Mode
referencia de
PorterDuff.Mode
, notará que
Android no tiene el modo Subtract Blend
en sus instalaciones.
El sistema operativo Android usa
la
biblioteca
Skia de Google
debajo para dibujar lienzos y, por alguna razón, realmente
carece del modo Restar
, por lo que tendremos que restar de otra manera.
Tal cosa es
comparativamente
simple en Open GL, el desafío principal es configurar un entorno Open GL para que nos permita dibujar lo que necesitamos de la manera que lo necesitamos.
Solución
No quiero obligarnos a hacer todo el trabajo duro nosotros mismos.
Android ya tiene
GLSurfaceView
, que configura el contexto Open GL bajo el capó y nos brinda toda la potencia necesaria, pero no funcionará hasta que agreguemos esta vista a la jerarquía de Vistas, por lo que mi plan es crear una instancia de
GLSurfaceView
, adjuntar nuestra aplicación ventana, déle un mapa de bits al que queremos aplicar nuestros efectos y realice todas las cosas elegantes allí.
No entraré en demasiados detalles sobre OpenGL, ya que no está directamente relacionado con la pregunta, sin embargo, si necesita algo para aclarar, no dude en preguntar en los comentarios.
Agregar
GLSurfaceView
Primero, hagamos una instancia de
GLSurfaceView
y configuremos todos los parámetros necesarios para nuestros objetivos:
GLSurfaceView hostView = new GLSurfaceView(activityContext);
hostView.setEGLContextClientVersion(2);
hostView.setEGLConfigChooser(8, 8, 8, 8, 0, 0);
Luego, debe agregar esta vista en la jerarquía de vistas para que ejecute su ciclo de dibujo:
// 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);
Agregué esta vista GL en nuestra ventana raíz, por lo que se puede llamar desde cualquier actividad en nuestra aplicación.
Los parámetros de
width
y
height
del diseño deben coincidir con el
width
y
height
del mapa de bits que queremos procesar.
Agregando Renderer
GLSurfaceView
no dibuja nada en sí mismo.
Este trabajo debe ser realizado por la clase
Renderer
.
Definamos una clase con algunos campos:
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) {}
}
El renderizador debe conservar el
Bitmap
que va a cambiar.
En lugar de la instancia real de
ColorMatrix
, usaremos la matriz java
float[]
, ya que eventualmente no usaremos las funciones de Android para aplicar este efecto y no necesitamos esta clase.
También necesitamos mantener una referencia a nuestro
GLSurfaceView
, para que podamos eliminarlo de la ventana de la aplicación cuando el trabajo esté terminado.
El último, pero no menos importante, es la devolución de llamada.
Todo el dibujo en un
GLSurfaceView
ocurre en un hilo separado, por lo que no podemos realizar este trabajo sincrónicamente y necesitamos una devolución de llamada para devolver el resultado.
Definí la interfaz de devolución de llamada de la siguiente manera:
interface Callback {
void onSuccess(@NonNull Bitmap blendedImage);
void onFailure(@Nullable Exception error);
}
Por lo tanto, devuelve un resultado exitoso o un error opcional.
mFinished
bandera finalizada al final, al publicar el resultado, para evitar futuras operaciones.
Después de definir el renderizador, regrese a la configuración de
GLSurfaceView
y configure nuestra instancia de renderizador.
También recomiendo establecer el modo de representación en
RENDERMODE_WHEN_DIRTY
para evitar un dibujo de 60 veces por segundo:
hostView.setRenderer(new BlendingFilterRenderer(hostView, image, filterValues, callback));
hostView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
Dibujar mallas
Todavía no podemos dibujar nuestro mapa de bits en la superficie de OpenGL. Primero necesitamos dibujar mallas que sean la superficie de la textura. Para hacerlo, tendremos que definir sombreadores: pequeños programas que se ejecutan en una GPU, un programa para definir la forma y posición de las mallas (sombreador de vértices) y otro para determinar el color de salida (sombreador de fragmentos). Cuando se compilan ambos sombreadores, deben vincularse a un programa. Bueno, suficiente teoría. Primero defina el siguiente método en la clase de renderizador, lo usaremos para crear nuestros programas de sombreado:
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;
}
El primer atributo en este método define el tipo de sombreador (Vértice o Fragmento), el segundo define el código real. Nuestro sombreador Vertex se verá de la siguiente manera:
attribute vec2 aPosition;
void main() {
gl_Position = vec4(aPosition.x, aPosition.y, 0.0, 1.0);
}
aPosition
atributo
aPosition
tomará las coordenadas x e y en el sistema de coordenadas normalizado (las coordenadas x e y son de -1 a 1) y las pasará a la variable global
gl_Position
.
Y aquí nuestro fragment shader:
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
En OpenGL versión 2 tenemos que especificar la precisión de flotación explícitamente, de lo contrario este programa no se compilará.
Este sombreador también escribe en la variable global
gl_FragColor
, que define el color de salida (aquí es donde tendrá lugar la magia real).
Ahora necesitamos compilar estos sombreadores y vincularlos a un programa:
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;
}
Ahora este programa está listo para tomar nuestros vértices. Para pasarlos, utilizaremos el siguiente método auxiliar:
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);
}
Necesitamos nuestras mallas para cubrir toda la superficie, por lo que coincide con
GLSurfaceSize
, en el sistema de coordenadas del dispositivo normalizado (NDCS) es bastante simple, se puede hacer referencia a las coordenadas de toda la superficie por rango de -1 a 1 para las coordenadas x e y , así que aquí están nuestras coordenadas:
new float[] {
-1, 1,
-1, -1,
1, 1,
1, -1,
}
Desafortunadamente, no es posible dibujar un cuadro, ya que solo existen tres tipos de primitivas en OpenGL: triángulos, líneas y puntos. Un par de triángulos rectángulos serán suficientes para hacer un rectángulo que cubra toda la superficie. Primero carguemos nuestros vértices en el búfer de matriz, para que sean accesibles para los sombreadores:
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);
}
Pongamos todo junto en nuestras funciones de interfaz de 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);
}
Si ejecuta el programa ahora, debería ver una superficie blanca en lugar de negra. Estamos casi a la mitad del camino ahora.
Dibuja el mapa de bits
Ahora necesitamos pasar nuestros programas de sombreado y dibujar sobre las mallas (triángulos). Además de la textura (mapa de bits en nuestro caso) en sí, necesitamos pasar coordenadas de textura, para que la textura se pueda interpolar a través de la superficie. Aquí está nuestro nuevo sombreador de vértices:
attribute vec2 aPosition;
attribute vec2 aTextureCoord;
varying vec2 vTextureCoord;
void main() {
gl_Position = vec4(aPosition.x, aPosition.y, 0.0, 1.0);
vTextureCoord = aTextureCoord;
}
La buena noticia es que este sombreador ya no cambiará. Vertex shader en su etapa final ahora. Echemos un vistazo al 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);
}
¿Entonces, Que esta pasando aquí?
Hablando en términos generales, pasamos las coordenadas de la textura al vértice (en el atributo
aTextureCoord
), después de que el sombreador de vértices pasa estas coordenadas a una especie de variable especial
vTextureCoord
de tipo variable, que interpola estas coordenadas entre los vértices y pasa el valor intrpolado al sombreador de fragmentos.
El sombreador de fragmentos toma nuestra textura a través del parámetro uniforme
uSampler
y toma el color requerido para el píxel actual de la función
texture2D
y las coordenadas de textura pasadas desde el sombreador de vértices.
Además de la posición de los vértices, ahora necesitamos pasar coordenadas de textura.
Las coordenadas de textura varían de 0.0 a 1.0 para x e y, con el comienzo (0.0, 0.0) en la esquina inferior izquierda.
Puede sonar poco común para aquellos que se acostumbran al sistema de coordenadas de Android, donde 0,0 siempre está en la esquina superior izquierda.
Por suerte, no tenemos que preocuparnos demasiado por eso, simplemente volteemos nuestra textura verticalmente en OpenGL para que al final podamos obtener una imagen correctamente posicionada.
Cambie
initVertices
para que se vea de la siguiente manera:
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);
}
Ahora pasemos el mapa de bits real al sombreador de fragmentos. Aquí está el método que lo hace por nosotros:
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);
}
No olvides llamar a este método en el método
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);
}
Aplicar filtro de color
Ahora estamos listos para aplicar el filtro de color. Nuevamente, comencemos con los sombreadores. Para el sombreador de vértices, nada cambia, solo el búfer de fragmentos está interesado en el cálculo del color. El filtro de color es una matriz de 4x5, y el problema es que OpenGL solo tiene matrices de hasta 4 en filas o columnas. Para redondearlo definiremos una nueva estructura, que consistirá en una matriz 4x4 y un vector 4x. Después de pasar el filtro de color, tenemos todas las cosas necesarias para realizar la transformación y combinación de colores. Ya conoces la fórmula, así que no la describiré más, aquí está nuestro sombreador de fragmentos casi final:
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;
}
Y así es como pasamos el filtro de color al sombreador:
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);
}
También debe llamar a este método en el método
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);
}
Mezcla de canal alfa
Al configurar este parámetro desde el principio:
hostView.setEGLConfigChooser(8, 8, 8, 8, 0, 0);
en realidad agregamos buffer para el canal Alpha en el contexto OpenGL.
De lo contrario, siempre obtendríamos algo de fondo para la imagen de salida (eso no es correcto, teniendo en cuenta que las imágenes png tienden a tener diferentes canales alfa para algunos píxeles).
La mala noticia es que rompió el mecanismo de mezcla alfa, y para algunos casos de esquina obtendrás colores inesperados.
La buena noticia es que podemos solucionarlo fácilmente.
Primero, debemos aplicar la mezcla alfa a nosotros mismos en nuestro 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);
}
También recomiendo establecer la función de mezcla en lo siguiente, para que nuestra salida no se vea afectada por lo que esté actualmente en el búfer de color y el comportamiento esté más cerca del
ImageView
de Android.
Sin embargo, no configuramos el color para un color claro y no parece cambiar nada:
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
GLES20.glEnable(GLES20.GL_BLEND);
GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO);
}
Publica el resultado
Casi lo logramos.
Lo único que queda es devolver el resultado al lado de la persona que llama.
Primero obtengamos un mapa de bits de
GLSurfaceView
, hay una solución brillante que tomé prestada de
otra respuesta de
:
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;
}
Ahora solo tome el mapa de bits, verifique si hay errores y devuelva el resultado:
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);
}
Y llame a esto desde el método
onDrawFrame
:
@Override
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
postResult();
}
Resultado
Ahora juguemos con la utilidad que acabamos de crear. Comencemos con el filtro 0, por lo que no afectará nuestra imagen original en ningún canal:
Código
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);
Salida
La imagen original está a la izquierda y la imagen sustraída del filtro está a la derecha. Son lo mismo, como se esperaba. Ahora hagamos algo más emocionante, por ejemplo, eliminar completamente los canales rojo y verde:
Código
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);
Salida
La salida ahora solo tiene un canal azul, dos restos se restaron por completo. Probemos el filtro que OP dio en su pregunta:
Código
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);
Salida
Esencia
Si tiene dificultades en cualquier paso, no dude en consultar Here con el código completo de la utilidad descrita anteriormente.
Espero que no se hayan aburrido demasiado con esta larga publicación. Traté de explicar brevemente cómo funciona, por lo que probablemente algo sea demasiado vago. Avísame si algo parece incorrecto o inconsistente.
No soy un experto en gráficos por computadora, pero supongo que desea iterar a través de cada píxel de la imagen que desea combinar, centrar su
colorMatrix
de
colorMatrix
en cada píxel, calcular el promedio utilizando los píxeles circundantes con los que entra en contacto su matriz, luego aplique este promedio a su píxel.
Obviamente, de alguna manera necesitarás manejar los píxeles del borde.
Ejemplo: suponga que tiene una imagen de 5x4 con valores de píxeles como
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) Tomando el píxel en la posición
(3,3)
y aplicando su matriz de transformación, es decir, multiplicando el píxel de la imagen
(i,j)
con la posición de la matriz
(i,j)
, obtenemos
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) Ahora tomando el promedio de esta transformación, es decir, sumando todos los números y dividiendo entre 20, obtenemos 224.5 o aproximadamente 225. Entonces nuestra imagen recién transformada se verá como
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
Para obtener la mezcla de resta completa, haga esto para cada píxel.
EDITAR: en realidad creo que lo anterior podría ser un desenfoque gaussiano.