返回首页

图像透视提取:Sobel + Hough

图像消失点提取算法实现链:Rec.709 灰度、Sobel 用于边缘、Hough 用于直线。针对 Kotlin/Compose 的像素打包优化。适用于艺术家应用和 AR。

Kotlin:使用 Sobel 和 Hough 的图像自动透视
Advertisement 728x90

Kotlin 实现图像自动消失点提取

PerspectiveSceneExtractor 类会分析图像,转为灰度图,使用 Sobel 算子检测边缘,然后应用霍夫变换来查找直线。最终输出是一组 PerspectivePoint 对象,包含消失点坐标或平行线的方向。

图像与像素表示

为了避免依赖 UI 框架,我们使用值类 Image,像素以 RGBPixel 打包存储,这样能最大限度节省内存。

data class Image(
    val path: String,
    val width: Int,
    val height: Int,
    val pixels: List<RGBPixel>
)

@JvmInline
value class RGBPixel(private val packed: Int) {
    constructor(r: Int, g: Int, b: Int, a: Int = 255) : this(
        ((a and 0xFF) shl 24) or
        ((r and 0xFF) shl 16) or
        ((g and 0xFF) shl 8) or
        (b and 0xFF)
    )

    val a: Int get() = (packed ushr 24) and 0xFF
    val r: Int get() = (packed shr 16) and 0xFF
    val g: Int get() = (packed shr 8) and 0xFF
    val b: Int get() = packed and 0xFF
}

消失点存储在 PerspectivePoint 中。如果坐标为 Float.MAX_VALUE,则表示无穷远点,并带有方向角度。

Google AdInline article slot
data class PerspectivePoint(
    val x: Float,
    val y: Float,
    val direction: Float? = null
) {
    val isFinite: Boolean get() =
        x != Float.MAX_VALUE && y != Float.MAX_VALUE

    companion object {
        fun infinite(directionDegrees: Float) =
            PerspectivePoint(Float.MAX_VALUE, Float.MAX_VALUE, directionDegrees)
    }
}

灰度转换

转换采用 Rec. 709 标准,权重分别为 R:0.2126、G:0.7152、B:0.0722,以匹配人眼亮度感知。结果是 GrayImage,每个像素用一个 Int 表示。

data class GrayImage(
    val image: IntArray,
    val height: Int,
    val width: Int,
)

private fun convertToGrayscale(image: Image): GrayImage {
    val gray = IntArray(image.width * image.height) { i ->
        val pixel = image.pixels[i]
        (0.2126 * pixel.r + 0.7152 * pixel.g + 0.0722 * pixel.b).toInt()
    }
    return GrayImage(gray, image.height, image.width)
}

Sobel 算子边缘检测

Sobel 算子为每个像素计算 X 和 Y 方向梯度。如果梯度幅值超过阈值(thresholdSq = 16384),则标记为边缘像素。

data class BlackWhiteImage(
    val image: BooleanArray,
    val height: Int,
    val width: Int,
)

private fun detectEdges(grayImage: GrayImage): BlackWhiteImage {
    val edges = BooleanArray(grayImage.height * grayImage.width) { false }
    val thresholdSq = 16384

    for (y in 1 until grayImage.height - 1) {
        for (x in 1 until grayImage.width - 1) {
            val gx = computeGradientX(grayImage, x, y)
            val gy = computeGradientY(grayImage, x, y)

            if (gx * gx + gy * gy > thresholdSq) {
                edges[y * grayImage.width + x] = true
            }
        }
    }

    return BlackWhiteImage(edges, grayImage.height, grayImage.width)
}

梯度计算会考虑邻域像素:

Google AdInline article slot
private fun computeGradientX(grayImage: GrayImage, x: Int, y: Int): Int {
    with(grayImage) {
        val idx = y * width + x
        return (
            -image[idx - width - 1]
            + image[idx - width + 1]
            -2 * image[idx - 1]
            + 2 * image[idx + 1]
            -image[idx + width - 1]
            + image[idx + width + 1]
        )
    }
}

private fun computeGradientY(grayImage: GrayImage, x: Int, y: Int): Int {
    // Y 方向类似实现
}

霍夫变换直线检测

直线参数化为 ρ = x·cos(θ) + y·sin(θ)。累加器是 ρ 和 θ(thetaSteps 个步长)的二维数组,每个边缘点为其经过的直线投票。

LineVote 类记录投票数、长度投影和密度:

private class LineVote(
    var votes: Int = 0,
    var minProj: Float = Float.MAX_VALUE,
    var maxProj: Float = -Float.MAX_VALUE
)

投票过程:

Google AdInline article slot
private fun vote(edges: BlackWhiteImage, accumulator: Array<Array<LineVote>>, maxRho: Int) {
    for (y in 0 until height) {
        for (x in 0 until width) {
            if (edges.image[y * width + x]) {
                for (thetaIdx in 0 until thetaSteps) {
                    val theta = Math.toRadians(thetaIdx.toDouble())
                    val rho = x * cos(theta) + y * sin(theta)
                    // 更新投票和投影
                }
            }
        }
    }
}

根据局部投票最大值、最小长度和密度来筛选直线。

核心算法参数

  • thetaSteps:离散角度数量(通常 180,表示 1° 步长)。
  • threshold:直线所需的最小投票数。
  • localMaxWindow:局部最大值检查的窗口大小。
  • thresholdSq:Sobel 算子阈值(16384)。

关键要点

  • 将像素打包到 Int 中,比分开存储通道节省 75% 内存。
  • Rec. 709 标准确保灰度图符合人眼感知。
  • Sobel 处理大图比 Canny 更快。
  • 通过长度和密度过滤霍夫变换,能去除噪声直线。
  • 无穷远消失点用方向角度建模,适用于地平线等场景。

— Editorial Team

Advertisement 728x90

继续阅读