Ответ 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 использует следующую формулу для получения выходного цвета:
Где Cout - итоговый цвет, Cdst - "текущий" цвет, а Csrc - значение цвета, используемое для изменения исходного цвета. Если для какого-либо канала разность отрицательна, применяется значение 0. Как можно догадаться, результат смешивания Subtract, как правило, темнее, чем исходное изображение, так как каналы становятся ближе к нулю. Я нахожу пример с этой страницы достаточно ясным, чтобы продемонстрировать эффект вычитания:
Место назначения
Источник
Вычесть вывод
Цветовая фильтрация
Для 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:
Цель
Теперь мы подошли к моменту, когда мне нужно определить нашу реальную цель. Я полагаю, что OP в своем вопросе рассказывает именно о ColorMatrixColorFilter
(поскольку других способов использовать эту матрицу нет). Как видно из приведенного выше описания, режим вычитания смешивания принимает два цвета, а цветной фильтр цветовой матрицы принимает цвет и матрицу, которая изменяет этот цвет. Это две разные функции, и они принимают разные типы аргументов. Единственный способ, которым я могу думать о том, как их можно комбинировать, - это взять оригинальный цвет (Cdst), ColorMatrix
применить к нему ColorMatrix
(функция фильтра) и вычесть результат этой операции из исходного цвета, поэтому мы должны получить ColorMatrix
формулу:
Эта проблема
Вышеуказанная задача не так сложна, мы могли бы использовать 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);
Выход
Исходное изображение находится слева, а изображение с вычитанным фильтром - справа. Они такие же, как и ожидалось. Теперь давайте сделаем что-то более захватывающее, например, полностью удалим красный и зеленый каналы:
Код
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);
Выход
Вывод теперь имеет только синий канал, два остатка были полностью вычтены. Давай попробуем фильтр ОП дал в своем вопросе:
Код
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);
Выход
Суть
Если вы боретесь на любом этапе, не стесняйтесь обращаться к сути с полным кодом утилиты, описанной выше.
Надеюсь, что вы, ребята, не слишком соскучились по этому длинному посту. Я попытался лишь кратко объяснить, как это работает, поэтому, возможно, что-то слишком расплывчато. Дайте мне знать, если что-то выглядит неправильно или противоречиво.