Welche Bildverarbeitungstechniken könnten verwendet werden, um eine Anwendung zu implementieren, die die in den folgenden Bildern angezeigten Weihnachtsbäume erkennt?
Ich suche nach Lösungen, die mit all diesen Bildern funktionieren. Aus diesem Grund sind Ansätze, die ein Training erfordern Haar-Kaskaden-Klassifikatoren oder Template-Matching, nicht sehr interessant.
Ich suche etwas, das in jeder Programmiersprache geschrieben werden kann, solange es verwendet nur Open Source Technologien. Die Lösung muss mit den Bildern getestet werden, die für diese Frage freigegeben wurden. Es gibt 6 Eingabebilder und die Antwort sollte die Ergebnisse der jeweiligen Verarbeitung anzeigen. Schließlich muss für jedes Ausgabebildrote Linien gezeichnet werden, um den erkannten Baum zu umgeben.
Wie würden Sie programmgesteuert die Bäume in diesen Bildern erkennen?
Ich habe einen Ansatz, den ich interessant finde und der sich ein bisschen vom Rest unterscheidet. Der Hauptunterschied zwischen meinem Ansatz und einigen anderen besteht darin, wie der Bildsegmentierungsschritt durchgeführt wird - ich habe den Clustering-Algorithmus DBSCAN von Python verwendet scikit-learn; Es ist für die Suche nach etwas amorphen Formen optimiert, die nicht unbedingt einen einzigen klaren Schwerpunkt haben müssen.
Auf der obersten Ebene ist mein Ansatz ziemlich einfach und kann in etwa 3 Schritte unterteilt werden. Zuerst wende ich einen Schwellenwert an (oder tatsächlich das logische "oder" von zwei getrennten und unterschiedlichen Schwellenwerten). Wie bei vielen anderen Antworten ging ich davon aus, dass der Weihnachtsbaum eines der helleren Objekte in der Szene ist. Die erste Schwelle ist also nur ein einfacher Test der Schwarzweißhelligkeit. Alle Pixel mit Werten über 220 auf einer Skala von 0 bis 255 (wobei Schwarz 0 und Weiß 255 ist) werden in einem binären Schwarzweißbild gespeichert. Die zweite Schwelle versucht, nach roten und gelben Lichtern zu suchen, die in den Bäumen oben links und unten rechts der sechs Bilder besonders hervorzuheben sind, und hebt sich gut vom blaugrünen Hintergrund ab, der in den meisten Fotos vorherrscht. Ich konvertiere das RGB-Bild in einen HSV-Raum und fordere, dass der Farbton entweder weniger als 0,2 auf einer Skala von 0,0 bis 1,0 (entspricht in etwa der Grenze zwischen Gelb und Grün) oder mehr als 0,95 (entspricht der Grenze zwischen Lila und Rot) beträgt. und zusätzlich benötige ich helle, gesättigte Farben: Sättigung und Wert müssen beide über 0,7 liegen. Die Ergebnisse der beiden Schwellenwertprozeduren sind logisch "oder" verknüpft, und die resultierende Matrix von Schwarzweiß-Binärbildern ist nachstehend gezeigt:
Sie können deutlich erkennen, dass jedes Bild einen großen Pixelcluster aufweist, der ungefähr der Position jedes Baums entspricht, und dass einige der Bilder auch einige andere kleine Cluster aufweisen, die entweder Lichtern in den Fenstern einiger Gebäude oder a entsprechen Hintergrundszene am Horizont. Der nächste Schritt besteht darin, den Computer zu veranlassen, zu erkennen, dass es sich um separate Cluster handelt, und jedes Pixel korrekt mit einer ID-Nummer für die Clustermitgliedschaft zu kennzeichnen.
Für diese Aufgabe habe ich DBSCAN gewählt. Es gibt einen ziemlich guten visuellen Vergleich des typischen Verhaltens von DBSCAN im Vergleich zu anderen verfügbaren Clustering-Algorithmen hier . Wie ich bereits sagte, funktioniert es gut mit amorphen Formen. Die Ausgabe von DBSCAN, wobei jeder Cluster in einer anderen Farbe dargestellt ist, wird hier gezeigt:
Bei der Betrachtung dieses Ergebnisses sind einige Dinge zu beachten. Erstens verlangt DBSCAN, dass der Benutzer einen "Proximity" -Parameter einstellt, um sein Verhalten zu regulieren, wodurch effektiv gesteuert wird, wie getrennt ein Punktepaar sein muss, damit der Algorithmus einen neuen separaten Cluster deklariert, anstatt einen Testpunkt zu agglomerieren ein bereits bestehender Cluster. Ich habe diesen Wert auf das 0,04-fache der Größe entlang der Diagonalen jedes Bildes festgelegt. Da die Größe der Bilder von ungefähr VGA bis zu ungefähr HD 1080 variiert, ist diese Art der skalierungsbezogenen Definition von entscheidender Bedeutung.
Ein weiterer erwähnenswerter Punkt ist, dass der DBSCAN-Algorithmus, wie er in scikit-learn implementiert ist, Speichergrenzen aufweist, die für einige der größeren Bilder in diesem Beispiel ziemlich herausfordernd sind. Daher musste ich für einige der größeren Bilder tatsächlich jeden Cluster "dezimieren" (d. H. Nur jedes dritte oder vierte Pixel beibehalten und die anderen fallen lassen), um innerhalb dieser Grenze zu bleiben. Infolge dieses Ausleseprozesses sind die verbleibenden einzelnen dünnbesetzten Pixel auf einigen der größeren Bilder schwer zu sehen. Daher wurden die farbcodierten Pixel in den obigen Bildern nur zu Anzeigezwecken effektiv nur geringfügig "gedehnt", damit sie besser hervorstechen. Es ist lediglich eine kosmetische Operation, um der Erzählung willen; Obwohl es Kommentare gibt, die diese Erweiterung in meinem Code erwähnen, können Sie sicher sein, dass sie nichts mit Berechnungen zu tun hat, die tatsächlich von Bedeutung sind.
Sobald die Cluster identifiziert und beschriftet sind, ist der dritte und letzte Schritt einfach: Ich nehme einfach den größten Cluster in jedem Bild (in diesem Fall habe ich die "Größe" in Bezug auf die Gesamtzahl der Elementpixel gemessen, obwohl dies möglich ist Verwenden Sie stattdessen einfach eine Art Metrik, die die physikalische Ausdehnung misst, und berechnen Sie die konvexe Hülle für diesen Cluster. Die konvexe Hülle wird dann zur Baumgrenze. Die sechs nach dieser Methode berechneten konvexen Rümpfe sind unten rot dargestellt:
Der Quellcode ist für Python 2.7.6 geschrieben und hängt von numpy , scipy , matplotlib und ab scikit-learn . Ich habe es in zwei Teile geteilt. Der erste Teil ist für die eigentliche Bildverarbeitung verantwortlich:
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
und der zweite Teil ist ein Skript auf Benutzerebene, das die erste Datei aufruft und alle oben genannten Diagramme generiert:
#!/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()
BEARBEITEN HINWEIS: Ich habe diesen Beitrag bearbeitet, um (i) jedes Baumbild einzeln zu verarbeiten, wie in den Anforderungen gefordert, (ii) um sowohl die Objekthelligkeit als auch die Helligkeit zu berücksichtigen formen, um die Qualität des Ergebnisses zu verbessern.
Im Folgenden wird ein Ansatz vorgestellt, der die Helligkeit und Form des Objekts berücksichtigt. Mit anderen Worten, es werden Objekte mit einer dreieckigen Form und einer signifikanten Helligkeit gesucht. Es wurde in Java unter Verwendung des Marvin Bildverarbeitungs-Frameworks implementiert.
Der erste Schritt ist die Farbschwelle. Ziel ist es hier, die Analyse auf Objekte mit signifikanter Helligkeit zu fokussieren.
Ausgabebilder:
Quellcode:
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();
}
}
Im zweiten Schritt werden die hellsten Punkte im Bild erweitert, um Formen zu bilden. Das Ergebnis dieses Prozesses ist die wahrscheinliche Form der Objekte mit signifikanter Helligkeit. Durch Anwenden der Flutfüllsegmentierung werden nicht verbundene Formen erkannt.
Ausgabebilder:
Quellcode:
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();
}
}
Wie im Ausgabebild gezeigt, wurden mehrere Formen erkannt. In diesem Problem gibt es nur ein paar helle Punkte in den Bildern. Dieser Ansatz wurde jedoch implementiert, um mit komplexeren Szenarien fertig zu werden.
Im nächsten Schritt wird jede Form analysiert. Ein einfacher Algorithmus erkennt Formen mit einem Muster ähnlich einem Dreieck. Der Algorithmus analysiert die Objektform Zeile für Zeile. Wenn der Massenmittelpunkt jeder Formlinie (bei gegebenem Schwellenwert) nahezu gleich ist und die Masse mit zunehmendem y zunimmt, hat das Objekt eine dreieckige Form. Die Masse der Formlinie ist die Anzahl der Pixel in der Linie, die zur Form gehört. Stellen Sie sich vor, Sie schneiden das Objekt horizontal und analysieren jedes horizontale Segment. Wenn sie zueinander zentriert sind und die Länge vom ersten bis zum letzten Segment in einem linearen Muster zunimmt, haben Sie wahrscheinlich ein Objekt, das einem Dreieck ähnelt.
Quellcode:
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;
}
Schließlich wird die Position jeder Form, die einem Dreieck ähnelt und eine signifikante Helligkeit aufweist, in diesem Fall ein Weihnachtsbaum, im Originalbild hervorgehoben, wie unten gezeigt.
endgültige Ausgabebilder:
endgültiger Quellcode:
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();
}
}
Der Vorteil dieses Ansatzes ist die Tatsache, dass er wahrscheinlich mit Bildern funktioniert, die andere leuchtende Objekte enthalten, da er die Objektform analysiert.
Fröhliche Weihnachten!
BEARBEITEN SIE ANMERKUNG 2
Es gibt eine Diskussion über die Ähnlichkeit der Ausgabebilder dieser und einiger anderer Lösungen. In der Tat sind sie sehr ähnlich. Dieser Ansatz segmentiert jedoch nicht nur Objekte. In gewisser Weise werden auch die Objektformen analysiert. Es kann mit mehreren leuchtenden Objekten in derselben Szene umgehen. Tatsächlich muss der Weihnachtsbaum nicht der hellste sein. Ich nehme es nur auf, um die Diskussion zu bereichern. In den Samples gibt es eine Tendenz, dass Sie die Bäume finden, wenn Sie nur nach dem hellsten Objekt suchen. Aber wollen wir die Diskussion an dieser Stelle wirklich beenden? Inwieweit erkennt der Computer tatsächlich ein Objekt, das einem Weihnachtsbaum ähnelt? Versuchen wir, diese Lücke zu schließen.
Nachfolgend finden Sie ein Ergebnis, um diesen Punkt zu erläutern:
Eingabebild
Ausgabe
Hier ist meine einfache und dumme Lösung. Es basiert auf der Annahme, dass der Baum das hellste und größte Objekt auf dem Bild ist.
//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;
}
Der erste Schritt besteht darin, die hellsten Pixel im Bild zu erkennen. Wir müssen jedoch zwischen dem Baum selbst und dem Schnee, der sein Licht reflektiert, unterscheiden. Hier versuchen wir, den Schnee auszuschließen, indem wir einen wirklich einfachen Filter für die Farbcodes anwenden:
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);
Dann finden wir jedes "helle" Pixel:
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);
Schließlich verbinden wir die beiden Ergebnisse:
bitwise_and(tmp, tmp1, tmp1);
Nun suchen wir nach dem größten hellen Objekt:
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);
Jetzt haben wir es fast geschafft, aber es gibt immer noch einige Unvollkommenheiten aufgrund des Schnees. Um sie abzuschneiden, erstellen wir eine Maske mit einem Kreis und einem Rechteck, um die Form eines Baumes zu approximieren und unerwünschte Teile zu löschen:
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);
Der letzte Schritt ist, die Kontur unseres Baumes zu finden und auf das Originalbild zu zeichnen.
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);
Es tut mir leid, aber im Moment habe ich eine schlechte Verbindung, so dass ich keine Bilder hochladen kann. Ich werde es später versuchen.
Fröhliche Weihnachten.
BEARBEITEN:
Hier einige Bilder der Endausgabe:
Ich habe den Code in Matlab R2007a geschrieben. Ich habe k-means benutzt, um den Weihnachtsbaum grob zu extrahieren. Ich zeige mein Zwischenergebnis nur mit einem Bild und das Endergebnis mit allen sechs.
Zuerst habe ich den RGB-Raum auf den Lab-Raum abgebildet, wodurch der Kontrast von Rot in seinem b-Kanal verbessert werden könnte:
colorTransform = makecform('srgb2lab');
I = applycform(I, colorTransform);
L = double(I(:,:,1));
a = double(I(:,:,2));
b = double(I(:,:,3));
Neben dem Merkmal im Farbraum habe ich auch ein Texturmerkmal verwendet, das für die Nachbarschaft relevant ist, und nicht für jedes Pixel. Hier habe ich die Intensität der 3 ursprünglichen Kanäle (R, G, B) linear kombiniert. Der Grund, warum ich so formatiert habe, ist, dass die Weihnachtsbäume auf dem Bild alle rot beleuchtet sind und manchmal auch grün/manchmal blau beleuchtet sind.
R=double(Irgb(:,:,1));
G=double(Irgb(:,:,2));
B=double(Irgb(:,:,3));
I0 = (3*R + max(G,B)-min(G,B))/2;
Ich habe ein lokales 3X3-Binärmuster auf I0
Angewendet, das mittlere Pixel als Schwelle verwendet und den Kontrast durch Berechnen der Differenz zwischen dem mittleren Pixelintensitätswert über der Schwelle und dem mittleren Wert darunter erhalten.
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
Da ich insgesamt 4 Features habe, würde ich bei meiner Clustering-Methode K = 5 wählen. Der Code für k-means ist unten gezeigt (er stammt aus dem maschinellen Lernkurs von Dr. Andrew Ng. Ich habe den Kurs zuvor besucht und den Code selbst in seiner Programmieraufgabe geschrieben).
[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
Da das Programm auf meinem Computer sehr langsam läuft, habe ich nur 3 Iterationen ausgeführt. Normalerweise ist das Stoppkriterium (i) eine Iterationszeit von mindestens 10 oder (ii) keine Änderung mehr an den Schwerpunkten. Nach meinem Test kann durch Erhöhen der Iteration der Hintergrund (Himmel und Baum, Himmel und Gebäude, ...) genauer unterschieden werden, es wurden jedoch keine drastischen Änderungen bei der Weihnachtsbaumextraktion festgestellt. Beachten Sie auch, dass k-means nicht gegen die zufällige Initialisierung des Schwerpunkts immun ist. Daher wird empfohlen, das Programm mehrmals auszuführen, um einen Vergleich durchzuführen.
Nach dem k-Mittel wurde der markierte Bereich mit der maximalen Intensität von I0
Ausgewählt. Die Grenzverfolgung wurde verwendet, um die Grenzen zu extrahieren. Für mich ist der letzte Weihnachtsbaum am schwierigsten zu extrahieren, da der Kontrast in diesem Bild nicht hoch genug ist, wie in den ersten fünf. Ein weiteres Problem bei meiner Methode ist, dass ich die Funktion bwboundaries
in Matlab verwendet habe, um die Grenze zu verfolgen, aber manchmal sind auch die inneren Grenzen enthalten, wie Sie in den Ergebnissen 3, 5 und 6 sehen können. Die dunkle Seite der Weihnachtsbäume wird nicht nur nicht mit der beleuchteten Seite geclustert, sondern führt auch dazu, dass so viele winzige innere Grenzen nachgezeichnet werden (imfill
verbessert sich nicht sehr). Insgesamt hat mein Algorithmus noch viel Verbesserungsraum.
Einige Veröffentlichung s weisen darauf hin, dass die Mittelwertverschiebung robuster sein kann als die k-Mittelwerte, und viele auf Graphenschnitten basierende Algorithmen sind auch bei der Segmentierung komplizierter Grenzen sehr wettbewerbsfähig. Ich habe selbst einen Mean-Shift-Algorithmus geschrieben, der die Regionen ohne genügend Licht besser extrahiert. Die Mittelwertverschiebung ist jedoch ein wenig übersegmentiert, und es ist eine Strategie zum Zusammenführen erforderlich. Es lief sogar viel langsamer als k-means auf meinem Computer, ich fürchte, ich muss es aufgeben. Ich freue mich sehr darauf, dass andere mit den oben genannten modernen Algorithmen hier hervorragende Ergebnisse liefern.
Ich bin jedoch immer der Meinung, dass die Auswahl der Features die Schlüsselkomponente bei der Bildsegmentierung ist. Mit einer geeigneten Feature-Auswahl, die den Abstand zwischen Objekt und Hintergrund maximieren kann, funktionieren viele Segmentierungsalgorithmen auf jeden Fall. Verschiedene Algorithmen können das Ergebnis von 1 auf 10 verbessern, aber die Merkmalsauswahl kann es von 0 auf 1 verbessern.
Fröhliche Weihnachten !
Dies ist mein letzter Beitrag mit den traditionellen Bildverarbeitungsansätzen ...
Hier verbinde ich irgendwie meine beiden anderen Vorschläge, um noch bessere Ergebnisse zu erzielen . Tatsächlich kann ich nicht sehen, wie diese Ergebnisse besser sein könnten (besonders wenn Sie sich die maskierten Bilder ansehen, die die Methode erzeugt).
Im Mittelpunkt des Ansatzes steht die Kombination von drei Schlüsselannahmen :
Unter diesen Voraussetzungen funktioniert die Methode wie folgt:
Hier ist der Code in MATLAB (wieder lädt das Skript alle JPG-Bilder im aktuellen Ordner und wieder ist dies alles andere als ein optimierter Code):
% 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
Hochauflösende Ergebnisse erhalten Sie hier noch !
Weitere Experimente mit zusätzlichen Bildern finden Sie hier.
Meine Lösungsschritte:
R-Kanal abrufen (von RGB) - alle Operationen, die wir auf diesem Kanal ausführen:
Region of Interest (ROI) erstellen
Schwelle R Kanal mit min Wert 149 (Bild oben rechts)
Ergebnisbereich erweitern (mittleres linkes Bild)
Erkennen Sie Ränder im berechneten Roi. Baum hat viele Kanten (mittleres rechtes Bild)
Ergebnis erweitern
Erodieren mit größerem Radius (Bild unten links)
Wählen Sie das größte (nach Fläche) Objekt aus - es ist die Ergebnisregion
ConvexHull (Baum ist konvexes Polygon) (Bild unten rechts)
Bounding Box (Bild unten rechts - Grren Box)
Schritt für Schritt:
Das erste Ergebnis - am einfachsten, aber nicht in Open-Source-Software - "Adaptive Vision Studio + Adaptive Vision Library": Dies ist kein Open-Source-Produkt, aber schnell zu einem Prototyp:
Ganzer Algorithmus zum Erkennen von Weihnachtsbaum (11 Blöcke):
Nächster Schritt. Wir wollen eine Open Source Lösung. Ändern Sie AVL-Filter in OpenCV-Filter: Hier habe ich kleine Änderungen vorgenommen, z. Kantenerkennung Verwenden Sie den cvCanny-Filter, um den roi zu berücksichtigen. Ich habe das Regionsbild mit dem Kantenbild multipliziert, um das größte Element auszuwählen, das ich verwendet habe. FindContours + contourArea, aber die Idee ist dieselbe.
https://www.youtube.com/watch?v=sfjB3MigLH0&index=1&list=UUpSRrkMHNHiLDXgylwhWNQQ
Ich kann jetzt keine Bilder mit Zwischenschritten anzeigen, da ich nur zwei Links einfügen kann.
Ok, jetzt verwenden wir OpenSource-Filter, aber es ist noch nicht ganz Open Source. Letzter Schritt - Portierung auf C++ - Code. Ich habe OpenCV in Version 2.4.4 verwendet
Das Ergebnis des endgültigen C++ - Codes ist:
c ++ Code ist auch ziemlich kurz:
#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;
}
... eine andere altmodische Lösung - rein basierend auf HSV-Verarbeitung:
Ein Wort zur Heuristik in der HSV-Bearbeitung:
Natürlich kann man mit zahlreichen anderen Möglichkeiten experimentieren, um diesen Ansatz zu verfeinern ...
Hier ist der MATLAB-Code, der den Trick ausführt (Warnung: Der Code ist keineswegs optimiert !!! Ich habe Techniken verwendet, die für die MATLAB-Programmierung nicht empfohlen wurden, um in der Lage zu sein, irgendetwas im Prozess zu verfolgen - dies kann stark optimiert werden):
% 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
In den Ergebnissen zeige ich das maskierte Bild und den Begrenzungsrahmen.
Ein altmodischer Bildverarbeitungsansatz ...
Die Idee basiert auf dem Annahme, dass Bilder beleuchtete Bäume auf normalerweise dunkleren und glatteren Hintergründen darstellen (oder in einigen Fällen auf den Vordergrund). Die beleuchtete Baumfläche ist "energetischer" und hat eine höhere Intensität.
Der Prozess ist wie folgt:
Sie erhalten für jedes Bild eine Binärmaske und einen Begrenzungsrahmen.
Hier sind die Ergebnisse mit dieser naiven Technik:
Code in MATLAB folgt: Der Code wird in einem Ordner mit JPG-Bildern ausgeführt. Lädt alle Bilder und gibt erkannte Ergebnisse zurück.
% 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
Mit einem ganz anderen Ansatz als dem, was ich gesehen habe, habe ich ein php Skript erstellt, das Weihnachtsbäume an ihren Lichtern erkennt. Das Ergebnis ist immer ein symmetrisches Dreieck und ggf. numerische Werte wie der Winkel ("Fett") des Baumes.
Die größte Bedrohung für diesen Algorithmus sind offensichtlich Lichter neben (in großer Zahl) oder vor dem Baum (das größere Problem bis zur weiteren Optimierung). Bearbeiten (hinzugefügt): Was es nicht kann: Finden Sie heraus, ob es einen Weihnachtsbaum gibt oder nicht, finden Sie mehrere Weihnachtsbäume in einem Bild, erkennen Sie einen Weihnachtsbaum mitten in Las Vegas richtig, erkennen Sie stark gebogene Weihnachtsbäume, kopfüber oder gehackt ...;)
Die verschiedenen Stadien sind:
Erklärung der Markierungen:
Quellcode:
<?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;
}
?>
Bilder:
Bonus: Ein deutscher Weihnachtsbaum aus Wikipedia http://commons.wikimedia.org/wiki/File:Weihnachtsbaum_R%C3%B6merberg.jpg
Ich habe python mit opencv verwendet.
Mein Algorithmus sieht so aus:
Der Code:
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 __== "__main__":
main()
Wenn ich den Kernel von (25,5) auf (10,5) ändere, erhalte ich bessere Ergebnisse bei allen Bäumen außer dem unteren linken Rand.
mein Algorithmus geht davon aus, dass der Baum beleuchtet ist, und im linken unteren Baum hat der obere weniger Licht als die anderen.