Jak wykryć choinkę? [Zamknięte]

382

Jakie techniki przetwarzania obrazu można zastosować do wdrożenia aplikacji, która wykrywa choinki wyświetlane na poniższych obrazach?

Szukam rozwiązań, które będą działać na wszystkich tych obrazach. Dlatego podejścia wymagające szkolenia klasyfikatorów kaskadowych haar lub dopasowywania szablonów nie są bardzo interesujące.

Szukam czegoś, co można napisać w dowolnym języku programowania, pod warunkiem, że używa tylko technologii Open Source . Rozwiązanie należy przetestować z obrazami udostępnionymi na to pytanie. Istnieje 6 obrazów wejściowych, a odpowiedź powinna zawierać wyniki przetwarzania każdego z nich. Wreszcie, dla każdego obrazu wyjściowego muszą zostać narysowane czerwone linie otaczające wykrywane drzewo.

Jak poszedłbyś programowo wykrywając drzewa na tych obrazach?

karlphillip
źródło
3
Czy wolno nam wykorzystywać niektóre obrazy do szkolenia, czy też wszystkie dostarczone obrazy należy wykorzystywać do sprawdzania poprawności? Tak czy inaczej, fajna konkurencja: D
Hannes Ovrén
7
@ karlphillip, czy chcesz, abyśmy używali tych obrazów do testowania i innych obrazów do szkolenia? Po prostu nie jest jasne, jaki jest zestaw treningowy.
GilLevi,
16
@karlphillip: Moja rada: porzuć wymóg „open source”. Naprawdę nie ma znaczenia, jakiego języka / frameworku używasz. Algorytmy przetwarzania obrazu / wizji komputerowej są niezależne od języka, więc jeśli możesz napisać to w MATLAB, z pewnością możesz to zrobić w OpenCV lub w dowolnym innym frameworku, który wolisz ... Ponadto nadal nie jestem pewien, co uważasz za szkolenie / testowanie obrazów !
Amro
2
@karlphillip thanx za mobilizowanie nas wszystkich do wniesienia wkładu w wasze „poszukiwania”! To była świetna okazja, aby spędzić kilka godzin produktywnie, ale co najważniejsze, aby zobaczyć, ile różnych podejść można znaleźć w przypadku jednego problemu ... Mam nadzieję, że zrobisz to jeszcze raz 1 stycznia (być może sanie wyzwanie Mikołaj ;-))?
sepdek
2
OK, przeredagowałem pytanie, aby usunąć elementy konkursu. Myślę, że to powinno pozwolić mu stać samodzielnie.
Brad Larson

Odpowiedzi:

184

Mam podejście, które moim zdaniem jest interesujące i nieco inne niż reszta. Główną różnicą w moim podejściu, w porównaniu do niektórych innych, jest sposób przeprowadzania etapu segmentacji obrazu - użyłem algorytmu klastrowania DBSCAN z scikit-learn Pythona; jest zoptymalizowany do znajdowania nieco amorficznych kształtów, które niekoniecznie muszą mieć jeden wyraźny środek ciężkości.

Na najwyższym poziomie moje podejście jest dość proste i można je podzielić na około 3 kroki. Najpierw stosuję próg (a właściwie logiczne „lub” dwóch oddzielnych i odrębnych progów). Podobnie jak w przypadku wielu innych odpowiedzi, założyłem, że choinka będzie jednym z jaśniejszych obiektów na scenie, więc pierwszy próg to po prostu prosty test jasności monochromatycznej; wszelkie piksele o wartościach powyżej 220 w skali 0-255 (gdzie czerń to 0, a biel to 255) są zapisywane w binarnym czarno-białym obrazie. Drugi próg próbuje szukać czerwonych i żółtych świateł, które są szczególnie widoczne na drzewach w lewym górnym i prawym dolnym rogu sześciu zdjęć, i wyróżniają się dobrze na niebiesko-zielonym tle, które dominuje na większości zdjęć. Konwertuję obraz rgb na przestrzeń hsv, i wymagam, aby odcień był mniejszy niż 0,2 w skali 0,0-1,0 (co odpowiada z grubsza granicy między żółtym a zielonym) lub większy niż 0,95 (odpowiadający granicy między fioletowym a czerwonym), a dodatkowo wymagam jasnych, nasyconych kolorów: nasycenie i wartość muszą być powyżej 0,7. Wyniki dwóch procedur progowych są logicznie „lub” razem, a wynikowa macierz czarno-białych obrazów binarnych pokazano poniżej:

Choinki, po przekroczeniu progu HSV, a także jasności monochromatycznej

Możesz wyraźnie zobaczyć, że każdy obraz ma jedną dużą grupę pikseli z grubsza odpowiadającą lokalizacji każdego drzewa, a kilka zdjęć ma również inne małe skupiska odpowiadające albo światłom w oknach niektórych budynków, albo scena tła na horyzoncie. Następnym krokiem jest sprawienie, aby komputer rozpoznał, że są to oddzielne klastry i odpowiednio oznaczył każdy piksel numerem identyfikacyjnym członkostwa w klastrze.

Do tego zadania wybrałem DBSCAN . Istnieje całkiem dobre wizualne porównanie tego, jak zwykle zachowuje się DBSCAN, w porównaniu z innymi algorytmami klastrowania, dostępnymi tutaj . Jak powiedziałem wcześniej, dobrze sobie radzi z amorficznymi kształtami. Dane wyjściowe DBSCAN, z każdym klastrem wykreślonym w innym kolorze, pokazano tutaj:

Dane klastrowe DBSCAN

Patrząc na ten wynik, należy pamiętać o kilku kwestiach. Po pierwsze, DBSCAN wymaga od użytkownika ustawienia parametru „bliskości” w celu regulacji jego zachowania, które skutecznie kontroluje, jak oddzielona musi być para punktów, aby algorytm mógł zadeklarować nowy oddzielny klaster, zamiast skupiać punkt testowy na już istniejący klaster. Ustawiam tę wartość na 0,04 razy rozmiar wzdłuż przekątnej każdego obrazu. Ponieważ obrazy różnią się wielkością, od z grubsza VGA do około HD 1080, ten typ definicji względem skali ma kluczowe znaczenie.

Inną kwestią wartą odnotowania jest to, że algorytm DBSCAN, który jest zaimplementowany w scikit-learn, ma limity pamięci, które są dość trudne dla niektórych większych obrazów w tej próbce. Dlatego w przypadku kilku większych obrazów musiałem „zdziesiątkować” (tzn. Zachować tylko co 3 lub 4 piksele i upuścić pozostałe) każdy klaster, aby pozostać w tym limicie. W wyniku tego procesu wygładzania trudno zobaczyć pozostałe pojedyncze rzadkie piksele na niektórych większych obrazach. Dlatego też, wyłącznie w celach wyświetlania, piksele oznaczone kolorami na powyższych obrazach zostały nieznacznie „rozszerzone”, aby lepiej się wyróżniały. To czysto kosmetyczna operacja dla samej narracji; chociaż w moim kodzie są komentarze dotyczące tej dylatacji,

Po zidentyfikowaniu i oznaczeniu klastrów trzeci i ostatni krok jest łatwy: po prostu biorę największy klaster na każdym obrazie (w tym przypadku zdecydowałem się zmierzyć „rozmiar” pod względem całkowitej liczby pikseli członka, chociaż można zamiast tego równie łatwo zastosował jakiś rodzaj miernika, który mierzy zasięg fizyczny) i obliczył wypukły kadłub dla tego gromady. Wypukły kadłub staje się wówczas granicą drzewa. Sześć wypukłych kadłubów obliczonych tą metodą pokazano poniżej na czerwono:

Choinki z ich obliczonymi granicami

Kod źródłowy jest napisany dla Pythona 2.7.6 i zależy od numpy , scipy , matplotlib i scikit-learn . Podzieliłem to na dwie części. Pierwsza część odpowiada za faktyczne przetwarzanie obrazu:

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

a druga część to skrypt na poziomie użytkownika, który wywołuje pierwszy plik i generuje wszystkie powyższe wykresy:

#!/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
źródło
Rozwiązaniem @ lennon310 jest grupowanie. (k-znaczy)
user3054997,
1
@stachyra Zastanawiałem się również nad tym podejściem, zanim zaproponowałem moje prostsze. Myślę, że ma to duży potencjał do rozszerzenia i uogólnienia w celu uzyskania dobrych wyników także w innych przypadkach. Możesz eksperymentować z sieciami neuronowymi do grupowania. Coś w rodzaju SOM lub gazu neuronowego wykonałoby doskonałą pracę. Niemniej jednak świetna propozycja i kciuki do góry ode mnie!
sepdek
4
@Faust & Ryan Carlson: dzięki, chłopaki! Tak, zgadzam się, że system głosowania, choć działa dobrze do rozstrzygania między 2 lub 3 krótkimi odpowiedziami złożonymi w ciągu kilku godzin od siebie, ma poważne uprzedzenia, jeśli chodzi o konkursy z długimi odpowiedziami rozgrywającymi się przez dłuższy czas . Po pierwsze, wczesne zgłoszenia zaczynają się gromadzić, zanim kolejne będą dostępne do publicznego przeglądu. A jeśli wszystkie odpowiedzi są długie, to gdy tylko ustalimy skromną przewagę, często pojawia się „efekt modowy”, ponieważ ludzie głosują tylko na pierwszym, nie zawracając sobie głowy resztą.
stachyra
2
@stachyra świetny przyjaciel wiadomości! Najcieplejsze gratulacje i niech to będzie początek nowego roku!
sepdek
1
@ lennon310: Nie próbowałem jeszcze lokalnego filtra maksymalnego wykrywania tego problemu, ale jeśli chcesz go zbadać sam, scipy obejmuje ten . Mój kod źródłowy Python dla tego projektu był tak krótki, że faktycznie mogłem opublikować 100% tego; dosłownie wszystko, co musisz zrobić, to skopiować i wkleić moje dwa fragmenty kodu do osobnych plików .py, a następnie zastąpić wywołanie scipy.ndimage.filters.maximum_filter()w tym samym miejscu, w którym użyłem progu.
stachyra
145

EDYTUJ UWAGĘ: Zredagowałem ten post, aby (i) przetworzyć każdy obraz drzewa indywidualnie, zgodnie z wymaganiami, (ii) w celu uwzględnienia zarówno jasności obiektu, jak i kształtu w celu poprawy jakości wyniku.


Poniżej przedstawiono podejście uwzględniające jasność i kształt obiektu. Innymi słowy, szuka obiektów o kształcie trójkąta i znacznej jasności. Został zaimplementowany w Javie, przy użyciu frameworka przetwarzania obrazu Marvin .

Pierwszym krokiem jest próg koloru. Celem jest skupienie analizy na obiektach o znacznej jasności.

obrazy wyjściowe:

kod źródłowy:

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

W drugim kroku najjaśniejsze punkty na obrazie są rozszerzane w celu utworzenia kształtów. Wynikiem tego procesu jest prawdopodobny kształt obiektów o znacznej jasności. Po zastosowaniu segmentacji wypełnienia powodziowego wykrywane są odłączone kształty.

obrazy wyjściowe:

kod źródłowy:

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

Jak pokazano na obrazie wyjściowym, wykryto wiele kształtów. W tym problemie jest tylko kilka jasnych punktów na obrazach. Jednak to podejście zostało wdrożone, aby poradzić sobie z bardziej złożonymi scenariuszami.

W następnym kroku analizowany jest każdy kształt. Prosty algorytm wykrywa kształty o wzorze podobnym do trójkąta. Algorytm analizuje kształt obiektu linia po linii. Jeśli środek masy każdej linii kształtu jest prawie taki sam (podany próg), a masa rośnie wraz ze wzrostem y, obiekt ma kształt trójkąta. Masa linii kształtu to liczba pikseli w tej linii, która należy do kształtu. Wyobraź sobie, że wycinasz obiekt poziomo i analizujesz każdy segment poziomy. Jeśli są one scentralizowane względem siebie, a długość zwiększa się od pierwszego segmentu do ostatniego w układzie liniowym, prawdopodobnie masz obiekt podobny do trójkąta.

kod źródłowy:

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

Na koniec pozycja każdego kształtu podobna do trójkąta i ze znaczną jasnością, w tym przypadku choinką, jest podświetlona na oryginalnym obrazie, jak pokazano poniżej.

końcowe zdjęcia wyjściowe:

ostateczny kod źródłowy:

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

Zaletą tego podejścia jest fakt, że prawdopodobnie będzie on działać z obrazami zawierającymi inne świecące obiekty, ponieważ analizuje kształt obiektu.

Wesołych Świąt!


EDYCJA UWAGA 2

Trwa dyskusja na temat podobieństwa obrazów wyjściowych tego rozwiązania i niektórych innych. W rzeczywistości są bardzo podobne. Ale to podejście nie tylko segmentuje obiekty. W pewnym sensie analizuje również kształty obiektów. Może obsłużyć wiele świecących obiektów w tej samej scenie. W rzeczywistości choinka nie musi być najjaśniejsza. Po prostu zgadzam się, aby wzbogacić dyskusję. Próbki zawierają błąd, który po prostu szukając najjaśniejszego obiektu, znajdziesz drzewa. Ale czy naprawdę chcemy na tym etapie zakończyć dyskusję? W tym momencie, jak daleko komputer naprawdę rozpoznaje obiekt przypominający choinkę? Spróbujmy wypełnić tę lukę.

Poniżej przedstawiono wynik, aby wyjaśnić ten punkt:

obraz wejściowy

wprowadź opis zdjęcia tutaj

wynik

wprowadź opis zdjęcia tutaj

Gabriel Ambrósio Archanjo
źródło
2
To interesujące. Mam nadzieję, że możesz uzyskać takie same wyniki, gdy każdy obraz jest przetwarzany osobno. Zredagowałem pytanie 4 godziny wcześniej, zamieszczając odpowiedź na ten temat. Byłoby wspaniale, gdybyś mógł zaktualizować swoją odpowiedź o te wyniki.
karlphillip
@Marvin w wykrywaniu trójkątów, w jaki sposób poradziłeś sobie z wahaniami masy? To nie jest ścisły trójkąt, masa nie zmienia się w mono, gdy zmienia się y
user3054997
2
@ user3054997: To kolejny punkt. Jak pisałem, algorytm nie szuka ścisłych kształtów trójkątów. Analizuje każdy obiekt i bierze pod uwagę drzewo, które „przypomina” trójkąt o prostych kryteriach: masa obiektu jest używana do zwiększania się wraz ze wzrostem y, a środek masy każdego poziomego segmentu obiektu jest prawie scentralizowany względem siebie .
Gabriel Ambrósio Archanjo
@Marvin Moje rozwiązanie jest bardzo proste, stwierdziłem to również w mojej odpowiedzi. Nawiasem mówiąc, działało to lepiej niż twoje pierwsze rozwiązanie. Jeśli dobrze pamiętam, w pierwszej odpowiedzi mówiłeś o deskryptorach funkcji służących do wykrywania niewielkiej lekkiej tekstury, co nie jest tym, co tutaj robisz. Powiedziałem po prostu, że twoje obecne podejście i wyniki są znacznie bardziej podobne do moich niż do pierwszego rozwiązania. Oczywiście nie oczekuję, że się do tego przyznasz, powiedziałem to tylko dla przypomnienia.
smeso,
1
@sepdek Istnieje kilka rozwiązań, które są naprawdę znacznie lepsze niż moje i wciąż otrzymują połowę moich opinii. Nie ma nic złego w „inspirowaniu się” innymi rozwiązaniami. Widziałem też twoje rozwiązania, nie mam nic przeciwko tobie, opublikowałeś je po mnie, a mój „pomysł” nie był tak oryginalny, by powiedzieć, że właśnie mnie skopiowałeś. Ale Marvin był jedynym, który napisał przede mną i zredagował rozwiązanie po tym, jak zobaczył mój przy użyciu tego samego algorytmu ... przynajmniej mógł powiedzieć „Tak, podobało mi się twoje rozwiązanie i użyłem go ponownie”, nie ma nic złego, po prostu gra.
smeso
75

Oto moje proste i głupie rozwiązanie. Opiera się na założeniu, że drzewo będzie najjaśniejszą i największą rzeczą na zdjęciu.

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

Pierwszym krokiem jest wykrycie najjaśniejszych pikseli na obrazie, ale musimy wprowadzić rozróżnienie między samym drzewem a śniegiem odbijającym jego światło. Tutaj staramy się wykluczyć śnieg, stosując naprawdę prosty filtr kodów kolorów:

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

Następnie znajdujemy każdy „jasny” piksel:

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

Wreszcie łączymy dwa wyniki:

bitwise_and(tmp, tmp1, tmp1);

Teraz szukamy największego jasnego obiektu:

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

Teraz już prawie zrobiliśmy, ale wciąż istnieje pewna niedoskonałość z powodu śniegu. Aby je odciąć, zbudujemy maskę za pomocą koła i prostokąta w celu przybliżenia kształtu drzewa w celu usunięcia niepożądanych elementów:

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

Ostatnim krokiem jest znalezienie konturu naszego drzewa i narysowanie go na oryginalnym zdjęciu.

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

Przepraszam, ale w tej chwili mam złe połączenie, więc nie mogę przesyłać zdjęć. Spróbuję to zrobić później.

Wesołych Świąt.

EDYTOWAĆ:

Oto kilka zdjęć końcowego wyniku:

smeso
źródło
1
Dzień dobry! Upewnij się, że twoja odpowiedź spełnia wszystkie wymagania: Jest 6 obrazów wejściowych, a odpowiedź powinna wyświetlać wyniki przetwarzania każdego z nich; .
karlphillip
Cześć! Można przekazać nazwy plików jako argumenty CLI do mojego programu: ./christmas_tree ./*.png. Może być ich tyle, ile chcesz, wyniki będą wyświetlane jeden po drugim po naciśnięciu dowolnego klawisza. Czy to źle?
smeso,
Jest w porządku, ale nadal musisz przesłać zdjęcia i udostępnić je w swoim pytaniu, aby widzowie wątku mogli zobaczyć Twój wynik. Pozwalanie ludziom zobaczyć, co zrobiłeś, zwiększy twoje szanse na zdobycie głosów;)
karlphillip
Próbuję znaleźć rozwiązanie tego problemu, mam pewne problemy z łącznością.
smeso,
2
Świetny! Teraz możesz przeskalować je w odpowiedzi za pomocą następującego kodu: <img src="http://i.stack.imgur.com/nmzwj.png" width="210" height="150">Wystarczy zmienić link do obrazka;)
karlphillip
60

Napisałem kod w Matlab R2007a. Użyłem k-średnich, aby z grubsza wydobyć choinkę. Pokażę mój wynik pośredni tylko z jednym obrazem, a wyniki końcowe ze wszystkimi sześcioma.

Najpierw zmapowałem przestrzeń RGB na przestrzeń Lab, co może zwiększyć kontrast czerwieni w kanale b:

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

wprowadź opis zdjęcia tutaj

Oprócz funkcji w przestrzeni kolorów, użyłem również funkcji tekstury, która jest odpowiednia dla sąsiedztwa, a nie dla każdego piksela. Tutaj liniowo połączyłem intensywność z 3 oryginalnych kanałów (R, G, B). Powodem, dla którego sformatowałem w ten sposób, jest to, że wszystkie choinki na zdjęciu mają czerwone światła, a czasem także zielone / czasami niebieskie podświetlenie.

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

wprowadź opis zdjęcia tutaj

Zastosowałem lokalny wzór binarny 3X3 I0, użyłem środkowego piksela jako wartości progowej i uzyskałem kontrast, obliczając różnicę między średnią wartością intensywności pikseli powyżej wartości progowej a średnią wartością poniżej niej.

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

wprowadź opis zdjęcia tutaj

Ponieważ mam w sumie 4 funkcje, wybrałbym K = 5 w mojej metodzie klastrowania. Kod k-średnich pokazano poniżej (pochodzi on z kursu uczenia maszynowego Dr. Andrew Nga. Wziąłem ten kurs wcześniej i sam napisałem kod w jego zadaniu programistycznym).

[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

Ponieważ program działa bardzo wolno na moim komputerze, właśnie uruchomiłem 3 iteracje. Zwykle kryterium zatrzymania jest (i) czas iteracji co najmniej 10 lub (ii) brak zmian na centroidach. Według mojego testu zwiększenie iteracji może różnicować tło (niebo i drzewo, niebo i budynek, ...) dokładniej, ale nie wykazało drastycznych zmian w pozyskiwaniu choinki. Zauważ też, że k-średnie nie jest odporne na losową inicjalizację centroidu, dlatego zalecane jest kilkukrotne uruchomienie programu w celu porównania.

Po k-średnich I0wybrano znakowany region o maksymalnej intensywności . Do wyodrębnienia granic zastosowano śledzenie granic. Dla mnie ostatnia choinka jest najtrudniejsza do wyodrębnienia, ponieważ kontrast na tym zdjęciu nie jest wystarczająco wysoki, jak w pierwszych pięciu. Kolejną kwestią w mojej metodzie jest to, że użyłem bwboundariesfunkcji w Matlabie do śledzenia granicy, ale czasami wewnętrzne granice są również uwzględnione, jak można zaobserwować w wynikach 3, 5, 6. Ciemna strona w choinkach nie tylko nie jest zgrupowana z oświetloną stroną, ale także prowadzi do tak wielu śladów wewnętrznych granic ( imfillniewiele się poprawia). W sumie mój algorytm wciąż ma dużo miejsca na ulepszenia.

Niektóre publikacje wskazują, że przesunięcie średnie może być bardziej niezawodne niż średnie k, a wiele algorytmów opartych na cięciu wykresu jest również bardzo konkurencyjnych w przypadku skomplikowanej segmentacji granic. Sam napisałem algorytm zmiany średniej, wydaje się, że lepiej wyodrębnia regiony bez wystarczającej ilości światła. Jednak zmiana średniej jest nieco nadmiernie podzielona na segmenty i konieczna jest pewna strategia łączenia. Na moim komputerze działało nawet znacznie wolniej niż k-średnie, obawiam się, że muszę się poddać. Z niecierpliwością oczekuję, że inni przedstawią tutaj doskonałe wyniki dzięki tym nowoczesnym algorytmom wspomnianym powyżej.

Jednak zawsze uważam, że wybór funkcji jest kluczowym elementem w segmentacji obrazu. Przy odpowiednim wyborze funkcji, który może zmaksymalizować margines między obiektem a tłem, wiele algorytmów segmentacji na pewno będzie działać. Różne algorytmy mogą poprawić wynik od 1 do 10, ale wybór funkcji może poprawić go od 0 do 1.

Wesołych Świąt !

lennon310
źródło
2
Dziękuję za odpowiedź! Chciałem tylko zaznaczyć, że Matlab nie jest oprogramowaniem typu open source , ale Scilab jest. Bardzo chciałbym, aby ta odpowiedź konkurowała z innymi. ;)
karlphillip
6
Dziękuję Karl. Octave to kolejne oprogramowanie typu open source, które ma prawie taką samą gramatykę kodowania jak Matlab: mathworks.fr/matlabcentral/answers/14399-gnu-octave-vs-matlab .
lennon310
Ciekawe, nie wiedziałem o tym, dzięki! Czy twój kod działa na Octave?
karlphillip
Jeszcze nie testowałem, ale myślę, że to żaden problem :)
lennon310
@ lennon310 Myślę, że jeśli upuścisz granice i zdobędziesz wypukły kadłub, pozbędziesz się problemu z dziurami. Pamiętaj, że wypukły kadłub to najmniejszy obszar, który obejmuje wszystkie punkty w zestawie.
sepdek
57

To mój ostatni post wykorzystujący tradycyjne metody przetwarzania obrazu ...

Tutaj w jakiś sposób łączę moje dwie inne propozycje, osiągając jeszcze lepsze wyniki . W rzeczywistości nie widzę, w jaki sposób te wyniki mogą być lepsze (szczególnie, gdy spojrzysz na zamaskowane obrazy wytwarzane przez tę metodę).

U podstaw tego podejścia leży połączenie trzech kluczowych założeń :

  1. Obrazy powinny mieć duże fluktuacje w obszarach drzew
  2. Obrazy powinny mieć większą intensywność w obszarach drzew
  3. Regiony tła powinny mieć niską intensywność i być przeważnie niebieskie

Mając na uwadze te założenia, metoda działa w następujący sposób:

  1. Konwertuj obrazy na HSV
  2. Przefiltruj kanał V za pomocą filtra LoG
  3. Zastosuj twarde próg na obrazie filtrowanym LoG, aby uzyskać maskę A „aktywności”
  4. Zastosuj twarde progowanie do kanału V, aby uzyskać maskę intensywności B.
  5. Zastosuj próg kanału H, aby uchwycić regiony niebieskiego koloru o niskiej intensywności do maski tła C
  6. Połącz maski za pomocą AND, aby uzyskać ostateczną maskę
  7. Rozwiń maskę, aby powiększyć regiony i połączyć rozproszone piksele
  8. Wyeliminuj małe regiony i zdobądź ostatnią maskę, która ostatecznie będzie reprezentować tylko drzewo

Oto kod w MATLAB (ponownie, skrypt ładuje wszystkie obrazy jpg do bieżącego folderu i, znowu, nie jest to zoptymalizowany fragment kodu):

% 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

Wyniki

wyniki

Wyniki w wysokiej rozdzielczości są nadal dostępne tutaj!
Jeszcze więcej eksperymentów z dodatkowymi obrazami można znaleźć tutaj.

sepdek
źródło
1
Świetne rzeczy! Upewnij się, że Twoje pozostałe odpowiedzi również mają ten format. Aby konkurować o nagrodę, musisz użyć technologii open source , i niestety Matlab nie jest jedną z nich. Jednak SciLab i Octave są i zapewniają podobną składnię i funkcje. ;)
karlphillip
Kod oktawowy jest taki sam ...
seddek
@karlphillip Jakoś to pytanie skończyło się tagiem Matlab. Jeśli open source jest naprawdę koniecznością, polecam go usunąć.
Dennis Jaheruddin,
@sepdek Bardzo fajnie, być może coś jeszcze można zrobić, aby dodać „dziury” na ostatecznym zdjęciu. (Dodać wszystkie piksele, które są całkowicie otoczone zatwierdzonymi pikselami ?!)
Dennis Jaheruddin
1
@karlphillip thanx man! Cieszę się, że moje podejście było interesujące. Ponadto chciałbym pogratulować wybrania najbardziej eleganckiego rozwiązania, a nie tego, które uzyska najwięcej głosów !!!
sepdek
36

Moje kroki rozwiązania:

  1. Pobierz kanał R (z RGB) - wszystkie operacje, które wykonujemy na tym kanale:

  2. Utwórz region zainteresowania (ROI)

    • Kanał progowy R o wartości minimalnej 149 (prawy górny obraz)

    • Obszar wyniku rozszerzenia (środkowy lewy obraz)

  3. Wykryj ery w obliczonych roi. Drzewo ma dużo krawędzi (środkowy prawy obraz)

    • Rozwiń wynik

    • Erozja z większym promieniem (lewy dolny obraz)

  4. Wybierz największy obiekt (według obszaru) - jest to region wynikowy

  5. ConvexHull (drzewo jest wypukłym wielokątem) (prawy dolny obraz)

  6. Ramka ograniczająca (prawy dolny obraz - skrzynka Grren)

Krok po kroku: wprowadź opis zdjęcia tutaj

Pierwszy wynik - najprostszy, ale nie w oprogramowaniu open source - „Adaptive Vision Studio + Adaptive Vision Library”: To nie jest open source, ale bardzo szybko prototypuje:

Cały algorytm do wykrywania choinki (11 bloków): Rozwiązanie AVL

Następny krok. Chcemy rozwiązania typu open source. Zmień filtry AVL na filtry OpenCV: Tutaj zrobiłem małe zmiany, np. Wykrywanie krawędzi korzystam z filtra cvCanny, aby uszanować roi pomnożyłem obraz regionu z obrazem krawędzi, aby wybrać największy element, którego użyłem findContours + contourArea, ale pomysł jest taki sam.

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

Rozwiązanie OpenCV

Nie mogę teraz wyświetlać zdjęć z pośrednimi krokami, ponieważ mogę umieścić tylko 2 linki.

Ok, teraz używamy filtrów openSource, ale wciąż nie jest to cały open source. Ostatni krok - port do kodu c ++. Użyłem OpenCV w wersji 2.4.4

Wynik końcowego kodu c ++ to: wprowadź opis zdjęcia tutaj

Kod c ++ jest również dość krótki:

#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
źródło
Który kompilator może zbudować ten program bez błędów?
karlphillip
Użyłem Visual Studio 2012, aby go zbudować. Powinieneś użyć kompilatora c ++ z obsługą c ++ 11.
AdamF
Nie mam do tego do dyspozycji systemu. Czy możesz przepisać std::max_element()połączenie? Chciałbym również wynagrodzić twoją odpowiedź. Myślę, że mam gcc 4.2.
karlphillip
Ok, to jest funkcja c ++ 11;) Zmieniłem kod źródłowy powyżej. Proszę spróbować teraz.
AdamF
W porządku, dzięki. Przetestowałem to i jest piękne. Jak tylko to pytanie zostanie ponownie otwarte (inni użytkownicy muszą mi w tym pomóc), mogę ustawić kolejną nagrodę, aby cię wynagrodzić. Gratulacje!
karlphillip
31

... inne staromodne rozwiązanie - oparte wyłącznie na przetwarzaniu HSV :

  1. Konwertuj obrazy do przestrzeni kolorów HSV
  2. Twórz maski zgodnie z heurystyką w HSV (patrz poniżej)
  3. Zastosuj rozszerzenie morfologiczne do maski, aby połączyć rozłączone obszary
  4. Odrzuć małe obszary i bloki poziome (pamiętaj, że drzewa to bloki pionowe)
  5. Oblicz obwiednię

Słowo o heurystyce w przetwarzaniu HSV:

  1. wszystko z odcieniami (H) pomiędzy 210 - 320 stopni jest odrzucane jako niebiesko-magenta, które powinno znajdować się w tle lub w nieistotnych obszarach
  2. wszystko z wartościami (V) niższymi niż 40% jest również odrzucane jako zbyt ciemne, aby było istotne

Oczywiście można eksperymentować z wieloma innymi możliwościami dostrojenia tego podejścia ...

Oto kod MATLAB, aby załatwić sprawę (ostrzeżenie: kod jest daleki od optymalizacji !!! Użyłem technik niezalecanych do programowania MATLAB tylko po to, aby móc śledzić cokolwiek w procesie - można to znacznie zoptymalizować):

% 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

Wyniki:

W wynikach pokazuję zamaskowany obraz i obwiednię. wprowadź opis zdjęcia tutaj

sepdek
źródło
Witam, dziękuję za odpowiedź. Poświęć chwilę, aby przeczytać sekcję Wymagania, aby upewnić się, że Twoja odpowiedź jest zgodna ze wszystkimi instrukcjami. Zapomniałeś udostępnić powstałe obrazy. ;)
karlphillip
2
@karlphillip sepdek nie ma wystarczającej reputacji, aby dzielić się zdjęciami, przeniosłem zdjęcia do ciała odpowiedzi zgodnie z jego linkiem i instrukcjami. Nie jestem jednak pewien, czy to są poprawne, możesz skomentować tę część.
alko
@ Alko Wiem, dziękuję. Ale niektóre z udostępnionych obrazów nie były w zestawie wejściowym . Odpowiedź musi zawierać wynik przetwarzania wszystkich 6 zdjęć udostępnionych w pytaniu.
karlphillip
@ karlphillip to jego obrazy, nie moje. właśnie to miałem na myśli przez „skomentuj tę część”;)
alko
2
Przepraszam, że powoduję problemy ... nie moja intencja.
Umieściłem
23

Niektóre staromodne podejście do przetwarzania obrazu ...
Pomysł opiera się na założeniu, że obrazy przedstawiają oświetlone drzewa na zazwyczaj ciemniejszym i gładszym tle (lub w niektórych przypadkach na pierwszym planie). Oświetlony obszar drzewo jest bardziej „energiczny” i ma większą intensywność .
Proces przebiega następująco:

  1. Konwertuj na graylevel
  2. Zastosuj filtrowanie LoG, aby uzyskać najbardziej „aktywne” obszary
  3. Zastosuj próg intensywności, aby uzyskać najbardziej jasne obszary
  4. Połącz poprzednie 2, aby uzyskać wstępną maskę
  5. Zastosuj rozszerzenie morfologiczne, aby powiększyć obszary i połączyć sąsiednie elementy
  6. Wyeliminuj małe obszary kandydujące zgodnie z ich wielkością

Otrzymasz maskę binarną i obwiednię dla każdego obrazu.

Oto wyniki przy użyciu tej naiwnej techniki: wprowadź opis zdjęcia tutaj

Kod w MATLAB wygląda następująco: Kod działa na folderze z obrazami JPG. Ładuje wszystkie obrazy i zwraca wykryte wyniki.

% 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
źródło
Nie zapomnij przesłać powstałych obrazów, tak jak zrobił to Faust.
karlphillip
Jestem tu noobem, więc nie mogę przesyłać zdjęć. Proszę zobaczyć wyniki pod podanymi linkami w moim opisie.
sepdek
Ok, ale nadal musisz używać zdjęć udostępnionych w pytaniu, tak jak wszyscy inni. Po ich przetworzeniu prześlij je gdzieś i edytuj swoją odpowiedź, aby dodać linki. Później edytuję twoją odpowiedź i umieszczam w niej obrazy.
karlphillip
Łącze wydaje się teraz zawierać prawidłowe obrazy.
Dennis Jaheruddin,
22

Używając zupełnie innego podejścia niż to, co widziałem, stworzyłem skrypt wykrywający choinki na podstawie ich świateł. Rezultatem jest zawsze symetryczny trójkąt, a w razie potrzeby wartości liczbowe, takie jak kąt („grubość”) drzewa.

Największym zagrożeniem dla tego algorytmu są oczywiście światła obok (w dużych liczbach) lub przed drzewem (większy problem do dalszej optymalizacji). Edycja (dodano): Czego nie można zrobić: Dowiedz się, czy jest tam choinka, czy nie, znajdź wiele choinek na jednym obrazie, poprawnie wykryj choinkę w środku Las Vegas, wykrywaj mocno wygięte choinki, do góry nogami lub posiekany ...;)

Różne etapy to:

  • Oblicz dodaną jasność (R + G + B) dla każdego piksela
  • Dodaj tę wartość dla wszystkich 8 sąsiadujących pikseli na wierzchu każdego piksela
  • Uszereguj wszystkie piksele według tej wartości (najpierw najjaśniejsze) - wiem, nie bardzo subtelne ...
  • Wybierz N, zaczynając od góry, pomijając te, które są zbyt blisko
  • Oblicz z tych górnych N (daje nam przybliżony środek drzewa)
  • Zacznij od środkowej pozycji w górę w rozszerzającej się wiązce poszukiwania najwyższego światła spośród wybranych najjaśniejszych (ludzie zwykle umieszczają co najmniej jedno światło na samym szczycie)
  • Stamtąd wyobraź sobie linie przechodzące o 60 stopni w lewo i prawo w dół (choinki nie powinny być tak grube)
  • Zmniejsz te 60 stopni, aż 20% najjaśniejszych świateł znajdzie się poza tym trójkątem
  • Znajdź światło na samym dole trójkąta, które daje dolną poziomą krawędź drzewa
  • Gotowy

Objaśnienie oznaczeń:

  • Duży czerwony krzyż na środku drzewa: Mediana najwyższych N najjaśniejszych świateł
  • Linia przerywana stamtąd w górę: „wiązka wyszukiwania” na szczycie drzewa
  • Mniejszy czerwony krzyż: wierzchołek drzewa
  • Naprawdę małe czerwone krzyżyki: wszystkie z N najjaśniejszych świateł
  • Czerwony trójkąt: D'uh!

Kod źródłowy:

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


?>

Obrazy: Górny lewy Niższy środek U dołu po lewej Górny prawy Górny środek Prawy dolny

Bonus: niemiecki Weihnachtsbaum, z Wikipedii Römerberg http://commons.wikimedia.org/wiki/File:Weihnachtsbaum_R%C3%B6merberg.jpg

chrześcijanin
źródło
17

Użyłem Pythona z OpenCV.

Mój algorytm wygląda następująco:

  1. Najpierw pobiera czerwony kanał z obrazu
  2. Zastosuj próg (wartość minimalna 200) do kanału czerwonego
  3. Następnie zastosuj gradient morfologiczny, a następnie wykonaj „zamknięcie” (rozszerzenie, a następnie erozję)
  4. Następnie znajduje kontury w płaszczyźnie i wybiera najdłuższy kontur.

Wynik:

Kod:

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

Jeśli zmienię jądro z (25,5) na (10,5), otrzymam ładniejsze wyniki na wszystkich drzewach, z wyjątkiem lewego dolnego rogu, wprowadź opis zdjęcia tutaj

mój algorytm zakłada, że ​​drzewo ma na sobie światła, aw lewym dolnym drzewie góra ma mniej światła niż inne.

ifryed
źródło