Last active
May 10, 2025 12:43
-
-
Save stanio/076b46fc3238565aba8ad2b977baa072 to your computer and use it in GitHub Desktop.
xBRZ in Java https://sourceforge.net/projects/xbrz/ (moved to https://github.com/stanio/xbrz-java)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* xBRZ project is distributed under | |
* GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 | |
* Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved | |
* | |
* Additionally and as a special exception, the author gives permission | |
* to link the code of this program with the following libraries | |
* (or with modified versions that use the same licenses), and distribute | |
* linked combinations including the two: MAME, FreeFileSync, Snes9x, ePSXe | |
* You must obey the GNU General Public License in all respects for all of | |
* the code used other than MAME, FreeFileSync, Snes9x, ePSXe. | |
* If you modify this file, you may extend this exception to your version | |
* of the file, but you are not obligated to do so. If you do not wish to | |
* do so, delete this exception statement from your version. | |
* | |
* Java port of xBRZ 1.8 by Stanio (stanio AT yahoo DOT com) | |
* | |
* As a special exception the author of this port gives you | |
* permission to link this library with independent modules to produce an | |
* executable, regardless of the license terms of these independent modules, | |
* and to copy and distribute the resulting executable under terms of your | |
* choice, provided that you also meet, for each linked independent module, | |
* the terms and conditions of the license of that module. An independent | |
* module is a module which is not derived from or based on this library. | |
* If you modify this library, you may extend this exception to your version | |
* of the library, but you are not obliged to do so. If you do not wish to | |
* do so, delete this exception statement from your version. | |
*/ | |
package xbrz; | |
import static xbrz.BlendInfo.*; | |
import static xbrz.BlendType.*; | |
import static xbrz.Color.*; | |
import static xbrz.ColorGradient.*; | |
import static xbrz.MatrixRotation.HALF_BYTE; | |
import static xbrz.RotationDegree.*; | |
/* | |
* xBRZ: "Scale by rules" - high quality image upscaling filter by Zenju | |
* | |
* https://sourceforge.net/projects/xbrz/ | |
*/ | |
public class Xbrz { | |
public static final class ScalerCfg | |
{ | |
public double luminanceWeight = 1; | |
public double equalColorTolerance = 30; | |
public double centerDirectionBias = 4; | |
public double dominantDirectionThreshold = 3.6; | |
public double steepDirectionThreshold = 2.2; | |
} | |
private Scaler scaler; | |
private ScalerCfg cfg; | |
private ColorDistance colorDistance; | |
public Xbrz(int factor) { | |
this(factor, true); | |
} | |
public Xbrz(int factor, boolean withAlpha) { | |
this(factor, withAlpha, new ScalerCfg()); | |
} | |
public Xbrz(int factor, boolean withAlpha, ScalerCfg cfg) { | |
this(factor, withAlpha, cfg, ColorDistance.yCbCr(cfg.luminanceWeight)); | |
} | |
public Xbrz(int factor, boolean withAlpha, ScalerCfg cfg, ColorDistance colorDistance) { | |
this.scaler = Scaler.forFactor(factor, withAlpha); | |
this.cfg = cfg; | |
this.colorDistance = withAlpha ? ColorDistance.withAlpha(colorDistance) : colorDistance; | |
} | |
private final double dist(int pix1, int pix2) { return colorDistance.calc(pix1, pix2); } | |
private final boolean eq(int pix1, int pix2) { return dist(pix1, pix2) < cfg.equalColorTolerance; } | |
private final BlendResult preProcessCornersResult = new BlendResult(); | |
/* detect blend direction | |
preprocessing blend result: | |
--------- | |
| F | G | evaluate corner between F, G, J, K | |
|---+---| current input pixel is at position F | |
| J | K | | |
--------- F, G, J, K corners of "BlendType" */ | |
private BlendResult preProcessCorners(Kernel_4x4 ker) { | |
BlendResult result = preProcessCornersResult.reset(); | |
if ((ker.f == ker.g && | |
ker.j == ker.k) || | |
(ker.f == ker.j && | |
ker.g == ker.k)) | |
return result; | |
final double jg = dist(ker.i, ker.f) + dist(ker.f, ker.c) + dist(ker.n, ker.k) + dist(ker.k, ker.h) + cfg.centerDirectionBias * dist(ker.j, ker.g); | |
final double fk = dist(ker.e, ker.j) + dist(ker.j, ker.o) + dist(ker.b, ker.g) + dist(ker.g, ker.l) + cfg.centerDirectionBias * dist(ker.f, ker.k); | |
if (jg < fk) | |
{ | |
final boolean dominantGradient = cfg.dominantDirectionThreshold * jg < fk; | |
if (ker.f != ker.g && ker.f != ker.j) | |
result.blend_f = dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL; | |
if (ker.k != ker.j && ker.k != ker.g) | |
result.blend_k = dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL; | |
} | |
else if (fk < jg) | |
{ | |
final boolean dominantGradient = cfg.dominantDirectionThreshold * fk < jg; | |
if (ker.j != ker.f && ker.j != ker.k) | |
result.blend_j = dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL; | |
if (ker.g != ker.f && ker.g != ker.k) | |
result.blend_g = dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL; | |
} | |
return result; | |
} | |
private final BlendInfo blendPixelInfo = new BlendInfo(); | |
private void blendPixel(RotationDegree rotDeg, | |
Kernel_3x3 ker, | |
OutputMatrix out, | |
BlendInfo blendInfo) //result of preprocessing all four corners of pixel "e" | |
{ | |
BlendInfo blend = blendPixelInfo.reset(blendInfo, rotDeg); | |
if (blend.getBottomR() >= BLEND_NORMAL) | |
{ | |
ker.rotDeg(rotDeg); | |
out.rotDeg(rotDeg); | |
final int e = ker.e(); | |
final int f = ker.f(); | |
final int h = ker.h(); | |
final int g = ker.g(); | |
final int c = ker.c(); | |
final int i = ker.i(); | |
boolean doLineBlend; | |
if (blend.getBottomR() >= BLEND_DOMINANT) | |
doLineBlend = true; | |
//make sure there is no second blending in an adjacent rotation for this pixel: handles insular pixels, mario eyes | |
else if (blend.getTopR() != BLEND_NONE && !eq(e, g)) //but support double-blending for 90° corners | |
doLineBlend = false; | |
else if (blend.getBottomL() != BLEND_NONE && !eq(e, c)) | |
doLineBlend = false; | |
//no full blending for L-shapes; blend corner only (handles "mario mushroom eyes") | |
else if (!eq(e, i) && eq(g, h) && eq(h, i) && eq(i, f) && eq(f, c)) | |
doLineBlend = false; | |
else | |
doLineBlend = true; | |
final int px = dist(e, f) <= dist(e, h) ? f : h; //choose most similar color | |
if (doLineBlend) | |
{ | |
final double fg = dist(f, g); | |
final double hc = dist(h, c); | |
final boolean haveShallowLine = cfg.steepDirectionThreshold * fg <= hc && e != g && ker.d() != g; | |
final boolean haveSteepLine = cfg.steepDirectionThreshold * hc <= fg && e != c && ker.b() != c; | |
if (haveShallowLine) | |
{ | |
if (haveSteepLine) | |
scaler.blendLineSteepAndShallow(px, out); | |
else | |
scaler.blendLineShallow(px, out); | |
} | |
else | |
{ | |
if (haveSteepLine) | |
scaler.blendLineSteep(px, out); | |
else | |
scaler.blendLineDiagonal(px, out); | |
} | |
} | |
else | |
scaler.blendCorner(px, out); | |
} | |
} | |
public void scaleImage(int[] src, int[] trg, int srcWidth, int srcHeight) { | |
int yFirst = 0; | |
int yLast = srcHeight; | |
byte[] preProcBuf = new byte[srcWidth]; | |
Kernel_4x4 ker4 = new Kernel_4x4(src, srcWidth, srcHeight); | |
OutputMatrix out = new OutputMatrix(scaler.scale(), trg, srcWidth * scaler.scale()); | |
//initialize preprocessing buffer for first row of current stripe: detect upper left and right corner blending | |
{ | |
ker4.positionY(yFirst - 1); | |
{ | |
final BlendResult res = preProcessCorners(ker4); | |
clearAddTopL(preProcBuf, 0, res.blend_k); //set 1st known corner for (0, yFirst) | |
} | |
for (int x = 0; x < srcWidth; ++x) | |
{ | |
ker4.shift(); //shift previous kernel to the left | |
ker4.readDhlp(x); // (x, yFirst - 1) is at position F | |
final BlendResult res = preProcessCorners(ker4); | |
addTopR(preProcBuf, x, res.blend_j); //set 2nd known corner for (x, yFirst) | |
if (x + 1 < srcWidth) | |
clearAddTopL(preProcBuf, x + 1, res.blend_k); //set 1st known corner for (x + 1, yFirst) | |
} | |
} | |
//------------------------------------------------------------------------------------ | |
Kernel_3x3 ker3 = new Kernel_3x3(ker4); | |
BlendInfo blend_xy = new BlendInfo(); | |
BlendInfo blend_xy1 = new BlendInfo(); | |
for (int y = yFirst; y < yLast; ++y) | |
{ | |
out.positionY(y); | |
//initialize at position x = -1 | |
ker4.positionY(y); | |
//corner blending for current (x, y + 1) position | |
{ | |
final BlendResult res = preProcessCorners(ker4); | |
blend_xy1.clearAddTopL(res.blend_k); //set 1st known corner for (0, y + 1) and buffer for use on next column | |
addBottomL(preProcBuf, 0, res.blend_g); //set 3rd known corner for (0, y) | |
} | |
for (int x = 0; x < srcWidth; ++x, out.incrementX()) | |
{ | |
ker4.shift(); //shift previous kernel to the left | |
ker4.readDhlp(x); // (x, y) is at position F | |
//evaluate the four corners on bottom-right of current pixel | |
blend_xy.val = preProcBuf[x]; //for current (x, y) position | |
{ | |
final BlendResult res = preProcessCorners(ker4); | |
blend_xy.addBottomR(res.blend_f); //all four corners of (x, y) have been determined at this point due to processing sequence! | |
blend_xy1.addTopR(res.blend_j); //set 2nd known corner for (x, y + 1) | |
preProcBuf[x] = blend_xy1.val; //store on current buffer position for use on next row | |
if (x + 1 < srcWidth) | |
{ | |
//blend_xy1 -> blend_x1y1 | |
blend_xy1.clearAddTopL(res.blend_k); //set 1st known corner for (x + 1, y + 1) and buffer for use on next column | |
addBottomL(preProcBuf, x + 1, res.blend_g); //set 3rd known corner for (x + 1, y) | |
} | |
} | |
out.fillBlock(ker4.f); | |
//blend all four corners of current pixel | |
if (blend_xy.blendingNeeded()) | |
{ | |
blendPixel(ROT_0, ker3, out, blend_xy); | |
blendPixel(ROT_90, ker3, out, blend_xy); | |
blendPixel(ROT_180, ker3, out, blend_xy); | |
blendPixel(ROT_270, ker3, out, blend_xy); | |
} | |
} | |
} | |
} | |
public static void scaleImage(int factor, boolean hasAlpha, int[] src, int[] trg, int srcWidth, int srcHeight) { | |
new Xbrz(factor, hasAlpha).scaleImage(src, trg, srcWidth, srcHeight); | |
} | |
} | |
enum RotationDegree { //clock-wise | |
ROT_0, | |
ROT_90, | |
ROT_180, | |
ROT_270 | |
} | |
class Color { | |
static int getAlpha(int pix) { return (pix >> 24) & 0xFF; } | |
static int getRed (int pix) { return (pix >> 16) & 0xFF; } | |
static int getGreen(int pix) { return (pix >> 8) & 0xFF; } | |
static int getBlue (int pix) { return (pix >> 0) & 0xFF; } | |
static int makePixel(int a, int r, int g, int b) { return (a << 24) | (r << 16) | (g << 8) | b; } | |
static int makePixel( int r, int g, int b) { return (0xFF << 24) | (r << 16) | (g << 8) | b; } | |
} | |
interface ColorDistance { | |
double calc(int pix1, int pix2); | |
static ColorDistance rgb() { | |
return (pix1, pix2) -> { | |
final int r_diff = getRed (pix1) - getRed (pix2); | |
final int g_diff = getGreen(pix1) - getGreen(pix2); | |
final int b_diff = getBlue (pix1) - getBlue (pix2); | |
//euklidean RGB distance | |
return Math.sqrt(r_diff * r_diff + g_diff * g_diff + b_diff * b_diff); | |
}; | |
} | |
static ColorDistance yCbCr(double lumaWeight) { | |
return new ColorDistanceYCbCr(lumaWeight); | |
} | |
static ColorDistance bufferedYCbCr(int sigBits) { | |
return new ColorDistanceYCbCrBuffered(sigBits); | |
} | |
static ColorDistance withAlpha(ColorDistance dist) { | |
return (pix1, pix2) -> { | |
final int a1 = getAlpha(pix1); | |
final int a2 = getAlpha(pix2); | |
/* | |
Requirements for a color distance handling alpha channel: with a1, a2 in [0, 1] | |
1. if a1 = a2, distance should be: a1 * distYCbCr() | |
2. if a1 = 0, distance should be: a2 * distYCbCr(black, white) = a2 * 255 | |
3. if a1 = 1, ??? maybe: 255 * (1 - a2) + a2 * distYCbCr() | |
*/ | |
final double d = dist.calc(pix1, pix2); | |
return (a1 < a2) ? a1 / 255.0 * d + (a2 - a1) | |
: a2 / 255.0 * d + (a1 - a2); | |
}; | |
} | |
} | |
class ColorDistanceYCbCr implements ColorDistance { | |
final double lumaWeight; | |
public ColorDistanceYCbCr(double lumaWeigth) { | |
this.lumaWeight = lumaWeigth; | |
} | |
//final double k_b = 0.0722; // ITU-R BT.709 conversion | |
//final double k_r = 0.2126; // | |
static final double k_b = 0.0593f; // ITU-R BT.2020 conversion | |
static final double k_r = 0.2627f; // | |
static final double k_g = 1 - k_b - k_r; | |
static final double scale_b = 0.5f / (1 - k_b); | |
static final double scale_r = 0.5f / (1 - k_r); | |
@Override | |
public double calc(int pix1, int pix2) { | |
// https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion | |
// YCbCr conversion is a matrix multiplication => take advantage of linearity by subtracting first! | |
final int r_diff = getRed (pix1) - getRed (pix2); // we may delay division by 255 to after matrix multiplication | |
final int g_diff = getGreen(pix1) - getGreen(pix2); // | |
final int b_diff = getBlue (pix1) - getBlue (pix2); // substraction for int is noticeable faster than for double! | |
final double y = k_r * r_diff + k_g * g_diff + k_b * b_diff; //[!], analog YCbCr! | |
final double c_b = scale_b * (b_diff - y); | |
final double c_r = scale_r * (r_diff - y); | |
// we skip division by 255 to have similar range like other distance functions | |
return Math.sqrt(square(lumaWeight * y) + square(c_b) + square(c_r)); | |
} | |
static double square(double value) { return value * value; } | |
} | |
class ColorDistanceYCbCrBuffered extends ColorDistanceYCbCr { | |
// -255 .. 255 | |
private static final int diffSize = 9; | |
private final int sigBits; | |
private final int adjBits; // diffSize - sigBits | |
private final float diffToDist[]; | |
// (1 << (3 * 5 sigBits)) = 32K * Float.BYTES = 128K buffer | |
// (1 << (3 * 8 sigBits)) = 16M * Float.BYTES = 64M buffer | |
public ColorDistanceYCbCrBuffered(int sigBits) { | |
super(1); | |
if (sigBits < 2 || sigBits > 8) { | |
throw new IllegalArgumentException("Illegal sigBits: " + sigBits); | |
} | |
this.sigBits = sigBits; | |
this.adjBits = diffSize - sigBits; | |
this.diffToDist = new float[1 << (3 * sigBits)]; | |
int bitMask = (1 << sigBits) - 1; | |
for (int i = 0, len = diffToDist.length; i < len; i++) { | |
// compressed values | |
int r_diff = i >> (sigBits << 1) & bitMask; | |
int g_diff = i >> sigBits & bitMask; | |
int b_diff = i & bitMask; | |
// expanded values | |
r_diff = (r_diff << adjBits) - 255 + (1 << (adjBits - 1)); | |
g_diff = (g_diff << adjBits) - 255 + (1 << (adjBits - 1)); | |
b_diff = (b_diff << adjBits) - 255 + (1 << (adjBits - 1)); | |
final double y = k_r * r_diff + k_g * g_diff + k_b * b_diff; //[!], analog YCbCr! | |
final double c_b = scale_b * (b_diff - y); | |
final double c_r = scale_r * (r_diff - y); | |
diffToDist[i] = (float) Math.sqrt(square(y) + square(c_b) + square(c_r)); | |
} | |
} | |
@Override | |
public double calc(int pix1, int pix2) { | |
final int r_diff = getRed (pix1) - getRed (pix2) + 255; | |
final int g_diff = getGreen(pix1) - getGreen(pix2) + 255; | |
final int b_diff = getBlue (pix1) - getBlue (pix2) + 255; | |
// compressed index | |
final int index = ((r_diff >> adjBits) << (sigBits << 1)) | | |
((g_diff >> adjBits) << sigBits) | | |
(b_diff >> adjBits); | |
return diffToDist[index]; | |
} | |
} | |
/* input kernel area naming convention: | |
----------------- | |
| A | B | C | D | | |
|---|---|---|---| | |
| E | F | G | H | | |
|---|---|---|---| input pixel is at position F | |
| I | J | K | L | | |
|---|---|---|---| | |
| M | N | O | P | | |
----------------- | |
*/ | |
final class Kernel_4x4 { | |
private final int[] src; | |
private final int srcWidth; | |
private final int srcHeight; | |
private int s_m1; | |
private int s_0; | |
private int s_p1; | |
private int s_p2; | |
int | |
a, b, c, // | |
e, f, g, // support reinterpret_cast from Kernel_4x4 => Kernel_3x3 | |
i, j, k, // | |
m, n, o, | |
d, h, l, p; | |
Kernel_4x4(int[] src, int srcWidth, int srcHeight) { | |
this.src = src; | |
this.srcWidth = srcWidth; | |
this.srcHeight = srcHeight; | |
} | |
final void positionY(int y) { | |
s_m1 = 0 <= y - 1 && y - 1 < srcHeight ? srcWidth * (y - 1) : -1; | |
s_0 = 0 <= y && y < srcHeight ? srcWidth * y : -1; | |
s_p1 = 0 <= y + 1 && y + 1 < srcHeight ? srcWidth * (y + 1) : -1; | |
s_p2 = 0 <= y + 2 && y + 2 < srcHeight ? srcWidth * (y + 2) : -1; | |
readDhlp(-4); //hack: read a, e, i, m at x = -1 | |
a = d; | |
e = h; | |
i = l; | |
m = p; | |
readDhlp(-3); | |
b = d; | |
f = h; | |
j = l; | |
n = p; | |
readDhlp(-2); | |
c = d; | |
g = h; | |
k = l; | |
o = p; | |
readDhlp(-1); | |
} | |
final void readDhlp(int x) //(x, y) is at kernel position F | |
{ | |
final int x_p2 = x + 2; | |
if (0 <= x_p2 && x_p2 < srcWidth) | |
{ | |
d = (s_m1 >= 0) ? src[s_m1 + x_p2] : 0; | |
h = (s_0 >= 0) ? src[s_0 + x_p2] : 0; | |
l = (s_p1 >= 0) ? src[s_p1 + x_p2] : 0; | |
p = (s_p2 >= 0) ? src[s_p2 + x_p2] : 0; | |
} | |
else | |
{ | |
d = 0; | |
h = 0; | |
l = 0; | |
p = 0; | |
} | |
} | |
final void shift() { | |
a = b; //shift kernel to the left | |
e = f; // ----------------- | |
i = j; // | A | B | C | D | | |
m = n; // |---|---|---|---| | |
/**/ // | E | F | G | H | | |
b = c; // |---|---|---|---| | |
f = g; // | I | J | K | L | | |
j = k; // |---|---|---|---| | |
n = o; // | M | N | O | P | | |
/**/ // ----------------- | |
c = d; | |
g = h; | |
k = l; | |
o = p; | |
} | |
} | |
/* input kernel area naming convention: | |
------------- | |
| A | B | C | | |
|---|---|---| | |
| D | E | F | input pixel is at position E | |
|---|---|---| | |
| G | H | I | | |
------------- | |
*/ | |
final class Kernel_3x3 { | |
private final Kernel_4x4 ker4; | |
private RotationDegree rotDeg = ROT_0; | |
Kernel_3x3(Kernel_4x4 ker4) { | |
this.ker4 = ker4; | |
} | |
final int a() { | |
switch (rotDeg) { | |
default: return ker4.a; | |
case ROT_90: return ker4.i; | |
case ROT_180: return ker4.k; | |
case ROT_270: return ker4.c; | |
} | |
} | |
final int b() { | |
switch (rotDeg) { | |
default: return ker4.b; | |
case ROT_90: return ker4.e; | |
case ROT_180: return ker4.j; | |
case ROT_270: return ker4.g; | |
} | |
} | |
final int c() { | |
switch (rotDeg) { | |
default: return ker4.c; | |
case ROT_90: return ker4.a; | |
case ROT_180: return ker4.i; | |
case ROT_270: return ker4.k; | |
} | |
} | |
final int d() { | |
switch (rotDeg) { | |
default: return ker4.e; | |
case ROT_90: return ker4.j; | |
case ROT_180: return ker4.g; | |
case ROT_270: return ker4.b; | |
} | |
} | |
final int e() { | |
return ker4.f; // center | |
} | |
final int f() { | |
switch (rotDeg) { | |
default: return ker4.g; | |
case ROT_90: return ker4.b; | |
case ROT_180: return ker4.e; | |
case ROT_270: return ker4.j; | |
} | |
} | |
final int g() { | |
switch (rotDeg) { | |
default: return ker4.i; | |
case ROT_90: return ker4.k; | |
case ROT_180: return ker4.c; | |
case ROT_270: return ker4.a; | |
} | |
} | |
final int h() { | |
switch (rotDeg) { | |
default: return ker4.j; | |
case ROT_90: return ker4.g; | |
case ROT_180: return ker4.b; | |
case ROT_270: return ker4.e; | |
} | |
} | |
final int i() { | |
switch (rotDeg) { | |
default: return ker4.k; | |
case ROT_90: return ker4.c; | |
case ROT_180: return ker4.a; | |
case ROT_270: return ker4.i; | |
} | |
} | |
final void rotDeg(RotationDegree deg) { | |
this.rotDeg = deg; | |
} | |
} | |
//access matrix area, top-left at current position | |
final class OutputMatrix { | |
private final int N; | |
private final int[] out; | |
private final int outWidth; | |
private int offset; | |
private RotationDegree rotDeg = ROT_0; | |
private final MatrixRotation rot; | |
OutputMatrix(int N, int[] out, int outWidth) { | |
this.N = N; | |
this.out = out; | |
this.outWidth = outWidth; | |
this.rot = new MatrixRotation(N); | |
} | |
final void positionY(int y) { | |
offset = N * y * outWidth; | |
} | |
final void incrementX() { | |
offset += N; | |
} | |
final void rotDeg(RotationDegree deg) { | |
this.rotDeg = deg; | |
} | |
private final int position(final int I, final int J) { | |
final byte IJ_old = rot.calc(rotDeg, I, J); | |
final int I_old = IJ_old >> HALF_BYTE & 0xF; | |
final int J_old = IJ_old & 0xF; | |
return offset + J_old + I_old * outWidth; | |
} | |
final void set(int I, int J, int val) { | |
out[position(I, J)] = val; | |
} | |
final void set(int I, int J, IntFunction func) { | |
final int pos = position(I, J); | |
out[pos] = func.apply(out[pos]); | |
} | |
//fill block of size scale * scale with the given color | |
final void fillBlock(int col) { | |
fillBlock(col, N, N); | |
} | |
final void fillBlock(int col, int blockWidth, int blockHeight) { | |
for (int y = 0, trg = y * outWidth + offset; y < blockHeight; ++y, trg += outWidth) | |
for (int x = 0; x < blockWidth; ++x) | |
out[trg + x] = col; | |
} | |
} | |
@FunctionalInterface interface IntFunction { | |
int apply(int a); | |
} | |
final class MatrixRotation { | |
static final int HALF_BYTE = Byte.SIZE / 2; | |
private final int N; | |
private final int Nsq; | |
private final byte[] lookup; | |
MatrixRotation(int N) { | |
this.N = N; | |
this.Nsq = N * N; | |
if (N > 16) { | |
throw new IllegalArgumentException("N should be <= 16"); | |
} | |
byte[] lookup = new byte[4 * Nsq]; | |
for (int rotDeg = 0; rotDeg < 4; rotDeg++) { | |
int offset = rotDeg * Nsq; | |
for (int I = 0; I < N; I++) { | |
for (int J = 0; J < N; J++) { | |
lookup[offset + I * N + J] = | |
calc(rotDeg, (byte) ((I << HALF_BYTE) | J)); | |
} | |
} | |
} | |
this.lookup = lookup; | |
} | |
private final byte calc(int rotDeg, byte IJ) { | |
if (rotDeg == 0) { | |
return IJ; | |
} | |
byte IJ_old = calc(rotDeg - 1, IJ); | |
int I_old = IJ_old >> HALF_BYTE & 0xF; | |
int J_old = IJ_old & 0xF; | |
int rot_I = N - 1 - J_old; | |
int rot_J = I_old; | |
return (byte) (rot_I << HALF_BYTE | rot_J); | |
} | |
final byte calc(RotationDegree rotDeg, int I, int J) { | |
final int offset = rotDeg.ordinal() * Nsq; | |
return lookup[offset + I * N + J]; | |
} | |
} | |
final class BlendType { | |
static final byte BLEND_NONE = 0; | |
static final byte BLEND_NORMAL = 1; //a normal indication to blend | |
static final byte BLEND_DOMINANT = 2; //a strong indication to blend | |
} | |
/* | |
--------- | |
| F | G | | |
|---+---| current input pixel is at position F | |
| J | K | | |
--------- */ | |
final class BlendResult | |
{ | |
byte | |
/**/blend_f, blend_g, | |
/**/blend_j, blend_k; | |
final BlendResult reset() { | |
blend_f = blend_g = blend_j = blend_k = BLEND_NONE; | |
return this; | |
} | |
} | |
final class BlendInfo { | |
byte val; | |
final BlendInfo reset(BlendInfo other, RotationDegree rotDeg) { | |
byte b = other.val; | |
switch (rotDeg) { | |
default: val = b; break; | |
case ROT_90: val = (byte) (((b << 2) & 0xFF) | ((b & 0xFF) >> 6)); break; | |
case ROT_180: val = (byte) (((b << 4) & 0xFF) | ((b & 0xFF) >> 4)); break; | |
case ROT_270: val = (byte) (((b << 6) & 0xFF) | ((b & 0xFF) >> 2)); break; | |
} | |
return this; | |
} | |
final boolean blendingNeeded() { | |
return val != BLEND_NONE; | |
} | |
//final byte getTopL () { return (byte) (0x3 & val); } | |
final byte getTopR () { return (byte) (0x3 & (val >> 2)); } | |
final byte getBottomR() { return (byte) (0x3 & (val >> 4)); } | |
final byte getBottomL() { return (byte) (0x3 & (val >> 6)); } | |
final void clearAddTopL(byte bt) { val = bt; } | |
final void addTopR (byte bt) { val |= bt << 2; } //buffer is assumed to be initialized before preprocessing! | |
final void addBottomR (byte bt) { val |= bt << 4; } //e.g. via clearAddTopL() | |
//final void addBottomL (byte bt) { val |= bt << 6; } // | |
static void clearAddTopL(byte[] buf, int i, byte bt) { buf[i] = bt; } | |
static void addTopR (byte[] buf, int i, byte bt) { buf[i] |= bt << 2; } //buffer is assumed to be initialized before preprocessing! | |
//static void addBottomR (byte[] buf, int i, byte bt) { buf[i] |= bt << 4; } //e.g. via clearAddTopL() | |
static void addBottomL (byte[] buf, int i, byte bt) { buf[i] |= bt << 6; } // | |
} | |
interface Scaler { | |
int scale(); | |
void blendLineShallow(int col, OutputMatrix out); | |
void blendLineSteep(int col, OutputMatrix out); | |
void blendLineSteepAndShallow(int col, OutputMatrix out); | |
void blendLineDiagonal(int col, OutputMatrix out); | |
void blendCorner(int col, OutputMatrix out); | |
static Scaler forFactor(int factor, boolean withAlpha) { | |
switch (factor) { | |
case 2: return new Scaler2x(withAlpha); | |
case 3: return new Scaler3x(withAlpha); | |
case 4: return new Scaler4x(withAlpha); | |
case 5: return new Scaler5x(withAlpha); | |
case 6: return new Scaler6x(withAlpha); | |
default: | |
throw new IllegalArgumentException("Illegal scaling factor: " + factor); | |
} | |
} | |
} | |
abstract class AbstractScaler implements Scaler { | |
protected final int scale; | |
private ColorGradient colorGradient; | |
protected AbstractScaler(int scale, boolean withAlpha) { | |
this(scale, withAlpha ? gradientARGB() : gradientRGB()); | |
} | |
protected AbstractScaler(int scale, ColorGradient colorGradient) { | |
this.scale = scale; | |
this.colorGradient = colorGradient; | |
} | |
@Override | |
public final int scale() { | |
return scale; | |
} | |
protected final int alphaGrad(int M, int N, int pixBack, int pixFront) { | |
return colorGradient.alphaGrad(M, N, pixBack, pixFront); | |
} | |
} | |
class Scaler2x extends AbstractScaler { | |
public Scaler2x(boolean withAlpha) { | |
super(2, withAlpha); | |
} | |
@Override | |
public void blendLineShallow(int col, OutputMatrix out) { | |
out.set(scale - 1, 0, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(scale - 1, 1, ref -> alphaGrad(3, 4, ref, col)); | |
} | |
@Override | |
public void blendLineSteep(int col, OutputMatrix out) { | |
out.set(0, scale - 1, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(1, scale - 1, ref -> alphaGrad(3, 4, ref, col)); | |
} | |
@Override | |
public void blendLineSteepAndShallow(int col, OutputMatrix out) { | |
out.set(1, 0, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(0, 1, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(1, 1, ref -> alphaGrad(5, 6, ref, col)); // [!] fixes 7/8 used in xBR | |
} | |
@Override | |
public void blendLineDiagonal(int col, OutputMatrix out) { | |
out.set(1, 1, ref -> alphaGrad(1, 2, ref, col)); | |
} | |
@Override | |
public void blendCorner(int col, OutputMatrix out) { | |
// model a round corner | |
out.set(1, 1, ref -> alphaGrad(21, 100, ref, col)); // exact: 1 - pi/4 = 0.2146018366 | |
} | |
} | |
class Scaler3x extends AbstractScaler { | |
public Scaler3x(boolean withAlpha) { | |
super(3, withAlpha); | |
} | |
@Override | |
public void blendLineShallow(int col, OutputMatrix out) { | |
out.set(scale - 1, 0, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(scale - 2, 2, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(scale - 1, 1, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(scale - 1, 2, col); | |
} | |
@Override | |
public void blendLineSteep(int col, OutputMatrix out) { | |
out.set(0, scale - 1, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(2, scale - 2, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(1, scale - 1, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(2, scale - 1, col); | |
} | |
@Override | |
public void blendLineSteepAndShallow(int col, OutputMatrix out) { | |
out.set(2, 0, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(0, 2, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(2, 1, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(1, 2, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(2, 2, col); | |
} | |
@Override | |
public void blendLineDiagonal(int col, OutputMatrix out) { | |
out.set(1, 2, ref -> alphaGrad(1, 8, ref, col)); // conflict with other rotations for this odd scale | |
out.set(2, 1, ref -> alphaGrad(1, 8, ref, col)); | |
out.set(2, 2, ref -> alphaGrad(7, 8, ref, col)); // | |
} | |
@Override | |
public void blendCorner(int col, OutputMatrix out) { | |
// model a round corner | |
out.set(2, 2, ref -> alphaGrad(45, 100, ref, col)); // exact: 0.4545939598 | |
//out.set(2, 1, ref -> alphaGrad(7, 256, ref, col)); // 0.02826017254 -> negligible + avoid conflicts with other rotations for this odd scale | |
//out.set(1, 2, ref -> alphaGrad(7, 256, ref, col)); // 0.02826017254 | |
} | |
} | |
class Scaler4x extends AbstractScaler { | |
public Scaler4x(boolean withAlpha) { | |
super(4, withAlpha); | |
} | |
@Override | |
public void blendLineShallow(int col, OutputMatrix out) { | |
out.set(scale - 1, 0, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(scale - 2, 2, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(scale - 1, 1, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(scale - 2, 3, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(scale - 1, 2, col); | |
out.set(scale - 1, 3, col); | |
} | |
@Override | |
public void blendLineSteep(int col, OutputMatrix out) { | |
out.set(0, scale - 1, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(2, scale - 2, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(1, scale - 1, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(3, scale - 2, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(2, scale - 1, col); | |
out.set(3, scale - 1, col); | |
} | |
@Override | |
public void blendLineSteepAndShallow(int col, OutputMatrix out) { | |
out.set(3, 1, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(1, 3, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(3, 0, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(0, 3, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(2, 2, ref -> alphaGrad(1, 3, ref, col)); //[!] fixes 1/4 used in xBR | |
out.set(3, 3, col); | |
out.set(3, 2, col); | |
out.set(2, 3, col); | |
} | |
@Override | |
public void blendLineDiagonal(int col, OutputMatrix out) { | |
out.set(scale - 1, scale / 2 , ref -> alphaGrad(1, 2, ref, col)); | |
out.set(scale - 2, scale / 2 + 1, ref -> alphaGrad(1, 2, ref, col)); | |
out.set(scale - 1, scale - 1, col); | |
} | |
@Override | |
public void blendCorner(int col, OutputMatrix out) { | |
// model a round corner | |
out.set(3, 3, ref -> alphaGrad(68, 100, ref, col)); // exact: 0.6848532563 | |
out.set(3, 2, ref -> alphaGrad( 9, 100, ref, col)); // 0.08677704501 | |
out.set(2, 3, ref -> alphaGrad( 9, 100, ref, col)); // 0.08677704501 | |
} | |
} | |
class Scaler5x extends AbstractScaler { | |
public Scaler5x(boolean withAlpha) { | |
super(5, withAlpha); | |
} | |
@Override | |
public void blendLineShallow(int col, OutputMatrix out) { | |
out.set(scale - 1, 0, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(scale - 2, 2, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(scale - 3, 4, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(scale - 1, 1, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(scale - 2, 3, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(scale - 1, 2, col); | |
out.set(scale - 1, 3, col); | |
out.set(scale - 1, 4, col); | |
out.set(scale - 2, 4, col); | |
} | |
@Override | |
public void blendLineSteep(int col, OutputMatrix out) { | |
out.set(0, scale - 1, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(2, scale - 2, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(4, scale - 3, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(1, scale - 1, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(3, scale - 2, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(2, scale - 1, col); | |
out.set(3, scale - 1, col); | |
out.set(4, scale - 1, col); | |
out.set(4, scale - 2, col); | |
} | |
@Override | |
public void blendLineSteepAndShallow(int col, OutputMatrix out) { | |
out.set(0, scale - 1, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(2, scale - 2, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(1, scale - 1, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(scale - 1, 0, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(scale - 2, 2, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(scale - 1, 1, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(3, 3, ref -> alphaGrad(2, 3, ref, col)); | |
out.set(2, scale - 1, col); | |
out.set(3, scale - 1, col); | |
out.set(4, scale - 1, col); | |
out.set(scale - 1, 2, col); | |
out.set(scale - 1, 3, col); | |
} | |
@Override | |
public void blendLineDiagonal(int col, OutputMatrix out) { | |
out.set(scale - 1, scale / 2 , ref -> alphaGrad(1, 8, ref, col)); //conflict with other rotations for this odd scale | |
out.set(scale - 2, scale / 2 + 1, ref -> alphaGrad(1, 8, ref, col)); | |
out.set(scale - 3, scale / 2 + 2, ref -> alphaGrad(1, 8, ref, col)); // | |
out.set(4, 3, ref -> alphaGrad(7, 8, ref, col)); | |
out.set(3, 4, ref -> alphaGrad(7, 8, ref, col)); | |
out.set(4, 4, col); | |
} | |
@Override | |
public void blendCorner(int col, OutputMatrix out) { | |
// model a round corner | |
out.set(4, 4, ref -> alphaGrad(86, 100, ref, col)); // exact: 0.8631434088 | |
out.set(4, 3, ref -> alphaGrad(23, 100, ref, col)); // 0.2306749731 | |
out.set(3, 4, ref -> alphaGrad(23, 100, ref, col)); // 0.2306749731 | |
//out.set(4, 2, ref -> alphaGrad(1, 64, ref, col)); // 0.01676812367 -> negligible + avoid conflicts with other rotations for this odd scale | |
//out.set(2, 4, ref -> alphaGrad(1, 64, ref, col)); // 0.01676812367 | |
} | |
} | |
class Scaler6x extends AbstractScaler { | |
public Scaler6x(boolean withAlpha) { | |
super(6, withAlpha); | |
} | |
@Override | |
public void blendLineShallow(int col, OutputMatrix out) { | |
out.set(scale - 1, 0, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(scale - 2, 2, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(scale - 3, 4, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(scale - 1, 1, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(scale - 2, 3, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(scale - 3, 5, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(scale - 1, 2, col); | |
out.set(scale - 1, 3, col); | |
out.set(scale - 1, 4, col); | |
out.set(scale - 1, 5, col); | |
out.set(scale - 2, 4, col); | |
out.set(scale - 2, 5, col); | |
} | |
@Override | |
public void blendLineSteep(int col, OutputMatrix out) { | |
out.set(0, scale - 1, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(2, scale - 2, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(4, scale - 3, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(1, scale - 1, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(3, scale - 2, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(5, scale - 3, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(2, scale - 1, col); | |
out.set(3, scale - 1, col); | |
out.set(4, scale - 1, col); | |
out.set(5, scale - 1, col); | |
out.set(4, scale - 2, col); | |
out.set(5, scale - 2, col); | |
} | |
@Override | |
public void blendLineSteepAndShallow(int col, OutputMatrix out) { | |
out.set(0, scale - 1, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(2, scale - 2, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(1, scale - 1, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(3, scale - 2, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(scale - 1, 0, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(scale - 2, 2, ref -> alphaGrad(1, 4, ref, col)); | |
out.set(scale - 1, 1, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(scale - 2, 3, ref -> alphaGrad(3, 4, ref, col)); | |
out.set(2, scale - 1, col); | |
out.set(3, scale - 1, col); | |
out.set(4, scale - 1, col); | |
out.set(5, scale - 1, col); | |
out.set(4, scale - 2, col); | |
out.set(5, scale - 2, col); | |
out.set(scale - 1, 2, col); | |
out.set(scale - 1, 3, col); | |
} | |
@Override | |
public void blendLineDiagonal(int col, OutputMatrix out) { | |
out.set(scale - 1, scale / 2 , ref -> alphaGrad(1, 2, ref, col)); | |
out.set(scale - 2, scale / 2 + 1, ref -> alphaGrad(1, 2, ref, col)); | |
out.set(scale - 3, scale / 2 + 2, ref -> alphaGrad(1, 2, ref, col)); | |
out.set(scale - 2, scale - 1, col); | |
out.set(scale - 1, scale - 1, col); | |
out.set(scale - 1, scale - 2, col); | |
} | |
@Override | |
public void blendCorner(int col, OutputMatrix out) { | |
// model a round corner | |
out.set(5, 5, ref -> alphaGrad(97, 100, ref, col)); // exact: 0.9711013910 | |
out.set(4, 5, ref -> alphaGrad(42, 100, ref, col)); // 0.4236372243 | |
out.set(5, 4, ref -> alphaGrad(42, 100, ref, col)); // 0.4236372243 | |
out.set(5, 3, ref -> alphaGrad( 6, 100, ref, col)); // 0.05652034508 | |
out.set(3, 5, ref -> alphaGrad( 6, 100, ref, col)); // 0.05652034508 | |
} | |
} | |
interface ColorGradient { | |
int alphaGrad(int M, int N, int pixBack, int pixFront); | |
static ColorGradient gradientRGB() { | |
return new ColorGradientRGB(); | |
} | |
static ColorGradient gradientARGB() { | |
return new ColorGradientARGB(); | |
} | |
} | |
class ColorGradientRGB implements ColorGradient { | |
private static int calcColor(int M, int N, int colFront, int colBack) { | |
return (colFront * M + colBack * (N - M)) / N; | |
} | |
@Override | |
// blend front color with opacity M / N over opaque background: https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending | |
public int alphaGrad(int M, int N, int pixBack, int pixFront) { | |
//assert (0 < M && M < N && N <= 1000); | |
return makePixel(calcColor(M, N, getRed(pixFront), getRed(pixBack)), | |
calcColor(M, N, getGreen(pixFront), getGreen(pixBack)), | |
calcColor(M, N, getBlue(pixFront), getBlue(pixBack))); | |
} | |
} | |
class ColorGradientARGB implements ColorGradient { | |
private static int calcColor(int weightFront, int weightBack, int weightSum, int colFront, int colBack) { | |
return (colFront * weightFront + colBack * weightBack) / weightSum; | |
} | |
@Override | |
// find intermediate color between two colors with alpha channels (=> NO alpha blending!!!) | |
public int alphaGrad(int M, int N, int pixBack, int pixFront) { | |
//assert (0 < M && M < N && N <= 1000); | |
final int weightFront = getAlpha(pixFront) * M; | |
final int weightBack = getAlpha(pixBack) * (N - M); | |
final int weightSum = weightFront + weightBack; | |
if (weightSum == 0) | |
return 0; | |
return makePixel(weightSum / N, | |
calcColor(weightFront, weightBack, weightSum, getRed(pixFront), getRed(pixBack)), | |
calcColor(weightFront, weightBack, weightSum, getGreen(pixFront), getGreen(pixBack)), | |
calcColor(weightFront, weightBack, weightSum, getBlue(pixFront), getBlue(pixBack))); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//package ; | |
import java.awt.image.BufferedImage; | |
import java.io.File; | |
import javax.imageio.ImageIO; | |
import xbrz.Xbrz; | |
public class XbrzTest { | |
public static BufferedImage scaleImage(BufferedImage source, int factor) { | |
int width = source.getWidth(); | |
int height = source.getHeight(); | |
int[] inPixels = new int[width * height]; | |
source.getRGB(0, 0, width, height, inPixels, 0, width); | |
int destWidth = width * factor; | |
int destHeight = height * factor; | |
int[] outPixels = new int[destWidth * destHeight]; | |
Xbrz.scaleImage(factor, true, inPixels, outPixels, width, height); | |
BufferedImage dest = new BufferedImage(destWidth, destHeight, BufferedImage.TYPE_INT_ARGB); | |
dest.setRGB(0, 0, destWidth, destHeight, outPixels, 0, destWidth); | |
return dest; | |
} | |
public static void main(String[] args) throws Exception { | |
if (args.length == 0) { | |
printUsage(); | |
System.exit(1); | |
} | |
int factor = 2; | |
if (args.length > 1) { | |
try { | |
factor = Integer.parseInt(args[1]); | |
} catch (NumberFormatException e) { | |
System.err.println(e.toString()); | |
printUsage(); | |
System.exit(2); | |
} | |
} | |
BufferedImage source = ImageIO.read(new File(args[0])); | |
BufferedImage scaled = scaleImage(source, factor); | |
String target = args[0].replaceFirst("((?<!^|[/\\\\])\\.[^.]+)?$", "@" + factor + "x.png"); | |
ImageIO.write(scaled, "png", new File(target)); | |
System.out.println(target); | |
} | |
private static void printUsage() { | |
System.err.println("Usage: XbrzTest <source> [scaling_factor]"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The most time-consuming part in this Java implementation is the color distance calculation: ~50%. Using the Euclidean RGB distance, or the buffered YCbCr may show a 20-25% improvement. Both of these produce slightly (mostly unnoticeable) different output around some edges.