【问题标题】:Writing a functional and yet functional image processing library in Scala用 Scala 编写一个功能强大的图像处理库
【发布时间】:2011-03-18 03:02:15
【问题描述】:

我们正在为 Scala(学生项目)开发一个小型图像处理库。该库功能齐全(即没有可变性)。图像栅格存储为Stream[Stream[Int]],以便以最少的努力利用惰性评估的好处。但是,在对图像执行一些操作后,堆会变满并抛出OutOfMemoryError。 (例如,在 JVM 堆空间不足之前,可以对大小为 500 x 400、35 kb 的 jpeg 图像执行多达 4 次操作。)

我们想到的方法有:

  • 使用 JVM 选项并增加堆大小。 (我们不知道如何在 IDEA 下执行此操作 - 我们正在使用的 IDE。)
  • 选择与Stream[Stream[Int]] 不同的数据结构,后者更适合图像处理任务。 (同样,我们对简单的ListStream 之外的函数数据结构了解不多。)

我们的最后一个选择是放弃不变性并使其成为一个可变库(就像流行的图像处理库一样),我们并不想这样做。如果您明白我的意思,请向我们建议一些方法来保持这个库的功能和仍然有效。

谢谢你,
悉达多·雷娜。

附录:
对于大小为 1024 x 768 的图像,即使是单个映射操作,JVM 也会耗尽堆空间。我们测试中的一些示例代码:

val image = Image from "E:/metallica.jpg"
val redded = image.map(_ & 0xff0000)
redded.display(title = "Redded")

还有输出:

"C:\Program Files (x86)\Java\jdk1.6.0_02\bin\java" -Didea.launcher.port=7533 "-Didea.launcher.bin.path=C:\Program Files (x86)\JetBrains\IntelliJ IDEA Community Edition 10.0.2\bin" -Dfile.encoding=windows-1252 -classpath "C:\Program Files (x86)\Java\jdk1.6.0_02\jre\lib\charsets.jar;C:\Program Files (x86)\Java\jdk1.6.0_02\jre\lib\deploy.jar;C:\Program Files (x86)\Java\jdk1.6.0_02\jre\lib\javaws.jar;C:\Program Files (x86)\Java\jdk1.6.0_02\jre\lib\jce.jar;C:\Program Files (x86)\Java\jdk1.6.0_02\jre\lib\jsse.jar;C:\Program Files (x86)\Java\jdk1.6.0_02\jre\lib\management-agent.jar;C:\Program Files (x86)\Java\jdk1.6.0_02\jre\lib\plugin.jar;C:\Program Files (x86)\Java\jdk1.6.0_02\jre\lib\resources.jar;C:\Program Files (x86)\Java\jdk1.6.0_02\jre\lib\rt.jar;C:\Program Files (x86)\Java\jdk1.6.0_02\jre\lib\ext\dnsns.jar;C:\Program Files (x86)\Java\jdk1.6.0_02\jre\lib\ext\localedata.jar;C:\Program Files (x86)\Java\jdk1.6.0_02\jre\lib\ext\sunjce_provider.jar;C:\Program Files (x86)\Java\jdk1.6.0_02\jre\lib\ext\sunmscapi.jar;C:\Program Files (x86)\Java\jdk1.6.0_02\jre\lib\ext\sunpkcs11.jar;C:\new Ph\Phoebe\out\production\Phoebe;E:\Inventory\Marvin.jar;C:\scala-2.8.1.final\lib\scala-library.jar;C:\scala-2.8.1.final\lib\scala-swing.jar;C:\scala-2.8.1.final\lib\scala-dbc.jar;C:\new Ph;C:\scala-2.8.1.final\lib\scala-compiler.jar;E:\Inventory\commons-math-2.2.jar;E:\Inventory\commons-math-2.2-sources.jar;E:\Inventory\commons-math-2.2-javadoc.jar;E:\Inventory\jmathplot.jar;E:\Inventory\jmathio.jar;E:\Inventory\jmatharray.jar;E:\Inventory\Javax Media.zip;E:\Inventory\jai-core-1.1.3-alpha.jar;C:\Program Files (x86)\JetBrains\IntelliJ IDEA Community Edition 10.0.2\lib\idea_rt.jar" com.intellij.rt.execution.application.AppMain phoebe.test.ImageTest
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at scala.collection.Iterator$class.toStream(Iterator.scala:1011)
    at scala.collection.IndexedSeqLike$Elements.toStream(IndexedSeqLike.scala:52)
    at scala.collection.Iterator$$anonfun$toStream$1.apply(Iterator.scala:1011)
    at scala.collection.Iterator$$anonfun$toStream$1.apply(Iterator.scala:1011)
    at scala.collection.immutable.Stream$Cons.tail(Stream.scala:565)
    at scala.collection.immutable.Stream$Cons.tail(Stream.scala:557)
    at scala.collection.immutable.Stream$$anonfun$map$1.apply(Stream.scala:168)
    at scala.collection.immutable.Stream$$anonfun$map$1.apply(Stream.scala:168)
    at scala.collection.immutable.Stream$Cons.tail(Stream.scala:565)
    at scala.collection.immutable.Stream$Cons.tail(Stream.scala:557)
    at scala.collection.immutable.Stream$$anonfun$flatten1$1$1.apply(Stream.scala:453)
    at scala.collection.immutable.Stream$$anonfun$flatten1$1$1.apply(Stream.scala:453)
    at scala.collection.immutable.Stream$Cons.tail(Stream.scala:565)
    at scala.collection.immutable.Stream$Cons.tail(Stream.scala:557)
    at scala.collection.immutable.Stream.length(Stream.scala:113)
    at scala.collection.SeqLike$class.size(SeqLike.scala:221)
    at scala.collection.immutable.Stream.size(Stream.scala:48)
    at scala.collection.TraversableOnce$class.toArray(TraversableOnce.scala:388)
    at scala.collection.immutable.Stream.toArray(Stream.scala:48)
    at phoebe.picasso.Image.force(Image.scala:85)
    at phoebe.picasso.SimpleImageViewer.<init>(SimpleImageViewer.scala:10)
    at phoebe.picasso.Image.display(Image.scala:91)
    at phoebe.test.ImageTest$.main(ImageTest.scala:14)
    at phoebe.test.ImageTest.main(ImageTest.scala)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:115)

Process finished with exit code 1

【问题讨论】:

  • 为什么这个标签是 [haskell]?
  • @FUZxxl:可能假设 Haskell 程序员熟悉不可变惰性列表的性能问题。我怀疑这是一个普遍的纯粹性和惰性只是让 Haskell 编译器发挥许多其他人无法做到的优化魔法的情况。
  • @FUZxxl:@camccann 说了什么。
  • 我最近加入了一个比较严肃的图像处理项目,发现原始操作(即基本过滤、形态、边缘......)必须在绝对机器级别完成:内联汇编是你最好的朋友。尽管功能抽象非常好,但它们肯定必须建立在超快的低级图像算法之上。
  • 使用 Haskell 并行数组 (Repa) 的图像处理变得越来越普遍(至少有两个项目)。它们利用了 GHC 对纯度的保证使之成为可能的可熔阵列操作。

标签: scala programming-languages image-processing functional-programming


【解决方案1】:

如果我理解正确,您可以将每个单独的像素存储在一个 Stream 元素中,这可能效率低下。您可以做的是创建您的自定义LazyRaster 类,其中包含对某种大小(例如,20x20)图像块的惰性引用。第一次写入某个块时,会初始化其对应的数组,然后从那里更改像素意味着写入该数组。

这是更多的工作,但可能会带来更好的性能。此外,如果您希望支持图像操作的堆叠(例如,做一个地图 - 拍摄 - 地图),然后“一次性”评估图像,实现可能会变得棘手 - 流实现是最好的证据。

可以做的另一件事是确保旧的Streams 是properly garbage collected。我怀疑您示例中的 image 对象是您的流的包装器。如果您希望将多个图像操作(如映射)堆叠在一起并能够 gc 不再需要的引用,则必须确保您不持有对流的任何引用 - 请注意,如果出现以下情况,则无法保证:

  1. 您在堆栈中引用了您的图像(示例中为image
  2. 您的 Image 包装器包含此类引用。

在不了解具体用例的情况下,很难说更多。

就个人而言,我会完全避免Streams,而只是使用一些基于数组的不可变数据结构,既节省空间又避免装箱。我唯一可能看到使用Streams 的地方是迭代图像转换,例如卷积或应用一堆过滤器。你不会有Stream 的像素,而是Stream 的图像。这可能是表达一系列转换的好方法 - 在这种情况下,适用于上面给出的链接中关于 gc 的 cmets。

【讨论】:

  • 大部分块处理逻辑已经可以通过Vector 获得,它在内部被实现为Trie。尽管是可变的,但这为它提供了一个很好的性能配置文件,可以不可变地使用。尽管如此,如果您追求速度,拳击的成本仍然是一个重要的痛点。
  • 没错,这里可以使用Vectors。装箱是一个问题——这就是为什么自定义类应该为Ints 硬编码或专门用于避免装箱的原因。因为他们只需要Ints,我会选择前者。
【解决方案2】:

如果您处理大型流,则需要避免持有对流头部的引用。这将阻止垃圾收集。

Stream 上调用某些方法可能会在内部保持头部。请参阅此处的讨论:Functional processing of Scala streams without OutOfMemory errors

【讨论】:

    【解决方案3】:

    Stream 不太可能是这里的最佳结构。鉴于 JPEG 的性质,将其逐行“流式传输”到内存中毫无意义。

    Stream 还具有读取元素的线性访问时间。同样,除非您正在流式传输数据,否则可能不是您想要的。

    我建议在这种情况下使用IndexedSeq[IndexedSeq[Int]]。或者(如果性能很重要)Array[Array[Int]],这将使您避免一些装箱/拆箱成本。

    Martin 写了一个good overview of the 2.8 collections API,它应该可以帮助您了解各种可用集合类型的内在权衡。

    即使使用数组,仍然有充分的理由将它们用作不可变结构并保持函数式编程风格。仅仅因为一个结构是可变的并不意味着你必须改变它!

    【讨论】:

    • 我认为在这里讨论具体的数据结构是有道理的。我对 Vector 进行了思考,但它没有懒惰的好处。我认为手动滚动他们自己的惰性数据结构将是可行的方法。
    • 取决于目标。如果性能是关键,那么 Java2D API 上的某种形式的不可变包装器是显而易见的解决方案,这在很大程度上要归功于硬件加速。作为一项学术练习,我认为您对手卷结构的看法是正确的。
    • 我的意思是性能方面。根据将要做什么,懒惰可能会让你免于计算永远不会使用的东西。
    • @Daniel 如果在图像上构建一组转换,当然可以!尽管硬件通常可以通过图层和 Alpha 通道来做这种事情。懒惰地解码 JPEG 文件的想法让我更加烦恼,这种格式似乎不适合这种技巧。
    • 嗯,确实如此。我没有考虑他们的具体用例。
    【解决方案4】:

    我建议您同时查看连续,而不仅仅是图像的离散模型。连续通常比离散更模块化/可组合——无论是时间还是空间。

    【讨论】:

    • 对于连续图像,请查看论文 Functional Images,其中图像是连续且无限的。这两个方面都使语义非常简单,因此是可组合的。令人愉快的属性比比皆是,例如缩小和放大或左右旋转是完全相反的。离散和/或有限使语义复杂,这些定律失效。编译生成非常快的代码。请参阅 Compiling Embedded Languages。与功能反应动画/编程中的时间类似。
    【解决方案5】:

    作为第一步,您应该进行内存转储并对其进行分析。很有可能您会立即看到问题。

    有一个特殊的命令行选项可以强制 JVM 在 OOME 上进行转储:-XX:+HeapDumpOnOutOfMemoryError。还有好的工具,比如jhatVisualVM,可以帮助你进行分析。

    【讨论】:

      【解决方案6】:

      Stream 更多的是关于惰性求值而不是不变性。而你 通过 这样做。此外,Streams 仅在您可以推迟 确定(计算或检索)单个像素值。 当然,随机访问是不可能的。我不得不认为 流式传输完全不适合图像处理的数据结构。

      我强烈建议您管理自己的光栅内存(奖励 不将单个光栅图像组织固定到您的点 码)并为整个通道或平面或频段分配存储空间 其中(取决于正在使用的栅格组织)。

      更新:前面的意思是不使用嵌套数组或IndexedSeq,而是分配一个块并使用行和列值计算哪个元素。

      然后采取“初始化后不可变”的方法。一旦给定 像素或样本已经在光栅中建立,你永远不允许它 被改变。这可能需要一位光栅平面来跟踪 建立的像素。或者,如果你知道你将如何填充 您可以获得的栅格(分配像素的顺序) 用一种更简单、更便宜的方式来表示 栅格已建立,还有多少要填充。

      然后,当您对光栅图像执行处理时,请在管道中执行此操作 没有图像被原地改变,而是总是有一个新的图像 在应用各种变换时生成。

      您可能会认为对于某些图像转换(卷积、 例如)您必须采用这种方法,否则您将无法获得正确的 结果。

      【讨论】:

        【解决方案7】:

        如果您对函数式数据结构没有任何经验(如您所言),我强烈推荐 Okasaki 的 Purely Functional Data Structures

        【讨论】:

          【解决方案8】:

          要使用 intellij 增加堆大小,您需要将以下内容添加到运行/调试配置的 VM 参数部分:

          -Xms256m -Xmx256m
          

          这会将最大堆大小增加到 256MB,并确保 VM 在启动时请求此数量,这通常表示性能提升。

          此外,您使用的是相对较旧的 JDK。如果可能,我建议您更新到最新的可用版本,因为较新的版本支持逃逸分析,这在某些情况下会对性能产生巨大影响。

          现在,就算法而言,我建议您按照上面的建议,将图像分成 9x9 的块(尽管任何尺寸都可以)。然后我会去看看Huet's Zipper 并考虑如何将其应用于表示为树结构的图像,以及如何将图像建模为持久数据结构。

          【讨论】:

            【解决方案9】:

            在idea中增加堆大小可以在vmoptions文件中完成,该文件可以在你的idea安装目录的bin目录中找到(例如添加-Xmx512m将堆大小设置为512兆字节)。 除此之外,如果不知道您究竟执行了哪些操作,很难说出导致内存不足的原因,但也许this question 提供了一些有用的提示。

            【讨论】:

            • 这会改变 intellij 本身的 VMOptions,而不是他的程序正在运行的 vm。
            【解决方案10】:

            一种解决方案是将图像放入一个数组中,并让像“map”这样的过滤器返回该数组的包装器。基本上,您有一个名为 Image 的特征。该特征需要抽象的像素检索操作。例如,当调用“map”函数时,您返回一个实现,它将调用委托给旧 Image,并在其上执行函数。唯一的问题是转换可能最终会被执行多次,但由于它是一个函数库,所以这不是很重要。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2011-04-05
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2021-11-01
              相关资源
              最近更新 更多