不可接受的性能阅读透明.png像素由像素

我正在创建一个工具来检测精灵表中的精灵并将每个找到的精灵转换成一个新的BufferedImage。 这个过程是有效的,但是对于某些图像格式(主要是透明的图像)来说速度过于缓慢,比如这个:

肯尼的游戏资产 - 动物包 - 回合

( 肯尼的游戏资产 – 动物包 )

我已经对我的代码进行了剖析,并确定绝大多数应用程序的时间超过99%都是由于getRGB()调用而花费在这个方法中的。

 private fun findContiguousSprite(image: BufferedImage, startingPoint: Point, backgroundColor: Color): List<Point> { val unvisited = LinkedList<Point>() val visited = arrayListOf(startingPoint) unvisited.addAll(neighbors(startingPoint, image).filter { Color(image.getRGB(it.x, it.y)) != backgroundColor }) while (unvisited.isNotEmpty()) { val currentPoint = unvisited.pop() val currentColor = Color(image.getRGB(currentPoint.x, currentPoint.y)) if (currentColor != backgroundColor) { unvisited.addAll(neighbors(currentPoint, image).filter { !visited.contains(it) && !unvisited.contains(it) && (Color(image.getRGB(it.x, it.y)) != backgroundColor) }) visited.add(currentPoint) } } return visited.distinct() } 

我曾试图优化提取rgb颜色的过程,如问题Java中所示 -通过访问图像的栅格数据缓冲区从图像中获取像素数组 ,但是这在具有java.lang.ClassCastException: java.awt.image.DataBufferInt cannot be cast to java.awt.image.DataBufferByte的最新版本的Java中失败java.lang.ClassCastException: java.awt.image.DataBufferInt cannot be cast to java.awt.image.DataBufferByte

其他的绊脚石包括Color(image.getRGB(it.x, it.y)) != backgroundColor这样的颜色的欺骗性的Color(image.getRGB(it.x, it.y)) != backgroundColor 。 但是,虽然image.getRGB()在RGBA颜色空间中默认返回,但background.rgb只返回sRGB颜色空间。

问题:如何提高读取BufferedImage的性能,特别是在透明图像的情况下? 为什么它几乎任何其他.png图像,除了这些扔在它呢?

注意:代码在Kotlin中时,我接受Java或任何其他JVM语言作为答案。

代码转储:如果您想要全部代码:

 private fun findSpriteDimensions(image: BufferedImage, backgroundColor: Color): List<Rectangle> { val workingImage = image.copy() val spriteDimensions = ArrayList<Rectangle>() for (pixel in workingImage) { val (point, color) = pixel if (color != backgroundColor) { logger.debug("Found a sprite starting at (${point.x}, ${point.y})") val spritePlot = findContiguousSprite(workingImage, point, backgroundColor) val spriteRectangle = spanRectangleFrom(spritePlot) logger.debug("The identified sprite has an area of ${spriteRectangle.width}x${spriteRectangle.height}") spriteDimensions.add(spriteRectangle) workingImage.erasePoints(spritePlot, backgroundColor) } } return spriteDimensions } private fun findContiguousSprite(image: BufferedImage, startingPoint: Point, backgroundColor: Color): List<Point> { val unvisited = LinkedList<Point>() val visited = arrayListOf(startingPoint) unvisited.addAll(neighbors(startingPoint, image).filter { Color(image.getRGB(it.x, it.y)) != backgroundColor }) while (unvisited.isNotEmpty()) { val currentPoint = unvisited.pop() val currentColor = Color(image.getRGB(currentPoint.x, currentPoint.y)) if (currentColor != backgroundColor) { unvisited.addAll(neighbors(currentPoint, image).filter { !visited.contains(it) && !unvisited.contains(it) && (Color(image.getRGB(it.x, it.y)) != backgroundColor) }) visited.add(currentPoint) } } return visited.distinct() } private fun neighbors(point: Point, image: Image): List<Point> { val points = ArrayList<Point>() val imageWidth = image.getWidth(null) - 1 val imageHeight = image.getHeight(null) - 1 // Left neighbor if (point.x > 0) points.add(Point(point.x - 1, point.y)) // Right neighbor if (point.x < imageWidth) points.add(Point(point.x + 1, point.y)) // Top neighbor if (point.y > 0) points.add(Point(point.x, point.y - 1)) // Bottom neighbor if (point.y < imageHeight) points.add(Point(point.x, point.y + 1)) // Top-left neighbor if (point.x > 0 && point.y > 0) points.add(Point(point.x - 1, point.y - 1)) // Top-right neighbor if (point.x < imageWidth && point.y > 0) points.add(Point(point.x + 1, point.y - 1)) // Bottom-left neighbor if (point.x > 0 && point.y < imageHeight - 1) points.add(Point(point.x - 1, point.y + 1)) // Bottom-right neighbor if (point.x < imageWidth && point.y < imageHeight) points.add(Point(point.x + 1, point.y + 1)) return points } 

第一优化

在@Durandal的评论的驱动下,我决定把我的ArrayList改成一个HashSet。 我还找到了一种方法来使用Color, Color(rgb, preserveAlpha)的替代构造函数来保存alpha值。 现在我不再需要在比较这两个值之前将getRGB()框起来。

 private fun findContiguousSprite(image: BufferedImage, point: Point, backgroundColor: Color): List<Point> { val unvisited = LinkedList<Point>() val visited = hashSetOf(point) unvisited.addAll(neighbors(point, image).filter { image.getRGB(it.x, it.y) != backgroundColor.rgb }) while (unvisited.isNotEmpty()) { val currentPoint = unvisited.pop() val currentColor = image.getRGB(currentPoint.x, currentPoint.y) if (currentColor != backgroundColor.rgb) { unvisited.addAll(neighbors(currentPoint, image).filter { !visited.contains(it) && !unvisited.contains(it) && image.getRGB(it.x, it.y) != backgroundColor.rgb }) visited.add(currentPoint) } } return visited.toList() } 

这在319ms处理了上面的图像。 真棒!

使用包含在ArrayList或LinkedList上的复杂度为O(n)。 考虑到您在过滤器中执行了许多 contains()调用,这可能会迅速变成大量开销。 查询的复杂度visited.contains()随着处理图像的大小而增长(更多的像素访问和更多的像素访问,把复杂度变成O(n ^ 2))。

减少这种成本的最简单方法是使用具有快速包含的集合类型; 如HashSet的情况下,O(1)。 Set语义也适合你的要求比列表好一点,因为据我所知,访问/未访问的集合不应该允许重复。 由于集合不允许通过契约重复,所以一些明确的包含检查可能被消除; 在你想要对第一次添加点的事件做出响应的地方,你也可以使用add()方法给出的布尔结果(当元素不存在和添加时为true)。

虽然拳击/拆箱确实花费了一些时间,但它是一个线性成本。 减少拳击开销将需要对代码进行相当多的更改,并且“仅”使您获得恒定的加速因子。 标准集合不适用于基元(没有首先要避免的自动装箱)。 虽然有特殊目的的第三方收集(Trove想到),处理原始没有拳击。 如果绝对必要,我只能这样做,在可能的情况下更改为原始类型最有可能使您的代码更长,并且更加混淆。