Skip to content

Instantly share code, notes, and snippets.

@stanio
Last active May 10, 2025 12:43
Show Gist options
  • Save stanio/076b46fc3238565aba8ad2b977baa072 to your computer and use it in GitHub Desktop.
Save stanio/076b46fc3238565aba8ad2b977baa072 to your computer and use it in GitHub Desktop.
/*
* 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)));
}
}
//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]");
}
}
@stanio
Copy link
Author

stanio commented Jul 2, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment