Skip to content

Commit 72ffefe

Browse files
committed
bicubic interpolation in filters
1 parent ac1310b commit 72ffefe

3 files changed

Lines changed: 120 additions & 17 deletions

File tree

src/main/java/com/jhlabs/image/ImageMath.java

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ private ImageMath() {
5555
* @return the output value
5656
*/
5757
public static float bias(float a, float b) {
58-
// return (float)Math.pow(a, Math.log(b) / Math.log(0.5));
5958
return a / ((1.0f / b - 2) * (1.0f - a) + 1);
6059
}
6160

@@ -67,18 +66,6 @@ public static float bias(float a, float b) {
6766
* @return the output value
6867
*/
6968
public static float gain(float a, float b) {
70-
/*
71-
float p = (float)Math.log(1.0 - b) / (float)Math.log(0.5);
72-
73-
if (a < .001)
74-
return 0.0f;
75-
else if (a > .999)
76-
return 1.0f;
77-
if (a < 0.5)
78-
return (float)Math.pow(2 * a, p) / 2;
79-
else
80-
return 1.0f - (float)Math.pow(2 * (1. - a), p) / 2;
81-
*/
8269
float c = (1.0f / b - 2.0f) * (1.0f - 2.0f * a);
8370
if (a < 0.5) {
8471
return a / (c + 1.0f);
@@ -526,6 +513,60 @@ public static int bilinearInterpolateD(double x, double y, int nw, int ne, int s
526513
return (a << 24) | (r << 16) | (g << 8) | b;
527514
}
528515

516+
/**
517+
* Performs a 1D cubic interpolation between 4 points for each color channel.
518+
* This is based on the Catmull-Rom spline.
519+
*
520+
* @param p0 the first point
521+
* @param p1 the second point (interpolation starts here)
522+
* @param p2 the third point (interpolation ends here)
523+
* @param p3 the fourth point
524+
* @param t the interpolation parameter (0..1)
525+
* @return the interpolated ARGB value
526+
*/
527+
private static int cubicInterpolate(int p0, int p1, int p2, int p3, float t) {
528+
int v = 0;
529+
for (int i = 0; i < 4; i++) {
530+
int shift = i * 8;
531+
float k0 = (p0 >> shift) & 0xff;
532+
float k1 = (p1 >> shift) & 0xff;
533+
float k2 = (p2 >> shift) & 0xff;
534+
float k3 = (p3 >> shift) & 0xff;
535+
536+
float c3 = m00 * k0 + m01 * k1 + m02 * k2 + m03 * k3;
537+
float c2 = m10 * k0 + m11 * k1 + m12 * k2 + m13 * k3;
538+
float c1 = m20 * k0 + m21 * k1 + m22 * k2 + m23 * k3;
539+
float c0 = m30 * k0 + m31 * k1 + m32 * k2 + m33 * k3;
540+
float result = ((c3 * t + c2) * t + c1) * t + c0;
541+
542+
int n = (int) result;
543+
if (n < 0) {
544+
n = 0;
545+
} else if (n > 255) {
546+
n = 255;
547+
}
548+
v |= n << shift;
549+
}
550+
return v;
551+
}
552+
553+
/**
554+
* Bicubic interpolation of ARGB values.
555+
*
556+
* @param x the X interpolation parameter 0..1
557+
* @param y the y interpolation parameter 0..1
558+
* @param p a 4x4 grid of ARGB values
559+
* @return the interpolated value
560+
*/
561+
public static int bicubicInterpolate(float x, float y, int[][] p) {
562+
int[] a = new int[4];
563+
a[0] = cubicInterpolate(p[0][0], p[0][1], p[0][2], p[0][3], x);
564+
a[1] = cubicInterpolate(p[1][0], p[1][1], p[1][2], p[1][3], x);
565+
a[2] = cubicInterpolate(p[2][0], p[2][1], p[2][2], p[2][3], x);
566+
a[3] = cubicInterpolate(p[3][0], p[3][1], p[3][2], p[3][3], x);
567+
return cubicInterpolate(a[0], a[1], a[2], a[3], y);
568+
}
569+
529570
/**
530571
* Returns the ITU-R BT.601 luminance of an sRGB value.
531572
*/

src/main/java/com/jhlabs/image/TransformFilter.java

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public abstract class TransformFilter extends AbstractBufferedImageOp {
4545
// Interpolation methods for sampling between pixel centers
4646
public static final int NEAREST_NEIGHBOUR = 0;
4747
public static final int BILINEAR = 1;
48+
public static final int BICUBIC = 2;
4849
protected int interpolation = BILINEAR;
4950

5051
protected TransformFilter(String filterName) {
@@ -63,7 +64,7 @@ public void setEdgeAction(int edgeAction) {
6364
/**
6465
* Sets the interpolation method used when sampling between pixel centers.
6566
*
66-
* @param interpolation one of NEAREST_NEIGHBOUR or BILINEAR
67+
* @param interpolation one of NEAREST_NEIGHBOUR, BILINEAR, or BICUBIC
6768
*/
6869
public void setInterpolation(int interpolation) {
6970
this.interpolation = interpolation;
@@ -86,13 +87,14 @@ public BufferedImage filter(BufferedImage src, BufferedImage dst) {
8687
if (dst == null) {
8788
ColorModel dstCM = src.getColorModel();
8889
dst = new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(0, 0), dstCM
89-
.isAlphaPremultiplied(), null);
90+
.isAlphaPremultiplied(), null);
9091
}
9192

9293
int[] inPixels = getRGB(src, 0, 0, srcWidth, srcHeight, null);
9394

9495
return switch (interpolation) {
9596
case BILINEAR -> filterPixelsBilinear(dst, srcWidth, srcHeight, inPixels);
97+
case BICUBIC -> filterPixelsBicubic(dst, srcWidth, srcHeight, inPixels);
9698
case NEAREST_NEIGHBOUR -> filterPixelsNN(dst, srcWidth, srcHeight, inPixels);
9799
default -> throw new IllegalStateException("should not get here");
98100
};
@@ -190,6 +192,65 @@ private BufferedImage filterPixelsBilinear(BufferedImage dst, int width, int hei
190192
return dst;
191193
}
192194

195+
/**
196+
* Applies the transform using bicubic interpolation.
197+
*/
198+
private BufferedImage filterPixelsBicubic(BufferedImage dst, int width, int height, int[] inPixels) {
199+
pt = createProgressTracker(height);
200+
@SuppressWarnings("unchecked")
201+
Future<int[]>[] rowFutures = new Future[height];
202+
203+
// process each output line in parallel
204+
for (int y = 0; y < height; y++) {
205+
float[] out = new float[2];
206+
int finalY = y;
207+
208+
Callable<int[]> rowTask = () -> {
209+
int[] outLine = new int[width];
210+
int[][] p = new int[4][4];
211+
for (int x = 0; x < width; x++) {
212+
transformInverse(x, finalY, out);
213+
214+
float srcX_f = out[0];
215+
float srcY_f = out[1];
216+
int srcX = (int) FastMath.floor(srcX_f);
217+
int srcY = (int) FastMath.floor(srcY_f);
218+
float xWeight = srcX_f - srcX;
219+
float yWeight = srcY_f - srcY;
220+
221+
// check if the 4x4 neighborhood is completely within the image
222+
if (srcX >= 1 && srcX < srcWidth - 2 && srcY >= 1 && srcY < srcHeight - 2) {
223+
// fast path
224+
int i = (srcWidth * (srcY - 1)) + srcX - 1;
225+
for (int row = 0; row < 4; row++) {
226+
p[row][0] = inPixels[i];
227+
p[row][1] = inPixels[i + 1];
228+
p[row][2] = inPixels[i + 2];
229+
p[row][3] = inPixels[i + 3];
230+
i += srcWidth;
231+
}
232+
} else {
233+
// slow path with edge handling
234+
for (int row = 0; row < 4; row++) {
235+
for (int col = 0; col < 4; col++) {
236+
p[row][col] = sampleBL(inPixels, srcX - 1 + col, srcY - 1 + row, srcWidth, srcHeight);
237+
}
238+
}
239+
}
240+
outLine[x] = ImageMath.bicubicInterpolate(xWeight, yWeight, p);
241+
}
242+
return outLine;
243+
};
244+
245+
rowFutures[finalY] = ThreadPool.submit2(rowTask);
246+
}
247+
248+
ThreadPool.waitFor2(rowFutures, dst, width, pt);
249+
finishProgressTracker();
250+
251+
return dst;
252+
}
253+
193254
/**
194255
* Samples a pixel for bilinear interpolation.
195256
*/

src/main/java/pixelitor/filters/gui/IntChoiceParam.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,9 @@ public static IntChoiceParam forEdgeAction(boolean reflectFirst) {
127127
}
128128

129129
private static final Item[] interpolationMethods = {
130-
new Item("Bilinear (Better)", TransformFilter.BILINEAR),
131-
new Item("Nearest Neighbour (Faster)", TransformFilter.NEAREST_NEIGHBOUR),
130+
new Item("Bilinear (Smooth, Balanced)", TransformFilter.BILINEAR),
131+
new Item("Nearest Neighbour (Fastest)", TransformFilter.NEAREST_NEIGHBOUR),
132+
new Item("Bicubic (High Quality)", TransformFilter.BICUBIC),
132133
};
133134

134135
public static IntChoiceParam forInterpolation() {

0 commit comments

Comments
 (0)