ラベル アルゴリズム の投稿を表示しています。 すべての投稿を表示
ラベル アルゴリズム の投稿を表示しています。 すべての投稿を表示

2012年10月19日金曜日

Java による画像縮小 lanczos 編

JDK を使った縮小の結果納得行かなかったので、GIMP なんかにもある lanczos による縮小を実装してみることに。
元画像は Jpeg なので、RGB の 3 色のみを扱う。

こちらの Ruby バージョンをとりあえず Java にポーティング。
今回縮小のみなので、拡大部分を削除。実行してみると 6 秒前後かかった・・・

もう少し早くならないかなと、さらにコメントのリンク先を読んでみる。

何をやっているかというと、中心を 1 として距離によって重みを付けるということのようだ。
| 0.3 | 0.5 | 0.3 |
|-----+-----+-----|
| 0.5 | 1.0 | 0.5 |
|-----+-----+-----|
| 0.3 | 0.5 | 0.3 |

といった具合に。
重みを計算するのが lanczos 関数。
lanczos2 や lanczos3 の数字は距離で、縮小画像のあるピクセルを計算するために、対応する元画像ピクセルからどれくらいの範囲を参照するかを決めるものっぽい。

となると、縮小率と距離の数字が決まれば、対象のピクセルがどこであろうと、重みは一緒?
というわけで、先に上記のような重みの表を作成しておき、各ピクセル計算時はそれを使用すれば計算量は減るだろう。

中心からのオフセットと重みを保持するクラス

public class Weight {
  public final int offsetX;
  public final int offsetY;
  public final double weight;

  public Weight(int x, int y, double w) {
    offsetX = x;
    offsetY = y;
    weight = w;
  }
}

重みの一覧と合計を保持するクラス

public class WeightFilter {
  public final double weightTotal;
  public final Weight[] weightList;


  public WeightFilter(double sumw, Weight[] weights) {
    weightTotal = sumw;
    weightList = weights;
  }
}

lanczos 関数

public static double lanczos(double x, double n) {
  if (x == 0.0) return 1.0;
  if (Math.abs(x) >= n) return 0.0;
  return Math.sin(Math.PI * x) / Math.PI / x * (Math.sin(Math.PI * x / n) / Math.PI / x * n);
}

縮小率と距離で重み一覧作成

縦・横別でなく斜めも距離を計算してみる
public static WeightFilter getReduceFilter(double scale, int distance) {
  double bdx0 = 0 + 0.5, bdy0 = 0 + 0.5;
  int ss = (int)((bdx0 - distance) / scale), se = (int)((bdx0 + distance) / scale);
  double sumw = 0.0;
  List<Weight> weightList = new ArrayList<>((se - ss) * (se - ss));

  for (int sy = ss; sy <= se; ++sy) {
    for (int sx = ss; sx <= se; ++sx) {
      double xl = (sx + 0.5) * scale - bdx0;
      double yl = (sy + 0.5) * scale - bdy0;
      double w = lanczos(Math.sqrt(xl * xl + yl * yl), distance);
      if (w == 0.0) continue;
      weightList.add(new Weight(sx, sy, w));
      sumw += w;
    }
  }
  return new WeightFilter(sumw, weightList.toArray(new Weight[0]));
}


ユーティリティ関数も作成。

指定座標の RGB 値取得

範囲外は鏡像として取得。
public static int getRGB(int[] image, int x, int y, int w, int h) {
  x = Math.abs(x); y = Math.abs(y);
  if (x >= w) x = w + w - x - 1;
  if (y >= h) y = h + h - y - 1;
  return image[y * w + x];
}

0 - 255 の範囲で色値取得

public static int getColor(double val, double sumw)
{
  if (sumw != 0.0) val /= sumw;
  if (val < 0.0) return 0;
  if (val > 255.0) return 255;
  return (int)val;
}

縮小処理

public static BufferedImage reduce3(BufferedImage image, int n, int dw, int dh)
  int sw = image.getWidth(), sh = image.getHeight();

  double scale = (double)dw / sw;
  BufferedImage thumb = new BufferedImage(dw, dh, image.getType());
  WeightFilter filter = getFilter(scale, n);
  double sumw = filter.weightTotal;
  int[] colors = image.getRGB(0, 0, sw, sh, null, 0, sw);

  for (int dy = 0; dy < dh; ++dy) {
    for (int dx = 0; dx < dw; ++dx) {
      double dr = 0.0, db = 0.0, dg = 0.0;
      int bsx = (int)(dx / scale), bsy = (int)(dy / scale);
      for (Weight weight : filter.weightList) {
        int rgb = getRGB(colors, bsx + weight.offsetX, bsy + weight.offsetY, sw, sh);
        dr += ((rgb >> 16) & 0xFF) * weight.weight;
        dg += ((rgb >> 8) & 0xFF) * weight.weight;
        db += (rgb & 0xFF) * weight.weight;
      }
      thumb.setRGB(dx, dy, (getColor(dr, sumw) << 16) | (getColor(dg, sumw) << 8) | getColor(db, sumw));
    }
  }
  return thumb;
}

呼び出し

lanczos2 と lanczos3 で縮小。
long start3_1 = System.currentTimeMillis();
BufferedImage reduce3_1 = reduce3(base, 2, 120, 90);
System.out.println("reduce3_1: " + (System.currentTimeMillis() - start3_1));
outputImage("reduce3_1.jpg", reduce3_1);

long start3_2 = System.currentTimeMillis();
BufferedImage reduce3_2 = reduce3(base, 3, 120, 90);
System.out.println("reduce3_2: " + (System.currentTimeMillis() - start3_2));
outputImage("reduce3_2.jpg", reduce3_2);

結果

lanczos2: 126.3ms
lanczos3: 233.0ms

元画像と左から、パターン1 3段階、パターン2 3段階、lanczos2、lanczos3




大分早くなった。JDK に比べると遅いが画質はいい。
一度作成してた後は、キャッシュするようにすればいいので、満足。