Reiskörner zählen

81

Betrachten Sie diese 10 Bilder von verschiedenen Mengen von ungekochten Körnern von weißem Reis.
DAS SIND NUR DAUMENNÄGEL. Klicken Sie auf ein Bild, um es in voller Größe anzuzeigen.

A: B: C: D: E:EIN B C D E

F: G: H: I: J:F G H ich J

Getreideanzahl: A: 3, B: 5, C: 12, D: 25, E: 50, F: 83, G: 120, H:150, I: 151, J: 200

Beachte das...

  • Die Körner berühren sich zwar, überlappen sich jedoch nie. Die Anordnung der Körner ist niemals mehr als ein Korn hoch.
  • Die Bilder haben unterschiedliche Dimensionen, aber der Maßstab des Reises ist in allen konsistent, da die Kamera und der Hintergrund stationär waren.
  • Die Körner überschreiten niemals die Grenzen oder berühren die Bildgrenzen.
  • Der Hintergrund ist immer der gleiche gleichmäßige gelblich-weiße Farbton.
  • Kleine und große Körner werden jeweils als ein Korn gezählt.

Diese 5 Punkte sind Garantien für alle Bilder dieser Art.

Herausforderung

Schreiben Sie ein Programm, das solche Bilder aufnimmt und die Anzahl der Reiskörner so genau wie möglich zählt.

Ihr Programm sollte den Dateinamen des Bildes verwenden und die Anzahl der berechneten Körner ausgeben. Ihr Programm muss für mindestens eines dieser Bilddateiformate funktionieren: JPEG, Bitmap, PNG, GIF, TIFF (im Moment sind alle Bilder JPEGs).

Sie können Bildverarbeitungs- und Bildverarbeitungsbibliotheken verwenden.

Sie dürfen die Ausgaben der 10 Beispielbilder nicht fest codieren. Ihr Algorithmus sollte auf alle ähnlichen Reiskornbilder anwendbar sein. Es sollte auf einem anständigen modernen Computer in weniger als 5 Minuten ausgeführt werden können, wenn der Bildbereich weniger als 2000 * 2000 Pixel beträgt und weniger als 300 Reiskörner vorhanden sind.

Wertung

Nehmen Sie für jedes der 10 Bilder den absoluten Wert der tatsächlichen Anzahl der Körner abzüglich der Anzahl der Körner, die Ihr Programm vorhersagt. Summiere diese absoluten Werte, um deine Punktzahl zu erhalten. Die niedrigste Punktzahl gewinnt. Eine Punktzahl von 0 ist perfekt.

Bei Stimmengleichheit gewinnt die am höchsten bewertete Antwort. Ich kann Ihr Programm an zusätzlichen Bildern testen, um seine Gültigkeit und Richtigkeit zu überprüfen.

Calvins Hobbys
quelle
1
Sicherlich muss jemand versuchen, Skikit zu lernen!
Toller Wettbewerb! :) Übrigens - könnten Sie uns etwas über das Enddatum dieser Herausforderung erzählen?
Cyriel
1
@ Lembik Down to 7 :)
Dr. Belisarius
5
Eines Tages wird ein Reiswissenschaftler vorbeikommen und sich Hals über Kopf darüber freuen, dass es diese Frage gibt.
Nit
2
@Nit Sagen Sie ihnen einfach ncbi.nlm.nih.gov/pmc/articles/PMC3510117 :)
Dr. belisarius

Antworten:

22

Mathematica, Kerbe: 7

i = {"http://i.stack.imgur.com/8T6W2.jpg",  "http://i.stack.imgur.com/pgWt1.jpg", 
     "http://i.stack.imgur.com/M0K5w.jpg",  "http://i.stack.imgur.com/eUFNo.jpg", 
     "http://i.stack.imgur.com/2TFdi.jpg",  "http://i.stack.imgur.com/wX48v.jpg", 
     "http://i.stack.imgur.com/eXCGt.jpg",  "http://i.stack.imgur.com/9na4J.jpg",
     "http://i.stack.imgur.com/UMP9V.jpg",  "http://i.stack.imgur.com/nP3Hr.jpg"};

im = Import /@ i;

Ich denke, die Namen der Funktion sind beschreibend genug:

getSatHSVChannelAndBinarize[i_Image]             := Binarize@ColorSeparate[i, "HSB"][[2]]
removeSmallNoise[i_Image]                        := DeleteSmallComponents[i, 100]
fillSmallHoles[i_Image]                          := Closing[i, 1]
getMorphologicalComponentsAreas[i_Image]         := ComponentMeasurements[i, "Area"][[All, 2]]
roundAreaSizeToGrainCount[areaSize_, grainSize_] := Round[areaSize/grainSize]

Verarbeitung aller Bilder auf einmal:

counts = Plus @@@
  (roundAreaSizeToGrainCount[#, 2900] & /@
      (getMorphologicalComponentsAreas@
        fillSmallHoles@
         removeSmallNoise@
          getSatHSVChannelAndBinarize@#) & /@ im)

(* Output {3, 5, 12, 25, 49, 83, 118, 149, 152, 202} *)

Die Punktzahl ist:

counts - {3, 5, 12, 25, 50, 83, 120, 150, 151, 200} // Abs // Total
(* 7 *)

Hier sehen Sie die Punktempfindlichkeit bezogen auf die verwendete Korngröße:

Mathematica-Grafiken

Dr. belisarius
quelle
2
Viel klarer, danke!
Calvins Hobbys
Kann diese genaue Prozedur in Python kopiert werden, oder gibt es hier etwas Besonderes, das Mathematica nicht kann, was Python-Bibliotheken nicht können?
@ Lembik Keine Ahnung. Keine Python hier. Es tut uns leid. (Ich bezweifle jedoch, genau die gleichen Algorithmen für die EdgeDetect[], DeleteSmallComponents[]und Dilation[]werden an anderer Stelle implementiert)
Dr. belisarius
55

Python, Partitur: 24 16

Diese Lösung basiert wie die von Falko darauf, die "Vordergrund" -Fläche zu messen und durch die durchschnittliche Kornfläche zu dividieren.

Tatsächlich versucht dieses Programm, den Hintergrund und nicht den Vordergrund zu erkennen. Da Reiskörner niemals die Bildgrenze berühren, füllt das Programm zunächst die linke obere Ecke mit Weiß. Der Flood-Fill-Algorithmus zeichnet benachbarte Pixel, wenn der Unterschied zwischen ihnen und der Helligkeit des aktuellen Pixels innerhalb eines bestimmten Schwellenwerts liegt, und passt sich so an die allmähliche Änderung der Hintergrundfarbe an. Am Ende dieser Phase könnte das Bild ungefähr so ​​aussehen:

Abbildung 1

Wie Sie sehen, macht es eine ziemlich gute Arbeit beim Erkennen des Hintergrunds, aber es lässt alle Bereiche aus, die zwischen den Körnern "eingeschlossen" sind. Wir behandeln diese Bereiche, indem wir die Hintergrundhelligkeit für jedes Pixel abschätzen und alle gleich oder heller werdenden Pixel zusammenfassen. Diese Schätzung funktioniert folgendermaßen: Während der Überflutungsphase berechnen wir die durchschnittliche Hintergrundhelligkeit für jede Zeile und jede Spalte. Die geschätzte Hintergrundhelligkeit bei jedem Pixel ist der Durchschnitt der Zeilen- und Spaltenhelligkeit bei diesem Pixel. Dies erzeugt so etwas:

Figur 2

BEARBEITEN: Schließlich wird die Fläche jeder zusammenhängenden Vordergrundregion (dh nicht weißen Region) durch die durchschnittliche, vorberechnete Kornfläche dividiert, was uns eine Schätzung der Kornzahl in dieser Region gibt. Die Summe dieser Größen ergibt das Ergebnis. Anfangs haben wir dasselbe für den gesamten Vordergrundbereich als Ganzes getan, aber dieser Ansatz ist buchstäblich feinkörniger.


from sys import argv; from PIL import Image

# Init
I = Image.open(argv[1]); W, H = I.size; A = W * H
D = [sum(c) for c in I.getdata()]
Bh = [0] * H; Ch = [0] * H
Bv = [0] * W; Cv = [0] * W

# Flood-fill
Background = 3 * 255 + 1; S = [0]
while S:
    i = S.pop(); c = D[i]
    if c != Background:
        D[i] = Background
        Bh[i / W] += c; Ch[i / W] += 1
        Bv[i % W] += c; Cv[i % W] += 1
        S += [(i + o) % A for o in [1, -1, W, -W] if abs(D[(i + o) % A] - c) < 10]

# Eliminate "trapped" areas
for i in xrange(H): Bh[i] /= float(max(Ch[i], 1))
for i in xrange(W): Bv[i] /= float(max(Cv[i], 1))
for i in xrange(A):
    a = (Bh[i / W] + Bv[i % W]) / 2
    if D[i] >= a: D[i] = Background

# Estimate grain count
Foreground = -1; avg_grain_area = 3038.38; grain_count = 0
for i in xrange(A):
    if Foreground < D[i] < Background:
        S = [i]; area = 0
        while S:
            j = S.pop() % A
            if Foreground < D[j] < Background:
                D[j] = Foreground; area += 1
                S += [j - 1, j + 1, j - W, j + W]
        grain_count += int(round(area / avg_grain_area))

# Output
print grain_count

Übernimmt den eingegebenen Dateinamen durch die Kommandozeile.

Ergebnisse

      Actual  Estimate  Abs. Error
A         3         3           0
B         5         5           0
C        12        12           0
D        25        25           0
E        50        48           2
F        83        83           0
G       120       116           4
H       150       145           5
I       151       156           5
J       200       200           0
                        ----------
                Total:         16

EIN B C D E

F G H ich J

Ell
quelle
2
Das ist eine wirklich clevere Lösung, gute Arbeit!
Chris Cirefice
1
woher avg_grain_area = 3038.38;kommen aus?
njzk2
2
Zählt das nicht als hardcoding the result?
njzk2
5
@ njzk2 Nein. Angesichts der Regel The images have different dimensions but the scale of the rice in all of them is consistent because the camera and background were stationary.Dies ist nur ein Wert, der diese Regel darstellt. Das Ergebnis ändert sich jedoch je nach Eingabe. Wenn Sie die Regel ändern, ändert sich dieser Wert, aber das Ergebnis ist das gleiche - basierend auf der Eingabe.
Adam Davis
6
Mir geht es gut mit der Durchschnittsfläche. Die Kornfläche ist über die Bilder (ungefähr) konstant.
Calvins Hobbys
28

Python + OpenCV: Punktzahl 27

Horizontale Linienabtastung

Idee: Scannen Sie das Bild zeilenweise. Zählen Sie für jede Zeile die Anzahl der Reiskörner (indem Sie prüfen, ob das Pixel schwarz zu weiß oder umgekehrt wird). Wenn sich die Anzahl der Körner für die Linie erhöht (im Vergleich zur vorherigen Linie), bedeutet dies, dass wir auf ein neues Korn gestoßen sind. Wenn diese Zahl abnimmt, bedeutet dies, dass wir über ein Korn gefahren sind. In diesem Fall addieren Sie +1 zum Gesamtergebnis.

Bildbeschreibung hier eingeben

Number in red = rice grains encountered for that line
Number in gray = total amount of grains encountered (what we are looking for)

Aufgrund der Funktionsweise des Algorithmus ist es wichtig, ein sauberes Schwarzweißbild zu haben. Viele Geräusche führen zu schlechten Ergebnissen. Zuerst wird der Haupthintergrund mit Floodfill (Lösung ähnlich der Ell-Antwort) gereinigt, dann wird der Schwellenwert angewendet, um ein Schwarzweiß-Ergebnis zu erzielen.

Bildbeschreibung hier eingeben

Es ist alles andere als perfekt, liefert aber gute Ergebnisse in Bezug auf die Einfachheit. Es gibt wahrscheinlich viele Möglichkeiten, dies zu verbessern (indem ein besseres S / W-Bild bereitgestellt wird, in andere Richtungen gescannt wird (z. B. vertikal, diagonal) und der Durchschnitt ermittelt wird usw.).

import cv2
import numpy
import sys

filename = sys.argv[1]
I = cv2.imread(filename, 0)
h,w = I.shape[:2]
diff = (3,3,3)
mask = numpy.zeros((h+2,w+2),numpy.uint8)
cv2.floodFill(I,mask,(0,0), (255,255,255),diff,diff)
T,I = cv2.threshold(I,180,255,cv2.THRESH_BINARY)
I = cv2.medianBlur(I, 7)

totalrice = 0
oldlinecount = 0
for y in range(0, h):
    oldc = 0
    linecount = 0
    start = 0   
    for x in range(0, w):
        c = I[y,x] < 128;
        if c == 1 and oldc == 0:
            start = x
        if c == 0 and oldc == 1 and (x - start) > 10:
            linecount += 1
        oldc = c
    if oldlinecount != linecount:
        if linecount < oldlinecount:
            totalrice += oldlinecount - linecount
        oldlinecount = linecount
print totalrice

Die Fehler pro Bild: 0, 0, 0, 3, 0, 12, 4, 0, 7, 1

tigrou
quelle
24

Python + OpenCV: Ergebnis 84

Hier ist ein erster naiver Versuch. Es wendet eine adaptive Schwelle mit manuell eingestellten Parametern an, schließt einige Löcher mit anschließender Erosion und Verdünnung und leitet die Anzahl der Körner aus dem Vordergrundbereich ab.

import cv2
import numpy as np

filename = raw_input()

I = cv2.imread(filename, 0)
I = cv2.medianBlur(I, 3)
bw = cv2.adaptiveThreshold(I, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 101, 1)

kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (17, 17))
bw = cv2.dilate(cv2.erode(bw, kernel), kernel)

print np.round_(np.sum(bw == 0) / 3015.0)

Hier sehen Sie die Zwischenbilder (schwarz steht im Vordergrund):

Bildbeschreibung hier eingeben

Die Fehler pro Bild sind 0, 0, 2, 2, 4, 0, 27, 42, 0 und 7 Körner.

Falko
quelle
20

C # + OpenCvSharp, Ergebnis: 2

Dies ist mein zweiter Versuch. Es ist ganz anders als bei meinem ersten Versuch , der viel einfacher ist, und ich veröffentliche ihn als separate Lösung.

Die Grundidee besteht darin, jedes einzelne Korn durch eine iterative Ellipsenanpassung zu identifizieren und zu kennzeichnen. Entfernen Sie dann die Pixel für dieses Korn aus der Quelle und versuchen Sie, das nächste Korn zu finden, bis jedes Pixel markiert wurde.

Dies ist nicht die schönste Lösung. Es ist ein Riesenschwein mit 600 Codezeilen. Für das größte Bild werden 1,5 Minuten benötigt. Und ich entschuldige mich wirklich für den chaotischen Code.

Es gibt so viele Parameter und Denkweisen in dieser Sache, dass ich ziemlich Angst habe, mein Programm für die 10 Beispielbilder zu überarbeiten. Das Endergebnis von 2 ist fast definitiv ein Fall von Überanpassung: Ich habe zwei Parameter, average grain size in pixelund minimum ratio of pixel / elipse_area, und am Ende erschöpft ich einfach alle Kombinationen dieser beiden Parameter , bis ich die niedrigste Punktzahl bekam. Ich bin mir nicht sicher, ob das mit den Regeln dieser Herausforderung so koscher ist.

average_grain_size_in_pixel = 2530
pixel / elipse_area >= 0.73

Aber auch ohne diese überanpassenden Kupplungen sind die Ergebnisse ganz nett. Ohne eine feste Korngröße oder ein festes Pixelverhältnis beträgt die Punktzahl, einfach durch Schätzen der durchschnittlichen Korngröße aus den Trainingsbildern, immer noch 27.

Und als Ergebnis erhalte ich nicht nur die Anzahl, sondern auch die tatsächliche Position, Ausrichtung und Form jedes Korns. Es gibt eine kleine Anzahl von falsch etikettierten Körnern, aber insgesamt stimmen die meisten Etiketten genau mit den tatsächlichen Körnern überein:

A EIN B B C C D D EE

F F G G H H I ich JJ

(Klicken Sie auf jedes Bild für die Vollversion)

Nach diesem Markierungsschritt untersucht mein Programm jedes einzelne Korn und schätzt basierend auf der Anzahl der Pixel und dem Pixel / Ellipsen-Flächenverhältnis, ob dies der Fall ist

  • ein einzelnes Korn (+1)
  • mehrere Körner als eins falsch beschriftet (+ X)
  • Ein Fleck, der zu klein ist, um ein Korn zu sein (+0)

Die Fehlerwerte für jedes Bild sind A:0; B:0; C:0; D:0; E:2; F:0; G:0 ; H:0; I:0, J:0

Der tatsächliche Fehler ist jedoch wahrscheinlich etwas höher. Einige Fehler im selben Bild heben sich gegenseitig auf. Insbesondere Bild H weist einige stark falsch etikettierte Körner auf, wohingegen in Bild E die Etiketten größtenteils korrekt sind

Das Konzept ist ein wenig ausgedacht:

  • Zuerst wird der Vordergrund durch Otsu-Thresholding auf dem Sättigungskanal getrennt (siehe meine vorherige Antwort für Details).

  • Wiederholen, bis keine Pixel mehr übrig sind:

    • wähle den größten Blob aus
    • Wählen Sie 10 zufällige Randpixel auf diesem Blob als Startpositionen für ein Korn

    • für jeden Startpunkt

      • Nehmen Sie an dieser Position eine Körnung mit einer Höhe und einer Breite von 10 Pixel an.

      • Wiederholen bis zur Konvergenz

        • Gehen Sie von diesem Punkt aus in verschiedenen Winkeln radial nach außen, bis Sie auf ein Randpixel stoßen (weiß zu schwarz).

        • Die gefundenen Pixel sollten hoffentlich die Randpixel eines einzelnen Korns sein. Versuchen Sie, Inliers von Outliers zu trennen, indem Sie Pixel verwerfen, die von der angenommenen Ellipse weiter entfernt sind als die anderen

        • versuche wiederholt, eine Ellipse durch eine Teilmenge der Lieferanten zu passen, behalte die beste Ellipse (RANSACK)

        • Aktualisieren Sie die Position, Ausrichtung, Breite und Höhe der Körnung mit dem gefundenen Elipse

        • Wenn sich die Kornposition nicht wesentlich ändert, stoppen Sie

    • Wählen Sie unter den 10 angepassten Körnern die beste Körnung entsprechend der Form und der Anzahl der Kantenpixel aus. Werfen Sie die anderen weg

    • Entfernen Sie alle Pixel für diese Körnung aus dem Quellbild und wiederholen Sie den Vorgang

    • Gehen Sie schließlich die Liste der gefundenen Körner durch und zählen Sie jedes Korn entweder als 1 Korn, 0 Körner (zu klein) oder 2 Körner (zu groß).

Eines meiner Hauptprobleme war, dass ich keine vollständige Ellipsenpunkt-Distanzmetrik implementieren wollte, da die Berechnung dieser Metrik an sich ein komplizierter iterativer Prozess ist. Also habe ich verschiedene Workarounds mit den OpenCV-Funktionen Ellipse2Poly und FitEllipse verwendet, und die Ergebnisse sind nicht allzu hübsch.

Anscheinend habe ich auch die Größenbeschränkung für Codegolf überschritten.

Eine Antwort ist auf 30000 Zeichen begrenzt, ich bin derzeit bei 34000. Also muss ich den Code unten etwas kürzen.

Der vollständige Code kann unter http://pastebin.com/RgM7hMxq eingesehen werden

Entschuldigung, mir war nicht bewusst, dass es eine Größenbeschränkung gibt.

class Program
{
    static void Main(string[] args)
    {

                // Due to size constraints, I removed the inital part of my program that does background separation. For the full source, check the link, or see my previous program.


                // list of recognized grains
                List<Grain> grains = new List<Grain>();

                Random rand = new Random(4); // determined by fair dice throw, guaranteed to be random

                // repeat until we have found all grains (to a maximum of 10000)
                for (int numIterations = 0; numIterations < 10000; numIterations++ )
                {
                    // erode the image of the remaining foreground pixels, only big blobs can be grains
                    foreground.Erode(erodedForeground,null,7);

                    // pick a number of starting points to fit grains
                    List<CvPoint> startPoints = new List<CvPoint>();
                    using (CvMemStorage storage = new CvMemStorage())
                    using (CvContourScanner scanner = new CvContourScanner(erodedForeground, storage, CvContour.SizeOf, ContourRetrieval.List, ContourChain.ApproxNone))
                    {
                        if (!scanner.Any()) break; // no grains left, finished!

                        // search for grains within the biggest blob first (this is arbitrary)
                        var biggestBlob = scanner.OrderByDescending(c => c.Count()).First();

                        // pick 10 random edge pixels
                        for (int i = 0; i < 10; i++)
                        {
                            startPoints.Add(biggestBlob.ElementAt(rand.Next(biggestBlob.Count())).Value);
                        }
                    }

                    // for each starting point, try to fit a grain there
                    ConcurrentBag<Grain> candidates = new ConcurrentBag<Grain>();
                    Parallel.ForEach(startPoints, point =>
                    {
                        Grain candidate = new Grain(point);
                        candidate.Fit(foreground);
                        candidates.Add(candidate);
                    });

                    Grain grain = candidates
                        .OrderByDescending(g=>g.Converged) // we don't want grains where the iterative fit did not finish
                        .ThenBy(g=>g.IsTooSmall) // we don't want tiny grains
                        .ThenByDescending(g => g.CircumferenceRatio) // we want grains that have many edge pixels close to the fitted elipse
                        .ThenBy(g => g.MeanSquaredError)
                        .First(); // we only want the best fit among the 10 candidates

                    // count the number of foreground pixels this grain has
                    grain.CountPixel(foreground);

                    // remove the grain from the foreground
                    grain.Draw(foreground,CvColor.Black);

                    // add the grain to the colection fo found grains
                    grains.Add(grain);
                    grain.Index = grains.Count;

                    // draw the grain for visualisation
                    grain.Draw(display, CvColor.Random());
                    grain.DrawContour(display, CvColor.Random());
                    grain.DrawEllipse(display, CvColor.Random());

                    //display.SaveImage("10-foundGrains.png");
                }

                // throw away really bad grains
                grains = grains.Where(g => g.PixelRatio >= 0.73).ToList();

                // estimate the average grain size, ignoring outliers
                double avgGrainSize =
                    grains.OrderBy(g => g.NumPixel).Skip(grains.Count/10).Take(grains.Count*9/10).Average(g => g.NumPixel);

                //ignore the estimated grain size, use a fixed size
                avgGrainSize = 2530;

                // count the number of grains, using the average grain size
                double numGrains = grains.Sum(g => Math.Round(g.NumPixel * 1.0 / avgGrainSize));

                // get some statistics
                double avgWidth = grains.Where(g => Math.Round(g.NumPixel * 1.0 / avgGrainSize) == 1).Average(g => g.Width);
                double avgHeight = grains.Where(g => Math.Round(g.NumPixel * 1.0 / avgGrainSize) == 1).Average(g => g.Height);
                double avgPixelRatio = grains.Where(g => Math.Round(g.NumPixel * 1.0 / avgGrainSize) == 1).Average(g => g.PixelRatio);

                int numUndersized = grains.Count(g => Math.Round(g.NumPixel * 1.0 / avgGrainSize) < 1);
                int numOversized = grains.Count(g => Math.Round(g.NumPixel * 1.0 / avgGrainSize) > 1);

                double avgWidthUndersized = grains.Where(g => Math.Round(g.NumPixel * 1.0 / avgGrainSize) < 1).Select(g=>g.Width).DefaultIfEmpty(0).Average();
                double avgHeightUndersized = grains.Where(g => Math.Round(g.NumPixel * 1.0 / avgGrainSize) < 1).Select(g => g.Height).DefaultIfEmpty(0).Average();
                double avgGrainSizeUndersized = grains.Where(g => Math.Round(g.NumPixel * 1.0 / avgGrainSize) < 1).Select(g => g.NumPixel).DefaultIfEmpty(0).Average();
                double avgPixelRatioUndersized = grains.Where(g => Math.Round(g.NumPixel * 1.0 / avgGrainSize) < 1).Select(g => g.PixelRatio).DefaultIfEmpty(0).Average();

                double avgWidthOversized = grains.Where(g => Math.Round(g.NumPixel * 1.0 / avgGrainSize) > 1).Select(g => g.Width).DefaultIfEmpty(0).Average();
                double avgHeightOversized = grains.Where(g => Math.Round(g.NumPixel * 1.0 / avgGrainSize) > 1).Select(g => g.Height).DefaultIfEmpty(0).Average();
                double avgGrainSizeOversized = grains.Where(g => Math.Round(g.NumPixel * 1.0 / avgGrainSize) > 1).Select(g => g.NumPixel).DefaultIfEmpty(0).Average();
                double avgPixelRatioOversized = grains.Where(g => Math.Round(g.NumPixel * 1.0 / avgGrainSize) > 1).Select(g => g.PixelRatio).DefaultIfEmpty(0).Average();


                Console.WriteLine("===============================");
                Console.WriteLine("Grains: {0}|{1:0.} of {2} (e{3}), size {4:0.}px, {5:0.}x{6:0.}  {7:0.000}  undersized:{8}  oversized:{9}   {10:0.0} minutes  {11:0.0} s per grain",grains.Count,numGrains,expectedGrains[fileNo],expectedGrains[fileNo]-numGrains,avgGrainSize,avgWidth,avgHeight, avgPixelRatio,numUndersized,numOversized,watch.Elapsed.TotalMinutes, watch.Elapsed.TotalSeconds/grains.Count);



                // draw the description for each grain
                foreach (Grain grain in grains)
                {
                    grain.DrawText(avgGrainSize, display, CvColor.Black);
                }

                display.SaveImage("10-foundGrains.png");
                display.SaveImage("X-" + file + "-foundgrains.png");
            }
        }
    }
}



public class Grain
{
    private const int MIN_WIDTH = 70;
    private const int MAX_WIDTH = 130;
    private const int MIN_HEIGHT = 20;
    private const int MAX_HEIGHT = 35;

    private static CvFont font01 = new CvFont(FontFace.HersheyPlain, 0.5, 1);
    private Random random = new Random(4); // determined by fair dice throw; guaranteed to be random


    /// <summary> center of grain </summary>
    public CvPoint2D32f Position { get; private set; }
    /// <summary> Width of grain (always bigger than height)</summary>
    public float Width { get; private set; }
    /// <summary> Height of grain (always smaller than width)</summary>
    public float Height { get; private set; }

    public float MinorRadius { get { return this.Height / 2; } }
    public float MajorRadius { get { return this.Width / 2; } }
    public double Angle { get; private set; }
    public double AngleRad { get { return this.Angle * Math.PI / 180; } }

    public int Index { get; set; }
    public bool Converged { get; private set; }
    public int NumIterations { get; private set; }
    public double CircumferenceRatio { get; private set; }
    public int NumPixel { get; private set; }
    public List<EllipsePoint> EdgePoints { get; private set; }
    public double MeanSquaredError { get; private set; }
    public double PixelRatio { get { return this.NumPixel / (Math.PI * this.MajorRadius * this.MinorRadius); } }
    public bool IsTooSmall { get { return this.Width < MIN_WIDTH || this.Height < MIN_HEIGHT; } }

    public Grain(CvPoint2D32f position)
    {
        this.Position = position;
        this.Angle = 0;
        this.Width = 10;
        this.Height = 10;
        this.MeanSquaredError = double.MaxValue;
    }

    /// <summary>  fit a single rice grain of elipsoid shape </summary>
    public void Fit(CvMat img)
    {
        // distance between the sampled points on the elipse circumference in degree
        int angularResolution = 1;

        // how many times did the fitted ellipse not change significantly?
        int numConverged = 0;

        // number of iterations for this fit
        int numIterations;

        // repeat until the fitted ellipse does not change anymore, or the maximum number of iterations is reached
        for (numIterations = 0; numIterations < 100 && !this.Converged; numIterations++)
        {
            // points on an ideal ellipse
            CvPoint[] points;
            Cv.Ellipse2Poly(this.Position, new CvSize2D32f(MajorRadius, MinorRadius), Convert.ToInt32(this.Angle), 0, 359, out points,
                            angularResolution);

            // points on the edge of foregroudn to background, that are close to the elipse
            CvPoint?[] edgePoints = new CvPoint?[points.Length];

            // remeber if the previous pixel in a given direction was foreground or background
            bool[] prevPixelWasForeground = new bool[points.Length];

            // when the first edge pixel is found, this value is updated
            double firstEdgePixelOffset = 200;

            // from the center of the elipse towards the outside:
            for (float offset = -this.MajorRadius + 1; offset < firstEdgePixelOffset + 20; offset++)
            {
                // draw an ellipse with the given offset
                Cv.Ellipse2Poly(this.Position, new CvSize2D32f(MajorRadius + offset, MinorRadius + (offset > 0 ? offset : MinorRadius / MajorRadius * offset)), Convert.ToInt32(this.Angle), 0,
                                359, out points, angularResolution);

                // for each angle
                Parallel.For(0, points.Length, i =>
                {
                    if (edgePoints[i].HasValue) return; // edge for this angle already found

                    // check if the current pixel is foreground
                    bool foreground = points[i].X < 0 || points[i].Y < 0 || points[i].X >= img.Cols || points[i].Y >= img.Rows
                                          ? false // pixel outside of image borders is always background
                                          : img.Get2D(points[i].Y, points[i].X).Val0 > 0;


                    if (prevPixelWasForeground[i] && !foreground)
                    {
                        // found edge pixel!
                        edgePoints[i] = points[i];

                        // if this is the first edge pixel we found, remember its offset. the other pixels cannot be too far away, so we can stop searching soon
                        if (offset < firstEdgePixelOffset && offset > 0) firstEdgePixelOffset = offset;
                    }

                    prevPixelWasForeground[i] = foreground;
                });
            }

            // estimate the distance of each found edge pixel from the ideal elipse
            // this is a hack, since the actual equations for estimating point-ellipse distnaces are complicated
            Cv.Ellipse2Poly(this.Position, new CvSize2D32f(MajorRadius, MinorRadius), Convert.ToInt32(this.Angle), 0, 360,
                            out points, angularResolution);
            var pointswithDistance =
                edgePoints.Select((p, i) => p.HasValue ? new EllipsePoint(p.Value, points[i], this.Position) : null)
                          .Where(p => p != null).ToList();

            if (pointswithDistance.Count == 0)
            {
                Console.WriteLine("no points found! should never happen! ");
                break;
            }

            // throw away all outliers that are too far outside the current ellipse
            double medianSignedDistance = pointswithDistance.OrderBy(p => p.SignedDistance).ElementAt(pointswithDistance.Count / 2).SignedDistance;
            var goodPoints = pointswithDistance.Where(p => p.SignedDistance < medianSignedDistance + 15).ToList();

            // do a sort of ransack fit with the inlier points to find a new better ellipse
            CvBox2D bestfit = ellipseRansack(goodPoints);

            // check if the fit has converged
            if (Math.Abs(this.Angle - bestfit.Angle) < 3 && // angle has not changed much (<3°)
                Math.Abs(this.Position.X - bestfit.Center.X) < 3 && // position has not changed much (<3 pixel)
                Math.Abs(this.Position.Y - bestfit.Center.Y) < 3)
            {
                numConverged++;
            }
            else
            {
                numConverged = 0;
            }

            if (numConverged > 2)
            {
                this.Converged = true;
            }

            //Console.WriteLine("Iteration {0}, delta {1:0.000} {2:0.000} {3:0.000}    {4:0.000}-{5:0.000} {6:0.000}-{7:0.000} {8:0.000}-{9:0.000}",
            //  numIterations, Math.Abs(this.Angle - bestfit.Angle), Math.Abs(this.Position.X - bestfit.Center.X), Math.Abs(this.Position.Y - bestfit.Center.Y), this.Angle, bestfit.Angle, this.Position.X, bestfit.Center.X, this.Position.Y, bestfit.Center.Y);

            double msr = goodPoints.Sum(p => p.Distance * p.Distance) / goodPoints.Count;

            // for drawing the polygon, filter the edge points more strongly
            if (goodPoints.Count(p => p.SignedDistance < 5) > goodPoints.Count / 2)
                goodPoints = goodPoints.Where(p => p.SignedDistance < 5).ToList();
            double cutoff = goodPoints.Select(p => p.Distance).OrderBy(d => d).ElementAt(goodPoints.Count * 9 / 10);
            goodPoints = goodPoints.Where(p => p.SignedDistance <= cutoff + 1).ToList();

            int numCertainEdgePoints = goodPoints.Count(p => p.SignedDistance > -2);
            this.CircumferenceRatio = numCertainEdgePoints * 1.0 / points.Count();

            this.Angle = bestfit.Angle;
            this.Position = bestfit.Center;
            this.Width = bestfit.Size.Width;
            this.Height = bestfit.Size.Height;
            this.EdgePoints = goodPoints;
            this.MeanSquaredError = msr;

        }
        this.NumIterations = numIterations;
        //Console.WriteLine("Grain found after {0,3} iterations, size={1,3:0.}x{2,3:0.}   pixel={3,5}    edgePoints={4,3}   msr={5,2:0.00000}", numIterations, this.Width,
        //                        this.Height, this.NumPixel, this.EdgePoints.Count, this.MeanSquaredError);
    }

    /// <summary> a sort of ransakc fit to find the best ellipse for the given points </summary>
    private CvBox2D ellipseRansack(List<EllipsePoint> points)
    {
        using (CvMemStorage storage = new CvMemStorage(0))
        {
            // calculate minimum bounding rectangle
            CvSeq<CvPoint> fullPointSeq = CvSeq<CvPoint>.FromArray(points.Select(p => p.Point), SeqType.EltypePoint, storage);
            var boundingRect = fullPointSeq.MinAreaRect2();

            // the initial candidate is the previously found ellipse
            CvBox2D bestEllipse = new CvBox2D(this.Position, new CvSize2D32f(this.Width, this.Height), (float)this.Angle);
            double bestError = calculateEllipseError(points, bestEllipse);

            Queue<EllipsePoint> permutation = new Queue<EllipsePoint>();
            if (points.Count >= 5) for (int i = -2; i < 20; i++)
                {
                    CvBox2D ellipse;
                    if (i == -2)
                    {
                        // first, try the ellipse described by the boundingg rect
                        ellipse = boundingRect;
                    }
                    else if (i == -1)
                    {
                        // then, try the best-fit ellipsethrough all points
                        ellipse = fullPointSeq.FitEllipse2();
                    }
                    else
                    {
                        // then, repeatedly fit an ellipse through a random sample of points

                        // pick some random points
                        if (permutation.Count < 5) permutation = new Queue<EllipsePoint>(permutation.Concat(points.OrderBy(p => random.Next())));
                        CvSeq<CvPoint> pointSeq = CvSeq<CvPoint>.FromArray(permutation.Take(10).Select(p => p.Point), SeqType.EltypePoint, storage);
                        for (int j = 0; j < pointSeq.Count(); j++) permutation.Dequeue();

                        // fit an ellipse through these points
                        ellipse = pointSeq.FitEllipse2();
                    }

                    // assure that the width is greater than the height
                    ellipse = NormalizeEllipse(ellipse);

                    // if the ellipse is too big for agrain, shrink it
                    ellipse = rightSize(ellipse, points.Where(p => isOnEllipse(p.Point, ellipse, 10, 10)).ToList());

                    // sometimes the ellipse given by FitEllipse2 is totally off
                    if (boundingRect.Center.DistanceTo(ellipse.Center) > Math.Max(boundingRect.Size.Width, boundingRect.Size.Height) * 2)
                    {
                        // ignore this bad fit
                        continue;
                    }

                    // estimate the error
                    double error = calculateEllipseError(points, ellipse);

                    if (error < bestError)
                    {
                        // found a better ellipse!
                        bestError = error;
                        bestEllipse = ellipse;
                    }
                }

            return bestEllipse;
        }
    }

    /// <summary> The proper thing to do would be to use the actual distance of each point to the elipse.
    /// However that formula is complicated, so ...  </summary>
    private double calculateEllipseError(List<EllipsePoint> points, CvBox2D ellipse)
    {
        const double toleranceInner = 5;
        const double toleranceOuter = 10;
        int numWrongPoints = points.Count(p => !isOnEllipse(p.Point, ellipse, toleranceInner, toleranceOuter));
        double ratioWrongPoints = numWrongPoints * 1.0 / points.Count;

        int numTotallyWrongPoints = points.Count(p => !isOnEllipse(p.Point, ellipse, 10, 20));
        double ratioTotallyWrongPoints = numTotallyWrongPoints * 1.0 / points.Count;

        // this pseudo-distance is biased towards deviations on the major axis
        double pseudoDistance = Math.Sqrt(points.Sum(p => Math.Abs(1 - ellipseMetric(p.Point, ellipse))) / points.Count);

        // primarily take the number of points far from the elipse border as an error metric.
        // use pseudo-distance to break ties between elipses with the same number of wrong points
        return ratioWrongPoints * 1000  + ratioTotallyWrongPoints+ pseudoDistance / 1000;
    }


    /// <summary> shrink an ellipse if it is larger than the maximum grain dimensions </summary>
    private static CvBox2D rightSize(CvBox2D ellipse, List<EllipsePoint> points)
    {
        if (ellipse.Size.Width < MAX_WIDTH && ellipse.Size.Height < MAX_HEIGHT) return ellipse;

        // elipse is bigger than the maximum grain size
        // resize it so it fits, while keeping one edge of the bounding rectangle constant

        double desiredWidth = Math.Max(10, Math.Min(MAX_WIDTH, ellipse.Size.Width));
        double desiredHeight = Math.Max(10, Math.Min(MAX_HEIGHT, ellipse.Size.Height));

        CvPoint2D32f average = points.Average();

        // get the corners of the surrounding bounding box
        var corners = ellipse.BoxPoints().ToList();

        // find the corner that is closest to the center of mass of the points
        int i0 = ellipse.BoxPoints().Select((point, index) => new { point, index }).OrderBy(p => p.point.DistanceTo(average)).First().index;
        CvPoint p0 = corners[i0];

        // find the two corners that are neighbouring this one
        CvPoint p1 = corners[(i0 + 1) % 4];
        CvPoint p2 = corners[(i0 + 3) % 4];

        // p1 is the next corner along the major axis (widht), p2 is the next corner along the minor axis (height)
        if (p0.DistanceTo(p1) < p0.DistanceTo(p2))
        {
            CvPoint swap = p1;
            p1 = p2;
            p2 = swap;
        }

        // calculate the three other corners with the desired widht and height

        CvPoint2D32f edge1 = (p1 - p0);
        CvPoint2D32f edge2 = p2 - p0;
        double edge1Length = Math.Max(0.0001, p0.DistanceTo(p1));
        double edge2Length = Math.Max(0.0001, p0.DistanceTo(p2));

        CvPoint2D32f newCenter = (CvPoint2D32f)p0 + edge1 * (desiredWidth / edge1Length) + edge2 * (desiredHeight / edge2Length);

        CvBox2D smallEllipse = new CvBox2D(newCenter, new CvSize2D32f((float)desiredWidth, (float)desiredHeight), ellipse.Angle);

        return smallEllipse;
    }

    /// <summary> assure that the width of the elipse is the major axis, and the height is the minor axis.
    /// Swap widht/height and rotate by 90° otherwise  </summary>
    private static CvBox2D NormalizeEllipse(CvBox2D ellipse)
    {
        if (ellipse.Size.Width < ellipse.Size.Height)
        {
            ellipse = new CvBox2D(ellipse.Center, new CvSize2D32f(ellipse.Size.Height, ellipse.Size.Width), (ellipse.Angle + 90 + 360) % 360);
        }
        return ellipse;
    }

    /// <summary> greater than 1 for points outside ellipse, smaller than 1 for points inside ellipse </summary>
    private static double ellipseMetric(CvPoint p, CvBox2D ellipse)
    {
        double theta = ellipse.Angle * Math.PI / 180;
        double u = Math.Cos(theta) * (p.X - ellipse.Center.X) + Math.Sin(theta) * (p.Y - ellipse.Center.Y);
        double v = -Math.Sin(theta) * (p.X - ellipse.Center.X) + Math.Cos(theta) * (p.Y - ellipse.Center.Y);

        return u * u / (ellipse.Size.Width * ellipse.Size.Width / 4) + v * v / (ellipse.Size.Height * ellipse.Size.Height / 4);
    }

    /// <summary> Is the point on the ellipseBorder, within a certain tolerance </summary>
    private static bool isOnEllipse(CvPoint p, CvBox2D ellipse, double toleranceInner, double toleranceOuter)
    {
        double theta = ellipse.Angle * Math.PI / 180;
        double u = Math.Cos(theta) * (p.X - ellipse.Center.X) + Math.Sin(theta) * (p.Y - ellipse.Center.Y);
        double v = -Math.Sin(theta) * (p.X - ellipse.Center.X) + Math.Cos(theta) * (p.Y - ellipse.Center.Y);

        double innerEllipseMajor = (ellipse.Size.Width - toleranceInner) / 2;
        double innerEllipseMinor = (ellipse.Size.Height - toleranceInner) / 2;
        double outerEllipseMajor = (ellipse.Size.Width + toleranceOuter) / 2;
        double outerEllipseMinor = (ellipse.Size.Height + toleranceOuter) / 2;

        double inside = u * u / (innerEllipseMajor * innerEllipseMajor) + v * v / (innerEllipseMinor * innerEllipseMinor);
        double outside = u * u / (outerEllipseMajor * outerEllipseMajor) + v * v / (outerEllipseMinor * outerEllipseMinor);
        return inside >= 1 && outside <= 1;
    }


    /// <summary> count the number of foreground pixels for this grain </summary>
    public int CountPixel(CvMat img)
    {
        // todo: this is an incredibly inefficient way to count, allocating a new image with the size of the input each time
        using (CvMat mask = new CvMat(img.Rows, img.Cols, MatrixType.U8C1))
        {
            mask.SetZero();
            mask.FillPoly(new CvPoint[][] { this.EdgePoints.Select(p => p.Point).ToArray() }, CvColor.White);
            mask.And(img, mask);
            this.NumPixel = mask.CountNonZero();
        }
        return this.NumPixel;
    }

    /// <summary> draw the recognized shape of the grain </summary>
    public void Draw(CvMat img, CvColor color)
    {
        img.FillPoly(new CvPoint[][] { this.EdgePoints.Select(p => p.Point).ToArray() }, color);
    }

    /// <summary> draw the contours of the grain </summary>
    public void DrawContour(CvMat img, CvColor color)
    {
        img.DrawPolyLine(new CvPoint[][] { this.EdgePoints.Select(p => p.Point).ToArray() }, true, color);
    }

    /// <summary> draw the best-fit ellipse of the grain </summary>
    public void DrawEllipse(CvMat img, CvColor color)
    {
        img.DrawEllipse(this.Position, new CvSize2D32f(this.MajorRadius, this.MinorRadius), this.Angle, 0, 360, color, 1);
    }

    /// <summary> print the grain index and the number of pixels divided by the average grain size</summary>
    public void DrawText(double averageGrainSize, CvMat img, CvColor color)
    {
        img.PutText(String.Format("{0}|{1:0.0}", this.Index, this.NumPixel / averageGrainSize), this.Position + new CvPoint2D32f(-5, 10), font01, color);
    }

}

Ich schäme mich ein wenig für diese Lösung, weil a) ich nicht sicher bin, ob sie im Geiste dieser Herausforderung liegt, und b) sie zu groß für eine Codegolf-Antwort ist und die Eleganz der anderen Lösungen fehlt.

Andererseits bin ich sehr zufrieden mit den Fortschritten, die ich bei der Kennzeichnung der Körner erzielt habe , und nicht nur bei der Zählung.

HugoRune
quelle
Sie wissen, dass Sie diese Codelänge um Größenordnungen reduzieren können, indem Sie kleinere Namen verwenden und andere Golftechniken anwenden;)
Optimizer
Wahrscheinlich, aber ich wollte diese Lösung nicht weiter verschleiern. Es ist zu verschleiert für meinen Geschmack, wie es ist :)
HugoRune
+1 für den Aufwand und weil Sie der einzige sind, der eine Möglichkeit findet, jedes Korn einzeln anzuzeigen. Leider ist der Code ein wenig aufgebläht und hängt viel von hartcodierten Konstanten ab. Ich wäre gespannt, wie sich der von mir geschriebene Scanline-Algorithmus darauf auswirkt (auf die einzelnen farbigen Körner).
Tigrou
Ich denke wirklich, dass dies der richtige Ansatz für diese Art von Problem ist (+1 für Sie), aber ich frage mich, warum Sie "10 zufällige Randpixel wählen", ich würde denken, dass Sie eine bessere Leistung erzielen würden, wenn Sie auswählen Die Randpunkte mit der geringsten Anzahl von Randpunkten in der Nähe (dh Teile, die hervorstehen). Ich denke, dies würde (theoretisch) die "einfachsten" Körner zuerst beseitigen. Haben Sie dies in Betracht gezogen?
David Rogers
Ich habe darüber nachgedacht, es aber noch nicht ausprobiert. Die '10 zufällige Startposition 'war eine späte Hinzufügung, die leicht hinzuzufügen und leicht zu parallelisieren war. Vorher war 'eine zufällige Startposition' viel besser als 'immer die linke obere Ecke'. Die Gefahr, jedes Mal die Startpositionen mit der gleichen Strategie zu wählen, besteht darin, dass wenn ich die beste Übereinstimmung entferne, die anderen 9 wahrscheinlich das nächste Mal wieder gewählt werden und mit der Zeit die schlechtesten dieser Startpositionen zurückbleiben und erneut gewählt werden und nochmal. Ein Teil, der hervorsteht, sind möglicherweise nur die Reste eines unvollständig entfernten vorherigen Getreides.
HugoRune,
17

C ++, OpenCV, Punktzahl: 9

Die Grundidee meiner Methode ist recht einfach - versuchen Sie, einzelne Körner (und "doppelte Körner" - 2 (aber nicht mehr!) Körner, die nahe beieinander liegen) aus dem Bild zu entfernen und zählen Sie die Reste mit einer Methode basierend auf der Fläche (wie Falko, Ell und Belisarius). Die Verwendung dieses Ansatzes ist etwas besser als die standardmäßige "Flächenmethode", da es einfacher ist, einen guten AveragePixelsPerObject-Wert zu finden.

(1. Schritt) Zunächst müssen wir die Otsu-Binarisierung für den S-Kanal des Bildes in HSV verwenden. Der nächste Schritt ist die Verwendung des dilate-Operators, um die Qualität des extrahierten Vordergrunds zu verbessern. Dann müssen wir Konturen finden. Natürlich sind einige Konturen keine Reiskörner - wir müssen zu kleine Konturen löschen (mit einer Fläche kleiner als averagePixelsPerObject / 4. averagePixelsPerObject ist in meiner Situation 2855). Jetzt können wir endlich mit dem Zählen der Körner beginnen :) (2. Schritt) Das Finden von Einzel- und Doppelkörnern ist ganz einfach - suchen Sie in der Konturliste nach Konturen mit Flächen innerhalb bestimmter Bereiche. Wenn sich die Konturfläche im Bereich befindet, löschen Sie sie aus der Liste und fügen Sie 1 hinzu (oder 2, wenn es "doppeltes" Korn war), um Körner zu kontern. (3. Schritt) Der letzte Schritt ist natürlich das Teilen des Bereichs der verbleibenden Konturen durch den AveragePixelsPerObject-Wert und das Hinzufügen des Ergebnisses zum Körnerzähler.

Bilder (für Bild F.jpg) sollten diese Idee besser als Worte zeigen:
1. Schritt (ohne kleine Konturen (Rauschen)): 1. Stufe (ohne kleine Konturen (Lärm))
2. Schritt - nur einfache Konturen: 2. Schritt - nur einfache Konturen
3. Schritt - verbleibende Konturen: 3. Schritt - verbleibende Konturen

Hier ist der Code, er ist ziemlich hässlich, sollte aber problemlos funktionieren. Natürlich ist OpenCV erforderlich.

#include "stdafx.h"

#include <cv.hpp>
#include <cxcore.h>
#include <highgui.h>
#include <vector>

using namespace cv;
using namespace std;

//A: 3, B: 5, C: 12, D: 25, E: 50, F: 83, G: 120, H:150, I: 151, J: 200
const int goodResults[] = {3, 5, 12, 25, 50, 83, 120, 150, 151, 200};
const float averagePixelsPerObject = 2855.0;

const int singleObjectPixelsCountMin = 2320;
const int singleObjectPixelsCountMax = 4060;

const int doubleObjectPixelsCountMin = 5000;
const int doubleObjectPixelsCountMax = 8000;

float round(float x)
{
    return x >= 0.0f ? floorf(x + 0.5f) : ceilf(x - 0.5f);
}

Mat processImage(Mat m, int imageIndex, int &error)
{
    int objectsCount = 0;
    Mat output, thresholded;
    cvtColor(m, output, CV_BGR2HSV);
    vector<Mat> channels;
    split(output, channels);
    threshold(channels[1], thresholded, 0, 255, CV_THRESH_OTSU | CV_THRESH_BINARY);
    dilate(thresholded, output, Mat()); //dilate to imporove quality of binary image
    imshow("thresholded", thresholded);
    int nonZero = countNonZero(output); //not realy important - just for tests
    if (imageIndex != -1)
        cout << "non zero: " << nonZero << ", average pixels per object: " << nonZero/goodResults[imageIndex] << endl;
    else
        cout << "non zero: " << nonZero << endl;

    vector<vector<Point>> contours, contoursOnlyBig, contoursWithoutSimpleObjects, contoursSimple;
    findContours(output, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); //find only external contours
    for (int i=0; i<contours.size(); i++)
        if (contourArea(contours[i]) > averagePixelsPerObject/4.0)
            contoursOnlyBig.push_back(contours[i]); //add only contours with area > averagePixelsPerObject/4 ---> skip small contours (noise)

    Mat bigContoursOnly = Mat::zeros(output.size(), output.type());
    Mat allContours = bigContoursOnly.clone();
    drawContours(allContours, contours, -1, CV_RGB(255, 255, 255), -1);
    drawContours(bigContoursOnly, contoursOnlyBig, -1, CV_RGB(255, 255, 255), -1);
    //imshow("all contours", allContours);
    output = bigContoursOnly;

    nonZero = countNonZero(output); //not realy important - just for tests
    if (imageIndex != -1)
        cout << "non zero: " << nonZero << ", average pixels per object: " << nonZero/goodResults[imageIndex] << " objects: "  << goodResults[imageIndex] << endl;
    else
        cout << "non zero: " << nonZero << endl;

    for (int i=0; i<contoursOnlyBig.size(); i++)
    {
        double area = contourArea(contoursOnlyBig[i]);
        if (area >= singleObjectPixelsCountMin && area <= singleObjectPixelsCountMax) //is this contours a single grain ?
        {
            contoursSimple.push_back(contoursOnlyBig[i]);
            objectsCount++;
        }
        else
        {
            if (area >= doubleObjectPixelsCountMin && area <= doubleObjectPixelsCountMax) //is this contours a double grain ?
            {
                contoursSimple.push_back(contoursOnlyBig[i]);
                objectsCount+=2;
            }
            else
                contoursWithoutSimpleObjects.push_back(contoursOnlyBig[i]); //group of grainss
        }
    }

    cout << "founded single objects: " << objectsCount << endl;
    Mat thresholdedImageMask = Mat::zeros(output.size(), output.type()), simpleContoursMat = Mat::zeros(output.size(), output.type());
    drawContours(simpleContoursMat, contoursSimple, -1, CV_RGB(255, 255, 255), -1);
    if (contoursWithoutSimpleObjects.size())
        drawContours(thresholdedImageMask, contoursWithoutSimpleObjects, -1, CV_RGB(255, 255, 255), -1); //draw only contours of groups of grains
    imshow("simpleContoursMat", simpleContoursMat);
    imshow("thresholded image mask", thresholdedImageMask);
    Mat finalResult;
    thresholded.copyTo(finalResult, thresholdedImageMask); //copy using mask - only pixels whc=ich belongs to groups of grains will be copied
    //imshow("finalResult", finalResult);
    nonZero = countNonZero(finalResult); // count number of pixels in all gropus of grains (of course without single or double grains)
    int goodObjectsLeft = goodResults[imageIndex]-objectsCount;
    if (imageIndex != -1)
        cout << "non zero: " << nonZero << ", average pixels per object: " << (goodObjectsLeft ? (nonZero/goodObjectsLeft) : 0) << " objects left: " << goodObjectsLeft <<  endl;
    else
        cout << "non zero: " << nonZero << endl;
    objectsCount += round((float)nonZero/(float)averagePixelsPerObject);

    if (imageIndex != -1)
    {
        error = objectsCount-goodResults[imageIndex];
        cout << "final objects count: " << objectsCount << ", should be: " << goodResults[imageIndex] << ", error is: " << error <<  endl;
    }
    else
        cout << "final objects count: " << objectsCount << endl; 
    return output;
}

int main(int argc, char* argv[])
{
    string fileName = "A";
    int totalError = 0, error;
    bool fastProcessing = true;
    vector<int> errors;

    if (argc > 1)
    {
        Mat m = imread(argv[1]);
        imshow("image", m);
        processImage(m, -1, error);
        waitKey(-1);
        return 0;
    }

    while(true)
    {
        Mat m = imread("images\\" + fileName + ".jpg");
        cout << "Processing image: " << fileName << endl;
        imshow("image", m);
        processImage(m, fileName[0] - 'A', error);
        totalError += abs(error);
        errors.push_back(error);
        if (!fastProcessing && waitKey(-1) == 'q')
            break;
        fileName[0] += 1;
        if (fileName[0] > 'J')
        {
            if (fastProcessing)
                break;
            else
                fileName[0] = 'A';
        }
    }
    cout << "Total error: " << totalError << endl;
    cout << "Errors: " << (Mat)errors << endl;
    cout << "averagePixelsPerObject:" << averagePixelsPerObject << endl;

    return 0;
}

Wenn Sie die Ergebnisse aller Schritte anzeigen möchten, deaktivieren Sie alle imshow-Funktionsaufrufe (.., ..) und setzen Sie die Variable fastProcessing auf false. Bilder (A.jpg, B.jpg, ...) sollten sich in Verzeichnisbildern befinden. Alternativ können Sie natürlich auch den Namen eines Bildes als Parameter über die Befehlszeile eingeben.

Wenn etwas unklar ist, kann ich es natürlich erklären und / oder einige Bilder / Informationen bereitstellen.

Cyriel
quelle
12

C # + OpenCvSharp, Ergebnis: 71

Das ist am ärgerlichsten, ich habe versucht, eine Lösung zu finden, die tatsächlich jedes Getreide anhand der Wasserscheide identifiziert , aber ich habe es einfach. kippen. bekommen. es. zu. Arbeit.

Ich entschied mich für eine Lösung, die zumindest einige einzelne Körner trennt und diese Körner dann verwendet, um die durchschnittliche Korngröße abzuschätzen. Allerdings kann ich die Lösungen mit hartcodierter Körnung bisher nicht übertreffen.

Das wichtigste Highlight dieser Lösung: Sie setzt keine feste Pixelgröße für Getreide voraus und sollte auch dann funktionieren, wenn die Kamera bewegt oder die Reissorte geändert wird.

A.jpg; Anzahl der Körner: 3; erwartet 3; Fehler 0; Pixel pro Korn: 2525,0;
B.jpg; Anzahl der Körner: 7; erwartet 5; Fehler 2; Pixel pro Korn: 1920,0;
C.jpg; Anzahl der Körner: 6; erwartet 12; Fehler 6; Pixel pro Körnung: 4242,5;
D.jpg; Anzahl der Körner: 23; erwartet 25; Fehler 2; Pixel pro Körnung: 2415,5;
E.jpg; Anzahl der Körner: 47; erwartet 50; Fehler 3; Pixel pro Korn: 2729,9;
F.jpg; Anzahl der Körner: 65; erwartet 83; Fehler 18; Pixel pro Korn: 2860,5;
G.jpg; Anzahl der Körner: 120; erwartet 120; Fehler 0; Pixel pro Körnung: 2552,3;
H.jpg; Anzahl der Körner: 159; erwartet 150; Fehler 9; Pixel pro Körnung: 2624,7;
I.jpg; Anzahl der Körner: 141; erwartet 151; Fehler 10; Pixel pro Korn: 2697,4;
J.jpg; Anzahl der Körner: 179; erwartet 200; Fehler 21; Pixel pro Korn: 2847,1;
Gesamtfehler: 71

Meine Lösung funktioniert so:

Trennen Sie den Vordergrund, indem Sie das Bild in HSV umwandeln und Otsu-Schwellenwert auf den Sättigungskanal anwenden . Dies ist sehr einfach, funktioniert sehr gut und ich würde dies jedem empfehlen, der diese Herausforderung ausprobieren möchte:

saturation channel                -->         Otsu thresholding

Bildbeschreibung hier eingeben -> Bildbeschreibung hier eingeben

Dadurch wird der Hintergrund sauber entfernt.

Ich habe dann zusätzlich die Körnerschatten aus dem Vordergrund entfernt, indem ich eine feste Schwelle auf den Wertekanal angewendet habe. (Nicht sicher, ob das wirklich viel hilft, aber es war einfach genug, um hinzuzufügen)

Bildbeschreibung hier eingeben

Dann wende ich eine Distanztransformation auf das Vordergrundbild an.

Bildbeschreibung hier eingeben

und finde alle lokalen Maxima in dieser Distanztransformation.

Hier bricht meine Idee zusammen. Um zu vermeiden, dass mehrere lokale Maxima innerhalb desselben Korns auftreten, muss ich viel filtern. Momentan halte ich nur das stärkste Maximum in einem Radius von 45 Pixeln, was bedeutet, dass nicht jedes Korn ein lokales Maximum hat. Und ich habe keine Rechtfertigung für den 45-Pixel-Radius, es war nur ein Wert, der funktioniert hat.

Bildbeschreibung hier eingeben

(Wie Sie sehen können, sind das nicht annähernd genug Samen, um für jedes Korn verantwortlich zu sein.)

Dann verwende ich diese Maxima als Samen für den Wasserscheidealgorithmus:

Bildbeschreibung hier eingeben

Die Ergebnisse sind meh . Ich hoffte größtenteils auf einzelne Körner, aber die Klumpen sind immer noch zu groß.

Jetzt identifiziere ich die kleinsten Blobs, zähle ihre durchschnittliche Pixelgröße und schätze daraus die Anzahl der Körner. Dies ist nicht das, was ich am Anfang vorhatte, aber dies war der einzige Weg, dies zu retten.

using System ; 
using System . Sammlungen . Generisch ; 
using System . Linq ; 
using System . Text ; 
mit OpenCvSharp ;

Namespace GrainTest2 { class Program { static void Main ( string [] args ) { string

     
    
          
        
             [] files = new [] { "sourceA.jpg" , "sourceB.jpg" , "sourceC.jpg" , "sourceD.jpg" , " sourceE.jpg " , " sourceF.jpg " , " sourceG.jpg " , " sourceH.jpg " , " sourceI.jpg " , " sourceJ.jpg " , };int [] 
                               
                                     
                                     
                                      
                               
             expectedGrains= new [] { 3 , 5 , 12 , 25 , 50 , 83 , 120 , 150 , 151 , 200 ,};          

            int totalError = 0 ; int totalPixels = 0 ; 
             

            für ( int FileNo = 0 ; FileNo Marker = neue Liste (); 
                    mit ( CvMemStorage Speicher = neue CvMemStorage ()) 
                    mit ( CvContourScanner Scanner = neuer CvContourScanner ( localMaxima , Lagerung , CvContour . SizeOf , ContourRetrieval . Externe , ContourChain . ApproxNone ))         
                     { // setze jedes lokale Maximum als Startnummer 25, 35, 45, ... // (tatsächliche Zahlen spielen keine Rolle, zur besseren Sichtbarkeit im PNG ausgewählt) int markerNo =
                        
                        
                          20 ; foreach ( CvSeq c im Scanner ) { 
                            markerNo + = 5 ; 
                            Marker . Add ( markerNo ); 
                            waterShedMarkers . DrawContours ( c , neu CvScalar ( markerNo ), neu
                         
                             CvScalar ( markerNo ), 0 , - 1 ); } } 
                    waterShedMarkers . SaveImage ( "08-watershed-seeds.png" );  
                        
                    


                    Quelle . Wasserscheide ( waterShedMarkers ); 
                    waterShedMarkers . SaveImage ( "09-watershed-result.png" );


                    List pixelsPerBlob = new List ();  

                    // Terrible hack because I could not get Cv2.ConnectedComponents to work with this openCv wrapper
                    // So I made a workaround to count the number of pixels per blob
                    waterShedMarkers.ConvertScale(waterShedThreshold);
                    foreach (int markerNo in markers)
                    {
                        using (CvMat tmp = new CvMat(waterShedMarkers.Rows, waterShedThreshold.Cols, MatrixType.U8C1))
                        {
                            waterShedMarkers.CmpS(markerNo, tmp, ArrComparison.EQ);
                            pixelsPerBlob.Add(tmp.CountNonZero());

                        }
                    }

                    // estimate the size of a single grain
                    // step 1: assume that the 10% smallest blob is a whole grain;
                    double singleGrain = pixelsPerBlob.OrderBy(p => p).ElementAt(pixelsPerBlob.Count/15);

                    // step2: take all blobs that are not much bigger than the currently estimated singel grain size
                    //        average their size
                    //        repeat until convergence (too lazy to check for convergence)
                    for (int i = 0; i  p  Math.Round(p/singleGrain)).Sum());

                    Console.WriteLine("input: {0}; number of grains: {1,4:0.}; expected {2,4}; error {3,4}; pixels per grain: {4:0.0}; better: {5:0.}", file, numGrains, expectedGrains[fileNo], Math.Abs(numGrains - expectedGrains[fileNo]), singleGrain, pixelsPerBlob.Sum() / 1434.9);

                    totalError += Math.Abs(numGrains - expectedGrains[fileNo]);
                    totalPixels += pixelsPerBlob.Sum();

                    // this is a terrible hack to visualise the estimated number of grains per blob.
                    // i'm too tired to clean it up
                    #region please ignore
                    using (CvMemStorage storage = new CvMemStorage())
                    using (CvMat tmp = waterShedThreshold.Clone())
                    using (CvMat tmpvisu = new CvMat(source.Rows, source.Cols, MatrixType.S8C3))
                    {
                        foreach (int markerNo in markers)
                        {
                            tmp.SetZero();
                            waterShedMarkers.CmpS(markerNo, tmp, ArrComparison.EQ);
                            double curGrains = tmp.CountNonZero() * 1.0 / singleGrain;
                            using (
                                CvContourScanner scanner = new CvContourScanner(tmp, storage, CvContour . SizeOf , ContourRetrieval . Extern , ContourChain . "" + Math . Rund ( curGrains , 1 ), c . Zuerst  
                                                                                ApproxNone))
                            {
                                tmpvisu.Set(CvColor.Random(), tmp);
                                foreach (CvSeq c in scanner)
                                {
                                    //tmpvisu.DrawContours(c, CvColor.Random(), CvColor.DarkGreen, 0, -1);
                                    tmpvisu.PutText(   ().Value, new CvFont(FontFace.HersheyPlain, 2, 2),
                                                    CvColor.Red);
                                }

                            }


                        }
                        tmpvisu.SaveImage("10-visu.png");
                        tmpvisu.SaveImage("10-visu" + file + ".png");
                    }
                    #endregion

                }

            }
            Console.WriteLine("total error: {0}, ideal Pixel per Grain: {1:0.0}", totalError, totalPixels*1.0/expectedGrains.Sum());

        }
    }
}

Ein kleiner Test mit einer hartcodierten Pixel-pro-Korn-Größe von 2544,4 ergab einen Gesamtfehler von 36, der immer noch größer ist als bei den meisten anderen Lösungen.

Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben

HugoRune
quelle
Ich denke, Sie können den Schwellenwert (Erodiervorgang kann auch nützlich sein) mit einem kleinen Wert für das Ergebnis der Abstandstransformation verwenden - dies sollte einige Gruppen von Körnern in kleinere Gruppen aufteilen (vorzugsweise - mit nur 1 oder 2 Körnern). Dann sollte es einfacher sein, diese einsamen Körner zu zählen. Große Gruppen, die Sie als die meisten Leute hier zählen können - Fläche durch durchschnittliche Fläche des einzelnen Korns teilend.
Cyriel
9

HTML + Javascript: Punktzahl 39

Die genauen Werte sind:

Estimated | Actual
        3 |      3
        5 |      5
       12 |     12
       23 |     25
       51 |     50
       82 |     83
      125 |    120
      161 |    150
      167 |    151
      223 |    200

Es bricht bei den größeren Werten zusammen (ist nicht genau).

window.onload = function() {
  var $ = document.querySelector.bind(document);
  var canvas = $("canvas"),
    ctx = canvas.getContext("2d");

  function handleFileSelect(evt) {
    evt.preventDefault();
    var file = evt.target.files[0],
      reader = new FileReader();
    if (!file) return;
    reader.onload = function(e) {
      var img = new Image();
      img.onload = function() {
        canvas.width = this.width;
        canvas.height = this.height;
        ctx.drawImage(this, 0, 0);
        start();
      };
      img.src = e.target.result;
    };
    reader.readAsDataURL(file);
  }


  function start() {
    var imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height);
    var data = imgdata.data;
    var background = 0;
    var totalPixels = data.length / 4;
    for (var i = 0; i < data.length; i += 4) {
      var red = data[i],
        green = data[i + 1],
        blue = data[i + 2];
      if (Math.abs(red - 197) < 40 && Math.abs(green - 176) < 40 && Math.abs(blue - 133) < 40) {
        ++background;
        data[i] = 1;
        data[i + 1] = 1;
        data[i + 2] = 1;
      }
    }
    ctx.putImageData(imgdata, 0, 0);
    console.log("Pixels of rice", (totalPixels - background));
    // console.log("Total pixels", totalPixels);
    $("output").innerHTML = "Approximately " + Math.round((totalPixels - background) / 2670) + " grains of rice.";
  }

  $("input").onchange = handleFileSelect;
}
<input type="file" id="f" />
<canvas></canvas>
<output></output>

Erläuterung: Zählt im Allgemeinen die Anzahl der Reispixel und dividiert sie durch die durchschnittlichen Pixel pro Korn.

soktinpk
quelle
Mit dem 3-Reis-Bild schätzte es 0 für mich ...: /
Kroltan
1
@Kroltan Nicht, wenn Sie das Bild in voller Größe verwenden .
Calvins Hobbys
1
@ Calvin'sHobbies FF36 unter Windows bekommt 0, unter Ubuntu 3, mit dem Bild in voller Größe.
Kroltan
4
@BobbyJack Der Reis ist garantiert über alle Bilder hinweg ungefähr gleich groß. Ich sehe keine Probleme damit.
Calvins Hobbys
1
@githubphagocyte - eine Erklärung liegt auf der Hand - wenn Sie beim Digitalisieren des Bildes alle weißen Pixel zählen und diese Anzahl durch die Anzahl der Körner im Bild dividieren, erhalten Sie dieses Ergebnis. Natürlich kann das genaue Ergebnis aufgrund der verwendeten Binarisierungsmethode und anderer Faktoren (z. B. Operationen, die nach der Binarisierung ausgeführt werden) abweichen, aber wie Sie in anderen Antworten sehen können, liegt es im Bereich von 2500-3500.
Cyriel
4

Ein Versuch mit PHP. Nicht die Antwort mit der niedrigsten Punktzahl, aber der ziemlich einfache Code

SCORE: 31

<?php
for($c = 1; $c <= 10; $c++) {
  $a = imagecreatefromjpeg("/tmp/$c.jpg");
  list($width, $height) = getimagesize("/tmp/$c.jpg");
  $rice = 0;
  for($i = 0; $i < $width; $i++) {
    for($j = 0; $j < $height; $j++) {
      $colour = imagecolorat($a, $i, $j);
      if (($colour & 0xFF) < 95) $rice++;
    }
  }
  echo ceil($rice/2966);
}

Selbstbewertung

95 ist ein blauer Wert, der beim Testen mit GIMP 2966 zu funktionieren schien und die durchschnittliche Korngröße aufweist

exussum
quelle