在功能上穿越图像的惯用方法

时间:2015-11-08 05:02:30

标签: java image multidimensional-array functional-programming java-8

我正在使用Java 8并尝试编写纯功能代码。在我的最新项目中,我需要逐步浏览图像中的每个像素,并对每个像素执行一些计算。我提出的代码是这样的:

IntStream
        .range(0, newImage.getWidth())
        .forEach(i -> IntStream
                .range(0, newImage.getHeight())
                .forEach(n -> {
                    inspectPixel(i, n, newImage);
                })
        );

然而,命令式版本是这样的:

for (int i = 0; i < newImage.getWidth(); i++){
    for (int n = 0; n < newImage.getHeight(); n++){
        inspectPixel(i, n, newImage);
    }
}

也许这只是因为我太习惯于命令式编程,但后者似乎比前者更具可读性。其中一件事情正在发生:

  1. 我的代码错了,我是不是错了?如果是这样,应该代码是什么样的?你如何在功能上遍历任何二维数据结构,而不仅仅是图像?
  2. 这实际上是Java 8功能方案中程序的最佳版本,这种情况对于函数式编程来说很糟糕。

2 个答案:

答案 0 :(得分:1)

那是因为IntStream并非专门针对迭代像素。冗长是对流的力量的牺牲。虽然功能齐全,但它的目的并不符合您的需求。

你总是可以创建自己的界面来处理凌乱的工作:

class PixelScanner {
    public static void scan(BufferedImage image, PixelInspector inspector) {
        int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
        for(int y = 0; y < image.getHeight(); y++) {
            for(int x = 0; x < image.getWidth(); x++) {
                int pixel = pixels[x + y * image.getWidth()];
                inspector.inspect(pixel);
            }
        }
    }
}

interface PixelInspector {
    void inspect(int pixel);
}

然后您可以将其用作:

PixelScanner.scan(image, pixel -> {
    // inspect pixel
});

甚至可以添加更多参数进行检查,例如像素所在的(x,y)位置。您甚至可以包装每个像素以将更多关于它的数据传递给检查器。我还建议使scan非静态并使用PixelScanner对象。功能编程很有用,但OOP肯定有它的聚光灯时刻,两者都应该有效使用。

答案 1 :(得分:0)

你的代码是错误的,错误的我的意思是根本错误,而不是风格。但是,这是编码风格不好的结果。你应该给变量有意义的名字。 in等名称不适合实际持有xy坐标的变量。如果你给他们命名为xy,你会立即注意到这个问题,我想:

IntStream
        .range(0, newImage.getWidth())
        .forEach(x -> IntStream
                .range(x, newImage.getHeight())
                .forEach(y -> {
                    inspectPixel(x, y, newImage);
                })
        );

显然,range(x, newImage.getHeight())不能正确......

也就是说,嵌套forEach调用确实通常是对命令式代码进行转换的标志,如果找不到更好的解决方案,那么这些代码很可能仍然是必要的。

由于您希望单独处理像素,您可以使用flatMap生成坐标流,但是您需要一种类型来将这些作为结果流的元素保存,例如

IntStream.range(0, newImage.getWidth()).boxed()
    .flatMap(x -> IntStream.range(0, newImage.getHeight()).mapToObj(y -> new Point(x, y)))
    .forEach(point -> inspectPixel(point.x, point.y, newImage));

这里,我们在点实例中保持坐标。不幸的是,我们必须在此处设置x坐标,因为IntStream不提供flatMapToObj操作。

如果对象创建困扰您,您可以使用压缩long替换点实例,这也允许将x值作为原始数据类型进行处理,但当然,它不会不会增加可读性:

LongStream.range(0, newImage.getWidth())
    .flatMap(x -> IntStream.range(0, newImage.getHeight()).mapToLong(y -> (long)x<<32|y))
    .forEach(point -> inspectPixel((int)(point>>>32), (int)point, newImage));

当然,如果您只对像素数据感兴趣并且xy值只是为了帮助您访问它们,那么您可以首先在像素上流式传输像素:

Arrays.stream(newImage.getRGB(0, 0, newImage.getWidth(), newImage.getHeight(),
                              null, 0, newImage.getWidth()))
      .forEach(argb -> inspectPixel(argb)));