Contando i chicchi di riso


81

Considera queste 10 immagini di varie quantità di chicchi crudi di riso bianco.
QUESTI SONO SOLO POLLICI. Fai clic su un'immagine per visualizzarla a schermo intero.

A: B: C: D: E:UN B C D E

F: G: H: I: J:F sol H io J

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

Notare che...

  • I grani possono toccarsi ma non si sovrappongono mai. La disposizione dei grani non è mai superiore a un solo grano.
  • Le immagini hanno dimensioni diverse ma la scala del riso in tutte è coerente perché la fotocamera e lo sfondo erano fissi.
  • I grani non vanno mai oltre i limiti o toccano i confini dell'immagine.
  • Lo sfondo ha sempre la stessa tonalità uniforme di bianco-giallastro.
  • I grani piccoli e grandi sono considerati uguali come un grano ciascuno.

Questi 5 punti sono garanzie per tutte le immagini di questo tipo.

Sfida

Scrivi un programma che contiene tali immagini e, nel modo più accurato possibile, conta il numero di chicchi di riso.

Il tuo programma dovrebbe prendere il nome file dell'immagine e stampare il numero di grani che calcola. Il tuo programma deve funzionare per almeno uno di questi formati di file immagine: JPEG, Bitmap, PNG, GIF, TIFF (al momento le immagini sono tutte JPEG).

È possibile utilizzare le librerie di elaborazione delle immagini e visione artificiale.

Non è possibile codificare in modo rigido le uscite delle 10 immagini di esempio. Il tuo algoritmo dovrebbe essere applicabile a tutte le immagini simili di chicchi di riso. Dovrebbe essere in grado di funzionare in meno di 5 minuti su un computer moderno decente se l'area dell'immagine è inferiore a 2000 * 2000 pixel e ci sono meno di 300 chicchi di riso.

punteggio

Per ciascuna delle 10 immagini, prendi il valore assoluto del numero effettivo di granuli meno il numero di granuli previsto dal tuo programma. Somma questi valori assoluti per ottenere il tuo punteggio. Vince il punteggio più basso. Un punteggio di 0 è perfetto.

In caso di parità vince la risposta più votata. Potrei testare il tuo programma su immagini aggiuntive per verificarne la validità e la precisione.


1
Sicuramente qualcuno deve provare scikit-learn!

Grande contest! :) A proposito - potresti dirci qualcosa sulla data di fine di questa sfida?
Cyriel,

1
@Lembik Down to 7 :)
Dr. belisarius,

5
Un giorno, uno scienziato del riso verrà e sarà felice di sapere che questa domanda esiste.
Nit

Risposte:


22

Mathematica, punteggio: 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;

Penso che i nomi della funzione siano abbastanza descrittivi:

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]

Elaborazione di tutte le immagini contemporaneamente:

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

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

Il punteggio è:

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

Qui puoi vedere la sensibilità del punteggio rispetto alla dimensione del grano utilizzata:

Grafica Mathematica


2
Molto più chiaro, grazie!
Hobby di Calvin il

Questa procedura esatta può essere copiata in Python o c'è qualcosa di speciale che Mathematica sta facendo qui che le librerie Python non possono fare?

@Lembik Nessuna idea. Nessun pitone qui. Scusate. (Tuttavia, dubito esattamente degli stessi algoritmi per EdgeDetect[], DeleteSmallComponents[]e Dilation[]sono implementati altrove)
Dr. belisarius,

55

Python, Punteggio: 24 16

Questa soluzione, come quella di Falko, si basa sulla misurazione dell'area "primo piano" e sulla sua divisione per l'area media del grano.

In effetti, ciò che questo programma cerca di rilevare è lo sfondo, non tanto quanto il primo piano. Utilizzando il fatto che i chicchi di riso non toccano mai il confine dell'immagine, il programma inizia riempiendo il bianco nell'angolo in alto a sinistra. L'algoritmo flood-fill colora i pixel adiacenti se la differenza tra la loro e la luminosità del pixel corrente è entro una certa soglia, adattandosi così al cambiamento graduale del colore di sfondo. Alla fine di questa fase, l'immagine potrebbe assomigliare a questa:

Figura 1

Come puoi vedere, fa un buon lavoro nel rilevare lo sfondo, ma esclude tutte le aree "intrappolate" tra i grani. Gestiamo queste aree stimando la luminosità dello sfondo di ciascun pixel e paitning di tutti i pixel uguali o più luminosi. Questa stima funziona così: durante la fase di riempimento, calcoliamo la luminosità media dello sfondo per ogni riga e ogni colonna. La luminosità di sfondo stimata per ciascun pixel è la media della luminosità di riga e colonna per quel pixel. Questo produce qualcosa del genere:

figura 2

EDIT: Infine, l'area di ciascuna regione di primo piano continuo (cioè non bianca) è divisa per l'area media, precalcolata, del grano, dandoci una stima del conteggio del grano in detta regione. La somma di queste quantità è il risultato. Inizialmente, abbiamo fatto la stessa cosa per l'intera area di primo piano nel suo insieme, ma questo approccio è, letteralmente, più fine.


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

Prende il nome file di input attraverso la riga di comando.

risultati

      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

UN B C D E

F sol H io J


2
Questa è una soluzione davvero intelligente, bel lavoro!
Chris Cirefice,

1
da dove avg_grain_area = 3038.38;viene?
njzk2,

2
non conta come hardcoding the result?
njzk2,

5
@ njzk2 No. Data la regola The images have different dimensions but the scale of the rice in all of them is consistent because the camera and background were stationary.Questo è semplicemente un valore che rappresenta quella regola. Il risultato, tuttavia, cambia in base all'input. Se si modifica la regola, questo valore cambierà, ma il risultato sarà lo stesso, in base all'input.
Adam Davis,

6
Sto bene con la cosa media dell'area. L'area del grano è (approssimativamente) costante tra le immagini.
Calvin's Hobbies,

28

Python + OpenCV: punteggio 27

Scansione della linea orizzontale

Idea: scansiona l'immagine, una riga alla volta. Per ogni riga, conta il numero di chicchi di riso incontrati (controllando se il pixel diventa nero in bianco o viceversa). Se il numero di grani per la linea aumenta (rispetto alla linea precedente), significa che abbiamo riscontrato un nuovo grano. Se quel numero diminuisce, significa che abbiamo superato un grano. In questo caso, aggiungi +1 al risultato totale.

inserisci qui la descrizione dell'immagine

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

A causa del modo in cui funziona l'algoritmo, è importante avere un'immagine pulita e in b / n. Molto rumore produce risultati negativi. Il primo sfondo principale viene ripulito utilizzando il riempimento (soluzione simile alla risposta Ell), quindi viene applicata la soglia per produrre risultati in bianco e nero.

inserisci qui la descrizione dell'immagine

È tutt'altro che perfetto, ma produce buoni risultati per quanto riguarda la semplicità. Esistono probabilmente molti modi per migliorarlo (fornendo una migliore immagine in b / n, scansionando in altre direzioni (es: verticale, diagonale) prendendo la media ecc ...)

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

Gli errori per immagine: 0, 0, 0, 3, 0, 12, 4, 0, 7, 1


24

Python + OpenCV: punteggio 84

Ecco un primo ingenuo tentativo. Applica una soglia adattiva con parametri sintonizzati manualmente, chiude alcuni fori con successiva erosione e diluizione e deriva il numero di granuli dall'area di primo piano.

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)

Qui puoi vedere le immagini binarie intermedie (il nero è in primo piano):

inserisci qui la descrizione dell'immagine

Gli errori per immagine sono 0, 0, 2, 2, 4, 0, 27, 42, 0 e 7 grani.


20

C # + OpenCvSharp, Punteggio: 2

Questo è il mio secondo tentativo. È abbastanza diverso dal mio primo tentativo , che è molto più semplice, quindi lo sto pubblicando come soluzione separata.

L'idea di base è identificare ed etichettare ogni singolo grano con un adattamento ellittico iterativo. Quindi rimuovere i pixel per questo grano dalla sorgente e provare a trovare il grano successivo, fino a quando ogni pixel è stato etichettato.

Questa non è la soluzione più carina. È un maiale gigante con 600 righe di codice. Sono necessari 1,5 minuti per l'immagine più grande. E mi scuso davvero per il codice disordinato.

Ci sono così tanti parametri e modi di pensare in questa cosa che ho abbastanza paura di sovvertire il mio programma per le 10 immagini di esempio. Il punteggio finale di 2 è quasi sicuramente un caso di overfitting: ho due parametri average grain size in pixele minimum ratio of pixel / elipse_area, alla fine, ho semplicemente esaurito tutte le combinazioni di questi due parametri fino a ottenere il punteggio più basso. Non sono sicuro che questo sia tutto così kosher con le regole di questa sfida.

average_grain_size_in_pixel = 2530
pixel / elipse_area >= 0.73

Ma anche senza queste frizioni troppo adattate, i risultati sono abbastanza belli. Senza una dimensione della grana fissa o un rapporto pixel, semplicemente stimando la dimensione della grana media dalle immagini di allenamento, il punteggio è ancora 27.

E ottengo come output non solo il numero, ma la posizione, l'orientamento e la forma effettivi di ogni grano. ci sono un numero limitato di grani senza etichetta, ma nel complesso la maggior parte delle etichette corrisponde esattamente ai grani reali:

A UN B B C C D D EE

F F G sol H H I io JJ

(fai clic su ciascuna immagine per la versione ingrandita)

Dopo questa fase di etichettatura, il mio programma esamina ogni singola grana e stima in base al numero di pixel e al rapporto pixel / area dell'ellisse, sia che si tratti di

  • un singolo grano (+1)
  • più grani etichettati erroneamente come uno (+ X)
  • una chiazza troppo piccola per essere un grano (+0)

I punteggi di errore per ogni immagine sono A:0; B:0; C:0; D:0; E:2; F:0; G:0 ; H:0; I:0, J:0

Tuttavia, l'errore reale è probabilmente un po 'più alto. Alcuni errori nella stessa immagine si annullano a vicenda. L'immagine H in particolare ha alcuni grani mal etichettati, mentre nell'immagine E le etichette sono per lo più corrette

Il concetto è un po 'inventato:

  • Innanzitutto il primo piano viene separato tramite otsu-soglia sul canale di saturazione (vedere la mia risposta precedente per i dettagli)

  • ripetere fino a quando non rimangono più pixel:

    • seleziona il BLOB più grande
    • scegli 10 pixel di bordo casuali su questo blob come posizioni iniziali per un grano

    • per ogni punto di partenza

      • assumere una grana con altezza e larghezza di 10 pixel in questa posizione.

      • ripetere fino alla convergenza

        • andare radialmente verso l'esterno da questo punto, con diverse angolazioni, fino a quando non si incontra un pixel del bordo (da bianco a nero)

        • si spera che i pixel trovati siano i pixel del bordo di un singolo grano. Cerca di separare gli inlier dagli outlier, scartando i pixel che sono più distanti dall'ellisse presunta rispetto agli altri

        • prova ripetutamente a inserire un'ellisse in un sottoinsieme degli inlier, mantieni l'ellisse migliore (RANSACK)

        • aggiorna la posizione, l'orientamento, la larghezza e l'altezza della grana con l'ellisse trovata

        • se la posizione della granella non cambia in modo significativo, fermarsi

    • tra i 10 grani montati, scegli il grano migliore in base alla forma, al numero di pixel del bordo. Scarta gli altri

    • rimuovi tutti i pixel per questa grana dall'immagine sorgente, quindi ripeti

    • infine, consulta l'elenco dei grani trovati e conta ogni grano come 1 grano, 0 grani (troppo piccoli) o 2 grani (troppo grandi)

Uno dei miei problemi principali era che non volevo implementare una metrica della distanza completa del punto di ellisse, dal momento che il calcolo di per sé è un processo iterativo complicato. Quindi ho usato varie soluzioni alternative usando le funzioni OpenCV Ellipse2Poly e FitEllipse, e i risultati non sono troppo belli.

Apparentemente ho anche rotto il limite di dimensioni per codegolf.

Una risposta è limitata a 30000 caratteri, al momento sono a 34000. Quindi dovrò abbreviare un po 'il codice qui sotto.

Il codice completo è disponibile all'indirizzo http://pastebin.com/RgM7hMxq

Mi dispiace per questo, non ero a conoscenza che c'era un limite di dimensioni.

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

}

Sono un po 'imbarazzato con questa soluzione perché a) non sono sicuro che sia nello spirito di questa sfida eb) sia troppo grande per una risposta codegolf e manchi l'eleganza delle altre soluzioni.

D'altra parte, sono abbastanza contento dei progressi che ho realizzato nell'etichettare i grani, non solo nel contarli, quindi c'è.


Sai che puoi ridurre la lunghezza del codice di magnitudini usando nomi più piccoli e applicando alcune altre tecniche di golf;)
Ottimizzatore

Probabilmente, ma non volevo offuscare ulteriormente questa soluzione. È troppo offuscato per i miei gusti così com'è :)
HugoRune,

+1 per lo sforzo e perché sei l'unico che trova un modo per visualizzare individualmente ogni grano. Sfortunatamente il codice è un po 'gonfio e dipende molto dalle costanti codificate. Sarei curioso di vedere come funziona l'algoritmo scanline che ho scritto su questo (sui grani colorati inviduali).
Tigrou,

Penso davvero che questo sia l'approccio giusto per questo tipo di problema (+1 per te), ma una cosa mi chiedo, perché "scegli 10 pixel di bordo casuali", penso che otterrai prestazioni migliori se scegli i punti del bordo con il minor numero di punti del bordo vicini (cioè le parti che sporgono), penso (teoricamente) che questo eliminerebbe prima i grani "più facili", lo hai considerato?
David Rogers,

Ci ho pensato, ma non l'ho ancora provato. La '10 posizione iniziale casuale 'era un'aggiunta tardiva, che era facile da aggiungere e facile da parallelizzare. Prima di allora, "una posizione iniziale casuale" era molto meglio di "sempre nell'angolo in alto a sinistra". Il pericolo di scegliere le posizioni di partenza con la stessa strategia ogni volta è che quando rimuoverò la misura migliore, gli altri 9 saranno probabilmente scelti di nuovo la prossima volta, e nel tempo la peggiore di quelle posizioni di partenza rimarrà indietro e verrà scelta di nuovo e ancora. Una parte che sporge potrebbe essere solo i resti di un grano precedente rimosso in modo incompleto.
HugoRune,

17

C ++, OpenCV, punteggio: 9

L'idea di base del mio metodo è abbastanza semplice: prova a cancellare singoli grani (e "grani doppi" - 2 (ma non di più!) Grani, vicini l'uno all'altro) dall'immagine e poi contare il resto usando il metodo basato sull'area (come Falko, Ell e belisarius). L'uso di questo approccio è leggermente migliore del "metodo area" standard, perché è più facile trovare un buon valore medioPixelsPerObject.

(1 ° passo) Prima di tutto dobbiamo usare la binarizzazione Otsu sul canale S dell'immagine in HSV. Il passaggio successivo consiste nell'utilizzare l'operatore dilate per migliorare la qualità del primo piano estratto. Di che abbiamo bisogno di trovare contorni. Ovviamente alcuni contorni non sono chicchi di riso - dobbiamo eliminare contorni troppo piccoli (con un'area più piccola della mediaPixelsPerObject / 4. AveragePixelsPerObject è 2855 nella mia situazione). Ora finalmente possiamo iniziare a contare i grani :) (2 ° passaggio) Trovare grani singoli e doppi è abbastanza semplice - basta guardare nell'elenco dei contorni per contorni con area all'interno di intervalli specifici - se l'area del contorno è nell'intervallo, eliminarlo dall'elenco e aggiungere 1 (o 2 se era un grano "doppio") per contrastare i cereali. (3 ° passaggio) L'ultimo passaggio consiste ovviamente nel dividere l'area dei contorni rimanenti per il valore medioPixelsPerObject e aggiungere il risultato al contatore dei cereali.

Le immagini (per l'immagine F.jpg) dovrebbero mostrare questa idea meglio delle parole:
1 ° passaggio (senza contorni piccoli (rumore)): 1 ° passo (senza contorni piccoli (rumore))
2 ° passaggio - solo contorni semplici: 2 ° passaggio: solo contorni semplici
3 ° passaggio - contorni rimanenti: 3 ° passo - contorni rimanenti

Ecco il codice, è piuttosto brutto, ma dovrebbe funzionare senza problemi. Naturalmente è richiesto OpenCV.

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

Se vuoi vedere i risultati di tutti i passaggi, decommenta tutte le chiamate di funzione imshow (.., ..) e imposta la variabile fastProcessing su false. Le immagini (A.jpg, B.jpg, ...) dovrebbero essere nelle immagini della directory. In alternativa, puoi dare il nome di un'immagine come parametro dalla riga di comando.

Naturalmente se qualcosa non è chiaro posso spiegarlo e / o fornire alcune immagini / informazioni.


12

C # + OpenCvSharp, punteggio: 71

Questo è molto fastidioso, ho cercato di ottenere una soluzione che identifichi effettivamente ogni grano usando lo spartiacque , ma io proprio. non si può. ottenere. esso. per. lavoro.

Ho optato per una soluzione che separa almeno alcuni singoli grani e quindi li utilizza per stimare la dimensione media dei grani. Tuttavia, finora non posso battere le soluzioni con granulometria codificata.

Quindi, il punto culminante principale di questa soluzione: non presume una dimensione fissa dei pixel per i grani e dovrebbe funzionare anche se la fotocamera viene spostata o il tipo di riso viene cambiato.

a.jpg; numero di grani: 3; previsto 3; errore 0; pixel per grana: 2525,0;
b.jpg; numero di grani: 7; previsto 5; errore 2; pixel per grano: 1920,0;
C.jpg; numero di grani: 6; previsto 12; errore 6; pixel per grano: 4242,5;
D.jpg; numero di grani: 23; previsti 25; errore 2; pixel per grana: 2415,5;
E.jpg; numero di grani: 47; previsti 50; errore 3; pixel per grana: 2729,9;
F.jpg; numero di grani: 65; previsto 83; errore 18; pixel per grano: 2860,5;
G.jpg; numero di grani: 120; previsto 120; errore 0; pixel per grana: 2552,3;
H.jpg; numero di grani: 159; previsto 150; errore 9; pixel per grano: 2624,7;
i.jpg; numero di grani: 141; previsto 151; errore 10; pixel per grano: 2697,4;
j.jpg; numero di grani: 179; previsti 200; errore 21; pixel per grana: 2847,1;
errore totale: 71

La mia soluzione funziona in questo modo:

Separare il primo piano trasformando l'immagine in HSV e applicando la soglia Otsu sul canale di saturazione. Questo è molto semplice, funziona estremamente bene e lo consiglierei a tutti coloro che vogliono provare questa sfida:

saturation channel                -->         Otsu thresholding

inserisci qui la descrizione dell'immagine -> inserisci qui la descrizione dell'immagine

Questo rimuoverà in modo pulito lo sfondo.

Ho quindi rimosso le ombre granulose dal primo piano, applicando una soglia fissa al canale del valore. (Non sono sicuro se questo in realtà aiuta molto, ma è stato abbastanza semplice da aggiungere)

inserisci qui la descrizione dell'immagine

Quindi applico una trasformazione della distanza sull'immagine in primo piano.

inserisci qui la descrizione dell'immagine

e trova tutti i massimi locali in questa trasformazione di distanza.

Questo è dove la mia idea si rompe. per evitare di ottenere massimi locali multipli nello stesso grano, devo filtrare molto. Attualmente mantengo solo il massimo più forte entro un raggio di 45 pixel, il che significa che non ogni grano ha un massimo locale. E non ho davvero una giustificazione per il raggio di 45 pixel, era solo un valore che ha funzionato.

inserisci qui la descrizione dell'immagine

(come puoi vedere, quelli non sono abbastanza semi per tenere conto di ogni grano)

Quindi uso quei massimi come semi per l'algoritmo spartiacque:

inserisci qui la descrizione dell'immagine

I risultati sono meh . Speravo in gran parte dei singoli grani, ma i grumi sono ancora troppo grandi.

Ora identifico i BLOB più piccoli, ne conto la dimensione media dei pixel e quindi ne stima il numero di granuli. Questo è non è quello che ho pensato di fare all'inizio, ma questo era l'unico modo per salvare questo.

utilizzando il sistema ; 
utilizzando il sistema . Collezioni . Generico ; 
utilizzando il sistema . Linq ; 
utilizzando il sistema . Testo ; 
usando OpenCvSharp ;

spazio dei nomi GrainTest2 { class Program { vuoto statico 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

     
    
          
        
             
                               
                                     
                                     
                                      
                               
            = nuovo [] { 3 , 5 , 12 , 25 , 50 , 83 , 120 , 150 , 151 , 200 ,};          

            int totalError = 0 ; int totalPixels = 0 ; 
             

            per ( int fileNo = 0 ; fileNo marker = new List (); 
                    utilizzo ( CvMemStorage storage = new CvMemStorage ()) 
                    utilizzando ( CvContourScanner scanner = new CvContourScanner ( localMaxima , storage , CvContour . SizeOf , ContourRetrieval . External , ContourChain ) Approx .         
                    { // imposta ogni massimo locale come numero di seme 25, 35, 45, ... // (i numeri effettivi non contano, scelti per una migliore visibilità nel png) int markerNo = 20 ; foreach ( CvSeq c nello scanner ) { 
                            markerNo + = 5 ; 
                            marcatori . Aggiungi ( markerNo ); 
                            waterShedMarkers . DrawContours ( c , nuovo CvScalar ( markerNo ), nuovo
                        
                        
                         
                         
                             CvScalar ( markerNo ), 0 , - 1 ); } } 
                    waterShedMarkers . SaveImage ( "08-watershed-seeds.png" );  
                        
                    


                    fonte . Watershed ( 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.External,
                                                                                ContourChain.ApproxNone))
                            {
                                tmpvisu.Set(CvColor.Random(), tmp);
                                foreach (CvSeq c in scanner)
                                {
                                    //tmpvisu.DrawContours(c, CvColor.Random(), CvColor.DarkGreen, 0, -1);
                                    tmpvisu.PutText("" + Math.Round(curGrains, 1), c.First().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());

        }
    }
}

Un piccolo test che utilizzava una dimensione di pixel per grana di 2544,4 codificata in modo duro ha mostrato un errore totale di 36, che è ancora più grande della maggior parte delle altre soluzioni.

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine


Penso che tu possa usare la soglia (anche l'operazione di erosione potrebbe essere utile) con un piccolo valore sul risultato della trasformazione della distanza - questo dovrebbe dividere alcuni gruppi di grani in gruppi più piccoli (preferibilmente - con solo 1 o 2 grani). Di quanto dovrebbe essere più facile contare quei cereali solitari. Grandi gruppi che puoi contare come la maggior parte delle persone qui - dividendo l'area per area media del singolo grano.
Cyriel,

9

HTML + Javascript: punteggio 39

I valori esatti sono:

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

Si rompe (non è preciso) sui valori più grandi.

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>

Spiegazione: Fondamentalmente, conta il numero di pixel di riso e lo divide per i pixel medi per grano.


Usando l'immagine 3-riso, ha stimato 0 per me ...: /
Kroltan,

1
@Kroltan Non quando usi l' immagine a dimensione intera .
Hobby di Calvin,

1
@ Calvin'sHobbies FF36 su Windows ottiene 0, su Ubuntu ne ottiene 3, con l'immagine a dimensione intera.
Kroltan,

4
@BobbyJack Il riso è garantito per avere più o meno la stessa scala tra le immagini. Non ci vedo problemi.
Calvin's Hobbies,

1
@githubphagocyte - una spiegazione è abbastanza ovvia - se conti tutti i pixel bianchi sul risultato della binarizzazione dell'immagine e dividi questo numero per numero di grani nell'immagine otterrai questo risultato. Naturalmente il risultato esatto può differire, a causa del metodo di binarizzazione utilizzato e di altre cose (come le operazioni eseguite dopo la binarizzazione), ma come puoi vedere in altre risposte, sarà nell'intervallo 2500-3500.
Cyriel,

4

Un tentativo con php, non la risposta con il punteggio più basso ma il suo codice abbastanza semplice

PUNTEGGIO: 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);
}

Punteggio personale

95 è un valore blu che sembrava funzionare quando il test con GIMP 2966 ha una granulometria media

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.