Image Resizing for Processing Alpha 4

Hi folks,

I’ve been trying Processing 4 alpha lately, and needed a way to resize some images. (The original resize method depends on AWT, which I believe they’re trying to isolate from the rest of the library.) I found an implementation by Channel72, which I used as a primary reference when translating to Java-Processing.

PImage resizeBicubic ( PImage target, int wPx, int hPx ) {

  /*
   * References: https://stackoverflow.com/questions/
   * 17640173/implementation-of-bi-cubic-resize
   */

  int kernelSize = 4;
  int[] kernel = new int[kernelSize];

  target.loadPixels();
  int sw = target.pixelWidth;
  int sh = target.pixelHeight;
  int srcFmt = target.format;
  int pd = target.pixelDensity;
  int[] srcpx = target.pixels;

  int dw = ( wPx < 2 ? 2 : wPx ) * pd;
  int dh = ( hPx < 2 ? 2 : hPx ) * pd;

  /*
   * Subtracting by 1.0 from the source dimensions was not present in the
   * reference code, but seems to help reduce blurred alpha on the right and
   * bottom edges.
   */
  float tx = ( sw - 1.0f ) / dw;
  float ty = ( sh - 1.0f ) / dh;

  /* Despite the name, RGB images retain alpha, and so have 4 channels. */
  int chnlCount;
  switch ( srcFmt ) {
  case PConstants.ALPHA:
    chnlCount = 1;
    break;

  case PConstants.RGB:
  case PConstants.ARGB:
  default:
    chnlCount = 4;
  }

  /*
   * The original algorithm consists of 4 nested for loops: rows (height),
   * columns (width), kernel, channel. This flattens them to one loop.
   */
  int newPxlLen = dw * dh;
  int[] clrs = new int[newPxlLen * kernelSize];
  int len2 = kernelSize * chnlCount;
  int len3 = dw * len2;
  int len4 = dh * len3;

  for ( int k = 0; k < len4; ++k ) {
    int g = k / len3; /* row index */
    int m = k - g * len3; /* temporary */
    int h = m / len2; /* column index */
    int n = m - h * len2; /* temporary */
    int i = n / kernelSize; /* channel index */
    int j = n % kernelSize; /* kernel index */

    /* Row. */
    int y = ( int ) ( ty * g );
    float dy = ty * g - y;
    float dysq = dy * dy;

    /* Column. */
    int x = ( int ) ( tx * h );
    float dx = tx * h - x;
    float dxsq = dx * dx;

    int a0 = 0;
    int d0 = 0;
    int d2 = 0;
    int d3 = 0;

    int z = y - 1 + j;
    if ( z > -1 && z < sh ) {
      int zw = z * sw;
      int i8 = i * 8;
      int x1 = x - 1;
      int x2 = x + 1;
      int x3 = x + 2;

      if ( x > -1 && x < sw ) {
        a0 = srcpx[zw + x] >> i8 & 0xff;
      }
      if ( x1 > -1 && x1 < sw ) {
        d0 = srcpx[zw + x1] >> i8 & 0xff;
      }
      if ( x2 > -1 && x2 < sw ) {
        d2 = srcpx[zw + x2] >> i8 & 0xff;
      }
      if ( x3 > -1 && x3 < sw ) {
        d3 = srcpx[zw + x3] >> i8 & 0xff;
      }
    }

    /* Subtract a0 no matter the boundary condition. */
    d0 -= a0;
    d2 -= a0;
    d3 -= a0;

    float a1 = -d0 / 3.0f + d2 - d3 / 6.0f;
    float a2 = 0.5f * ( d0 + d2 );
    float a3 = -d0 / 6.0f - 0.5f * d2 + d3 / 6.0f;

    kernel[j] = constrain(( int ) ( a0 + a1 * dx + a2 * dxsq + a3 * ( dx
      * dxsq ) ), 0, 255);

    d0 = kernel[0] - kernel[1];
    d2 = kernel[2] - kernel[1];
    d3 = kernel[3] - kernel[1];
    a0 = kernel[1];

    a1 = -d0 / 3.0f + d2 - d3 / 6.0f;
    a2 = 0.5f * ( d0 + d2 );
    a3 = -d0 / 6.0f - 0.5f * d2 + d3 / 6.0f;

    // rowStride = dw * chnlCount
    // g * rowStride + h * chnlCount + i]
    clrs[k / kernelSize] = constrain(( int ) ( a0 + a1 * dy + a2 * dysq
      + a3 * ( dy * dysq ) ), 0, 255);
  }

  int[] trgpx = new int[newPxlLen];
  switch ( srcFmt ) {
  case PConstants.ALPHA:
    for ( int i = 0; i < newPxlLen; ++i ) {
      trgpx[i] = clrs[i];
    }
    break;

  case PConstants.RGB:
  case PConstants.ARGB:
  default:
    for ( int i = 0, j = 0; i < newPxlLen; ++i, j += 4 ) {
      trgpx[i] = clrs[j + 3] << 0x18 | clrs[j + 2] << 0x10 | clrs[j
        + 1] << 0x08 | clrs[j];
    }
  }

  target.pixels = trgpx;
  target.width = dw / pd;
  target.height = dh / pd;
  target.pixelWidth = dw;
  target.pixelHeight = dh;
  target.updatePixels();
  return target;
}

The original method uses four nested for-loops, which I flattened out. I also subtracted one from the source dimensions at the start of the method to try to reduce the edge haloing in the bottom right corner; there will still be haloes on some images. I’m given to understand that this is a known disadvantage of this method, but if it’s an error with the code, caveat emptor.

The original caches color channels as separate elements in an array, so that’s why I hacked in a second for loop to pack them together into an integer. Maybe it’s easier to resize ALPHA formatted images this way (I’ve found them in use when I try to get a glyph image from a font).

Here’s a simplified sketch with which the above can be run. It was not used to make the test images I’ll show later.

int samples = 8;
int displayIndex = 0;
int widthSource = 128;
int heightSource = 128;
float minScalar = 0.25f;
float maxScalar = 3.0f;
PImage source;
PImage[] resed = new PImage[samples];
float[] scalars = new float[samples];

void setup() {
  //size(720, 405, JAVA2D);
  size(720, 405, P2D);
  background(0xfffff7d5);
  frameRate(2f);
  imageMode(CENTER);
  textAlign(CENTER, CENTER);

  source = createImage(widthSource, heightSource, ARGB);
  rgb(source);

  for (int i = 0; i < samples; ++i) {
    PImage scaled = resed[i] = source.copy();
    float scalar = lerp(minScalar, maxScalar, i / (samples - 1.0f));
    int widthNew = (int)(0.5f + widthSource * scalar);
    int heightNew = (int)(0.5f + heightSource * scalar);
    resizeBicubic(scaled, widthNew, heightNew);
    scalars[i] = scalar;
  }
}

void draw() {
  background(0xff202020);
  image(resed[displayIndex], width * 0.5f, height * 0.5f);
  text(scalars[displayIndex],
    width * 0.5f,
    height * 0.5f + resed[displayIndex].height * 0.5f);
  displayIndex = (displayIndex + 1) % samples;
}

PImage rgb ( PImage target ) {
  target.loadPixels();

  int[] px = target.pixels;
  int len = px.length;
  int w = target.width;

  float hInv = 0xff / ( target.height - 1.0f );
  float wInv = 0xff / ( w - 1.0f );

  for ( int i = 0; i < len; ++i ) {
    px[i] = 0xff000080 | ( int ) ( 0.5f + wInv * ( i % w ) ) << 0x10
      | ( int ) ( 255.5f - hInv * ( i / w ) ) << 0x08;
  }

  target.format = PConstants.ARGB;
  target.updatePixels();
  return target;
}

The rgb method is (1) so you can test that the sketch works without having to find an image file; (2) to make sure that images returned by both loadImage and createImage work.

I made some test examples with a different sketch. (For what they’re worth… I’m aware the results will be device dependent, renderer dependent, etc.).

This is the source for the image.

And this is the source.

The next image is designed to challenge to scaling algorithms:

The source for this image is Eric Brasseur’s article Gamma error in picture scaling. There are more diagnostic images in the article. The image’s color has been linearized before scaling, then converted back. Well, I hope I did it correctly, anyway; as you can see, the method makes wonky images nevertheless. Gamma’s not really my focus here, except to point out that it may impact results. I’ve still alot to learn about the subject, so I’ll link to the Wikipedia article on sRGB and leave it at that.

Here’s a related discussion that popped up as a suggestion when I opened this topic (there may be more on earlier incarnations of the forum).

There are plenty of Java image libraries out there, I imagine, each containing multiple image scaling algorithms. I didn’t search them out because I didn’t want the dependency or to have to convert from a library class to PImage.

Best,
Jeremy

[EDIT] Fixed a likely bug related to pixel density, but I’m on a density 1 display, so I can’t test to make sure.

5 Likes