Lanczos function in pure Haxe/OpenFL?

Does anyone did or has or know where to find pure Haxe/OpenFL function (no additional libraries) that returns resampled bitmapData by using Lanczos filtering with custom filter size and transparency support?
Usage like: bmp.bitmapData = lanczos(bitmapData, width, height, filter_size = 3);

First result from a quick search on Google for “lanczos haxe” looks quite promising :

We do have support for image.resize in Lime, but I would be interested in improvements to the algorithm if someone wanted to try and improve this:



You can leverage the resize method in OpenFL by doing something such as:

var image = bitmapData.image;
image.resize(newWidth, newHeight);
bitmapData = BitmapData.fromImage(image);

It’s possible that BitmapData will not work properly if you resize the image after the BitmapData is created, since BitmapData is not resizable – so we can just wrap the image in a new BitmapData instance once it changes size

Ok, so I used a class ImageResize like tour1st suggested, modified it slightly so it works with pure Haxe + OpenFL, and tested it.
Lanczos quality scaling works like it should, but colors are wrong, green turned to purple, red to dark green, and 50% transparent area of green to red instead of darker green or even darker purple.
Did I miss something or is it a bug in this class code?
I tried various images and they always have completely different colors with high appearance of purple one.

Original image:
test

Image after effect:
openlanczos

Test code:

package;

import openfl.Assets;
import openfl.display.Sprite;
import openfl.display.Bitmap;
import ImageResize;

/**
 * ...
 * @author X
 */
class Main extends Sprite 
{
	public function new() 
	{
		super();
		
		var bitmapData = Assets.getBitmapData ("img/test.png");
		var lanczos = new ImageResize(new LanczosKernel());
        var bitmap = new Bitmap ();
		//bitmap.smoothing = true;
		bitmap.bitmapData = lanczos.scale(bitmapData, 0.5);
        bitmap.x = (1000-bitmap.width) / 2;
        bitmap.y = (700-bitmap.height) / 2;
		addChild (bitmap);
	}
}

Class code:

package;

import openfl.display.BitmapData;
import openfl.utils.ByteArray;
import openfl.Memory;

/**
 * ...
 * @author X
 */
class ImageResize 
{
  static var CACHE_PRE : Int = 100;

  var cache : Array<Float>;
  var kernel : IKernel;

  public function new(k : IKernel)
  {
    cache = new Array();
    kernel = k;

    var maxCache = kernel.radius() * kernel.radius() * CACHE_PRE;
    var invPre : Float = 1.0 / CACHE_PRE;
    for(cacheKey in 0...maxCache)
    {
      var x : Float = Math.sqrt(cacheKey * invPre);
      var v : Float = kernel.calculate(x);

      cache.push(v < 0 ? 0 : v);
    }
  }

  public function scale(source : BitmapData, ratio : Float)
  {

    var w : Int = Std.int(source.width);
    var h : Int = Std.int(source.height);

    var width : Int = Std.int(w * ratio);
    var height : Int = Std.int(h * ratio);

    var sw1 = w - 1;
    var sh1 = h - 1;

    var invRatio = 1.0 / ratio;
    var invRatio = 1.0 / ratio;

    var cw = 1.0 / (w * ratio);
    var ch = 1.0 / (h * ratio);

    var csx = Math.min(1, ratio) * Math.min(1, ratio);
    var csy = Math.min(1, ratio) * Math.min(1, ratio);

    var filterSize : Int = kernel.radius();
    var imageSize : Int = Std.int(Math.max(width * height, w * h));

    var vram : ByteArray = new ByteArray();
    var sourceBytes = source.getPixels(source.rect);
    var vramLength : Int = sourceBytes.length
      + imageSize * 8
      + imageSize * 4;

    var valuesOffset : Int = sourceBytes.length;
    var outputOffset : Int = sourceBytes.length + imageSize;

//#if (cpp)
//    vram.setLength(vramLength);
//#else
    vram.length = vramLength;
//#end

    vram.writeBytes(sourceBytes, 0, sourceBytes.length);
//#if (flash)
//    sourceBytes.clear();
//#end

    Memory.select(vram);

    for(y in 0...height)
    {
      var yCenter = (y + 0.5) * invRatio;
      var y1b : Int = Math.floor(Math.max(yCenter - filterSize, 0));
      var y1e : Int = Math.round(Math.min(yCenter + filterSize, h));

      var cy = y * ch - yCenter;
      var yRow = y * width;

      for(x in 0...width)
      {
        var xCenter = (x + 0.5) * invRatio;
        var x1b : Int = Math.floor(Math.max(xCenter - filterSize, 0));
        var x1e : Int = Math.round(Math.min(xCenter + filterSize, w));

        var cx = x * cw - xCenter;

        var i : Int = 0;
        var total : Float = 0;

        for(y1 in y1b...y1e)
        {
          var distY = (y1 + cy) * (y1 + cy) * csy;
          for(x1 in x1b...x1e)
          {
            var distX = (x1 + cx) * (x1 + cx) * csx;
            var dist = Math.round((distX + distY) * CACHE_PRE);
            var value : Float = dist < cache.length ? cache[dist] : 0;
            Memory.setDouble(valuesOffset + i * 8, value);
            total += value;

            i += 1;
          }
        }
        if (total == 0.0)
          total = 1.0;

        var color = getColor(x1b, x1e, y1b, y1e, w, 1.0 / total, valuesOffset);
        Memory.setI32(outputOffset + (x + yRow) * 4, color);
      }
    }

    var r : BitmapData = new BitmapData(width, height, true, 0x0);
    r.lock();
    vram.position = outputOffset;
    r.setPixels(r.rect, vram);
    r.unlock();

    return r;
  }

  function getColor(x1b : Int, x1e : Int, y1b : Int, y1e : Int,
      w : Int, total : Float, valuesOffset : Int) : Int
  {
    var i : Int = 0;

    var a : Float = 0;
    var r : Float = 0;
    var g : Float = 0;
    var b : Float = 0;
    for(y1 in y1b...y1e)
    {
      var y2 = y1 * w;
      for(x1 in x1b...x1e)
      {
        var color : Int = Memory.getI32((y2 + x1) * 4);
        var value = Memory.getDouble(valuesOffset + i * 8) * total;
        a += (color >> 24 & 0xff) * value;
        r += (color >> 16 & 0xff) * value;
        g += (color >> 8 & 0xff) * value;
        b += (color & 0xff) * value;
        ++i;
      }
    }

    var _a : Int = Std.int(a);
    var _r : Int = Std.int(r);
    var _g : Int = Std.int(g);
    var _b : Int = Std.int(b);

    var color : Int = _a << 24
      | _r << 16
      | _g << 8
      | _b;

    return color;
  }
}

interface IKernel
{
  function radius() : Int;
  function calculate(x : Float) : Float;
}

class LanczosKernel implements IKernel
{
  public static var FILTER_SIZE : Int = 3;

  public function new()
  {
  }

  public function radius() : Int
  {
    return FILTER_SIZE;
  }

  public function calculate(x : Float) : Float
  {
    var v : Float = 0.0;
    if (x < -FILTER_SIZE)
      v = 0.0;
    else if (x < 0.0)
      v = sinc(-x) * sinc(-x / FILTER_SIZE);
    else if (x < FILTER_SIZE)
      v = sinc(x) * sinc(x / FILTER_SIZE);
    else
      v = 0.0;

    return v;
  }

  function sinc(x : Float) : Float
  {
    if (x == 0.0)
      return 1.0;

    var arg = Math.PI * x;
    return Math.sin(arg) / arg;
  }
}

class BlackmanKernel implements IKernel
{
  static var FILTER_SIZE : Int = 3;
  public function new()
  {
  }

  public function radius() : Int
  {
    return FILTER_SIZE;
  }

  public function calculate(x : Float) : Float
  {
    if (x < 0)
      return blackman(-x);
    else
      return blackman(x);
  }

  function blackman(x : Float) : Float
  {
    if (x == 0.0)
      return 1.0;
    if (x > FILTER_SIZE)
      return 0.0;

    x *= Math.PI;
    var xr = x / FILTER_SIZE;
    return (Math.sin(x) / x) * (0.42 + 0.5*Math.cos(xr) + 0.08*Math.cos(2*xr));
  }
}

class BicubicKernel implements IKernel
{
  static var FILTER_SIZE : Int = 2;
  public function new()
  {
  }

  public function radius() : Int
  {
    return FILTER_SIZE;
  }
  public function calculate(x : Float) : Float
  {
    return (1.0/6.0) * (pow3(x + 2) -
        4 * pow3(x + 1) +
        6 * pow3(x) -
        4 * pow3(x - 1));
  }
  function pow3(x : Float) : Float
  {
    return x <= 0.0 ? 0.0 : x * x * x;
  }
}

class BilinearKernel implements IKernel
{
  static var FILTER_SIZE : Int = 1;
  public function new()
  {
  }

  public function radius() : Int
  {
    return FILTER_SIZE;
  }
  public function calculate(x : Float) : Float
  {
    if (x < 0.0)
      return 1.0 + x;
    return 1.0 - x;
  }
}

class MitchellKernel implements IKernel
{
  static var FILTER_SIZE : Int = 2;
  var B : Float;
  var C : Float;
  var P0 : Float;
  var P2 : Float;
  var P3 : Float;
  var Q0 : Float;
  var Q1 : Float;
  var Q2 : Float;
  var Q3 : Float;

  public function new()
  {
    B = (1.0 / 3.0);
    C = (1.0 / 3.0);
    P0 = ((6.0 - 2.0 * B) / 6.0);
    P2 = ((-18.0 + 12.0 * B + 6.0 * C) / 6.0);
    P3 = ((12.0 - 9.0 * B - 6.0 * C ) / 6.0);
    Q0 = ((8.0 * B + 24.0 * C) / 6.0);
    Q1 = ((-12.0 * B - 48.0 * C) / 6.0);
    Q2 = ((6.0 * B + 30.0 * C) / 6.0);
    Q3 = ((-1.0 * B - 6.0 * C) /6.0);
  }

  public function radius() : Int
  {
    return FILTER_SIZE;
  }
  public function calculate(x : Float) : Float
  {
    if (x < -2.0) return 0.0;
    if (x < -1.0) return Q0 - x * (Q1 - x * (Q2 - x * Q3));
    if (x < 0.0)  return P0 + x * x * (P2 - x * P3);
    if (x < 1.0)  return P0 + x * x * (P2 + x * P3);
    if (x < 2.0)  return Q0 + x * (Q1 + x * (Q2 + x * Q3));

    return 0.0;
  }
}

On native we store our pixels as RGBA premultiplied data – could this be related? You can change the image.buffer.premultiplied or image.buffer.format if you need to

Yes, that was it, I switched colors order in class and now it works like a charm!
Are there any plans to implement Lanczos scaling as a part of standard OpenFL library in Next, so high quality software resampling could be used if needed?

Class code RGBA version:

package;

import openfl.display.BitmapData;
import openfl.utils.ByteArray;
import openfl.Memory;

/**
 * ...
 * @author X
 */
class ImageResize 
{
  static var CACHE_PRE : Int = 100;

  var cache : Array<Float>;
  var kernel : IKernel;

  public function new(k : IKernel)
  {
    cache = new Array();
    kernel = k;

    var maxCache = kernel.radius() * kernel.radius() * CACHE_PRE;
    var invPre : Float = 1.0 / CACHE_PRE;
    for(cacheKey in 0...maxCache)
    {
      var x : Float = Math.sqrt(cacheKey * invPre);
      var v : Float = kernel.calculate(x);

      cache.push(v < 0 ? 0 : v);
    }
  }

  public function scale(source : BitmapData, ratio : Float)
  {

    var w : Int = Std.int(source.width);
    var h : Int = Std.int(source.height);

    var width : Int = Std.int(w * ratio);
    var height : Int = Std.int(h * ratio);

    var sw1 = w - 1;
    var sh1 = h - 1;

    var invRatio = 1.0 / ratio;
    var invRatio = 1.0 / ratio;

    var cw = 1.0 / (w * ratio);
    var ch = 1.0 / (h * ratio);

    var csx = Math.min(1, ratio) * Math.min(1, ratio);
    var csy = Math.min(1, ratio) * Math.min(1, ratio);

    var filterSize : Int = kernel.radius();
    var imageSize : Int = Std.int(Math.max(width * height, w * h));

    var vram : ByteArray = new ByteArray();
    var sourceBytes = source.getPixels(source.rect);
    var vramLength : Int = sourceBytes.length
      + imageSize * 8
      + imageSize * 4;

    var valuesOffset : Int = sourceBytes.length;
    var outputOffset : Int = sourceBytes.length + imageSize;

//#if (cpp)
//    vram.setLength(vramLength);
//#else
    vram.length = vramLength;
//#end

    vram.writeBytes(sourceBytes, 0, sourceBytes.length);
//#if (flash)
//    sourceBytes.clear();
//#end

    Memory.select(vram);

    for(y in 0...height)
    {
      var yCenter = (y + 0.5) * invRatio;
      var y1b : Int = Math.floor(Math.max(yCenter - filterSize, 0));
      var y1e : Int = Math.round(Math.min(yCenter + filterSize, h));

      var cy = y * ch - yCenter;
      var yRow = y * width;

      for(x in 0...width)
      {
        var xCenter = (x + 0.5) * invRatio;
        var x1b : Int = Math.floor(Math.max(xCenter - filterSize, 0));
        var x1e : Int = Math.round(Math.min(xCenter + filterSize, w));

        var cx = x * cw - xCenter;

        var i : Int = 0;
        var total : Float = 0;

        for(y1 in y1b...y1e)
        {
          var distY = (y1 + cy) * (y1 + cy) * csy;
          for(x1 in x1b...x1e)
          {
            var distX = (x1 + cx) * (x1 + cx) * csx;
            var dist = Math.round((distX + distY) * CACHE_PRE);
            var value : Float = dist < cache.length ? cache[dist] : 0;
            Memory.setDouble(valuesOffset + i * 8, value);
            total += value;

            i += 1;
          }
        }
        if (total == 0.0)
          total = 1.0;

        var color = getColor(x1b, x1e, y1b, y1e, w, 1.0 / total, valuesOffset);
        Memory.setI32(outputOffset + (x + yRow) * 4, color);
      }
    }

    var r : BitmapData = new BitmapData(width, height, true, 0x0);
    r.lock();
    vram.position = outputOffset;
    r.setPixels(r.rect, vram);
    r.unlock();

    return r;
  }

  function getColor(x1b : Int, x1e : Int, y1b : Int, y1e : Int,
      w : Int, total : Float, valuesOffset : Int) : Int
  {
    var i : Int = 0;

    var a : Float = 0;
    var r : Float = 0;
    var g : Float = 0;
    var b : Float = 0;
    for(y1 in y1b...y1e)
    {
      var y2 = y1 * w;
      for(x1 in x1b...x1e)
      {
        var color : Int = Memory.getI32((y2 + x1) * 4);
        var value = Memory.getDouble(valuesOffset + i * 8) * total;
        //a += (color >> 24 & 0xff) * value;
        //r += (color >> 16 & 0xff) * value;
        //g += (color >> 8 & 0xff) * value;
        //b += (color & 0xff) * value;
        b += (color >> 24 & 0xff) * value;
        g += (color >> 16 & 0xff) * value;
        r += (color >> 8 & 0xff) * value;
        a += (color & 0xff) * value;
        ++i;
      }
    }

    var _a : Int = Std.int(a);
    var _r : Int = Std.int(r);
    var _g : Int = Std.int(g);
    var _b : Int = Std.int(b);

    var color : Int = _a << 24
      | _r << 16
      | _g << 8
      | _b;

    return color;
  }
}

interface IKernel
{
  function radius() : Int;
  function calculate(x : Float) : Float;
}

class LanczosKernel implements IKernel
{
  public static var FILTER_SIZE : Int = 3;

  public function new()
  {
  }

  public function radius() : Int
  {
    return FILTER_SIZE;
  }

  public function calculate(x : Float) : Float
  {
    var v : Float = 0.0;
    if (x < -FILTER_SIZE)
      v = 0.0;
    else if (x < 0.0)
      v = sinc(-x) * sinc(-x / FILTER_SIZE);
    else if (x < FILTER_SIZE)
      v = sinc(x) * sinc(x / FILTER_SIZE);
    else
      v = 0.0;

    return v;
  }

  function sinc(x : Float) : Float
  {
    if (x == 0.0)
      return 1.0;

    var arg = Math.PI * x;
    return Math.sin(arg) / arg;
  }
}

class BlackmanKernel implements IKernel
{
  static var FILTER_SIZE : Int = 3;
  public function new()
  {
  }

  public function radius() : Int
  {
    return FILTER_SIZE;
  }

  public function calculate(x : Float) : Float
  {
    if (x < 0)
      return blackman(-x);
    else
      return blackman(x);
  }

  function blackman(x : Float) : Float
  {
    if (x == 0.0)
      return 1.0;
    if (x > FILTER_SIZE)
      return 0.0;

    x *= Math.PI;
    var xr = x / FILTER_SIZE;
    return (Math.sin(x) / x) * (0.42 + 0.5*Math.cos(xr) + 0.08*Math.cos(2*xr));
  }
}

class BicubicKernel implements IKernel
{
  static var FILTER_SIZE : Int = 2;
  public function new()
  {
  }

  public function radius() : Int
  {
    return FILTER_SIZE;
  }
  public function calculate(x : Float) : Float
  {
    return (1.0/6.0) * (pow3(x + 2) -
        4 * pow3(x + 1) +
        6 * pow3(x) -
        4 * pow3(x - 1));
  }
  function pow3(x : Float) : Float
  {
    return x <= 0.0 ? 0.0 : x * x * x;
  }
}

class BilinearKernel implements IKernel
{
  static var FILTER_SIZE : Int = 1;
  public function new()
  {
  }

  public function radius() : Int
  {
    return FILTER_SIZE;
  }
  public function calculate(x : Float) : Float
  {
    if (x < 0.0)
      return 1.0 + x;
    return 1.0 - x;
  }
}

class MitchellKernel implements IKernel
{
  static var FILTER_SIZE : Int = 2;
  var B : Float;
  var C : Float;
  var P0 : Float;
  var P2 : Float;
  var P3 : Float;
  var Q0 : Float;
  var Q1 : Float;
  var Q2 : Float;
  var Q3 : Float;

  public function new()
  {
    B = (1.0 / 3.0);
    C = (1.0 / 3.0);
    P0 = ((6.0 - 2.0 * B) / 6.0);
    P2 = ((-18.0 + 12.0 * B + 6.0 * C) / 6.0);
    P3 = ((12.0 - 9.0 * B - 6.0 * C ) / 6.0);
    Q0 = ((8.0 * B + 24.0 * C) / 6.0);
    Q1 = ((-12.0 * B - 48.0 * C) / 6.0);
    Q2 = ((6.0 * B + 30.0 * C) / 6.0);
    Q3 = ((-1.0 * B - 6.0 * C) /6.0);
  }

  public function radius() : Int
  {
    return FILTER_SIZE;
  }
  public function calculate(x : Float) : Float
  {
    if (x < -2.0) return 0.0;
    if (x < -1.0) return Q0 - x * (Q1 - x * (Q2 - x * Q3));
    if (x < 0.0)  return P0 + x * x * (P2 - x * P3);
    if (x < 1.0)  return P0 + x * x * (P2 + x * P3);
    if (x < 2.0)  return Q0 + x * (Q1 + x * (Q2 + x * Q3));

    return 0.0;
  }
}
1 Like

Pull requests are welcome :wink: