【问题标题】:Idiomatic Way of Traversing Image, Functionally功能上遍历图像的惯用方式
【发布时间】:2023-03-18 20:56:01
【问题描述】:

我正在使用 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 函数式程序的最佳版本,而这种情况对于函数式编程来说简直是糟糕透顶。

【问题讨论】:

  • My code is wrong 你的代码怎么错了?它不工作吗?如果它不起作用,那是什么问题?
  • 错误,因为效率不高或不习惯@vincemigh
  • 我不明白你在找什么。使用更简单的界面?正如您在问Java是否具有扫描图像像素的功能支持?请更清楚一点,您的问题目前看起来像是“如何改进我的代码”的帖子。如果你不确定什么是对的,你怎么可能知道你错了?据您所知,您可能会以最好的方式做到这一点。如果您询问 JDK 是否带有用于轻松扫描图像中像素的 api,您应该将其放在您的答案中。您始终可以创建自己的界面来降低第二个版本的冗长程度。
  • 最好的方法是遍历像素而不是坐标。如果 API 不支持这个,那么实现你自己的迭代器。任何其他 n 维数据结构也是如此。

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


【解决方案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 肯定有它的亮点,两者都应该有效地使用。

【讨论】:

  • for(int y = 0; y &lt; image.getHeight(); y++) ...不破坏函数式风格吗?我正在尝试在功能上遍历图像;怎么可能呢?不过,我喜欢程序的第二部分。就像在PixelScanner.scan(image, pixel -&gt; {}) 中一样,它有一个很好的流程
  • @Michael Functions 必须有实现。它不可能是一直到核心的功能。甚至forEach 也使用了这样的技术(例如,ArrayList#forEach 实现,它使用了一个简单的索引循环。LinkedList 使用了一个增强的循环,这是使用带有循环的Iterator 的语法糖)。这只是掩盖了冗长,就像任何接口一样。您必须以某种方式遍历像素。 JDK 似乎没有为此提供自己的 API,但如果有,它很可能会在其核心上做一些非常相似的事情
【解决方案2】:

您的代码是错误的,我所说的错误是指根本上的错误,而不是风格上的错误。虽然,这是糟糕的编码风格的结果。你应该给变量起有意义的名字。 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)));

【讨论】:

    猜你喜欢
    • 2016-05-04
    • 1970-01-01
    • 2010-11-14
    • 1970-01-01
    • 1970-01-01
    • 2014-04-09
    • 1970-01-01
    • 1970-01-01
    • 2016-07-07
    相关资源
    最近更新 更多