Como detectar uma árvore de Natal? [fechadas]


382

Quais técnicas de processamento de imagem podem ser usadas para implementar um aplicativo que detecta as árvores de Natal exibidas nas imagens a seguir?

Estou procurando soluções que funcionem em todas essas imagens. Portanto, abordagens que requerem treinamento de classificadores em cascata de haar ou correspondência de modelos não são muito interessantes.

Estou procurando algo que possa ser escrito em qualquer linguagem de programação, desde que use apenas tecnologias de código aberto . A solução deve ser testada com as imagens compartilhadas nesta pergunta. Existem 6 imagens de entrada e a resposta deve exibir os resultados do processamento de cada uma delas. Finalmente, para cada imagem de saída deve haver linhas vermelhas desenhadas para cercar a árvore detectada.

Como você detectaria programaticamente as árvores nessas imagens?


3
É permitido o uso de algumas imagens para treinamento ou todas as imagens fornecidas devem ser usadas para validação? De qualquer maneira, a concorrência é legal: D
Hannes Ovrén 25/12/2013

7
@karlphillip, você quer que usemos essas imagens para testes e outras imagens para treinamento? Só que não está claro qual é o conjunto de treinamento.
GilLevi

16
@karlphillip: Meu conselho: abandone o requisito "código aberto". Realmente não importa qual idioma / estrutura você usa. Os algoritmos de processamento de imagem / visão computacional são independentes da linguagem; portanto, se você pode escrevê-lo no MATLAB, certamente pode fazê-lo no OpenCV ou em qualquer outra estrutura de sua preferência ... Além disso, ainda não estou claro o que você considera treinando / testando imagens !
Amro

2
@karlphillip thanx por mobilizar todos nós para contribuir com essa 'busca' sua! Foi uma ótima oportunidade de passar algumas horas produtivamente, mas o mais importante, para ver quantas abordagens diferentes podem ser encontradas para um único problema ... Espero que você faça isso novamente no dia 1º de janeiro (talvez um trenó 'Papai Noel' desafio? ;-))
sepdek

2
OK, reformulei a pergunta para remover os elementos da competição. Eu acho que isso deve permitir que ele se mantenha por conta própria.
Brad Larson

Respostas:


184

Eu tenho uma abordagem que eu acho interessante e um pouco diferente do resto. A principal diferença em minha abordagem, em comparação com algumas outras, está na maneira como a etapa de segmentação da imagem é executada - usei o algoritmo de agrupamento DBSCAN do scikit -learn do Python; é otimizado para encontrar formas um tanto amorfas que podem não ter necessariamente um único centróide claro.

No nível superior, minha abordagem é bastante simples e pode ser dividida em três etapas. Primeiro aplico um limite (ou, na verdade, o lógico "ou" de dois limites separados e distintos). Como em muitas outras respostas, presumi que a árvore de Natal seria um dos objetos mais brilhantes da cena; portanto, o primeiro limiar é apenas um simples teste de brilho monocromático; todos os pixels com valores acima de 220 em uma escala de 0 a 255 (onde preto é 0 e branco é 255) são salvos em uma imagem em preto e branco binária. O segundo limiar tenta procurar luzes vermelhas e amarelas, que são particularmente proeminentes nas árvores no canto superior esquerdo e no canto inferior direito das seis imagens, e se destacam bem contra o fundo azul esverdeado, predominante na maioria das fotos. Eu converto a imagem rgb em espaço hsv, e exija que o matiz seja menor que 0,2 em uma escala de 0,0-1,0 (correspondendo aproximadamente à borda entre amarelo e verde) ou maior que 0,95 (correspondente à borda entre roxo e vermelho) e, além disso, preciso de cores saturadas e brilhantes: a saturação e o valor devem estar acima de 0,7. Os resultados dos dois procedimentos de limite são logicamente "ou" juntos, e a matriz resultante de imagens binárias em preto e branco é mostrada abaixo:

Árvores de Natal, após limiar no HSV, bem como brilho monocromático

Você pode ver claramente que cada imagem possui um grande agrupamento de pixels, correspondendo aproximadamente à localização de cada árvore, e algumas também têm outros pequenos agrupamentos, correspondendo a luzes nas janelas de alguns dos edifícios ou a um cena de fundo no horizonte. A próxima etapa é fazer com que o computador reconheça que esses são clusters separados e rotule cada pixel corretamente com um número de identificação de associação ao cluster.

Para esta tarefa, escolhi o DBSCAN . Há uma boa comparação visual de como o DBSCAN normalmente se comporta, em relação a outros algoritmos de cluster, disponíveis aqui . Como eu disse anteriormente, ele se dá bem com formas amorfas. A saída do DBSCAN, com cada cluster plotado em uma cor diferente, é mostrada aqui:

Saída de armazenamento em cluster do DBSCAN

Há algumas coisas a serem observadas ao analisar esse resultado. Primeiro, o DBSCAN exige que o usuário defina um parâmetro de "proximidade" para regular seu comportamento, que controla efetivamente a separação de um par de pontos para que o algoritmo declare um novo cluster separado em vez de aglomerar um ponto de teste no um cluster já pré-existente. Defino esse valor como 0,04 vezes o tamanho na diagonal de cada imagem. Como as imagens variam em tamanho, de aproximadamente VGA a HD 1080, esse tipo de definição relativa à escala é crítico.

Outro ponto digno de nota é que o algoritmo DBSCAN, conforme implementado no scikit-learn, possui limites de memória bastante desafiadores para algumas das imagens maiores desta amostra. Portanto, para algumas das imagens maiores, eu realmente tive que "dizimar" (ou seja, reter apenas todos os 3 ou 4 pixels e soltar os outros) cada cluster para permanecer dentro desse limite. Como resultado desse processo de seleção, os pixels esparsos individuais restantes são difíceis de visualizar em algumas das imagens maiores. Portanto, apenas para fins de exibição, os pixels codificados por cores nas imagens acima foram efetivamente "dilatados" apenas um pouco para que se destacem melhor. É uma operação puramente cosmética em prol da narrativa; embora haja comentários mencionando essa dilatação no meu código,

Depois que os clusters são identificados e rotulados, a terceira e última etapa é fácil: eu simplesmente pego o maior cluster de cada imagem (nesse caso, escolhi medir o "tamanho" em termos do número total de pixels de membros, embora se possa com a mesma facilidade, utilizaram algum tipo de métrica que mede a extensão física) e calculam o casco convexo para esse cluster. O casco convexo torna-se então a borda da árvore. Os seis cascos convexos calculados por esse método são mostrados abaixo em vermelho:

Árvores de Natal com bordas calculadas

O código fonte foi escrito para o Python 2.7.6 e depende do numpy , scipy , matplotlib e scikit-learn . Dividi em duas partes. A primeira parte é responsável pelo processamento real da imagem:

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

e a segunda parte é um script no nível do usuário que chama o primeiro arquivo e gera todos os gráficos acima:

#!/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()

A solução da @ lennon310 está em cluster. (k-means)
user3054997

11
@ stachyra Eu também pensei sobre essa abordagem antes de propor minhas mais simples. Eu acho que isso tem um grande potencial de ser estendido e generalizado para produzir bons resultados em outros casos também. Você pode experimentar redes neurais para agrupar. Algo como um SOM ou gás neural faria um excelente trabalho. No entanto, ótima proposta e aprovação de mim!
sepdek

4
@Faust & Ryan Carlson: obrigado, pessoal! Sim, eu concordo que o sistema upvote, embora funcione bem para julgar entre 2 ou 3 respostas curtas, todas enviadas dentro de algumas horas uma da outra, tem preconceitos sérios quando se trata de concursos com respostas longas que se desenrolam por longos períodos de tempo . Por um lado, os envios iniciais começam a acumular upvotes antes que os posteriores estejam disponíveis para revisão pública. E se todas as respostas são longas, assim que se estabelece uma vantagem modesta, geralmente há um "efeito de onda", pois as pessoas apenas votam na primeira sem se preocupar em ler o resto.
Stachyra

2
@stachyra grande notícia amigo! Parabéns mais caloroso e que isso possa marcar o começo de seu novo ano!
sepdek

11
@ lennon310: Ainda não tentei um filtro de detecção máxima local para esse problema, mas se você quiser explorá-lo, o scipy inclui esse . Meu código-fonte Python para este projeto foi tão curto que eu consegui publicar 100% dele; literalmente, tudo o que você precisa fazer é copiar e colar meus dois trechos de código em arquivos .py separados e, em seguida, substituir uma chamada scipy.ndimage.filters.maximum_filter()no mesmo local em que eu havia usado um limite.
Stachyra

145

NOTA DE EDIÇÃO: Editei este post para (i) processar cada imagem da árvore individualmente, conforme solicitado nos requisitos, (ii) para considerar o brilho e a forma do objeto, a fim de melhorar a qualidade do resultado.


A seguir, é apresentada uma abordagem que leva em consideração o brilho e a forma do objeto. Em outras palavras, procura objetos com formato de triângulo e com brilho significativo. Foi implementado em Java, usando Marvin estrutura de processamento de imagem .

O primeiro passo é o limiar de cores. O objetivo aqui é focar a análise em objetos com brilho significativo.

imagens de saída:

Código fonte:

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

Na segunda etapa, os pontos mais brilhantes da imagem são dilatados para formar formas. O resultado desse processo é a provável forma dos objetos com brilho significativo. Aplicando a segmentação de preenchimento, formas desconectadas são detectadas.

imagens de saída:

Código fonte:

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 mostrado na imagem de saída, várias formas foram detectadas. Nesse problema, existem apenas alguns pontos brilhantes nas imagens. No entanto, essa abordagem foi implementada para lidar com cenários mais complexos.

Na próxima etapa, cada forma é analisada. Um algoritmo simples detecta formas com um padrão semelhante a um triângulo. O algoritmo analisa a forma do objeto linha por linha. Se o centro da massa de cada linha de forma é quase o mesmo (dado um limite) e a massa aumenta à medida que y aumenta, o objeto tem uma forma de triângulo. A massa da linha da forma é o número de pixels nessa linha que pertence à forma. Imagine dividir o objeto horizontalmente e analisar cada segmento horizontal. Se eles estiverem centralizados entre si e o comprimento aumentar do primeiro para o último em um padrão linear, você provavelmente terá um objeto que se assemelha a um triângulo.

Código fonte:

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, a posição de cada forma semelhante a um triângulo e com brilho significativo, neste caso uma árvore de Natal, é destacada na imagem original, como mostrado abaixo.

imagens finais de saída:

código fonte 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();
}
}

A vantagem dessa abordagem é o fato de provavelmente funcionar com imagens contendo outros objetos luminosos, uma vez que analisa a forma do objeto.

Feliz Natal!


NOTA DE EDIÇÃO 2

Há uma discussão sobre a semelhança das imagens de saída desta solução e algumas outras. De fato, eles são muito parecidos. Mas essa abordagem não apenas segmenta objetos. Também analisa as formas do objeto em algum sentido. Ele pode lidar com vários objetos luminosos na mesma cena. De fato, a árvore de Natal não precisa ser a mais brilhante. Estou apenas escrevendo para enriquecer a discussão. Há um viés nas amostras de que, apenas procurando o objeto mais brilhante, você encontrará as árvores. Mas, realmente queremos parar a discussão neste momento? Neste ponto, até que ponto o computador está realmente reconhecendo um objeto que se assemelha a uma árvore de Natal? Vamos tentar fechar essa lacuna.

Abaixo é apresentado um resultado apenas para elucidar esse ponto:

imagem de entrada

insira a descrição da imagem aqui

resultado

insira a descrição da imagem aqui


2
Isso é interessante. Espero que você possa obter os mesmos resultados quando cada imagem for processada individualmente. Eu editei a pergunta 4 horas anteriormente para você postar a resposta para especificar isso especificamente. Seria incrível se você pudesse atualizar sua resposta com esses resultados.
karlphillip

@ Marvin na sua detecção de triângulo, como você lidou com a flutuação de massa? Não é um triângulo estrito, a massa não é mono quando y muda #
user3054997:

2
@ user3054997: Esse é outro ponto. Como eu postei, o algoritmo não procura as formas estritas de triângulo. Ele analisa cada objeto e considera uma árvore que se assemelha a um triângulo com um critério simples: a massa do objeto é usada para aumentar à medida que y aumenta e o centro da massa de cada segmento de objeto horizontal é quase centralizado um para o outro .
Gabriel Ambrósio Archanjo

@ Marvin Minha solução é realmente simples, afirmei na minha resposta também. A propósito, funcionou melhor do que sua primeira solução. Se bem me lembro, na sua primeira resposta, você falou sobre descritores de recursos para detectar pequenas texturas leves, o que não é o que você está fazendo aqui. Eu simplesmente disse que sua abordagem e resultados atuais são muito mais semelhantes aos meus do que à sua primeira solução. Claro que não espero que você admita, afirmei apenas para constar.
smeso

11
@sepdek Existem aqui algumas soluções que são realmente muito melhores que as minhas e que ainda estão recebendo metade dos meus votos. Não há nada errado em "se inspirar" em outras soluções. Vi suas soluções também, não tenho nada a dizer contra você, você as postou depois de mim e minha "ideia" não era tão original para dizer que você acabou de me copiar. Mas Marvin foi o único que postou antes de mim e editou a solução depois de ver o meu usando o mesmo algoritmo ... pelo menos ele poderia ter dito "Sim, gostei da sua solução e reutilizei" não há nada errado, é apenas um jogo.
smeso

75

Aqui está a minha solução simples e burra. É baseado na suposição de que a árvore será a coisa mais brilhante e grande da imagem.

//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;
}

O primeiro passo é detectar os pixels mais brilhantes da imagem, mas precisamos fazer uma distinção entre a própria árvore e a neve que reflete sua luz. Aqui tentamos excluir a neve aplicando um filtro realmente simples nos códigos de cores:

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

Então encontramos todos os pixels "brilhantes":

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, juntamos os dois resultados:

bitwise_and(tmp, tmp1, tmp1);

Agora, procuramos o maior objeto brilhante:

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

Agora estamos quase terminando, mas ainda há alguma imperfeição devido à neve. Para cortá-los, criaremos uma máscara usando um círculo e um retângulo para aproximar a forma de uma árvore e excluir partes indesejadas:

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

O último passo é encontrar o contorno da nossa árvore e desenhá-lo na figura 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);

Sinto muito, mas no momento tenho uma conexão ruim, portanto não é possível fazer upload de fotos. Vou tentar fazer isso mais tarde.

Feliz Natal.

EDITAR:

Aqui estão algumas fotos da saída final:


11
Olá! Verifique se a sua resposta segue todos os requisitos: Existem 6 imagens de entrada e a resposta deve exibir os resultados do processamento de cada uma delas; .
karlphillip

Oi! Você pode passar nomes de arquivos como argumentos CLI para o meu programa: ./christmas_tree ./*.png. Eles podem ser quantos você quiser, os resultados serão mostrados um após o outro pressionando qualquer tecla. Isso está errado?
smeso

Tudo bem, mas você ainda precisa fazer o upload das imagens e compartilhá-las na sua pergunta para que os visualizadores do tópico possam ver o resultado. Deixar as pessoas verem o que você fez aumentará suas chances de conseguir votos;) #
karlphillip

Estou tentando encontrar uma solução para isso, tenho alguns problemas de conectividade.
smeso

2
Ótimo! Agora você pode redimensioná-las dentro da resposta com o seguinte código: <img src="http://i.stack.imgur.com/nmzwj.png" width="210" height="150">Basta alterar o link para a imagem;) #
karlphillip

60

Eu escrevi o código no Matlab R2007a. Eu usei k-means para extrair aproximadamente a árvore de natal. Mostrarei meu resultado intermediário apenas com uma imagem e os resultados finais com todas as seis.

Primeiro, mapeei o espaço RGB no espaço Lab, o que poderia aumentar o contraste do vermelho em seu canal b:

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

insira a descrição da imagem aqui

Além do recurso no espaço de cores, também usei o recurso de textura relevante para a vizinhança e não para cada pixel. Aqui eu combinei linearmente a intensidade dos 3 canais originais (R, G, B). A razão pela qual eu formatei dessa maneira é porque as árvores de natal na imagem têm luzes vermelhas e, às vezes, iluminação verde / às vezes azul.

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

insira a descrição da imagem aqui

Apliquei um padrão binário local 3X3 I0, usei o pixel central como limite e obtive o contraste calculando a diferença entre o valor médio da intensidade do pixel acima do limite e o valor médio abaixo dele.

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

insira a descrição da imagem aqui

Como tenho 4 recursos no total, eu escolheria K = 5 no meu método de clustering. O código para k-means é mostrado abaixo (é do curso de aprendizado de máquina do Dr. Andrew Ng. Eu fiz o curso antes e escrevi o código em sua tarefa de programação).

[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 o programa está muito lento no meu computador, eu apenas executei 3 iterações. Normalmente, o critério de parada é (i) tempo de iteração de pelo menos 10 ou (ii) nenhuma alteração nos centróides mais. Para o meu teste, aumentar a iteração pode diferenciar o plano de fundo (céu e árvore, céu e construção, ...) com mais precisão, mas não mostrou mudanças drásticas na extração da árvore de Natal. Observe também que o k-means não é imune à inicialização aleatória do centróide, portanto, é recomendável executar o programa várias vezes para fazer uma comparação.

Após as médias k, I0foi escolhida a região marcada com a intensidade máxima de . E o rastreamento de limites foi usado para extrair os limites. Para mim, a última árvore de natal é a mais difícil de extrair, já que o contraste nessa imagem não é alto o suficiente, pois são nos cinco primeiros. Outro problema no meu método é que eu usei a bwboundariesfunção no Matlab para rastrear os limites, mas às vezes os limites internos também são incluídos, como você pode observar nos resultados da 3ª, 5ª e 6ª. O lado escuro dentro das árvores de Natal não apenas deixa de ser agrupado com o lado iluminado, mas também leva a muitos pequenos traçados de limites internos ( imfillnão melhora muito). Em todo o meu algoritmo ainda tem muito espaço para melhorias.

Algumas publicações s indicam que o deslocamento médio pode ser mais robusto do que o k-means e muitos algoritmos baseados em corte de gráfico também são muito competitivos na segmentação de limites complicada. Eu mesmo escrevi um algoritmo de mudança de média, parece extrair melhor as regiões sem luz suficiente. Mas a mudança na média é um pouco exagerada e é necessária alguma estratégia de fusão. Funcionou ainda mais devagar do que o k-means no meu computador, tenho medo de desistir. Estou ansioso para ver que outros apresentariam excelentes resultados aqui com os algoritmos modernos mencionados acima.

No entanto, eu sempre acredito que a seleção de recursos é o componente principal na segmentação de imagens. Com uma seleção adequada de recursos que pode maximizar a margem entre o objeto e o plano de fundo, muitos algoritmos de segmentação definitivamente funcionarão. Algoritmos diferentes podem melhorar o resultado de 1 para 10, mas a seleção de recursos pode melhorar de 0 para 1.

Feliz Natal !


2
Obrigado pela resposta! Eu só queria ressaltar que o Matlab não é de código aberto , mas o Scilab é. Eu adoraria ver essa resposta competindo com as outras. ;)
karlphillip

6
Obrigado Karl. O Octave é outro software de código aberto que compartilha quase a mesma gramática de codificação do Matlab: mathworks.fr/matlabcentral/answers/14399-gnu-octave-vs-matlab .
lennon310

Interessante, eu não sabia disso, obrigado! Seu código funciona no Octave?
karlphillip

Eu não testei ainda, mas eu acho que não é nenhum problema :)
lennon310

@ lennon310 Eu acho que se você derrubar os limites e conseguir o casco convexo, se livrará do problema dos buracos. Lembre-se de que o casco convexo é a menor área que inclui todos os pontos de um conjunto.
sepdek

57

Este é o meu post final usando as abordagens tradicionais de processamento de imagem ...

Aqui, de alguma forma, combino minhas duas outras propostas, obtendo resultados ainda melhores . De fato, não consigo ver como esses resultados poderiam ser melhores (especialmente quando você olha para as imagens mascaradas que o método produz).

No centro da abordagem está a combinação de três premissas principais :

  1. As imagens devem ter altas flutuações nas regiões das árvores
  2. As imagens devem ter maior intensidade nas regiões das árvores
  3. As regiões de fundo devem ter baixa intensidade e ser predominantemente azuladas

Com essas suposições em mente, o método funciona da seguinte maneira:

  1. Converta as imagens para HSV
  2. Filtre o canal V com um filtro LoG
  3. Aplique limiar rígido na imagem filtrada LoG para obter a máscara de 'atividade' A
  4. Aplique limiar rígido no canal V para obter a máscara de intensidade B
  5. Aplique o limiar do canal H para capturar regiões azuladas de baixa intensidade na máscara de fundo C
  6. Combine máscaras usando AND para obter a máscara final
  7. Dilate a máscara para ampliar regiões e conectar pixels dispersos
  8. Elimine pequenas regiões e obtenha a máscara final que eventualmente representará apenas a árvore

Aqui está o código no MATLAB (novamente, o script carrega todas as imagens jpg na pasta atual e, novamente, isso está longe de ser um trecho de código otimizado):

% 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

Resultados de alta resolução ainda disponíveis aqui!
Ainda mais experimentos com imagens adicionais podem ser encontrados aqui.


11
Coisas boas! Certifique-se de que suas outras respostas também sigam esse formato. Para competir pela recompensa, você deve usar uma tecnologia de código aberto e, infelizmente, o Matlab não é um deles. No entanto, SciLab e Octave são e fornecem sintaxe e funções semelhantes. ;)
karlphillip

Código Octave é o mesmo ...
sepdek

@karlphillip De alguma forma, essa pergunta acabou tendo uma tag Matlab. Se o código aberto é realmente necessário, eu recomendaria removê-lo.
Dennis Jaheruddin

@sepdek Muito bom, talvez algo ainda possa ser feito para incluir os 'buracos' na imagem final. (Adicionar todos os pixels que estão completamente cercados por pixels aprovados ?!)
Dennis Jaheruddin

11
@karlphillip thanx man! Fico feliz que você tenha achado minha abordagem interessante. Além disso, gostaria de parabenizá-lo por escolher a solução mais elegante e não a que tem mais votos !!!
sepdek

36

Minhas etapas da solução:

  1. Obter canal R (de RGB) - todas as operações que fazemos neste canal:

  2. Criar região de interesse (ROI)

    • Canal R de limite com valor mínimo 149 (imagem superior direita)

    • Região de resultado dilatada (imagem do meio à esquerda)

  3. Detecte eges no roi computado. A árvore tem muitas arestas (imagem no meio à direita)

    • Resultado dilatado

    • Corroer com raio maior (imagem inferior esquerda)

  4. Selecione o maior objeto (por área) - é a região de resultado

  5. ConvexHull (árvore é polígono convexo) (imagem inferior direita)

  6. Caixa delimitadora (imagem inferior direita - caixa grren)

Passo a passo: insira a descrição da imagem aqui

O primeiro resultado - mais simples, mas não no software de código aberto - "Adaptive Vision Studio + Adaptive Vision Library": esse não é um código aberto, mas é muito rápido para prototipar:

Algoritmo inteiro para detectar a árvore de natal (11 blocos): Solução AVL

Próxima Etapa. Queremos uma solução de código aberto. Altere os filtros AVL para os filtros OpenCV: Aqui, fiz pequenas alterações, por exemplo, a detecção de borda usa o filtro cvCanny, para respeitar o roi, multipliquei a imagem da região pela imagem das bordas, para selecionar o maior elemento que usei findContours + contourArea, mas a idéia é a mesma.

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

Solução OpenCV

Não posso mostrar imagens com etapas intermediárias agora porque posso colocar apenas 2 links.

Ok, agora usamos filtros de código aberto, mas ainda não é todo o código aberto. Última etapa - porta para o código c ++. Eu usei o OpenCV na versão 2.4.4

O resultado do código c ++ final é: insira a descrição da imagem aqui

O código c ++ também é bastante curto:

#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;
}

Qual compilador pode criar este programa sem erros?
precisa saber é o seguinte

Eu usei o Visual Studio 2012 para construí-lo. Você deve usar o compilador c ++ com suporte c ++ 11.
AdamF 4/14

Eu não tenho um sistema à minha disposição com isso. Você poderia reescrever a std::max_element()ligação? Gostaria de recompensar sua resposta também. Eu acho que tenho o gcc 4.2.
precisa saber é o seguinte

Ok, este é o recurso do c ++ 11;) Alterei o código fonte acima. Por favor tente agora.
AdamF 4/04

Tudo bem, obrigado. Eu testei e é lindo. Assim que essa pergunta for reaberta (outros usuários precisam me ajudar com isso), posso definir outra recompensa para recompensá-lo. Parabéns!
precisa saber é o seguinte

31

... outra solução antiquada - puramente baseada no processamento HSV :

  1. Converter imagens no espaço de cores HSV
  2. Crie máscaras de acordo com heurísticas no HSV (veja abaixo)
  3. Aplique dilatação morfológica à máscara para conectar áreas desconectadas
  4. Descarte pequenas áreas e blocos horizontais (lembre-se de que as árvores são blocos verticais)
  5. Calcular a caixa delimitadora

Uma palavra sobre as heurísticas no processamento HSV:

  1. tudo com matizes (H) entre 210 - 320 graus é descartado como azul-magenta que deveria estar em segundo plano ou em áreas não relevantes
  2. tudo com valores (V) menor que 40% também é descartado como muito escuro para ser relevante

É claro que se pode experimentar inúmeras outras possibilidades para ajustar essa abordagem ...

Aqui está o código do MATLAB para executar o truque (aviso: o código está longe de ser otimizado !!! Eu usei técnicas não recomendadas para a programação do MATLAB apenas para poder rastrear qualquer coisa no processo - isso pode ser bastante otimizado):

% 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:

Nos resultados, mostro a imagem mascarada e a caixa delimitadora. insira a descrição da imagem aqui


Olá, obrigado pela resposta. Leia a seção Requisitos para garantir que sua resposta siga todas as instruções. Você esqueceu de compartilhar as imagens resultantes. ;)
karlphillip

2
@karlphillip sepdek não tem reputação suficiente para compartilhar imagens, mudei as imagens para o corpo da resposta de acordo com o link e as instruções. Não tendo certeza, porém, de que são os corretos, fique à vontade para comentar esta parte.
alko

@ alko eu sei, obrigado. Mas algumas das imagens que você compartilhou não estavam no conjunto de entrada . A resposta deve mostrar o resultado do processamento das 6 imagens compartilhadas na pergunta.
karlphillip

@karlphillip são suas imagens, não minhas. que é exatly o que eu quis dizer com "comentar esta parte";)
alko

2
Desculpe por causar problemas ... não é minha intenção. Eu incluí todas as imagens no conjunto de dados inicial e avançado com ainda mais só para provar que o meu conceito é robusto ...
sepdek

23

Alguma abordagem antiquada de processamento de imagem ...
A idéia baseia-se no pressuposto de que as imagens mostram árvores iluminadas em fundos tipicamente mais escuros e suaves (ou em primeiro plano em alguns casos). A área da árvore iluminada é mais "energética" e tem maior intensidade .
O processo é como se segue:

  1. Converter em nível de cinza
  2. Aplique a filtragem LoG para obter as áreas mais "ativas"
  3. Aplique um limiar intencional para obter as áreas mais brilhantes
  4. Combine os 2 anteriores para obter uma máscara preliminar
  5. Aplique uma dilatação morfológica para ampliar áreas e conectar componentes vizinhos
  6. Eliminar pequenas áreas candidatas de acordo com o tamanho da área

O que você obtém é uma máscara binária e uma caixa delimitadora para cada imagem.

Aqui estão os resultados usando esta técnica ingênua: insira a descrição da imagem aqui

O código no MATLAB é o seguinte: O código é executado em uma pasta com imagens JPG. Carrega todas as imagens e retorna os 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

Não se esqueça de enviar as imagens resultantes, como Faust fez.
karlphillip

Eu sou um noob aqui, então não consigo carregar imagens. Por favor, veja os resultados nos links fornecidos na minha descrição.
sepdek

Ok, mas você ainda precisa usar as imagens compartilhadas na pergunta, como todo mundo está fazendo. Depois de processá-los, faça o upload em algum lugar e edite sua resposta para adicionar os links. Depois, editarei sua resposta e colocarei as imagens dentro dela para você.
karlphillip

O link parece conter as imagens corretas agora.
Dennis Jaheruddin

22

Usando uma abordagem bem diferente da que eu já vi, criei um script que detecta árvores de natal por suas luzes. O resultado é sempre um triângulo simétrico e, se necessário, valores numéricos como o ângulo ("gordura") da árvore.

A maior ameaça a esse algoritmo obviamente são as luzes próximas (em grandes números) ou na frente da árvore (o maior problema até uma otimização adicional). Editar (adicionado): o que não pode ser feito: descubra se há uma árvore de Natal ou não, encontre várias árvores de Natal em uma imagem, detecte corretamente uma árvore de Natal no meio de Las Vegas, detecte árvores de Natal muito dobradas, de cabeça para baixo ou picado ...;)

As diferentes etapas são:

  • Calcular o brilho adicionado (R + G + B) para cada pixel
  • Adicione esse valor de todos os 8 pixels vizinhos em cima de cada pixel
  • Classifique todos os pixels por esse valor (primeiro o mais brilhante) - eu sei, não muito sutil ...
  • Escolha N, começando do topo, pulando os que estão muito próximos
  • Calcule o destes N superiores (nos dá o centro aproximado da árvore)
  • Comece da posição mediana para cima em um feixe de busca cada vez maior da luz mais alta dentre as mais brilhantes selecionadas (as pessoas tendem a colocar pelo menos uma luz no topo)
  • A partir daí, imagine linhas indo 60 graus para a esquerda e direita para baixo (as árvores de Natal não devem ser tão gordas)
  • Diminua esses 60 graus até 20% das luzes mais brilhantes ficarem fora deste triângulo
  • Encontre a luz na parte inferior do triângulo, fornecendo a borda horizontal inferior da árvore
  • Feito

Explicação das marcações:

  • Grande cruz vermelha no centro da árvore: mediana das N luzes mais brilhantes
  • Linha pontilhada de lá para cima: "feixe de busca" para o topo da árvore
  • Menor cruz vermelha: topo da árvore
  • Cruzes vermelhas realmente pequenas: todas as N luzes mais brilhantes
  • Triângulo vermelho: D'uh!

Código fonte:

<?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;
}


?>

Imagens: Superior esquerdo Centro inferior Canto inferior esquerdo Canto superior direito Centro superior Inferior direito

Bônus: um alemão Weihnachtsbaum, da Wikipedia Römerberg http://commons.wikimedia.org/wiki/File:Weihnachtsbaum_R%C3%B6merberg.jpg


17

Eu usei python com opencv.

Meu algoritmo é assim:

  1. Primeiro, pega o canal vermelho da imagem
  2. Aplique um limite (valor mínimo 200) ao canal vermelho
  3. Em seguida, aplique o gradiente morfológico e faça um 'fechamento' (dilatação seguida de erosão)
  4. Então ele encontra os contornos no plano e escolhe o contorno mais longo.

O resultado:

O 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()

Se eu mudar o kernel de (25,5) para (10,5), obtive melhores resultados em todas as árvores, exceto no canto inferior esquerdo, insira a descrição da imagem aqui

meu algoritmo assume que a árvore tem luzes e, na árvore inferior esquerda, o topo tem menos luz que os outros.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.