CUBは子供の白熊

Java SE 8 実践プログラミングの練習問題を解く

第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();
}

■ 実行結果 f:id:ClosedUnBounded:20150615201739p:plain