Retour à l'accueil

Extraction de perspective à partir d'images : Sobel + Hough

Algorithme d'extraction de points de fuite à partir d'images implémente la chaîne : Niveaux de gris Rec.709, Sobel pour les contours, Hough pour les lignes. Optimisé pour Kotlin/Compose avec empaquetage de pixels. Convient aux applications d'artistes et à l'AR.

Kotlin : Auto-perspective des images avec Sobel et Hough
Advertisement 728x90

Extraction automatique des points de fuite d'images en Kotlin

La classe PerspectiveSceneExtractor analyse une image, la convertit en niveaux de gris, détecte les contours à l'aide de l'opérateur Sobel, et applique la transformée de Hough pour identifier les lignes. Le résultat est une liste d'objets PerspectivePoint contenant les coordonnées des points de fuite ou les directions des lignes parallèles.

Représentation de l'image et des pixels

Pour rester indépendant de l'interface utilisateur, nous utilisons une classe de valeur Image avec des pixels compactés dans RGBPixel. Cela minimise l'utilisation de la mémoire.

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
}

Les points de fuite sont stockés dans PerspectivePoint. Des coordonnées fixées à Float.MAX_VALUE indiquent un point à l'infini avec une direction en degrés.

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)
    }
}

Conversion en niveaux de gris

La conversion utilise la norme Rec. 709 avec des poids 0,2126 (R), 0,7152 (G) et 0,0722 (B) pour correspondre à la perception humaine de la luminosité. Le résultat est une GrayImage avec un Int par pixel.

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)
}

Détection des contours avec l'opérateur Sobel

L'opérateur Sobel calcule les gradients en X et Y pour chaque pixel. Si la magnitude du gradient dépasse le seuil (thresholdSq = 16384), le pixel est marqué comme contour.

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)
}

Les gradients sont calculés en tenant compte des pixels voisins :

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 {
    // implémentation similaire pour Y
}

Transformée de Hough pour la détection de lignes

Les lignes sont paramétrées par ρ = x·cos(θ) + y·sin(θ). L'accumulateur est un tableau 2D sur ρ et θ (thetaSteps étapes). Chaque point de contour vote pour les lignes qui le traversent.

La classe LineVote suit les votes, les projections pour la longueur et la densité :

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

Processus de vote :

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)
                    // mise à jour des votes et projections
                }
            }
        }
    }
}

Les lignes sont sélectionnées en fonction des maxima locaux de votes, de la longueur minimale et de la densité.

Paramètres clés de l'algorithme

  • thetaSteps : Nombre d'angles discrets (généralement 180 pour des pas de 1°).
  • threshold : Nombre minimum de votes requis pour une ligne.
  • localMaxWindow : Taille de la fenêtre pour les vérifications de maximum local.
  • thresholdSq : Seuil de l'opérateur Sobel (16384).

Points clés à retenir

  • Le compactage des pixels en Int réduit l'utilisation mémoire de 75 % par rapport aux canaux séparés.
  • Rec. 709 garantit une conversion en gris fidèle à la perception humaine.
  • Sobel est plus rapide que Canny pour les grandes images.
  • Le filtrage de Hough par longueur et densité élimine les lignes bruitées.
  • Les points de fuite infinis sont modélisés avec des directions pour les horizons.

— Editorial Team

Advertisement 728x90

Lire ensuite