第3章 ラムダ式を使ったプログラミング : 問題 13 : 畳み込みフィルターの遅延処理
問題
ぼやけ検出あるいはエッジ検出といった畳み込みフィルターは、隣接するピクセルから一つのピクセルを計算する
畳み込みフィルターを扱えるように LatentImage クラスを機能強化せよ
解答
まずColorTransformer
を拡張した畳み込みフィルターの関数型インターフェースImageFilter
を導入する
■ ImageFilter
@FunctionalInterface public interface ImageFilter { /** * 変換 * @param x カレント位置のX座標 * @param y カレント位置のY座標 * @param matrix 3x3の色マトリックス<br>カレント位置は[1][1] * @return 変換後の色 */ Color apply(int x, int y, Color[][] matrix); /** UnaryOperator<Color> から生成 */ static ImageFilter onlyCurrentColor(UnaryOperator<Color> f) { return (x, y, matrix) -> f.apply(matrix[1][1]); } /** ColorTransformer から生成 */ static ImageFilter onlyCurrent(ColorTransformer f) { return (x, y, matrix) -> f.apply(x, y, matrix[1][1]); } /** IllegalArgumentException を起こさない Color の生成 */ static Color color(double red, double green, double blue) { return Color.color( Math.max(Math.min(red , 1.0), 0.0), Math.max(Math.min(green, 1.0), 0.0), Math.max(Math.min(blue , 1.0), 0.0) ); } }
次に遅延画像クラスLatentImage
を拡張する
方針は、以下のとおり
- 座標ごとではなく畳み込みフィルター単位で変換
- メモリ上にオフスクリーン(単なる
Color
の二次元配列)を確保 - 畳み込みフィルターの変換結果をオフスクリーンに記録
■ LatentImage
public class LatentImage { /** 変換対象の画像 */ private final Image in; /** 適用する画像変換 */ private List<ImageFilter> pendingOperations; /** コンストラクタ */ private LatentImage(Image in) { this.in = in; pendingOperations = new ArrayList<ImageFilter>(); } /** オブジェクトの生成 */ public static LatentImage from(Image in) { return new LatentImage(in); } /** 座標を無視した色変換の適用 */ public LatentImage transform(UnaryOperator<Color> f) { pendingOperations.add(ImageFilter.onlyCurrentColor(f)); return this; } /** カレントの色だけを参照する色変換の適用 */ public LatentImage transform(ColorTransformer f) { pendingOperations.add(ImageFilter.onlyCurrent(f)); return this; } /** 色変換の適用 */ public LatentImage transform(ImageFilter f) { pendingOperations.add(f); return this; } /** 色変換適用済みの画像の取得 */ public Image toImage() { // 画像変換がないときは元画像を返す if (pendingOperations.isEmpty()) { return in; } // オフスクリーン(画像の境界の外側に1ピクセルある)を用意 int width = (int)in.getWidth(); int height = (int)in.getHeight(); Color[][] screen1 = new Color[width + 2][height + 2]; Color[][] screen2 = new Color[width + 2][height + 2]; fill(screen1, Color.BLACK); fill(screen2, Color.BLACK); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { screen1[x + 1][y + 1] = in.getPixelReader().getColor(x, y); } } // 画像変換 Color[][] before = screen1; Color[][] after = screen2; for (ImageFilter f : pendingOperations) { for (int x = 1; x <= width; x++) { for (int y = 1; y <= height; y++) { Color[][] matrix = new Color[3][3]; matrix[0][0] = before[x-1][y-1]; matrix[0][1] = before[x-1][y ]; matrix[0][2] = before[x-1][y+1]; matrix[1][0] = before[x ][y-1]; matrix[1][1] = before[x ][y ]; matrix[1][2] = before[x ][y+1]; matrix[2][0] = before[x+1][y-1]; matrix[2][1] = before[x+1][y ]; matrix[2][2] = before[x+1][y+1]; after[x][y] = f.apply(x - 1, y - 1, matrix); } } Color[][] temp = before; before = after; after = temp; } // オフスクリーンから画像を生成 WritableImage out = new WritableImage(width, height); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { out.getPixelWriter().setColor(x, y, before[x+1][y+1]); } } return out; } /** オフスクリーンを指定の色で埋める */ private static void fill(Color[][] screen, Color color) { for (int x = 0; x < screen.length; x++) { for (int y = 0; y < screen[x].length; y++) { screen[x][y] = color; } } } }
それでは、ぼかし(ピクセルとその隣接する8個のピクセルの平均で置き換え)と枠線の変換を適用してみよう
■ ぼかしと枠線
public void start(Stage stage) throws Exception { Image image = new Image("queen-mary.png"); int width = (int)image.getWidth(); int height = (int)image.getHeight(); Image image2; ColorTransformer frame = (x, y, c) -> // 枠線を付ける x < 15 || y < 15 || x >= width - 15 || y >= height - 15 ? Color.AQUAMARINE : c; ImageFilter blur = (x, y, m) -> { // ぼかしフィルター double red = 0.0; double green = 0.0; double blue = 0.0; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { red += m[i][j].getRed(); green += m[i][j].getGreen(); blue += m[i][j].getBlue(); } } return ImageFilter.color(red/9.0, green/9.0, blue/9.0); }; image2 = LatentImage.from(image) .transform(blur) .transform(frame) .toImage(); stage.setScene(new Scene(new HBox(new ImageView(image), new ImageView(image2)))); stage.show(); }
■ 実行結果