不可接受的性能读取Pixel的透明.png像素

时间:2016-05-25 15:40:16

标签: java image bufferedimage kotlin

我正在创建一个工具来检测精灵表中的精灵,并将每个找到的精灵转换为新的BufferedImage。这个过程有效,但某些图像格式 - 主要是透明图像 - 例如这个:

非常慢

Kenney's Game Assets - Animal Pack - Round

Kenney's Game Assets - Animal Pack

我已经对我的代码进行了分析,并确定绝大多数人,由于许多getRGB()次调用,我的应用时间的99%以上仅用于此方法。

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 - get pixel array from image中所示,但是在最新版本的Java中java.lang.ClassCastException: java.awt.image.DataBufferInt cannot be cast to java.awt.image.DataBufferByte会失败。< / p>

其他绊脚石包括欺骗性的不必要的颜色装箱,例如行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处理了上述图像。真棒!

1 个答案:

答案 0 :(得分:1)

在ArrayList或LinkedList上使用contains具有复杂度O(n)。考虑到您在过滤器中进行许多 contains()调用,这很快就会变成很大的开销。查找 visited.contains()的复杂性随着处理图像的大小而增加(访问的像素越多,访问的像素越多,将复杂性转换为O(n ^ 2))。

降低此成本的最简单方法是使用具有快速包含的集合类型;在HashSet的情况下喜欢O(1)。 Set语义也比列表更适合您的要求,因为据我所知,访问/未访问的集合不应该允许重复。由于集合不允许按合同重复,因此可以消除一些显式包含检查;在你想要响应首次添加点的事件的地方,你也可以使用add() - 方法给出的布尔结果(当元素不存在并添加时为true)。

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