¿Cómo detectar un árbol de navidad? [cerrado]

382

¿Qué técnicas de procesamiento de imágenes podrían usarse para implementar una aplicación que detecte los árboles de Navidad que se muestran en las siguientes imágenes?

Estoy buscando soluciones que funcionen en todas estas imágenes. Por lo tanto, los enfoques que requieren capacitación en clasificadores de cascada de haar o coincidencia de plantillas no son muy interesantes.

Estoy buscando algo que pueda escribirse en cualquier lenguaje de programación, siempre que use solo tecnologías de código abierto . La solución debe probarse con las imágenes que se comparten en esta pregunta. Hay 6 imágenes de entrada y la respuesta debe mostrar los resultados del procesamiento de cada una de ellas. Finalmente, para cada imagen de salida debe haber líneas rojas dibujadas para rodear el árbol detectado.

¿Cómo haría para detectar mediante programación los árboles en estas imágenes?

karlphillip
fuente
3
¿Se nos permite usar algunas de las imágenes para la capacitación, o se deben usar todas las imágenes suministradas para la validación? De cualquier manera, buena competencia: D
Hannes Ovrén
77
@karlphillip, ¿quieres que usemos estas imágenes para probar y otras imágenes para entrenamiento? Es solo que no está claro cuál es el conjunto de entrenamiento.
GilLevi
16
@karlphillip: Mi consejo: elimine el requisito de "código abierto". Realmente no importa qué idioma / marco utilices. Los algoritmos de procesamiento de imágenes / visión por computadora son independientes del lenguaje, por lo que si puede escribirlo en MATLAB, ciertamente puede hacerlo OpenCV o cualquier otro marco que prefiera ... Además, aún no estoy claro qué considera que es la capacitación / prueba de imágenes !
Amro
2
¡@karlphillip gracias por movilizarnos a todos para contribuir a esta 'búsqueda' tuya! Ha sido una gran oportunidad para pasar algunas horas productivamente, pero lo más importante, para ver cuántos enfoques diferentes se pueden encontrar para un solo problema ... Espero que lo vuelvan a hacer el 1 de enero (tal vez un trineo de ¿El desafío de Papá Noel? ;-))
sepdek
2
OK, reformulé la pregunta para eliminar los elementos de la competencia. Creo que eso debería permitir que se mantenga por sí solo bien.
Brad Larson

Respuestas:

184

Tengo un enfoque que creo que es interesante y un poco diferente del resto. La principal diferencia en mi enfoque, en comparación con algunos de los otros, está en cómo se realiza el paso de segmentación de imagen: utilicé el algoritmo de agrupación DBSCAN del scikit-learn de Python; Está optimizado para encontrar formas algo amorfas que no necesariamente tienen un solo centroide claro.

En el nivel superior, mi enfoque es bastante simple y se puede dividir en unos 3 pasos. Primero aplico un umbral (o en realidad, el "o" lógico de dos umbrales separados y distintos). Al igual que con muchas de las otras respuestas, supuse que el árbol de Navidad sería uno de los objetos más brillantes de la escena, por lo que el primer umbral es solo una simple prueba de brillo monocromático; todos los píxeles con valores superiores a 220 en una escala de 0-255 (donde el negro es 0 y el blanco 255) se guardan en una imagen binaria en blanco y negro. El segundo umbral intenta buscar luces rojas y amarillas, que son particularmente prominentes en los árboles en la parte superior izquierda e inferior derecha de las seis imágenes, y se destacan bien contra el fondo azul-verde que prevalece en la mayoría de las fotos. Convierto la imagen rgb a espacio hsv, y requieren que el tono sea menor que 0.2 en una escala de 0.0-1.0 (que corresponde aproximadamente al borde entre amarillo y verde) o mayor que 0.95 (que corresponde al borde entre púrpura y rojo) y además requiero colores brillantes y saturados: la saturación y el valor deben estar por encima de 0.7. Los resultados de los dos procedimientos de umbral se combinan lógicamente "o", y la matriz resultante de imágenes binarias en blanco y negro se muestra a continuación:

Árboles de Navidad, después de limitar el HSV y el brillo monocromático

Puede ver claramente que cada imagen tiene un gran grupo de píxeles que corresponde aproximadamente a la ubicación de cada árbol, además de algunas de las imágenes también tienen algunos otros pequeños grupos correspondientes a las luces en las ventanas de algunos de los edificios o a un Escena de fondo en el horizonte. El siguiente paso es lograr que la computadora reconozca que se trata de grupos separados y etiquetar cada píxel correctamente con un número de identificación de miembro del grupo.

Para esta tarea elegí DBSCAN . Hay una comparación visual bastante buena de cómo DBSCAN se comporta típicamente, en relación con otros algoritmos de agrupamiento, disponibles aquí . Como dije antes, le va bien con formas amorfas. La salida de DBSCAN, con cada grupo trazado en un color diferente, se muestra aquí:

Salida de agrupación DBSCAN

Hay algunas cosas a tener en cuenta al mirar este resultado. Primero es que DBSCAN requiere que el usuario establezca un parámetro de "proximidad" para regular su comportamiento, que controla efectivamente qué tan separados deben estar un par de puntos para que el algoritmo declare un nuevo grupo separado en lugar de aglomerar un punto de prueba en un clúster ya preexistente. Establezco este valor en 0.04 veces el tamaño a lo largo de la diagonal de cada imagen. Dado que las imágenes varían en tamaño desde aproximadamente VGA hasta aproximadamente HD 1080, este tipo de definición relativa a la escala es crítica.

Otro punto que vale la pena señalar es que el algoritmo DBSCAN, tal como se implementa en scikit-learn, tiene límites de memoria que son bastante desafiantes para algunas de las imágenes más grandes de esta muestra. Por lo tanto, para algunas de las imágenes más grandes, en realidad tuve que "diezmar" (es decir, retener solo cada 3er o 4to píxel y soltar los demás) cada grupo para permanecer dentro de este límite. Como resultado de este proceso de selección, los píxeles dispersos individuales restantes son difíciles de ver en algunas de las imágenes más grandes. Por lo tanto, solo para fines de visualización, los píxeles codificados por colores en las imágenes anteriores se han "dilatado" de manera efectiva solo ligeramente para que se destaquen mejor. Es puramente una operación cosmética por el bien de la narrativa; aunque hay comentarios que mencionan esta dilatación en mi código,

Una vez que los grupos están identificados y etiquetados, el tercer y último paso es fácil: simplemente tomo el grupo más grande de cada imagen (en este caso, elegí medir el "tamaño" en términos del número total de píxeles de miembros, aunque uno podría en su lugar, he utilizado con facilidad algún tipo de métrica que mide la extensión física) y calcula el casco convexo para ese grupo. El casco convexo se convierte en el borde del árbol. Los seis cascos convexos calculados mediante este método se muestran a continuación en rojo:

Árboles de navidad con sus bordes calculados

El código fuente está escrito para Python 2.7.6 y depende de numpy , scipy , matplotlib y scikit-learn . Lo he dividido en dos partes. La primera parte es responsable del procesamiento real de la imagen:

from PIL import Image
import numpy as np
import scipy as sp
import matplotlib.colors as colors
from sklearn.cluster import DBSCAN
from math import ceil, sqrt

"""
Inputs:

    rgbimg:         [M,N,3] numpy array containing (uint, 0-255) color image

    hueleftthr:     Scalar constant to select maximum allowed hue in the
                    yellow-green region

    huerightthr:    Scalar constant to select minimum allowed hue in the
                    blue-purple region

    satthr:         Scalar constant to select minimum allowed saturation

    valthr:         Scalar constant to select minimum allowed value

    monothr:        Scalar constant to select minimum allowed monochrome
                    brightness

    maxpoints:      Scalar constant maximum number of pixels to forward to
                    the DBSCAN clustering algorithm

    proxthresh:     Proximity threshold to use for DBSCAN, as a fraction of
                    the diagonal size of the image

Outputs:

    borderseg:      [K,2,2] Nested list containing K pairs of x- and y- pixel
                    values for drawing the tree border

    X:              [P,2] List of pixels that passed the threshold step

    labels:         [Q,2] List of cluster labels for points in Xslice (see
                    below)

    Xslice:         [Q,2] Reduced list of pixels to be passed to DBSCAN

"""

def findtree(rgbimg, hueleftthr=0.2, huerightthr=0.95, satthr=0.7, 
             valthr=0.7, monothr=220, maxpoints=5000, proxthresh=0.04):

    # Convert rgb image to monochrome for
    gryimg = np.asarray(Image.fromarray(rgbimg).convert('L'))
    # Convert rgb image (uint, 0-255) to hsv (float, 0.0-1.0)
    hsvimg = colors.rgb_to_hsv(rgbimg.astype(float)/255)

    # Initialize binary thresholded image
    binimg = np.zeros((rgbimg.shape[0], rgbimg.shape[1]))
    # Find pixels with hue<0.2 or hue>0.95 (red or yellow) and saturation/value
    # both greater than 0.7 (saturated and bright)--tends to coincide with
    # ornamental lights on trees in some of the images
    boolidx = np.logical_and(
                np.logical_and(
                  np.logical_or((hsvimg[:,:,0] < hueleftthr),
                                (hsvimg[:,:,0] > huerightthr)),
                                (hsvimg[:,:,1] > satthr)),
                                (hsvimg[:,:,2] > valthr))
    # Find pixels that meet hsv criterion
    binimg[np.where(boolidx)] = 255
    # Add pixels that meet grayscale brightness criterion
    binimg[np.where(gryimg > monothr)] = 255

    # Prepare thresholded points for DBSCAN clustering algorithm
    X = np.transpose(np.where(binimg == 255))
    Xslice = X
    nsample = len(Xslice)
    if nsample > maxpoints:
        # Make sure number of points does not exceed DBSCAN maximum capacity
        Xslice = X[range(0,nsample,int(ceil(float(nsample)/maxpoints)))]

    # Translate DBSCAN proximity threshold to units of pixels and run DBSCAN
    pixproxthr = proxthresh * sqrt(binimg.shape[0]**2 + binimg.shape[1]**2)
    db = DBSCAN(eps=pixproxthr, min_samples=10).fit(Xslice)
    labels = db.labels_.astype(int)

    # Find the largest cluster (i.e., with most points) and obtain convex hull   
    unique_labels = set(labels)
    maxclustpt = 0
    for k in unique_labels:
        class_members = [index[0] for index in np.argwhere(labels == k)]
        if len(class_members) > maxclustpt:
            points = Xslice[class_members]
            hull = sp.spatial.ConvexHull(points)
            maxclustpt = len(class_members)
            borderseg = [[points[simplex,0], points[simplex,1]] for simplex
                          in hull.simplices]

    return borderseg, X, labels, Xslice

y la segunda parte es un script de nivel de usuario que llama al primer archivo y genera todos los gráficos anteriores:

#!/usr/bin/env python

from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from findtree import findtree

# Image files to process
fname = ['nmzwj.png', 'aVZhC.png', '2K9EF.png',
         'YowlH.png', '2y4o5.png', 'FWhSP.png']

# Initialize figures
fgsz = (16,7)        
figthresh = plt.figure(figsize=fgsz, facecolor='w')
figclust  = plt.figure(figsize=fgsz, facecolor='w')
figcltwo  = plt.figure(figsize=fgsz, facecolor='w')
figborder = plt.figure(figsize=fgsz, facecolor='w')
figthresh.canvas.set_window_title('Thresholded HSV and Monochrome Brightness')
figclust.canvas.set_window_title('DBSCAN Clusters (Raw Pixel Output)')
figcltwo.canvas.set_window_title('DBSCAN Clusters (Slightly Dilated for Display)')
figborder.canvas.set_window_title('Trees with Borders')

for ii, name in zip(range(len(fname)), fname):
    # Open the file and convert to rgb image
    rgbimg = np.asarray(Image.open(name))

    # Get the tree borders as well as a bunch of other intermediate values
    # that will be used to illustrate how the algorithm works
    borderseg, X, labels, Xslice = findtree(rgbimg)

    # Display thresholded images
    axthresh = figthresh.add_subplot(2,3,ii+1)
    axthresh.set_xticks([])
    axthresh.set_yticks([])
    binimg = np.zeros((rgbimg.shape[0], rgbimg.shape[1]))
    for v, h in X:
        binimg[v,h] = 255
    axthresh.imshow(binimg, interpolation='nearest', cmap='Greys')

    # Display color-coded clusters
    axclust = figclust.add_subplot(2,3,ii+1) # Raw version
    axclust.set_xticks([])
    axclust.set_yticks([])
    axcltwo = figcltwo.add_subplot(2,3,ii+1) # Dilated slightly for display only
    axcltwo.set_xticks([])
    axcltwo.set_yticks([])
    axcltwo.imshow(binimg, interpolation='nearest', cmap='Greys')
    clustimg = np.ones(rgbimg.shape)    
    unique_labels = set(labels)
    # Generate a unique color for each cluster 
    plcol = cm.rainbow_r(np.linspace(0, 1, len(unique_labels)))
    for lbl, pix in zip(labels, Xslice):
        for col, unqlbl in zip(plcol, unique_labels):
            if lbl == unqlbl:
                # Cluster label of -1 indicates no cluster membership;
                # override default color with black
                if lbl == -1:
                    col = [0.0, 0.0, 0.0, 1.0]
                # Raw version
                for ij in range(3):
                    clustimg[pix[0],pix[1],ij] = col[ij]
                # Dilated just for display
                axcltwo.plot(pix[1], pix[0], 'o', markerfacecolor=col, 
                    markersize=1, markeredgecolor=col)
    axclust.imshow(clustimg)
    axcltwo.set_xlim(0, binimg.shape[1]-1)
    axcltwo.set_ylim(binimg.shape[0], -1)

    # Plot original images with read borders around the trees
    axborder = figborder.add_subplot(2,3,ii+1)
    axborder.set_axis_off()
    axborder.imshow(rgbimg, interpolation='nearest')
    for vseg, hseg in borderseg:
        axborder.plot(hseg, vseg, 'r-', lw=3)
    axborder.set_xlim(0, binimg.shape[1]-1)
    axborder.set_ylim(binimg.shape[0], -1)

plt.show()
stachyra
fuente
La solución de @lennon310 es la agrupación. (k-means)
usuario 3054997
1
@stachyra También pensé en este enfoque antes de proponer los más simples. Creo que esto tiene un gran potencial para extenderse y generalizarse para producir buenos resultados en otros casos también. Podrías experimentar con redes neuronales para la agrupación. Algo como un SOM o gas neuronal haría un trabajo excelente. Sin embargo, ¡gran propuesta y aprobado por mí!
sepdek
44
@Faust y Ryan Carlson: ¡gracias chicos! Sí, estoy de acuerdo en que el sistema de votación positiva, aunque funciona bien para adjudicar entre 2 o 3 respuestas cortas, todas enviadas dentro de unas pocas horas entre sí, tiene sesgos serios cuando se trata de concursos con respuestas largas que se desarrollan durante largos períodos de tiempo . Por un lado, las presentaciones tempranas comienzan a acumular votos positivos antes de que los posteriores estén disponibles para revisión pública. Y si las respuestas son largas, tan pronto como uno establece una ventaja modesta, a menudo hay un "efecto de carro" ya que la gente solo vota el primero sin molestarse en leer el resto.
stachyra 01 de
2
@stachyra gran noticia amigo! ¡Felicitaciones más cálidas y que esto marque un comienzo para su nuevo año!
sepdek
1
@ lennon310: Todavía no he probado un filtro de detección máxima local en este problema, pero si desea explorarlo usted mismo, scipy incluye este . Mi código fuente de Python para este proyecto fue tan corto que pude publicar el 100%; literalmente, todo lo que necesitaría hacer es copiar y pegar mis dos fragmentos de código en archivos .py separados y luego sustituir una llamada scipy.ndimage.filters.maximum_filter()en el mismo lugar donde había usado un umbral.
stachyra
145

NOTA DE EDICIÓN: edité esta publicación para (i) procesar cada imagen de árbol individualmente, según lo solicitado en los requisitos, (ii) para considerar tanto el brillo como la forma del objeto para mejorar la calidad del resultado.


A continuación se presenta un enfoque que toma en consideración el brillo y la forma del objeto. En otras palabras, busca objetos con forma de triángulo y con un brillo significativo. Fue implementado en Java, utilizando el marco de procesamiento de imágenes Marvin .

El primer paso es el umbral de color. El objetivo aquí es enfocar el análisis en objetos con brillo significativo.

imágenes de salida:

código fuente:

public class ChristmasTree {

private MarvinImagePlugin fill = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.fill.boundaryFill");
private MarvinImagePlugin threshold = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.color.thresholding");
private MarvinImagePlugin invert = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.color.invert");
private MarvinImagePlugin dilation = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.morphological.dilation");

public ChristmasTree(){
    MarvinImage tree;

    // Iterate each image
    for(int i=1; i<=6; i++){
        tree = MarvinImageIO.loadImage("./res/trees/tree"+i+".png");

        // 1. Threshold
        threshold.setAttribute("threshold", 200);
        threshold.process(tree.clone(), tree);
    }
}
public static void main(String[] args) {
    new ChristmasTree();
}
}

En el segundo paso, los puntos más brillantes de la imagen se dilatan para formar formas. El resultado de este proceso es la forma probable de los objetos con brillo significativo. Aplicando segmentación de relleno de inundación, se detectan formas desconectadas.

imágenes de salida:

código fuente:

public class ChristmasTree {

private MarvinImagePlugin fill = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.fill.boundaryFill");
private MarvinImagePlugin threshold = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.color.thresholding");
private MarvinImagePlugin invert = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.color.invert");
private MarvinImagePlugin dilation = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.morphological.dilation");

public ChristmasTree(){
    MarvinImage tree;

    // Iterate each image
    for(int i=1; i<=6; i++){
        tree = MarvinImageIO.loadImage("./res/trees/tree"+i+".png");

        // 1. Threshold
        threshold.setAttribute("threshold", 200);
        threshold.process(tree.clone(), tree);

        // 2. Dilate
        invert.process(tree.clone(), tree);
        tree = MarvinColorModelConverter.rgbToBinary(tree, 127);
        MarvinImageIO.saveImage(tree, "./res/trees/new/tree_"+i+"threshold.png");
        dilation.setAttribute("matrix", MarvinMath.getTrueMatrix(50, 50));
        dilation.process(tree.clone(), tree);
        MarvinImageIO.saveImage(tree, "./res/trees/new/tree_"+1+"_dilation.png");
        tree = MarvinColorModelConverter.binaryToRgb(tree);

        // 3. Segment shapes
        MarvinImage trees2 = tree.clone();
        fill(tree, trees2);
        MarvinImageIO.saveImage(trees2, "./res/trees/new/tree_"+i+"_fill.png");
}

private void fill(MarvinImage imageIn, MarvinImage imageOut){
    boolean found;
    int color= 0xFFFF0000;

    while(true){
        found=false;

        Outerloop:
        for(int y=0; y<imageIn.getHeight(); y++){
            for(int x=0; x<imageIn.getWidth(); x++){
                if(imageOut.getIntComponent0(x, y) == 0){
                    fill.setAttribute("x", x);
                    fill.setAttribute("y", y);
                    fill.setAttribute("color", color);
                    fill.setAttribute("threshold", 120);
                    fill.process(imageIn, imageOut);
                    color = newColor(color);

                    found = true;
                    break Outerloop;
                }
            }
        }

        if(!found){
            break;
        }
    }

}

private int newColor(int color){
    int red = (color & 0x00FF0000) >> 16;
    int green = (color & 0x0000FF00) >> 8;
    int blue = (color & 0x000000FF);

    if(red <= green && red <= blue){
        red+=5;
    }
    else if(green <= red && green <= blue){
        green+=5;
    }
    else{
        blue+=5;
    }

    return 0xFF000000 + (red << 16) + (green << 8) + blue;
}

public static void main(String[] args) {
    new ChristmasTree();
}
}

Como se muestra en la imagen de salida, se detectaron múltiples formas. En este problema, solo hay algunos puntos brillantes en las imágenes. Sin embargo, este enfoque se implementó para lidiar con escenarios más complejos.

En el siguiente paso se analiza cada forma. Un algoritmo simple detecta formas con un patrón similar a un triángulo. El algoritmo analiza la forma del objeto línea por línea. Si el centro de la masa de cada línea de forma es casi la misma (dado un umbral) y la masa aumenta a medida que aumenta, el objeto tiene una forma de triángulo. La masa de la línea de forma es el número de píxeles en esa línea que pertenece a la forma. Imagine que corta el objeto horizontalmente y analiza cada segmento horizontal. Si están centralizados entre sí y la longitud aumenta desde el primer segmento hasta el último en un patrón lineal, es probable que tenga un objeto que se asemeje a un triángulo.

código fuente:

private int[] detectTrees(MarvinImage image){
    HashSet<Integer> analysed = new HashSet<Integer>();
    boolean found;
    while(true){
        found = false;
        for(int y=0; y<image.getHeight(); y++){
            for(int x=0; x<image.getWidth(); x++){
                int color = image.getIntColor(x, y);

                if(!analysed.contains(color)){
                    if(isTree(image, color)){
                        return getObjectRect(image, color);
                    }

                    analysed.add(color);
                    found=true;
                }
            }
        }

        if(!found){
            break;
        }
    }
    return null;
}

private boolean isTree(MarvinImage image, int color){

    int mass[][] = new int[image.getHeight()][2];
    int yStart=-1;
    int xStart=-1;
    for(int y=0; y<image.getHeight(); y++){
        int mc = 0;
        int xs=-1;
        int xe=-1;
        for(int x=0; x<image.getWidth(); x++){
            if(image.getIntColor(x, y) == color){
                mc++;

                if(yStart == -1){
                    yStart=y;
                    xStart=x;
                }

                if(xs == -1){
                    xs = x;
                }
                if(x > xe){
                    xe = x;
                }
            }
        }
        mass[y][0] = xs;
        mass[y][3] = xe;
        mass[y][4] = mc;    
    }

    int validLines=0;
    for(int y=0; y<image.getHeight(); y++){
        if
        ( 
            mass[y][5] > 0 &&
            Math.abs(((mass[y][0]+mass[y][6])/2)-xStart) <= 50 &&
            mass[y][7] >= (mass[yStart][8] + (y-yStart)*0.3) &&
            mass[y][9] <= (mass[yStart][10] + (y-yStart)*1.5)
        )
        {
            validLines++;
        }
    }

    if(validLines > 100){
        return true;
    }
    return false;
}

Finalmente, la posición de cada forma similar a un triángulo y con un brillo significativo, en este caso un árbol de Navidad, se resalta en la imagen original, como se muestra a continuación.

imágenes de salida final:

código fuente final:

public class ChristmasTree {

private MarvinImagePlugin fill = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.fill.boundaryFill");
private MarvinImagePlugin threshold = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.color.thresholding");
private MarvinImagePlugin invert = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.color.invert");
private MarvinImagePlugin dilation = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.morphological.dilation");

public ChristmasTree(){
    MarvinImage tree;

    // Iterate each image
    for(int i=1; i<=6; i++){
        tree = MarvinImageIO.loadImage("./res/trees/tree"+i+".png");

        // 1. Threshold
        threshold.setAttribute("threshold", 200);
        threshold.process(tree.clone(), tree);

        // 2. Dilate
        invert.process(tree.clone(), tree);
        tree = MarvinColorModelConverter.rgbToBinary(tree, 127);
        MarvinImageIO.saveImage(tree, "./res/trees/new/tree_"+i+"threshold.png");
        dilation.setAttribute("matrix", MarvinMath.getTrueMatrix(50, 50));
        dilation.process(tree.clone(), tree);
        MarvinImageIO.saveImage(tree, "./res/trees/new/tree_"+1+"_dilation.png");
        tree = MarvinColorModelConverter.binaryToRgb(tree);

        // 3. Segment shapes
        MarvinImage trees2 = tree.clone();
        fill(tree, trees2);
        MarvinImageIO.saveImage(trees2, "./res/trees/new/tree_"+i+"_fill.png");

        // 4. Detect tree-like shapes
        int[] rect = detectTrees(trees2);

        // 5. Draw the result
        MarvinImage original = MarvinImageIO.loadImage("./res/trees/tree"+i+".png");
        drawBoundary(trees2, original, rect);
        MarvinImageIO.saveImage(original, "./res/trees/new/tree_"+i+"_out_2.jpg");
    }
}

private void drawBoundary(MarvinImage shape, MarvinImage original, int[] rect){
    int yLines[] = new int[6];
    yLines[0] = rect[1];
    yLines[1] = rect[1]+(int)((rect[3]/5));
    yLines[2] = rect[1]+((rect[3]/5)*2);
    yLines[3] = rect[1]+((rect[3]/5)*3);
    yLines[4] = rect[1]+(int)((rect[3]/5)*4);
    yLines[5] = rect[1]+rect[3];

    List<Point> points = new ArrayList<Point>();
    for(int i=0; i<yLines.length; i++){
        boolean in=false;
        Point startPoint=null;
        Point endPoint=null;
        for(int x=rect[0]; x<rect[0]+rect[2]; x++){

            if(shape.getIntColor(x, yLines[i]) != 0xFFFFFFFF){
                if(!in){
                    if(startPoint == null){
                        startPoint = new Point(x, yLines[i]);
                    }
                }
                in = true;
            }
            else{
                if(in){
                    endPoint = new Point(x, yLines[i]);
                }
                in = false;
            }
        }

        if(endPoint == null){
            endPoint = new Point((rect[0]+rect[2])-1, yLines[i]);
        }

        points.add(startPoint);
        points.add(endPoint);
    }

    drawLine(points.get(0).x, points.get(0).y, points.get(1).x, points.get(1).y, 15, original);
    drawLine(points.get(1).x, points.get(1).y, points.get(3).x, points.get(3).y, 15, original);
    drawLine(points.get(3).x, points.get(3).y, points.get(5).x, points.get(5).y, 15, original);
    drawLine(points.get(5).x, points.get(5).y, points.get(7).x, points.get(7).y, 15, original);
    drawLine(points.get(7).x, points.get(7).y, points.get(9).x, points.get(9).y, 15, original);
    drawLine(points.get(9).x, points.get(9).y, points.get(11).x, points.get(11).y, 15, original);
    drawLine(points.get(11).x, points.get(11).y, points.get(10).x, points.get(10).y, 15, original);
    drawLine(points.get(10).x, points.get(10).y, points.get(8).x, points.get(8).y, 15, original);
    drawLine(points.get(8).x, points.get(8).y, points.get(6).x, points.get(6).y, 15, original);
    drawLine(points.get(6).x, points.get(6).y, points.get(4).x, points.get(4).y, 15, original);
    drawLine(points.get(4).x, points.get(4).y, points.get(2).x, points.get(2).y, 15, original);
    drawLine(points.get(2).x, points.get(2).y, points.get(0).x, points.get(0).y, 15, original);
}

private void drawLine(int x1, int y1, int x2, int y2, int length, MarvinImage image){
    int lx1, lx2, ly1, ly2;
    for(int i=0; i<length; i++){
        lx1 = (x1+i >= image.getWidth() ? (image.getWidth()-1)-i: x1);
        lx2 = (x2+i >= image.getWidth() ? (image.getWidth()-1)-i: x2);
        ly1 = (y1+i >= image.getHeight() ? (image.getHeight()-1)-i: y1);
        ly2 = (y2+i >= image.getHeight() ? (image.getHeight()-1)-i: y2);

        image.drawLine(lx1+i, ly1, lx2+i, ly2, Color.red);
        image.drawLine(lx1, ly1+i, lx2, ly2+i, Color.red);
    }
}

private void fillRect(MarvinImage image, int[] rect, int length){
    for(int i=0; i<length; i++){
        image.drawRect(rect[0]+i, rect[1]+i, rect[2]-(i*2), rect[3]-(i*2), Color.red);
    }
}

private void fill(MarvinImage imageIn, MarvinImage imageOut){
    boolean found;
    int color= 0xFFFF0000;

    while(true){
        found=false;

        Outerloop:
        for(int y=0; y<imageIn.getHeight(); y++){
            for(int x=0; x<imageIn.getWidth(); x++){
                if(imageOut.getIntComponent0(x, y) == 0){
                    fill.setAttribute("x", x);
                    fill.setAttribute("y", y);
                    fill.setAttribute("color", color);
                    fill.setAttribute("threshold", 120);
                    fill.process(imageIn, imageOut);
                    color = newColor(color);

                    found = true;
                    break Outerloop;
                }
            }
        }

        if(!found){
            break;
        }
    }

}

private int[] detectTrees(MarvinImage image){
    HashSet<Integer> analysed = new HashSet<Integer>();
    boolean found;
    while(true){
        found = false;
        for(int y=0; y<image.getHeight(); y++){
            for(int x=0; x<image.getWidth(); x++){
                int color = image.getIntColor(x, y);

                if(!analysed.contains(color)){
                    if(isTree(image, color)){
                        return getObjectRect(image, color);
                    }

                    analysed.add(color);
                    found=true;
                }
            }
        }

        if(!found){
            break;
        }
    }
    return null;
}

private boolean isTree(MarvinImage image, int color){

    int mass[][] = new int[image.getHeight()][11];
    int yStart=-1;
    int xStart=-1;
    for(int y=0; y<image.getHeight(); y++){
        int mc = 0;
        int xs=-1;
        int xe=-1;
        for(int x=0; x<image.getWidth(); x++){
            if(image.getIntColor(x, y) == color){
                mc++;

                if(yStart == -1){
                    yStart=y;
                    xStart=x;
                }

                if(xs == -1){
                    xs = x;
                }
                if(x > xe){
                    xe = x;
                }
            }
        }
        mass[y][0] = xs;
        mass[y][12] = xe;
        mass[y][13] = mc;   
    }

    int validLines=0;
    for(int y=0; y<image.getHeight(); y++){
        if
        ( 
            mass[y][14] > 0 &&
            Math.abs(((mass[y][0]+mass[y][15])/2)-xStart) <= 50 &&
            mass[y][16] >= (mass[yStart][17] + (y-yStart)*0.3) &&
            mass[y][18] <= (mass[yStart][19] + (y-yStart)*1.5)
        )
        {
            validLines++;
        }
    }

    if(validLines > 100){
        return true;
    }
    return false;
}

private int[] getObjectRect(MarvinImage image, int color){
    int x1=-1;
    int x2=-1;
    int y1=-1;
    int y2=-1;

    for(int y=0; y<image.getHeight(); y++){
        for(int x=0; x<image.getWidth(); x++){
            if(image.getIntColor(x, y) == color){

                if(x1 == -1 || x < x1){
                    x1 = x;
                }
                if(x2 == -1 || x > x2){
                    x2 = x;
                }
                if(y1 == -1 || y < y1){
                    y1 = y;
                }
                if(y2 == -1 || y > y2){
                    y2 = y;
                }
            }
        }
    }

    return new int[]{x1, y1, (x2-x1), (y2-y1)};
}

private int newColor(int color){
    int red = (color & 0x00FF0000) >> 16;
    int green = (color & 0x0000FF00) >> 8;
    int blue = (color & 0x000000FF);

    if(red <= green && red <= blue){
        red+=5;
    }
    else if(green <= red && green <= blue){
        green+=30;
    }
    else{
        blue+=30;
    }

    return 0xFF000000 + (red << 16) + (green << 8) + blue;
}

public static void main(String[] args) {
    new ChristmasTree();
}
}

La ventaja de este enfoque es el hecho de que probablemente funcionará con imágenes que contienen otros objetos luminosos, ya que analiza la forma del objeto.

¡Feliz Navidad!


EDITAR NOTA 2

Hay una discusión sobre la similitud de las imágenes de salida de esta solución y algunas otras. De hecho, son muy similares. Pero este enfoque no solo segmenta objetos. También analiza las formas de los objetos en cierto sentido. Puede manejar múltiples objetos luminosos en la misma escena. De hecho, el árbol de Navidad no necesita ser el más brillante. Solo lo escribo para enriquecer la discusión. Hay un sesgo en las muestras que solo buscando el objeto más brillante, encontrará los árboles. Pero, ¿realmente queremos detener la discusión en este punto? En este punto, ¿hasta qué punto la computadora realmente reconoce un objeto que se parece a un árbol de Navidad? Intentemos cerrar esta brecha.

A continuación se presenta un resultado solo para dilucidar este punto:

imagen de entrada

ingrese la descripción de la imagen aquí

salida

ingrese la descripción de la imagen aquí

Gabriel Ambrósio Archanjo
fuente
2
Eso es interesante. Espero que pueda obtener los mismos resultados cuando cada imagen se procesa individualmente. Edité la pregunta 4 horas antes para que publicara la respuesta para indicar esto específicamente. Sería increíble si pudiera actualizar su respuesta con estos resultados.
karlphillip
@Marvin en tu detección triangular, ¿cómo manejaste la fluctuación de la masa? No es un triángulo estricto, la masa no es mono a medida que cambia y
user3054997
2
@ user3054997: Ese es otro punto. Como publiqué, el algoritmo no busca las formas estrictas de triángulos. Analiza cada objeto y considera un árbol que "se asemeja" a un triángulo con un criterio simple: la masa del objeto se usa para aumentar a medida que aumenta y el centro de la masa de cada segmento horizontal del objeto está casi centralizado entre sí. .
Gabriel Ambrósio Archanjo
@Marvin Mi solución es realmente simple, también lo dije en mi respuesta. Por cierto, funcionó mejor que su primera solución. Si recuerdo correctamente, en su primera respuesta, habló sobre descriptores de características para detectar pequeñas texturas claras, que no es lo que está haciendo aquí. Simplemente dije que su enfoque actual y sus resultados son mucho más similares a los míos que a su primera solución. Por supuesto, no espero que lo admitas, lo dije solo para el registro.
smeso
1
@sepdek Aquí hay un par de soluciones que son realmente mucho mejores que las mías y todavía reciben la mitad de mis votos positivos. No hay nada de malo en "inspirarse" con otras soluciones. También vi sus soluciones, no tengo nada que decir en su contra, las publicó después de mí y mi "idea" no fue tan original como para decir que me acaba de copiar. Pero Marvin fue el único que publicó antes que yo y editó su solución después de ver la mía usando el mismo algoritmo ... al menos podría haber dicho "Sí, me gustó su solución y la reutilicé". No hay nada malo, es solo un juego.
smeso
75

Aquí está mi solución simple y tonta. Se basa en la suposición de que el árbol será la cosa más brillante y más grande de la imagen.

//g++ -Wall -pedantic -ansi -O2 -pipe -s -o christmas_tree christmas_tree.cpp `pkg-config --cflags --libs opencv`
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int argc,char *argv[])
{
    Mat original,tmp,tmp1;
    vector <vector<Point> > contours;
    Moments m;
    Rect boundrect;
    Point2f center;
    double radius, max_area=0,tmp_area=0;
    unsigned int j, k;
    int i;

    for(i = 1; i < argc; ++i)
    {
        original = imread(argv[i]);
        if(original.empty())
        {
            cerr << "Error"<<endl;
            return -1;
        }

        GaussianBlur(original, tmp, Size(3, 3), 0, 0, BORDER_DEFAULT);
        erode(tmp, tmp, Mat(), Point(-1, -1), 10);
        cvtColor(tmp, tmp, CV_BGR2HSV);
        inRange(tmp, Scalar(0, 0, 0), Scalar(180, 255, 200), tmp);

        dilate(original, tmp1, Mat(), Point(-1, -1), 15);
        cvtColor(tmp1, tmp1, CV_BGR2HLS);
        inRange(tmp1, Scalar(0, 185, 0), Scalar(180, 255, 255), tmp1);
        dilate(tmp1, tmp1, Mat(), Point(-1, -1), 10);

        bitwise_and(tmp, tmp1, tmp1);

        findContours(tmp1, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
        max_area = 0;
        j = 0;
        for(k = 0; k < contours.size(); k++)
        {
            tmp_area = contourArea(contours[k]);
            if(tmp_area > max_area)
            {
                max_area = tmp_area;
                j = k;
            }
        }
        tmp1 = Mat::zeros(original.size(),CV_8U);
        approxPolyDP(contours[j], contours[j], 30, true);
        drawContours(tmp1, contours, j, Scalar(255,255,255), CV_FILLED);

        m = moments(contours[j]);
        boundrect = boundingRect(contours[j]);
        center = Point2f(m.m10/m.m00, m.m01/m.m00);
        radius = (center.y - (boundrect.tl().y))/4.0*3.0;
        Rect heightrect(center.x-original.cols/5, boundrect.tl().y, original.cols/5*2, boundrect.size().height);

        tmp = Mat::zeros(original.size(), CV_8U);
        rectangle(tmp, heightrect, Scalar(255, 255, 255), -1);
        circle(tmp, center, radius, Scalar(255, 255, 255), -1);

        bitwise_and(tmp, tmp1, tmp1);

        findContours(tmp1, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
        max_area = 0;
        j = 0;
        for(k = 0; k < contours.size(); k++)
        {
            tmp_area = contourArea(contours[k]);
            if(tmp_area > max_area)
            {
                max_area = tmp_area;
                j = k;
            }
        }

        approxPolyDP(contours[j], contours[j], 30, true);
        convexHull(contours[j], contours[j]);

        drawContours(original, contours, j, Scalar(0, 0, 255), 3);

        namedWindow(argv[i], CV_WINDOW_NORMAL|CV_WINDOW_KEEPRATIO|CV_GUI_EXPANDED);
        imshow(argv[i], original);

        waitKey(0);
        destroyWindow(argv[i]);
    }

    return 0;
}

El primer paso es detectar los píxeles más brillantes en la imagen, pero tenemos que hacer una distinción entre el árbol en sí y la nieve que refleja su luz. Aquí intentamos excluir la nieve aplicando un filtro realmente simple en los códigos de color:

GaussianBlur(original, tmp, Size(3, 3), 0, 0, BORDER_DEFAULT);
erode(tmp, tmp, Mat(), Point(-1, -1), 10);
cvtColor(tmp, tmp, CV_BGR2HSV);
inRange(tmp, Scalar(0, 0, 0), Scalar(180, 255, 200), tmp);

Luego encontramos cada píxel "brillante":

dilate(original, tmp1, Mat(), Point(-1, -1), 15);
cvtColor(tmp1, tmp1, CV_BGR2HLS);
inRange(tmp1, Scalar(0, 185, 0), Scalar(180, 255, 255), tmp1);
dilate(tmp1, tmp1, Mat(), Point(-1, -1), 10);

Finalmente unimos los dos resultados:

bitwise_and(tmp, tmp1, tmp1);

Ahora buscamos el objeto brillante más grande:

findContours(tmp1, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
max_area = 0;
j = 0;
for(k = 0; k < contours.size(); k++)
{
    tmp_area = contourArea(contours[k]);
    if(tmp_area > max_area)
    {
        max_area = tmp_area;
        j = k;
    }
}
tmp1 = Mat::zeros(original.size(),CV_8U);
approxPolyDP(contours[j], contours[j], 30, true);
drawContours(tmp1, contours, j, Scalar(255,255,255), CV_FILLED);

Ahora casi hemos terminado, pero todavía hay algunas imperfecciones debido a la nieve. Para cortarlos, construiremos una máscara usando un círculo y un rectángulo para aproximar la forma de un árbol para eliminar piezas no deseadas:

m = moments(contours[j]);
boundrect = boundingRect(contours[j]);
center = Point2f(m.m10/m.m00, m.m01/m.m00);
radius = (center.y - (boundrect.tl().y))/4.0*3.0;
Rect heightrect(center.x-original.cols/5, boundrect.tl().y, original.cols/5*2, boundrect.size().height);

tmp = Mat::zeros(original.size(), CV_8U);
rectangle(tmp, heightrect, Scalar(255, 255, 255), -1);
circle(tmp, center, radius, Scalar(255, 255, 255), -1);

bitwise_and(tmp, tmp1, tmp1);

El último paso es encontrar el contorno de nuestro árbol y dibujarlo en la imagen original.

findContours(tmp1, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
max_area = 0;
j = 0;
for(k = 0; k < contours.size(); k++)
{
    tmp_area = contourArea(contours[k]);
    if(tmp_area > max_area)
    {
        max_area = tmp_area;
        j = k;
    }
}

approxPolyDP(contours[j], contours[j], 30, true);
convexHull(contours[j], contours[j]);

drawContours(original, contours, j, Scalar(0, 0, 255), 3);

Lo siento, pero por el momento tengo una mala conexión, así que no me es posible subir fotos. Intentaré hacerlo más tarde.

Feliz Navidad.

EDITAR:

Aquí algunas fotos de la salida final:

smeso
fuente
1
¡Hola! Asegúrese de que su respuesta cumpla con todos los requisitos: hay 6 imágenes de entrada y la respuesta debe mostrar los resultados de procesar cada una de ellas; .
karlphillip
¡Hola! Puede pasar los nombres de archivo como argumentos de la CLI a mi programa: ./christmas_tree ./*.png. Pueden ser tantos como desee, los resultados se mostrarán uno tras otro presionando cualquier tecla. ¿Esto esta mal?
smeso
Está bien, pero aún necesita cargar las imágenes y compartirlas en su pregunta para que los espectadores del hilo puedan ver su resultado. Dejar que la gente vea lo que hiciste mejorará tus posibilidades de obtener votos;)
karlphillip
Estoy tratando de encontrar una solución para esto, tengo algunos problemas de conectividad.
smeso
2
¡Excelente! Ahora puede reescalarlos dentro de la respuesta con el siguiente código: <img src="http://i.stack.imgur.com/nmzwj.png" width="210" height="150">simplemente cambie el enlace a la imagen;)
karlphillip
60

Escribí el código en Matlab R2007a. Usé k-means para extraer aproximadamente el árbol de navidad. Mostraré mi resultado intermedio solo con una imagen, y los resultados finales con las seis.

Primero, asigné el espacio RGB al espacio Lab, lo que podría mejorar el contraste del rojo en su canal b:

colorTransform = makecform('srgb2lab');
I = applycform(I, colorTransform);
L = double(I(:,:,1));
a = double(I(:,:,2));
b = double(I(:,:,3));

ingrese la descripción de la imagen aquí

Además de la característica en el espacio de color, también utilicé la característica de textura que es relevante con el vecindario en lugar de cada píxel en sí. Aquí combiné linealmente la intensidad de los 3 canales originales (R, G, B). La razón por la que he formateado de esta manera es porque todos los árboles de navidad en la imagen tienen luces rojas y, a veces, también iluminación verde / azul.

R=double(Irgb(:,:,1));
G=double(Irgb(:,:,2));
B=double(Irgb(:,:,3));
I0 = (3*R + max(G,B)-min(G,B))/2;

ingrese la descripción de la imagen aquí

Apliqué un patrón binario local 3X3 I0, utilicé el píxel central como umbral y obtuve el contraste calculando la diferencia entre el valor medio de intensidad de píxel por encima del umbral y el valor medio por debajo de él.

I0_copy = zeros(size(I0));
for i = 2 : size(I0,1) - 1
    for j = 2 : size(I0,2) - 1
        tmp = I0(i-1:i+1,j-1:j+1) >= I0(i,j);
        I0_copy(i,j) = mean(mean(tmp.*I0(i-1:i+1,j-1:j+1))) - ...
            mean(mean(~tmp.*I0(i-1:i+1,j-1:j+1))); % Contrast
    end
end

ingrese la descripción de la imagen aquí

Como tengo 4 funciones en total, elegiría K = 5 en mi método de agrupación. El código para k-means se muestra a continuación (es del curso de aprendizaje automático del Dr. Andrew Ng. Tomé el curso antes y escribí el código yo mismo en su tarea de programación).

[centroids, idx] = runkMeans(X, initial_centroids, max_iters);
mask=reshape(idx,img_size(1),img_size(2));

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function [centroids, idx] = runkMeans(X, initial_centroids, ...
                                  max_iters, plot_progress)
   [m n] = size(X);
   K = size(initial_centroids, 1);
   centroids = initial_centroids;
   previous_centroids = centroids;
   idx = zeros(m, 1);

   for i=1:max_iters    
      % For each example in X, assign it to the closest centroid
      idx = findClosestCentroids(X, centroids);

      % Given the memberships, compute new centroids
      centroids = computeCentroids(X, idx, K);

   end
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function idx = findClosestCentroids(X, centroids)
   K = size(centroids, 1);
   idx = zeros(size(X,1), 1);
   for xi = 1:size(X,1)
      x = X(xi, :);
      % Find closest centroid for x.
      best = Inf;
      for mui = 1:K
        mu = centroids(mui, :);
        d = dot(x - mu, x - mu);
        if d < best
           best = d;
           idx(xi) = mui;
        end
      end
   end 
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function centroids = computeCentroids(X, idx, K)
   [m n] = size(X);
   centroids = zeros(K, n);
   for mui = 1:K
      centroids(mui, :) = sum(X(idx == mui, :)) / sum(idx == mui);
   end

Como el programa se ejecuta muy lento en mi computadora, solo ejecuté 3 iteraciones. Normalmente, el criterio de detención es (i) tiempo de iteración de al menos 10, o (ii) ya no hay cambio en los centroides. Para mi prueba, aumentar la iteración puede diferenciar el fondo (cielo y árbol, cielo y edificio, ...) con mayor precisión, pero no mostró cambios drásticos en la extracción del árbol de navidad. También tenga en cuenta que k-means no es inmune a la inicialización del centroide aleatorio, por lo que se recomienda ejecutar el programa varias veces para hacer una comparación.

Después del medio k, I0se eligió la región marcada con la intensidad máxima de . Y el trazado de límites se utilizó para extraer los límites. Para mí, el último árbol de Navidad es el más difícil de extraer, ya que el contraste en esa imagen no es lo suficientemente alto como en los primeros cinco. Otro problema en mi método es que utilicé la bwboundariesfunción en Matlab para trazar el límite, pero a veces los límites internos también se incluyen como se puede observar en los resultados tercero, quinto y sexto. El lado oscuro dentro de los árboles de navidad no solo no se puede agrupar con el lado iluminado, sino que también conduce a tantos pequeños trazados de límites internos ( imfillno mejora mucho). En general, mi algoritmo todavía tiene mucho espacio de mejora.

Algunos publicaciones indican que el cambio medio puede ser más robusto que el medio k, y muchos algoritmos basados ​​en corte de gráficos también son muy competitivos en la segmentación de límites complicada. Escribí un algoritmo de cambio medio yo mismo, parece extraer mejor las regiones sin suficiente luz. Pero el cambio medio está un poco sobre segmentado, y se necesita alguna estrategia de fusión. Funcionó mucho más lento que k-means en mi computadora, me temo que tengo que renunciar. Espero ansiosamente ver que otros presenten excelentes resultados aquí con los algoritmos modernos mencionados anteriormente.

Sin embargo, siempre creo que la selección de características es el componente clave en la segmentación de imágenes. Con una selección de funciones adecuada que puede maximizar el margen entre el objeto y el fondo, muchos algoritmos de segmentación definitivamente funcionarán. Diferentes algoritmos pueden mejorar el resultado de 1 a 10, pero la selección de características puede mejorarlo de 0 a 1.

Feliz Navidad !

lennon310
fuente
2
¡Gracias por la respuesta! Solo quería señalar que Matlab no es de código abierto , pero Scilab sí . También me encantaría ver esta respuesta compitiendo con los demás. ;)
karlphillip
66
Gracias Karl Octave es otro software de código abierto que comparte casi la misma gramática de codificación con Matlab: mathworks.fr/matlabcentral/answers/14399-gnu-octave-vs-matlab .
lennon310
Interesante, no lo sabía, ¡gracias! ¿Su código funciona en Octave?
karlphillip
Todavía no lo he probado, pero creo que no hay problema :)
lennon310
@ lennon310 Creo que si baja los límites y obtiene el casco convexo, se librará del problema de los agujeros. Recuerde que el casco convexo es el área más pequeña que incluye todos los puntos de un conjunto.
sepdek
57

Esta es mi publicación final usando los enfoques tradicionales de procesamiento de imágenes ...

Aquí de alguna manera combino mis otras dos propuestas, logrando resultados aún mejores . De hecho, no puedo ver cómo estos resultados podrían ser mejores (especialmente cuando miras las imágenes enmascaradas que produce el método).

La esencia del enfoque es la combinación de tres supuestos clave :

  1. Las imágenes deben tener altas fluctuaciones en las regiones de los árboles.
  2. Las imágenes deben tener mayor intensidad en las regiones arbóreas.
  3. Las regiones de fondo deben tener baja intensidad y ser principalmente azuladas

Con estos supuestos en mente, el método funciona de la siguiente manera:

  1. Convierte las imágenes a HSV
  2. Filtre el canal V con un filtro LoG
  3. Aplique un umbral duro en la imagen filtrada de LoG para obtener la máscara de 'actividad' A
  4. Aplique un umbral duro al canal V para obtener la máscara de intensidad B
  5. Aplique el umbral del canal H para capturar regiones azuladas de baja intensidad en la máscara de fondo C
  6. Combina máscaras con AND para obtener la máscara final
  7. Dilatar la máscara para agrandar regiones y conectar píxeles dispersos
  8. Elimine las regiones pequeñas y obtenga la máscara final que eventualmente representará solo el árbol.

Aquí está el código en MATLAB (nuevamente, el script carga todas las imágenes jpg en la carpeta actual y, nuevamente, esto está lejos de ser un código optimizado):

% clear everything
clear;
pack;
close all;
close all hidden;
drawnow;
clc;

% initialization
ims=dir('./*.jpg');
imgs={};
images={}; 
blur_images={}; 
log_image={}; 
dilated_image={};
int_image={};
back_image={};
bin_image={};
measurements={};
box={};
num=length(ims);
thres_div = 3;

for i=1:num, 
    % load original image
    imgs{end+1}=imread(ims(i).name);

    % convert to HSV colorspace
    images{end+1}=rgb2hsv(imgs{i});

    % apply laplacian filtering and heuristic hard thresholding
    val_thres = (max(max(images{i}(:,:,3)))/thres_div);
    log_image{end+1} = imfilter( images{i}(:,:,3),fspecial('log')) > val_thres;

    % get the most bright regions of the image
    int_thres = 0.26*max(max( images{i}(:,:,3)));
    int_image{end+1} = images{i}(:,:,3) > int_thres;

    % get the most probable background regions of the image
    back_image{end+1} = images{i}(:,:,1)>(150/360) & images{i}(:,:,1)<(320/360) & images{i}(:,:,3)<0.5;

    % compute the final binary image by combining 
    % high 'activity' with high intensity
    bin_image{end+1} = logical( log_image{i}) & logical( int_image{i}) & ~logical( back_image{i});

    % apply morphological dilation to connect distonnected components
    strel_size = round(0.01*max(size(imgs{i})));        % structuring element for morphological dilation
    dilated_image{end+1} = imdilate( bin_image{i}, strel('disk',strel_size));

    % do some measurements to eliminate small objects
    measurements{i} = regionprops( logical( dilated_image{i}),'Area','BoundingBox');

    % iterative enlargement of the structuring element for better connectivity
    while length(measurements{i})>14 && strel_size<(min(size(imgs{i}(:,:,1)))/2),
        strel_size = round( 1.5 * strel_size);
        dilated_image{i} = imdilate( bin_image{i}, strel('disk',strel_size));
        measurements{i} = regionprops( logical( dilated_image{i}),'Area','BoundingBox');
    end

    for m=1:length(measurements{i})
        if measurements{i}(m).Area < 0.05*numel( dilated_image{i})
            dilated_image{i}( round(measurements{i}(m).BoundingBox(2):measurements{i}(m).BoundingBox(4)+measurements{i}(m).BoundingBox(2)),...
                round(measurements{i}(m).BoundingBox(1):measurements{i}(m).BoundingBox(3)+measurements{i}(m).BoundingBox(1))) = 0;
        end
    end
    % make sure the dilated image is the same size with the original
    dilated_image{i} = dilated_image{i}(1:size(imgs{i},1),1:size(imgs{i},2));
    % compute the bounding box
    [y,x] = find( dilated_image{i});
    if isempty( y)
        box{end+1}=[];
    else
        box{end+1} = [ min(x) min(y) max(x)-min(x)+1 max(y)-min(y)+1];
    end
end 

%%% additional code to display things
for i=1:num,
    figure;
    subplot(121);
    colormap gray;
    imshow( imgs{i});
    if ~isempty(box{i})
        hold on;
        rr = rectangle( 'position', box{i});
        set( rr, 'EdgeColor', 'r');
        hold off;
    end
    subplot(122);
    imshow( imgs{i}.*uint8(repmat(dilated_image{i},[1 1 3])));
end

Resultados

resultados

¡Los resultados de alta resolución aún están disponibles aquí!
Incluso más experimentos con imágenes adicionales se pueden encontrar aquí.

sepdek
fuente
1
¡Buena cosa! Asegúrese de que sus otras respuestas también sigan este formato. Para competir por la recompensa debe usar una tecnología de código abierto , y desafortunadamente Matlab no es uno de ellos. Sin embargo, SciLab y Octave son y proporcionan una sintaxis y funciones similares. ;)
karlphillip
El código de octava es el mismo ...
sepdek
@karlphillip De alguna manera, esta pregunta terminó teniendo una etiqueta de Matlab. Si el código abierto es realmente imprescindible, recomendaría eliminarlo.
Dennis Jaheruddin
@sepdek Muy bien, quizás aún se podría hacer algo para incluir los 'agujeros' en la imagen final. (¿Añadir todos los píxeles que están completamente rodeados por píxeles aprobados?)
Dennis Jaheruddin
1
@karlphillip gracias hombre! Me alegra que haya encontrado interesante mi enfoque. Además, me gustaría felicitarte por seleccionar la solución más elegante y no la que tiene más votos.
sepdek
36

Mis pasos de solución:

  1. Obtener canal R (de RGB): todas las operaciones que realizamos en este canal:

  2. Crear región de interés (ROI)

    • Umbral del canal R con valor mínimo 149 (imagen superior derecha)

    • Región de resultado de dilatación (imagen del centro a la izquierda)

  3. Detectar eges en roi calculado. El árbol tiene muchos bordes (imagen central derecha)

    • Resultado dilatar

    • Erosión con mayor radio (imagen inferior izquierda)

  4. Seleccione el objeto más grande (por área): es la región resultante

  5. Casco convexo (el árbol es un polígono convexo) (imagen inferior derecha)

  6. Cuadro delimitador (imagen inferior derecha - cuadro grren)

Paso a paso: ingrese la descripción de la imagen aquí

El primer resultado, el más simple pero no en el software de código abierto, "Adaptive Vision Studio + Adaptive Vision Library": este no es de código abierto, pero es muy rápido para crear un prototipo:

Algoritmo completo para detectar el árbol de navidad (11 bloques): Solución AVL

Próximo paso. Queremos una solución de código abierto. Cambie los filtros AVL a filtros OpenCV: aquí hice pequeños cambios, p. Ej., La detección de bordes usó el filtro cvCanny, para respetar el aspecto real, multipliqué la imagen de región con la imagen de bordes, para seleccionar el elemento más grande que utilicé findContours + contourArea pero la idea es la misma.

https://www.youtube.com/watch?v=sfjB3MigLH0&index=1&list=UUpSRrkMHNHiLDXgylwhWNQQ

Solución OpenCV

Ahora no puedo mostrar imágenes con pasos intermedios porque solo puedo poner 2 enlaces.

Ok, ahora usamos filtros openSource pero aún no es de código abierto completo. Último paso: puerto a código c ++. Usé OpenCV en la versión 2.4.4

El resultado del código final de c ++ es: ingrese la descripción de la imagen aquí

El código c ++ también es bastante corto:

#include "opencv2/highgui/highgui.hpp"
#include "opencv2/opencv.hpp"
#include <algorithm>
using namespace cv;

int main()
{

    string images[6] = {"..\\1.png","..\\2.png","..\\3.png","..\\4.png","..\\5.png","..\\6.png"};

    for(int i = 0; i < 6; ++i)
    {
        Mat img, thresholded, tdilated, tmp, tmp1;
        vector<Mat> channels(3);

        img = imread(images[i]);
        split(img, channels);
        threshold( channels[2], thresholded, 149, 255, THRESH_BINARY);                      //prepare ROI - threshold
        dilate( thresholded, tdilated,  getStructuringElement( MORPH_RECT, Size(22,22) ) ); //prepare ROI - dilate
        Canny( channels[2], tmp, 75, 125, 3, true );    //Canny edge detection
        multiply( tmp, tdilated, tmp1 );    // set ROI

        dilate( tmp1, tmp, getStructuringElement( MORPH_RECT, Size(20,16) ) ); // dilate
        erode( tmp, tmp1, getStructuringElement( MORPH_RECT, Size(36,36) ) ); // erode

        vector<vector<Point> > contours, contours1(1);
        vector<Point> convex;
        vector<Vec4i> hierarchy;
        findContours( tmp1, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE, Point(0, 0) );

        //get element of maximum area
        //int bestID = std::max_element( contours.begin(), contours.end(), 
        //  []( const vector<Point>& A, const vector<Point>& B ) { return contourArea(A) < contourArea(B); } ) - contours.begin();

            int bestID = 0;
        int bestArea = contourArea( contours[0] );
        for( int i = 1; i < contours.size(); ++i )
        {
            int area = contourArea( contours[i] );
            if( area > bestArea )
            {
                bestArea  = area;
                bestID = i;
            }
        }

        convexHull( contours[bestID], contours1[0] ); 
        drawContours( img, contours1, 0, Scalar( 100, 100, 255 ), img.rows / 100, 8, hierarchy, 0, Point() );

        imshow("image", img );
        waitKey(0);
    }


    return 0;
}
AdamF
fuente
¿Qué compilador puede construir este programa sin errores?
karlphillip
Usé Visual Studio 2012 para construirlo. Debe usar el compilador de c ++ con soporte c ++ 11.
AdamF
No tengo un sistema a mi disposición con eso. ¿Podrías reescribir la std::max_element()llamada? Me gustaría recompensar su respuesta también. Creo que tengo gcc 4.2.
karlphillip
Ok, esta es la función c ++ 11;) Cambié el código fuente anterior. Por favor, intente ahora.
AdamF
OK gracias. Lo probé y es hermoso. Tan pronto como se vuelva a abrir esta pregunta (otros usuarios deben ayudarme con eso) puedo establecer otra recompensa para recompensarlo. ¡Felicidades!
karlphillip
31

... otra solución pasada de moda, basada exclusivamente en el procesamiento HSV :

  1. Convierta imágenes al espacio de color HSV
  2. Crear máscaras de acuerdo con la heurística en el HSV (ver más abajo)
  3. Aplique dilatación morfológica a la máscara para conectar áreas desconectadas.
  4. Deseche áreas pequeñas y bloques horizontales (recuerde que los árboles son bloques verticales)
  5. Calcule el cuadro delimitador

Una palabra sobre la heurística en el procesamiento de HSV:

  1. todo con Hues (H) entre 210 - 320 grados se descarta como azul magenta que se supone que está en el fondo o en áreas no relevantes
  2. todo con valores (V) inferiores a 40% también se descarta como demasiado oscuro para ser relevante

Por supuesto, uno puede experimentar con muchas otras posibilidades para afinar este enfoque ...

Aquí está el código de MATLAB para hacer el truco (advertencia: ¡el código está lejos de ser optimizado! Utilicé técnicas no recomendadas para la programación de MATLAB solo para poder rastrear cualquier cosa en el proceso; esto puede optimizarse enormemente):

% clear everything
clear;
pack;
close all;
close all hidden;
drawnow;
clc;

% initialization
ims=dir('./*.jpg');
num=length(ims);

imgs={};
hsvs={}; 
masks={};
dilated_images={};
measurements={};
boxs={};

for i=1:num, 
    % load original image
    imgs{end+1} = imread(ims(i).name);
    flt_x_size = round(size(imgs{i},2)*0.005);
    flt_y_size = round(size(imgs{i},1)*0.005);
    flt = fspecial( 'average', max( flt_y_size, flt_x_size));
    imgs{i} = imfilter( imgs{i}, flt, 'same');
    % convert to HSV colorspace
    hsvs{end+1} = rgb2hsv(imgs{i});
    % apply a hard thresholding and binary operation to construct the mask
    masks{end+1} = medfilt2( ~(hsvs{i}(:,:,1)>(210/360) & hsvs{i}(:,:,1)<(320/360))&hsvs{i}(:,:,3)>0.4);
    % apply morphological dilation to connect distonnected components
    strel_size = round(0.03*max(size(imgs{i})));        % structuring element for morphological dilation
    dilated_images{end+1} = imdilate( masks{i}, strel('disk',strel_size));
    % do some measurements to eliminate small objects
    measurements{i} = regionprops( dilated_images{i},'Perimeter','Area','BoundingBox'); 
    for m=1:length(measurements{i})
        if (measurements{i}(m).Area < 0.02*numel( dilated_images{i})) || (measurements{i}(m).BoundingBox(3)>1.2*measurements{i}(m).BoundingBox(4))
            dilated_images{i}( round(measurements{i}(m).BoundingBox(2):measurements{i}(m).BoundingBox(4)+measurements{i}(m).BoundingBox(2)),...
                round(measurements{i}(m).BoundingBox(1):measurements{i}(m).BoundingBox(3)+measurements{i}(m).BoundingBox(1))) = 0;
        end
    end
    dilated_images{i} = dilated_images{i}(1:size(imgs{i},1),1:size(imgs{i},2));
    % compute the bounding box
    [y,x] = find( dilated_images{i});
    if isempty( y)
        boxs{end+1}=[];
    else
        boxs{end+1} = [ min(x) min(y) max(x)-min(x)+1 max(y)-min(y)+1];
    end

end 

%%% additional code to display things
for i=1:num,
    figure;
    subplot(121);
    colormap gray;
    imshow( imgs{i});
    if ~isempty(boxs{i})
        hold on;
        rr = rectangle( 'position', boxs{i});
        set( rr, 'EdgeColor', 'r');
        hold off;
    end
    subplot(122);
    imshow( imgs{i}.*uint8(repmat(dilated_images{i},[1 1 3])));
end

Resultados:

En los resultados muestro la imagen enmascarada y el cuadro delimitador. ingrese la descripción de la imagen aquí

sepdek
fuente
Hola gracias por la respuesta Tómese un momento para leer la sección Requisitos para asegurarse de que su respuesta siga todas las instrucciones. Olvidó compartir las imágenes resultantes. ;)
karlphillip
2
@karlphillip sepdek no tiene suficiente reputación para compartir imágenes, moví las imágenes al cuerpo de respuesta de acuerdo con su enlace e instrucciones. Sin embargo, no estoy seguro de que esos sean los correctos, no dude en comentar esta parte.
alko
@alko lo sé, gracias. Pero algunas de las imágenes que compartiste no estaban en el conjunto de entrada . La respuesta debe mostrar el resultado del procesamiento de las 6 imágenes compartidas en la pregunta.
karlphillip
@karlphillip esas son sus imágenes, no las mías. eso es exactamente lo que quise decir con "comentar esta parte";)
alko
2
Perdón por causar problemas ... no es mi intención. He incluido todas las imágenes en el conjunto de datos inicial y lo he mejorado aún más para demostrar que mi concepto es robusto ...
sepdek
23

Algún enfoque de procesamiento de imágenes a la antigua usanza ...
La idea se basa en la suposición de que las imágenes representan árboles iluminados en fondos típicamente más oscuros y suaves (o en primer plano en algunos casos). El área del árbol iluminado es más "enérgica" y tiene mayor intensidad .
El proceso es el siguiente:

  1. Convertir a graylevel
  2. Aplique el filtro LoG para obtener las áreas más "activas"
  3. Aplique un umbral intenso para obtener las áreas más brillantes.
  4. Combina los 2 anteriores para obtener una máscara preliminar
  5. Aplique una dilatación morfológica para agrandar áreas y conectar componentes vecinos.
  6. Eliminar pequeñas áreas candidatas de acuerdo con el tamaño de su área

Lo que obtienes es una máscara binaria y un cuadro delimitador para cada imagen.

Aquí están los resultados usando esta técnica ingenua: ingrese la descripción de la imagen aquí

El código en MATLAB sigue: El código se ejecuta en una carpeta con imágenes JPG. Carga todas las imágenes y devuelve los resultados detectados.

% clear everything
clear;
pack;
close all;
close all hidden;
drawnow;
clc;

% initialization
ims=dir('./*.jpg');
imgs={};
images={}; 
blur_images={}; 
log_image={}; 
dilated_image={};
int_image={};
bin_image={};
measurements={};
box={};
num=length(ims);
thres_div = 3;

for i=1:num, 
    % load original image
    imgs{end+1}=imread(ims(i).name);

    % convert to grayscale
    images{end+1}=rgb2gray(imgs{i});

    % apply laplacian filtering and heuristic hard thresholding
    val_thres = (max(max(images{i}))/thres_div);
    log_image{end+1} = imfilter( images{i},fspecial('log')) > val_thres;

    % get the most bright regions of the image
    int_thres = 0.26*max(max( images{i}));
    int_image{end+1} = images{i} > int_thres;

    % compute the final binary image by combining 
    % high 'activity' with high intensity
    bin_image{end+1} = log_image{i} .* int_image{i};

    % apply morphological dilation to connect distonnected components
    strel_size = round(0.01*max(size(imgs{i})));        % structuring element for morphological dilation
    dilated_image{end+1} = imdilate( bin_image{i}, strel('disk',strel_size));

    % do some measurements to eliminate small objects
    measurements{i} = regionprops( logical( dilated_image{i}),'Area','BoundingBox');
    for m=1:length(measurements{i})
        if measurements{i}(m).Area < 0.05*numel( dilated_image{i})
            dilated_image{i}( round(measurements{i}(m).BoundingBox(2):measurements{i}(m).BoundingBox(4)+measurements{i}(m).BoundingBox(2)),...
                round(measurements{i}(m).BoundingBox(1):measurements{i}(m).BoundingBox(3)+measurements{i}(m).BoundingBox(1))) = 0;
        end
    end
    % make sure the dilated image is the same size with the original
    dilated_image{i} = dilated_image{i}(1:size(imgs{i},1),1:size(imgs{i},2));
    % compute the bounding box
    [y,x] = find( dilated_image{i});
    if isempty( y)
        box{end+1}=[];
    else
        box{end+1} = [ min(x) min(y) max(x)-min(x)+1 max(y)-min(y)+1];
    end
end 

%%% additional code to display things
for i=1:num,
    figure;
    subplot(121);
    colormap gray;
    imshow( imgs{i});
    if ~isempty(box{i})
        hold on;
        rr = rectangle( 'position', box{i});
        set( rr, 'EdgeColor', 'r');
        hold off;
    end
    subplot(122);
    imshow( imgs{i}.*uint8(repmat(dilated_image{i},[1 1 3])));
end
sepdek
fuente
No olvides cargar las imágenes resultantes, como hizo Fausto.
karlphillip
Soy un novato aquí, así que no puedo subir imágenes. Vea los resultados en los enlaces provistos en mi descripción.
sepdek
Ok, pero todavía tienes que usar las imágenes compartidas en la pregunta como lo están haciendo todos los demás. Una vez que los procese, cárguelos en algún lugar y edite su respuesta para agregar los enlaces. Luego editaré tu respuesta y colocaré las imágenes dentro de ti.
karlphillip
El enlace parece contener las imágenes correctas ahora.
Dennis Jaheruddin
22

Usando un enfoque bastante diferente de lo que he visto, creé un guión que detecta los árboles de navidad por sus luces. El resultado es siempre un triángulo simétrico y, si es necesario, valores numéricos como el ángulo ("gordura") del árbol.

La mayor amenaza para este algoritmo obviamente son las luces al lado (en grandes cantidades) o frente al árbol (el mayor problema hasta una mayor optimización). Editar (agregado): Lo que no puede hacer: averiguar si hay un árbol de Navidad o no, encontrar varios árboles de Navidad en una imagen, detectar correctamente un árbol de Navidad en el medio de Las Vegas, detectar árboles de Navidad que estén muy doblados, al revés o picado ...;)

Las diferentes etapas son:

  • Calcule el brillo agregado (R + G + B) para cada píxel
  • Suma este valor de los 8 píxeles vecinos encima de cada píxel
  • Clasifique todos los píxeles por este valor (el más brillante primero) - Lo sé, no es realmente sutil ...
  • Elija N de estos, comenzando desde arriba, omitiendo los que están demasiado cerca
  • Calcula el de estos N superiores (nos da el centro aproximado del árbol)
  • Comience desde la posición media hacia arriba en un haz de búsqueda cada vez más amplio para obtener la luz superior de las más brillantes seleccionadas (las personas tienden a colocar al menos una luz en la parte superior)
  • A partir de ahí, imagine líneas que van 60 grados hacia la izquierda y hacia la derecha hacia abajo (los árboles de navidad no deberían ser tan gordos)
  • Disminuya esos 60 grados hasta que el 20% de las luces más brillantes estén fuera de este triángulo
  • Encuentra la luz en la parte inferior del triángulo, dándote el borde horizontal inferior del árbol.
  • Hecho

Explicación de las marcas:

  • Gran cruz roja en el centro del árbol: mediana de las N luces más brillantes
  • Línea punteada desde allí hacia arriba: "haz de búsqueda" para la parte superior del árbol
  • Cruz roja más pequeña: parte superior del árbol
  • Cruces rojas realmente pequeñas: todas las luces N más brillantes
  • Triángulo rojo: D'uh!

Código fuente:

<?php

ini_set('memory_limit', '1024M');

header("Content-type: image/png");

$chosenImage = 6;

switch($chosenImage){
    case 1:
        $inputImage     = imagecreatefromjpeg("nmzwj.jpg");
        break;
    case 2:
        $inputImage     = imagecreatefromjpeg("2y4o5.jpg");
        break;
    case 3:
        $inputImage     = imagecreatefromjpeg("YowlH.jpg");
        break;
    case 4:
        $inputImage     = imagecreatefromjpeg("2K9Ef.jpg");
        break;
    case 5:
        $inputImage     = imagecreatefromjpeg("aVZhC.jpg");
        break;
    case 6:
        $inputImage     = imagecreatefromjpeg("FWhSP.jpg");
        break;
    case 7:
        $inputImage     = imagecreatefromjpeg("roemerberg.jpg");
        break;
    default:
        exit();
}

// Process the loaded image

$topNspots = processImage($inputImage);

imagejpeg($inputImage);
imagedestroy($inputImage);

// Here be functions

function processImage($image) {
    $orange = imagecolorallocate($image, 220, 210, 60);
    $black = imagecolorallocate($image, 0, 0, 0);
    $red = imagecolorallocate($image, 255, 0, 0);

    $maxX = imagesx($image)-1;
    $maxY = imagesy($image)-1;

    // Parameters
    $spread = 1; // Number of pixels to each direction that will be added up
    $topPositions = 80; // Number of (brightest) lights taken into account
    $minLightDistance = round(min(array($maxX, $maxY)) / 30); // Minimum number of pixels between the brigtests lights
    $searchYperX = 5; // spread of the "search beam" from the median point to the top

    $renderStage = 3; // 1 to 3; exits the process early


    // STAGE 1
    // Calculate the brightness of each pixel (R+G+B)

    $maxBrightness = 0;
    $stage1array = array();

    for($row = 0; $row <= $maxY; $row++) {

        $stage1array[$row] = array();

        for($col = 0; $col <= $maxX; $col++) {

            $rgb = imagecolorat($image, $col, $row);
            $brightness = getBrightnessFromRgb($rgb);
            $stage1array[$row][$col] = $brightness;

            if($renderStage == 1){
                $brightnessToGrey = round($brightness / 765 * 256);
                $greyRgb = imagecolorallocate($image, $brightnessToGrey, $brightnessToGrey, $brightnessToGrey);
                imagesetpixel($image, $col, $row, $greyRgb);
            }

            if($brightness > $maxBrightness) {
                $maxBrightness = $brightness;
                if($renderStage == 1){
                    imagesetpixel($image, $col, $row, $red);
                }
            }
        }
    }
    if($renderStage == 1) {
        return;
    }


    // STAGE 2
    // Add up brightness of neighbouring pixels

    $stage2array = array();
    $maxStage2 = 0;

    for($row = 0; $row <= $maxY; $row++) {
        $stage2array[$row] = array();

        for($col = 0; $col <= $maxX; $col++) {
            if(!isset($stage2array[$row][$col])) $stage2array[$row][$col] = 0;

            // Look around the current pixel, add brightness
            for($y = $row-$spread; $y <= $row+$spread; $y++) {
                for($x = $col-$spread; $x <= $col+$spread; $x++) {

                    // Don't read values from outside the image
                    if($x >= 0 && $x <= $maxX && $y >= 0 && $y <= $maxY){
                        $stage2array[$row][$col] += $stage1array[$y][$x]+10;
                    }
                }
            }

            $stage2value = $stage2array[$row][$col];
            if($stage2value > $maxStage2) {
                $maxStage2 = $stage2value;
            }
        }
    }

    if($renderStage >= 2){
        // Paint the accumulated light, dimmed by the maximum value from stage 2
        for($row = 0; $row <= $maxY; $row++) {
            for($col = 0; $col <= $maxX; $col++) {
                $brightness = round($stage2array[$row][$col] / $maxStage2 * 255);
                $greyRgb = imagecolorallocate($image, $brightness, $brightness, $brightness);
                imagesetpixel($image, $col, $row, $greyRgb);
            }
        }
    }

    if($renderStage == 2) {
        return;
    }


    // STAGE 3

    // Create a ranking of bright spots (like "Top 20")
    $topN = array();

    for($row = 0; $row <= $maxY; $row++) {
        for($col = 0; $col <= $maxX; $col++) {

            $stage2Brightness = $stage2array[$row][$col];
            $topN[$col.":".$row] = $stage2Brightness;
        }
    }
    arsort($topN);

    $topNused = array();
    $topPositionCountdown = $topPositions;

    if($renderStage == 3){
        foreach ($topN as $key => $val) {
            if($topPositionCountdown <= 0){
                break;
            }

            $position = explode(":", $key);

            foreach($topNused as $usedPosition => $usedValue) {
                $usedPosition = explode(":", $usedPosition);
                $distance = abs($usedPosition[0] - $position[0]) + abs($usedPosition[1] - $position[1]);
                if($distance < $minLightDistance) {
                    continue 2;
                }
            }

            $topNused[$key] = $val;

            paintCrosshair($image, $position[0], $position[1], $red, 2);

            $topPositionCountdown--;

        }
    }


    // STAGE 4
    // Median of all Top N lights
    $topNxValues = array();
    $topNyValues = array();

    foreach ($topNused as $key => $val) {
        $position = explode(":", $key);
        array_push($topNxValues, $position[0]);
        array_push($topNyValues, $position[1]);
    }

    $medianXvalue = round(calculate_median($topNxValues));
    $medianYvalue = round(calculate_median($topNyValues));
    paintCrosshair($image, $medianXvalue, $medianYvalue, $red, 15);


    // STAGE 5
    // Find treetop

    $filename = 'debug.log';
    $handle = fopen($filename, "w");
    fwrite($handle, "\n\n STAGE 5");

    $treetopX = $medianXvalue;
    $treetopY = $medianYvalue;

    $searchXmin = $medianXvalue;
    $searchXmax = $medianXvalue;

    $width = 0;
    for($y = $medianYvalue; $y >= 0; $y--) {
        fwrite($handle, "\nAt y = ".$y);

        if(($y % $searchYperX) == 0) { // Modulo
            $width++;
            $searchXmin = $medianXvalue - $width;
            $searchXmax = $medianXvalue + $width;
            imagesetpixel($image, $searchXmin, $y, $red);
            imagesetpixel($image, $searchXmax, $y, $red);
        }

        foreach ($topNused as $key => $val) {
            $position = explode(":", $key); // "x:y"

            if($position[1] != $y){
                continue;
            }

            if($position[0] >= $searchXmin && $position[0] <= $searchXmax){
                $treetopX = $position[0];
                $treetopY = $y;
            }
        }

    }

    paintCrosshair($image, $treetopX, $treetopY, $red, 5);


    // STAGE 6
    // Find tree sides
    fwrite($handle, "\n\n STAGE 6");

    $treesideAngle = 60; // The extremely "fat" end of a christmas tree
    $treeBottomY = $treetopY;

    $topPositionsExcluded = 0;
    $xymultiplier = 0;
    while(($topPositionsExcluded < ($topPositions / 5)) && $treesideAngle >= 1){
        fwrite($handle, "\n\nWe're at angle ".$treesideAngle);
        $xymultiplier = sin(deg2rad($treesideAngle));
        fwrite($handle, "\nMultiplier: ".$xymultiplier);

        $topPositionsExcluded = 0;
        foreach ($topNused as $key => $val) {
            $position = explode(":", $key);
            fwrite($handle, "\nAt position ".$key);

            if($position[1] > $treeBottomY) {
                $treeBottomY = $position[1];
            }

            // Lights above the tree are outside of it, but don't matter
            if($position[1] < $treetopY){
                $topPositionsExcluded++;
                fwrite($handle, "\nTOO HIGH");
                continue;
            }

            // Top light will generate division by zero
            if($treetopY-$position[1] == 0) {
                fwrite($handle, "\nDIVISION BY ZERO");
                continue;
            }

            // Lights left end right of it are also not inside
            fwrite($handle, "\nLight position factor: ".(abs($treetopX-$position[0]) / abs($treetopY-$position[1])));
            if((abs($treetopX-$position[0]) / abs($treetopY-$position[1])) > $xymultiplier){
                $topPositionsExcluded++;
                fwrite($handle, "\n --- Outside tree ---");
            }
        }

        $treesideAngle--;
    }
    fclose($handle);

    // Paint tree's outline
    $treeHeight = abs($treetopY-$treeBottomY);
    $treeBottomLeft = 0;
    $treeBottomRight = 0;
    $previousState = false; // line has not started; assumes the tree does not "leave"^^

    for($x = 0; $x <= $maxX; $x++){
        if(abs($treetopX-$x) != 0 && abs($treetopX-$x) / $treeHeight > $xymultiplier){
            if($previousState == true){
                $treeBottomRight = $x;
                $previousState = false;
            }
            continue;
        }
        imagesetpixel($image, $x, $treeBottomY, $red);
        if($previousState == false){
            $treeBottomLeft = $x;
            $previousState = true;
        }
    }
    imageline($image, $treeBottomLeft, $treeBottomY, $treetopX, $treetopY, $red);
    imageline($image, $treeBottomRight, $treeBottomY, $treetopX, $treetopY, $red);


    // Print out some parameters

    $string = "Min dist: ".$minLightDistance." | Tree angle: ".$treesideAngle." deg | Tree bottom: ".$treeBottomY;

    $px     = (imagesx($image) - 6.5 * strlen($string)) / 2;
    imagestring($image, 2, $px, 5, $string, $orange);

    return $topN;
}

/**
 * Returns values from 0 to 765
 */
function getBrightnessFromRgb($rgb) {
    $r = ($rgb >> 16) & 0xFF;
    $g = ($rgb >> 8) & 0xFF;
    $b = $rgb & 0xFF;

    return $r+$r+$b;
}

function paintCrosshair($image, $posX, $posY, $color, $size=5) {
    for($x = $posX-$size; $x <= $posX+$size; $x++) {
        if($x>=0 && $x < imagesx($image)){
            imagesetpixel($image, $x, $posY, $color);
        }
    }
    for($y = $posY-$size; $y <= $posY+$size; $y++) {
        if($y>=0 && $y < imagesy($image)){
            imagesetpixel($image, $posX, $y, $color);
        }
    }
}

// From http://www.mdj.us/web-development/php-programming/calculating-the-median-average-values-of-an-array-with-php/
function calculate_median($arr) {
    sort($arr);
    $count = count($arr); //total numbers in array
    $middleval = floor(($count-1)/2); // find the middle value, or the lowest middle value
    if($count % 2) { // odd number, middle is the median
        $median = $arr[$middleval];
    } else { // even number, calculate avg of 2 medians
        $low = $arr[$middleval];
        $high = $arr[$middleval+1];
        $median = (($low+$high)/2);
    }
    return $median;
}


?>

Imágenes: Arriba a la izquierda Centro inferior Abajo a la izquierda Superior derecha Centro superior Inferior derecha

Bono: un alemán Weihnachtsbaum, de Wikipedia Römerberg http://commons.wikimedia.org/wiki/File:Weihnachtsbaum_R%C3%B6merberg.jpg

cristiano
fuente
17

Usé python con opencv.

Mi algoritmo es así:

  1. Primero toma el canal rojo de la imagen.
  2. Aplicar un umbral (valor mínimo 200) al canal rojo
  3. Luego aplique el Gradiente Morfológico y luego haga un 'Cierre' (dilatación seguida de Erosión)
  4. Luego encuentra los contornos en el plano y elige el contorno más largo.

El resultado:

El código:

import numpy as np
import cv2
import copy


def findTree(image,num):
    im = cv2.imread(image)
    im = cv2.resize(im, (400,250))
    gray = cv2.cvtColor(im, cv2.COLOR_RGB2GRAY)
    imf = copy.deepcopy(im)

    b,g,r = cv2.split(im)
    minR = 200
    _,thresh = cv2.threshold(r,minR,255,0)
    kernel = np.ones((25,5))
    dst = cv2.morphologyEx(thresh, cv2.MORPH_GRADIENT, kernel)
    dst = cv2.morphologyEx(dst, cv2.MORPH_CLOSE, kernel)

    contours = cv2.findContours(dst,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)[0]
    cv2.drawContours(im, contours,-1, (0,255,0), 1)

    maxI = 0
    for i in range(len(contours)):
        if len(contours[maxI]) < len(contours[i]):
            maxI = i

    img = copy.deepcopy(r)
    cv2.polylines(img,[contours[maxI]],True,(255,255,255),3)
    imf[:,:,2] = img

    cv2.imshow(str(num), imf)

def main():
    findTree('tree.jpg',1)
    findTree('tree2.jpg',2)
    findTree('tree3.jpg',3)
    findTree('tree4.jpg',4)
    findTree('tree5.jpg',5)
    findTree('tree6.jpg',6)

    cv2.waitKey(0)
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()

Si cambio el núcleo de (25,5) a (10,5) obtengo mejores resultados en todos los árboles, excepto en la parte inferior izquierda, ingrese la descripción de la imagen aquí

mi algoritmo asume que el árbol tiene luces, y en el árbol inferior izquierdo, la parte superior tiene menos luz que las demás.

ifryed
fuente