/*
* Copyright (C) 2012 CyberAgent
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jp.co.cyberagent.android.gpuimage;
import android.graphics.Point;
import android.graphics.PointF;
import android.opengl.GLES20;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
public class GPUImageToneCurveFilter extends GPUImageFilter {
public static final String TONE_CURVE_FRAGMENT_SHADER = "" +
" varying highp vec2 textureCoordinate;\n" +
" uniform sampler2D inputImageTexture;\n" +
" uniform sampler2D toneCurveTexture;\n" +
"\n" +
" void main()\n" +
" {\n" +
" lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);\n" +
" lowp float redCurveValue = texture2D(toneCurveTexture, vec2(textureColor.r, 0.0)).r;\n" +
" lowp float greenCurveValue = texture2D(toneCurveTexture, vec2(textureColor.g, 0.0)).g;\n" +
" lowp float blueCurveValue = texture2D(toneCurveTexture, vec2(textureColor.b, 0.0)).b;\n" +
"\n" +
" gl_FragColor = vec4(redCurveValue, greenCurveValue, blueCurveValue, textureColor.a);\n" +
" }";
private int[] mToneCurveTexture = new int[]{OpenGlUtils.NO_TEXTURE};
private int mToneCurveTextureUniformLocation;
private PointF[] mRgbCompositeControlPoints;
private PointF[] mRedControlPoints;
private PointF[] mGreenControlPoints;
private PointF[] mBlueControlPoints;
private ArrayList<Float> mRgbCompositeCurve;
private ArrayList<Float> mRedCurve;
private ArrayList<Float> mGreenCurve;
private ArrayList<Float> mBlueCurve;
public GPUImageToneCurveFilter() {
super(NO_FILTER_VERTEX_SHADER, TONE_CURVE_FRAGMENT_SHADER);
PointF[] defaultCurvePoints = new PointF[]{new PointF(0.0f, 0.0f), new PointF(0.5f, 0.5f), new PointF(1.0f, 1.0f)};
mRgbCompositeControlPoints = defaultCurvePoints;
mRedControlPoints = defaultCurvePoints;
mGreenControlPoints = defaultCurvePoints;
mBlueControlPoints = defaultCurvePoints;
}
@Override
public void onInit() {
super.onInit();
mToneCurveTextureUniformLocation = GLES20.glGetUniformLocation(getProgram(), "toneCurveTexture");
GLES20.glActiveTexture(GLES20.GL_TEXTURE3);
GLES20.glGenTextures(1, mToneCurveTexture, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mToneCurveTexture[0]);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
}
@Override
public void onInitialized() {
super.onInitialized();
setRgbCompositeControlPoints(mRgbCompositeControlPoints);
setRedControlPoints(mRedControlPoints);
setGreenControlPoints(mGreenControlPoints);
setBlueControlPoints(mBlueControlPoints);
}
@Override
protected void onDrawArraysPre() {
if (mToneCurveTexture[0] != OpenGlUtils.NO_TEXTURE) {
GLES20.glActiveTexture(GLES20.GL_TEXTURE3);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mToneCurveTexture[0]);
GLES20.glUniform1i(mToneCurveTextureUniformLocation, 3);
}
}
public void setFromCurveFileInputStream(InputStream input) {
try {
int version = readShort(input);
int totalCurves = readShort(input);
ArrayList<PointF[]> curves = new ArrayList<PointF[]>(totalCurves);
float pointRate = 1.0f / 255;
for (int i = 0; i < totalCurves; i++) {
// 2 bytes, Count of points in the curve (short integer from 2...19)
short pointCount = readShort(input);
PointF[] points = new PointF[pointCount];
// point count * 4
// Curve points. Each curve point is a pair of short integers where
// the first number is the output value (vertical coordinate on the
// Curves dialog graph) and the second is the input value. All coordinates have range 0 to 255.
for (int j = 0; j < pointCount; j++) {
short y = readShort(input);
short x = readShort(input);
points[j] = new PointF(x * pointRate, y * pointRate);
}
curves.add(points);
}
input.close();
mRgbCompositeControlPoints = curves.get(0);
mRedControlPoints = curves.get(1);
mGreenControlPoints = curves.get(2);
mBlueControlPoints = curves.get(3);
} catch (IOException e) {
e.printStackTrace();
}
}
private short readShort(InputStream input) throws IOException {
return (short) (input.read() << 8 | input.read());
}
public void setRgbCompositeControlPoints(PointF[] points) {
mRgbCompositeControlPoints = points;
mRgbCompositeCurve = createSplineCurve(mRgbCompositeControlPoints);
updateToneCurveTexture();
}
public void setRedControlPoints(PointF[] points) {
mRedControlPoints = points;
mRedCurve = createSplineCurve(mRedControlPoints);
updateToneCurveTexture();
}
public void setGreenControlPoints(PointF[] points) {
mGreenControlPoints = points;
mGreenCurve = createSplineCurve(mGreenControlPoints);
updateToneCurveTexture();
}
public void setBlueControlPoints(PointF[] points) {
mBlueControlPoints = points;
mBlueCurve = createSplineCurve(mBlueControlPoints);
updateToneCurveTexture();
}
private void updateToneCurveTexture() {
runOnDraw(new Runnable() {
@Override
public void run() {
GLES20.glActiveTexture(GLES20.GL_TEXTURE3);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mToneCurveTexture[0]);
if ((mRedCurve.size() >= 256) && (mGreenCurve.size() >= 256) && (mBlueCurve.size() >= 256) && (mRgbCompositeCurve.size() >= 256)) {
byte[] toneCurveByteArray = new byte[256 * 4];
for (int currentCurveIndex = 0; currentCurveIndex < 256; currentCurveIndex++) {
// BGRA for upload to texture
toneCurveByteArray[currentCurveIndex * 4 + 2] = (byte) ((int) Math.min(Math.max(currentCurveIndex + mBlueCurve.get(currentCurveIndex) + mRgbCompositeCurve.get(currentCurveIndex), 0), 255) & 0xff);
toneCurveByteArray[currentCurveIndex * 4 + 1] = (byte) ((int) Math.min(Math.max(currentCurveIndex + mGreenCurve.get(currentCurveIndex) + mRgbCompositeCurve.get(currentCurveIndex), 0), 255) & 0xff);
toneCurveByteArray[currentCurveIndex * 4] = (byte) ((int) Math.min(Math.max(currentCurveIndex + mRedCurve.get(currentCurveIndex) + mRgbCompositeCurve.get(currentCurveIndex), 0), 255) & 0xff);
toneCurveByteArray[currentCurveIndex * 4 + 3] = (byte) (255 & 0xff);
}
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, 256 /*width*/, 1 /*height*/, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, ByteBuffer.wrap(toneCurveByteArray));
}
// Buffer pixels!
// GLES20.glTexImage2D(int target,
// int level,
// int internalformat,
// int width,
// int height,
// int border,
// int format,
// int type,
// java.nio.Buffer pixels);
}
});
}
private ArrayList<Float> createSplineCurve(PointF[] points) {
if (points == null || points.length <= 0) {
return null;
}
// Sort the array
PointF[] pointsSorted = points.clone();
Arrays.sort(pointsSorted, new Comparator<PointF>() {
@Override
public int compare(PointF point1, PointF point2) {
if (point1.x < point2.x) {
return -1;
} else if (point1.x > point2.x) {
return 1;
} else {
return 0;
}
}
});
// Convert from (0, 1) to (0, 255).
Point[] convertedPoints = new Point[pointsSorted.length];
for (int i = 0; i < points.length; i++) {
PointF point = pointsSorted[i];
convertedPoints[i] = new Point((int) (point.x * 255), (int) (point.y * 255));
}
ArrayList<Point> splinePoints = createSplineCurve2(convertedPoints);
// If we have a first point like (0.3, 0) we'll be missing some points at the beginning
// that should be 0.
Point firstSplinePoint = splinePoints.get(0);
if (firstSplinePoint.x > 0) {
for (int i = firstSplinePoint.x; i >= 0; i--) {
splinePoints.add(0, new Point(i, 0));
}
}
// Insert points similarly at the end, if necessary.
Point lastSplinePoint = splinePoints.get(splinePoints.size() - 1);
if (lastSplinePoint.x < 255) {
for (int i = lastSplinePoint.x + 1; i <= 255; i++) {
splinePoints.add(new Point(i, 255));
}
}
// Prepare the spline points.
ArrayList<Float> preparedSplinePoints = new ArrayList<Float>(splinePoints.size());
for (Point newPoint : splinePoints) {
Point origPoint = new Point(newPoint.x, newPoint.x);
float distance = (float) Math.sqrt(Math.pow((origPoint.x - newPoint.x), 2.0) + Math.pow((origPoint.y - newPoint.y), 2.0));
if (origPoint.y > newPoint.y) {
distance = -distance;
}
preparedSplinePoints.add(distance);
}
return preparedSplinePoints;
}
private ArrayList<Point> createSplineCurve2(Point[] points) {
ArrayList<Double> sdA = createSecondDerivative(points);
// Is [points count] equal to [sdA count]?
// int n = [points count];
int n = sdA.size();
if (n < 1) {
return null;
}
double sd[] = new double[n];
// From NSMutableArray to sd[n];
for (int i = 0; i < n; i++) {
sd[i] = sdA.get(i);
}
ArrayList<Point> output = new ArrayList<Point>(n + 1);
for (int i = 0; i < n - 1; i++) {
Point cur = points[i];
Point next = points[i + 1];
for (int x = cur.x; x < next.x; x++) {
double t = (double) (x - cur.x) / (next.x - cur.x);
double a = 1 - t;
double b = t;
double h = next.x - cur.x;
double y = a * cur.y + b * next.y + (h * h / 6) * ((a * a * a - a) * sd[i] + (b * b * b - b) * sd[i + 1]);
if (y > 255.0) {
y = 255.0;
} else if (y < 0.0) {
y = 0.0;
}
output.add(new Point(x, (int) Math.round(y)));
}
}
// If the last point is (255, 255) it doesn't get added.
if (output.size() == 255) {
output.add(points[points.length - 1]);
}
return output;
}
private ArrayList<Double> createSecondDerivative(Point[] points) {
int n = points.length;
if (n <= 1) {
return null;
}
double matrix[][] = new double[n][3];
double result[] = new double[n];
matrix[0][1] = 1;
// What about matrix[0][1] and matrix[0][0]? Assuming 0 for now (Brad L.)
matrix[0][0] = 0;
matrix[0][2] = 0;
for (int i = 1; i < n - 1; i++) {
Point P1 = points[i - 1];
Point P2 = points[i];
Point P3 = points[i + 1];
matrix[i][0] = (double) (P2.x - P1.x) / 6;
matrix[i][1] = (double) (P3.x - P1.x) / 3;
matrix[i][2] = (double) (P3.x - P2.x) / 6;
result[i] = (double) (P3.y - P2.y) / (P3.x - P2.x) - (double) (P2.y - P1.y) / (P2.x - P1.x);
}
// What about result[0] and result[n-1]? Assuming 0 for now (Brad L.)
result[0] = 0;
result[n - 1] = 0;
matrix[n - 1][1] = 1;
// What about matrix[n-1][0] and matrix[n-1][2]? For now, assuming they are 0 (Brad L.)
matrix[n - 1][0] = 0;
matrix[n - 1][2] = 0;
// solving pass1 (up->down)
for (int i = 1; i < n; i++) {
double k = matrix[i][0] / matrix[i - 1][1];
matrix[i][1] -= k * matrix[i - 1][2];
matrix[i][0] = 0;
result[i] -= k * result[i - 1];
}
// solving pass2 (down->up)
for (int i = n - 2; i >= 0; i--) {
double k = matrix[i][2] / matrix[i + 1][1];
matrix[i][1] -= k * matrix[i + 1][0];
matrix[i][2] = 0;
result[i] -= k * result[i + 1];
}
ArrayList<Double> output = new ArrayList<Double>(n);
for (int i = 0; i < n; i++) output.add(result[i] / matrix[i][1]);
return output;
}
}