Compter les grains de riz

81

Considérez ces 10 images de différentes quantités de grains de riz blanc non cuits.
CE SONT SEULEMENT DES PATTES. Cliquez sur une image pour la voir en taille réelle.

A: B: C: D: E:UNE B C ré E

F: G: H: I: J:F g H je J

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

Remarquerez que...

  • Les grains peuvent se toucher mais ils ne se chevauchent jamais. La disposition des grains n’est jamais supérieure à un grain.
  • Les images ont des dimensions différentes, mais l’échelle du riz est cohérente car la caméra et le fond étaient immobiles.
  • Les grains ne sortent jamais des limites et ne touchent pas les limites de l'image.
  • Le fond est toujours la même nuance consistante de blanc jaunâtre.
  • Les petits et les gros grains sont comptés de la même manière comme un grain chacun.

Ces 5 points sont des garanties pour toutes les images de ce type.

Défi

Ecrivez un programme qui prend de telles images et, le plus précisément possible, compte le nombre de grains de riz.

Votre programme devrait prendre le nom de fichier de l'image et imprimer le nombre de grains qu'il calcule. Votre programme doit fonctionner pour au moins un des formats de fichier image suivants: JPEG, Bitmap, PNG, GIF, TIFF (pour l’instant, les images sont toutes au format JPEG).

Vous pouvez utiliser des bibliothèques de traitement d'images et de vision par ordinateur.

Vous ne pouvez pas coder en dur les sorties des 10 exemples d’images. Votre algorithme devrait être applicable à toutes les images de grain de riz similaires. Il devrait pouvoir fonctionner en moins de 5 minutes sur un ordinateur moderne convenable si la zone d'image est inférieure à 2 000 * 2 000 pixels et s'il y a moins de 300 grains de riz.

Notation

Pour chacune des 10 images, prenez la valeur absolue du nombre réel de grains moins le nombre de grains prédit par votre programme. Faites la somme de ces valeurs absolues pour obtenir votre score. Le score le plus bas gagne. Un score de 0 est parfait.

En cas d'égalité, la réponse la plus votée l'emporte. Je peux tester votre programme sur des images supplémentaires pour vérifier sa validité et son exactitude.

Les passe-temps de Calvin
la source
1
Sûrement quelqu'un doit essayer scikit-learn!
Grand concours! :) Btw - pourrait nous dire quelque chose à propos de la date de fin de ce défi?
Cyriel
1
@Lembik A 7 heures :)
Dr. belisarius
5
Un jour, un scientifique du riz va venir et être fou de joie que cette question existe.
Nit
2
@Nit Dites-leur simplement que ncbi.nlm.nih.gov/pmc/articles/PMC3510117 :)
Dr. belisarius

Réponses:

22

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

Je pense que les noms des fonctions sont assez descriptifs:

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]

Traitement de toutes les images à la fois:

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

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

Le score est:

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

Vous pouvez voir ici la sensibilité de la partition par rapport à la taille de grain utilisée:

Mathematica graphiques

Dr. belisarius
la source
2
Beaucoup plus clair, merci!
Hobbies de Calvin le
Cette procédure exacte peut-elle être copiée en python ou existe-t-il quelque chose de spécial que Mathematica fait ici et que les bibliothèques python ne peuvent pas faire?
@Lembik Aucune idée. Pas de python ici. Pardon. (Cependant, je doute que les mêmes algorithmes exacts pour EdgeDetect[], DeleteSmallComponents[]et Dilation[]sont mises en œuvre ailleurs)
Dr. Bélisaire
55

Python, Score: 24 16

Cette solution, à l'instar de celle de Falko, consiste à mesurer la surface "au premier plan" et à la diviser par la surface moyenne des grains.

En fait, ce que ce programme essaie de détecter, c’est l’arrière-plan et non le premier plan. En utilisant le fait que les grains de riz ne touchent jamais la limite de l'image, le programme commence par un remplissage blanc dans le coin supérieur gauche. L'algorithme de remplissage par inondation peint les pixels adjacents si la différence entre la leur et la luminosité du pixel actuel est inférieure à un certain seuil, s'adaptant ainsi au changement progressif de la couleur d'arrière-plan. À la fin de cette étape, l'image pourrait ressembler à ceci:

Figure 1

Comme vous pouvez le constater, il détecte très bien l’arrière-plan, mais il ne tient pas compte des zones "piégées" entre les grains. Nous traitons ces zones en estimant la luminosité de l'arrière-plan à chaque pixel et en détectant tous les pixels égaux ou plus lumineux. Cette estimation fonctionne comme suit: pendant la phase de remplissage, nous calculons la luminosité moyenne de l'arrière-plan pour chaque ligne et chaque colonne. La luminosité de fond estimée à chaque pixel est la moyenne de la luminosité des lignes et des colonnes à ce pixel. Cela produit quelque chose comme ceci:

Figure 2

EDIT: Enfin, la surface de chaque région de premier plan continue (c’est-à-dire non blanche) est divisée par la surface moyenne, pré-calculée, des grains, ce qui nous donne une estimation du nombre de grains dans ladite région. La somme de ces quantités est le résultat. Au départ, nous avons fait la même chose pour l’ensemble du premier plan, mais cette approche est littéralement plus 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

Prend le nom du fichier d'entrée à travers la ligne de commande.

Résultats

      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

UNE B C ré E

F g H je J

Aune
la source
2
C'est une solution vraiment intelligente, beau travail!
Chris Cirefice
1
d'où avg_grain_area = 3038.38;vient-il?
njzk2
2
cela ne compte-t-il pas hardcoding the result?
njzk2
5
@ njzk2 Non. Compte tenu de la règle The images have different dimensions but the scale of the rice in all of them is consistent because the camera and background were stationary.Il s'agit simplement d'une valeur qui représente cette règle. Le résultat, cependant, change en fonction de l'entrée. Si vous modifiez la règle, cette valeur changera, mais le résultat sera le même - en fonction de l'entrée.
Adam Davis
6
Je suis d'accord avec le problème de la superficie moyenne. La surface des grains est (à peu près) constante d'une image à l'autre.
Hobbies de Calvin le
28

Python + OpenCV: Score 27

Balayage de ligne horizontale

Idée: numérisez l'image, une ligne à la fois. Pour chaque ligne, comptez le nombre de grains de riz rencontrés (en vérifiant si le pixel passe du noir au blanc ou l'inverse). Si le nombre de grains pour la ligne augmente (par rapport à la ligne précédente), cela signifie que nous avons rencontré un nouveau grain. Si ce nombre diminue, cela signifie que nous sommes passés au-dessus d'un grain. Dans ce cas, ajoutez +1 au résultat total.

entrez la description de l'image ici

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

En raison du fonctionnement de l’algorithme, il est important d’avoir une image nette en noir et blanc. Beaucoup de bruit produit de mauvais résultats. Le premier fond principal est nettoyé à l’aide de la méthode Floodfill (solution similaire à Ell answer), puis le seuil est appliqué pour produire un résultat noir et blanc.

entrez la description de l'image ici

C'est loin d'être parfait, mais cela donne de bons résultats en matière de simplicité. Il y a probablement plusieurs façons de l'améliorer (en fournissant une meilleure image en noir et blanc, en effectuant un balayage dans d'autres directions (par exemple: verticale, diagonale) en prenant la moyenne, etc.).

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

Les erreurs par image: 0, 0, 0, 3, 0, 12, 4, 0, 7, 1

tigrou
la source
24

Python + OpenCV: Score 84

Voici une première tentative naïve. Il applique un seuil adaptatif avec des paramètres réglés manuellement, ferme certains trous avec érosion et dilution ultérieures et déduit le nombre de grains de la zone de premier plan.

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)

Ici vous pouvez voir les images binaires intermédiaires (le noir est au premier plan):

entrez la description de l'image ici

Les erreurs par image sont 0, 0, 2, 2, 4, 0, 27, 42, 0 et 7 grains.

Falko
la source
20

C # + OpenCvSharp, Score: 2

C'est ma deuxième tentative. C'est assez différent de ma première tentative , qui est beaucoup plus simple, alors je la publie comme une solution séparée.

L'idée de base est d'identifier et d'étiqueter chaque grain individuellement par un ajustement ellipse itératif. Supprimez ensuite les pixels pour ce grain de la source et essayez de trouver le grain suivant, jusqu'à ce que chaque pixel soit étiqueté.

Ce n'est pas la plus jolie solution. C'est un porc géant avec 600 lignes de code. Il faut 1,5 minute pour la plus grande image. Et je m'excuse vraiment pour le code en désordre.

Il y a tellement de paramètres et de façons de penser en cette matière que j'ai bien peur de trop adapter mon programme pour les 10 exemples d'images. Le résultat final de 2 est presque définitivement un cas de surajustement: j'ai deux paramètres average grain size in pixelet minimum ratio of pixel / elipse_area, à la fin, j'ai simplement épuisé toutes les combinaisons de ces deux paramètres jusqu'à obtenir le score le plus bas. Je ne suis pas sûr que ce soit tout ce qui casher avec les règles de ce défi.

average_grain_size_in_pixel = 2530
pixel / elipse_area >= 0.73

Mais même sans ces embrayages trop lourds, les résultats sont plutôt bons. Sans une taille de grain ou un ratio de pixels fixe, simplement en estimant la taille de grain moyenne à partir des images d'apprentissage, le score est toujours de 27.

Et je reçois en sortie non seulement le nombre, mais la position réelle, l'orientation et la forme de chaque grain. il y a un petit nombre de grains mal étiquetés, mais dans l'ensemble, la plupart des étiquettes correspondent exactement aux grains réels:

A UNE B B C C D ré EE

F F G g H H I je JJ

(cliquez sur chaque image pour la version agrandie)

Après cette étape d’étiquetage, mon programme examine chaque grain et évalue chaque estimation en fonction du nombre de pixels et du rapport pixels / ellipse.

  • un seul grain (+1)
  • plusieurs grains mal étiquetés comme un (+ X)
  • une goutte trop petite pour être un grain (+0)

Les scores d'erreur pour chaque image sont A:0; B:0; C:0; D:0; E:2; F:0; G:0 ; H:0; I:0, J:0

Cependant, l'erreur réelle est probablement un peu plus élevée. Certaines erreurs dans la même image s’annulent. L'image H en particulier présente des grains mal étiquetés, alors que dans l'image E les étiquettes sont généralement correctes

Le concept est un peu artificiel:

  • D'abord, le premier plan est séparé via un otsu-seuillage sur le canal de saturation (voir ma réponse précédente pour plus de détails)

  • répétez jusqu'à ce qu'il ne reste plus de pixels:

    • sélectionnez le plus gros blob
    • choisissez 10 pixels de bord aléatoires sur cette goutte en tant que positions de départ pour un grain

    • pour chaque point de départ

      • supposons un grain avec une hauteur et une largeur de 10 pixels à cette position.

      • répéter jusqu'à convergence

        • allez radialement vers l'extérieur à partir de ce point, sous différents angles, jusqu'à rencontrer un pixel de bord (blanc à noir)

        • les pixels trouvés devraient, espérons-le, être les pixels de bord d'un seul grain. Essayez de séparer les éléments internes des valeurs éloignées, en éliminant les pixels qui sont plus éloignés de l’ellipse supposée que les autres.

        • essayez à plusieurs reprises de faire passer une ellipse dans un sous-ensemble d'inliers, conservez la meilleure ellipse (RANSACK)

        • mettre à jour la position du grain, l'orientation, la largeur et la hauteur avec l'élipse trouvé

        • si la position du grain ne change pas de manière significative, arrêtez

    • parmi les 10 grains ajustés, choisissez le meilleur grain en fonction de la forme, du nombre de pixels de bord. Jeter les autres

    • supprimer tous les pixels pour ce grain de l'image source, puis répétez

    • enfin, parcourez la liste des grains trouvés et comptez chaque grain comme 1 grain, 0 grain (trop petit) ou 2 grains (trop gros)

L'un de mes principaux problèmes était que je ne souhaitais pas mettre en œuvre une métrique de distance point-ellipse complète, car le calcul de cette distance est en soi un processus itératif complexe. J'ai donc utilisé diverses solutions de contournement à l'aide des fonctions OpenCV Ellipse2Poly et FitEllipse, et les résultats ne sont pas très beaux.

Apparemment, j'ai également dépassé la limite de taille pour codegolf.

Une réponse est limitée à 30000 caractères, je suis actuellement à 34000. Je vais donc devoir raccourcir un peu le code ci-dessous.

Le code complet peut être vu à http://pastebin.com/RgM7hMxq

Désolé, je ne savais pas qu'il y avait une limite de taille.

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

}

Je suis un peu gêné par cette solution car a) je ne suis pas sûr que cela soit dans l’esprit de ce défi et b) il est trop gros pour une réponse codegolf et manque de l’élégance des autres solutions.

D'autre part, je suis assez satisfait des progrès que j'ai accomplis dans l' étiquetage des grains, pas seulement en les comptant, alors c'est tout.

HugoRune
la source
Vous savez que vous pouvez réduire cette longueur de code par magnitude en utilisant des noms plus petits et en appliquant d'autres techniques de golf;)
Optimiseur
Probablement, mais je ne voulais pas obscurcir davantage cette solution. Il est trop brouillé à mon goût car il est :)
HugoRune
+1 pour l'effort et parce que tu es le seul à pouvoir afficher individuellement chaque grain. Malheureusement, le code est un peu gonflé et repose beaucoup sur des constantes codées en dur. Je serais curieux de voir comment l’algorithme de scanline que j’ai écrit s’effectue à ce sujet (sur les grains colorés habituels).
Tigrou
Je pense vraiment que c’est la bonne approche pour ce type de problème (+1 pour vous), mais je me demande pourquoi "choisissez-vous 10 pixels de bord aléatoires", je pense que vous obtiendrez de meilleures performances si vous choisissez les points de bord avec le nombre le plus bas de points de bord proches (c’est-à-dire les parties qui ressortent), je pense (théoriquement) que cela éliminerait d’abord les grains les plus "faciles", avez-vous pensé à cela?
David Rogers
J'y ai pensé, mais je ne l'ai pas encore essayé. La «position de départ aléatoire 10» était une addition tardive, facile à ajouter et à mettre en parallèle. Avant cela, "une position de départ aléatoire" était bien meilleure que "toujours le coin supérieur gauche". Le danger de choisir les positions de départ avec la même stratégie à chaque fois est que lorsque je supprime la meilleure correspondance, les 9 autres seront probablement choisies à nouveau la prochaine fois, et avec le temps, la pire de ces positions de départ restera en arrière et sera choisie à nouveau. encore. Une partie qui ressort peut être simplement les restes d'un grain précédent partiellement enlevé.
HugoRune
17

C ++, OpenCV, score: 9

L’idée de base de ma méthode est assez simple - essayez d’effacer les grains simples (et les «grains doubles» - 2 grains (mais pas plus!), Proches les uns des autres) de l’image puis de compter le repos en utilisant une méthode basée sur la surface (comme Falko, Ell et Bélisarius). L'utilisation de cette approche est un peu meilleure que la "méthode d'aire" standard, car il est plus facile de trouver une bonne valeur moyennePixelsPerObject.

(1ère étape) Avant tout, nous devons utiliser la binarisation Otsu sur le canal S de l’image en HSV. L'étape suivante consiste à utiliser l'opérateur dilate pour améliorer la qualité de l'avant-plan extrait. Que nous devons trouver des contours. Bien sûr, certains contours ne sont pas des grains de riz - nous devons supprimer les contours trop petits (avec une surface inférieure à averagePixelsPerObject / 4. AveragePixelsPerObject est de 2855 dans mon cas). Enfin, nous pouvons enfin commencer à compter les grains :) (2ème étape) La recherche de grains simples et doubles est assez simple - il suffit de regarder dans la liste des contours les contours avec une zone dans des plages spécifiques - si la zone de contour est dans la plage, supprimez-la de la liste et ajoutez 1 (ou 2 si c'était "double" grain) au compteur de grains. (3ème étape) La dernière étape est bien sûr de diviser la surface des contours restants par la valeur moyennePixelsPerObject et d'ajouter le résultat au compteur de grains.

Les images (pour l'image F.jpg) devraient montrer cette idée mieux que les mots:
1ère étape (sans petits contours (bruit)): 1ère marche (sans petits contours (bruit))
2ème étape - uniquement des contours simples: 2ème étape - seulement des contours simples
3ème étape - contours restants: 3ème étape - contours restants

Voici le code, c'est plutôt moche, mais devrait fonctionner sans problème. Bien sûr, OpenCV est requis.

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

Si vous souhaitez voir les résultats de toutes les étapes, supprimez la mise en commentaire de tous les appels de fonction imshow (.., ..) et définissez la variable fastProcessing sur false. Les images (A.jpg, B.jpg, ...) doivent être dans des images de répertoire. Vous pouvez également choisir le nom d’une image en tant que paramètre à partir de la ligne de commande.

Bien sûr, si quelque chose n'est pas clair, je peux l'expliquer et / ou fournir des images / informations.

cyriel
la source
12

C # + OpenCvSharp, score: 71

C’est très frustrant, j’ai essayé de trouver une solution permettant d’identifier chaque grain à l’aide d’un bassin versant , mais j’ai juste. ne peut pas. obtenir. il. à. travail.

J'ai opté pour une solution qui sépare au moins quelques grains individuels, puis utilise ces grains pour estimer la taille moyenne des grains. Cependant, jusqu'à présent, je ne peux pas battre les solutions avec une taille de grain codée en dur.

Donc, le point fort de cette solution: elle ne suppose pas une taille de pixel fixe pour les grains et devrait fonctionner même si la caméra est déplacée ou si le type de riz est modifié.

A.jpg; nombre de grains: 3; attendu 3; erreur 0; pixels par grain: 2525,0;
B.jpg; nombre de grains: 7; attendu 5; erreur 2; pixels par grain: 1920,0;
C.jpg; nombre de grains: 6; attendu 12; erreur 6; pixels par grain: 4242,5;
D.jpg; nombre de grains: 23; prévu 25; erreur 2; pixels par grain: 2415,5;
E.jpg; nombre de grains: 47; attendu 50; erreur 3; pixels par grain: 2729,9;
F.jpg; nombre de grains: 65; attendu 83; erreur 18; pixels par grain: 2860,5;
G.jpg; nombre de grains: 120; attendu 120; erreur 0; pixels par grain: 2552,3;
H.jpg; nombre de grains: 159; attendu 150; erreur 9; pixels par grain: 2624,7;
I.jpg; nombre de grains: 141; attendu 151; erreur 10; pixels par grain: 2697,4;
J.jpg; nombre de grains: 179; prévu 200; erreur 21; pixels par grain: 2847,1;
erreur totale: 71

Ma solution fonctionne comme ceci:

Séparez le premier plan en transformant l'image en HSV et en appliquant un seuillage Otsu sur le canal de saturation. Ceci est très simplement, fonctionne extrêmement bien, et je le recommanderais à tous ceux qui veulent essayer ce défi:

saturation channel                -->         Otsu thresholding

entrez la description de l'image ici -> entrez la description de l'image ici

Cela supprimera proprement l'arrière-plan.

J'ai ensuite supprimé les ombres de grain du premier plan en appliquant un seuil fixe au canal de valeur. (Je ne sais pas si cela aide beaucoup, mais c'était assez simple à ajouter)

entrez la description de l'image ici

Ensuite, j'applique une transformation de distance sur l'image au premier plan.

entrez la description de l'image ici

et trouver tous les maxima locaux dans cette distance transformer.

C'est là que mon idée s'effondre. pour ne pas avoir plusieurs maxima locaux dans le même grain, je dois filtrer beaucoup. Actuellement, je ne garde que le maximum le plus fort dans un rayon de 45 pixels, ce qui signifie que tous les grains n'ont pas de maximum local. Et je n'ai pas vraiment de justification pour le rayon de 45 pixels, c'était juste une valeur qui a fonctionné.

entrez la description de l'image ici

(comme vous pouvez le constater, ce ne sont pas assez de semences pour tenir compte de chaque grain)

Ensuite, j'utilise ces maxima comme semences de l'algorithme de gestion des bassins versants:

entrez la description de l'image ici

Les résultats sont meh . J'espérais surtout des grains individuels, mais les touffes sont encore trop grosses.

Maintenant, j'identifie les plus petites gouttes, compte leur taille moyenne en pixels, puis en estime le nombre de grains. Ce n’était pas ce que j’avais prévu de faire au début, mais c’était le seul moyen de sauver cela.

en utilisant le système ; 
using System . Collections . Générique ; 
using System . Linq ; 
using System . Texte ; 
using OpenCvSharp ;

espace de noms GrainTest2 { class Programme { static void Main ( string [] args ) { string [] fichiers = new [] { "sourceA.jpg" , "sourceB.jpg" , "sourceC.jpg" , "sourceD.jpg" , " sourceE.jpg " , " sourceF.jpg " , " sourceG.jpg " , " sourceH.jpg " , " sourceI.jpg " , " sourceJ.jpg " , };int [] attenduGrains

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

            int totalError = 0 ; int totalPixels = 0 ; 
             

            pour ( int Fileno = 0 ; marqueurs Fileno = nouvelle liste (); en 
                    utilisant ( CvMemStorage stockage = nouveau CvMemStorage ()) en 
                    utilisant ( CvContourScanner scanner = nouveau CvContourScanner ( localMaxima , stockage , CvContour . SizeOf , ContourRetrieval . externes , ContourChain . ApproxNone ))         
                    { // définit chaque maximum local comme numéro de départ 25, 35, 45, ... // (les nombres réels importe peu, choisi pour une meilleure visibilité dans le fichier png) int markerNo = 20 ; foreach ( CvSeq c dans le scanner ) { 
                            markerNo + = 5 ; 
                            marqueurs . Ajouter ( marqueurNo ); 
                            waterShedMarkers . DrawContours ( c , nouveau CvScalar ( markerNo ), nouveau
                        
                        
                         
                         
                             CvScalar ( markerNo ), 0 , - 1 ); } } 
                    waterShedMarkers . SaveImage ( "08-watershed-seeds.png" );  
                        
                    


                    source . Bassin versant ( 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 petit test utilisant une taille de pixel par grain codée en dur de 2544,4 a montré une erreur totale de 36, ce qui est encore plus grand que la plupart des autres solutions.

entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici

HugoRune
la source
Je pense que vous pouvez utiliser un seuil (l'opération erode pourrait être utile aussi) avec une petite valeur sur le résultat de la transformation de distance - ceci devrait diviser certains groupes de grains en groupes plus petits (de préférence - avec seulement 1 ou 2 grains). Alors il devrait être plus facile de compter ces grains solitaires. Vous pouvez compter ici comme de grands groupes - divisez la superficie par la superficie moyenne d'un grain.
Cyriel
9

HTML + Javascript: Score 39

Les valeurs exactes sont:

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

Il se décompose (n'est pas précis) sur les plus grandes valeurs.

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>

Explication: En gros, compte le nombre de pixels du riz et le divise par le nombre moyen de pixels par grain.

soktinpk
la source
En utilisant l'image de 3 riz, il a estimé 0 pour moi ...: /
Kroltan
1
@Kroltan Pas lorsque vous utilisez l' image en taille réelle .
Hobbies de Calvin le
1
@ Calvin'sHobbies FF36 sur Windows reçoit 0, sur Ubuntu, 3, avec l'image à sa taille d'origine.
Kroltan
4
@BobbyJack Il est garanti que le riz sera à peu près à la même échelle d'une image à l'autre. Je ne vois aucun problème avec cela.
Hobbies de Calvin le
1
@ githubphagocyte - une explication est assez évident - si vous comptez tous les pixels blancs lors de la binarisation de l'image et divisez ce nombre par le nombre de grains de l'image, vous obtiendrez ce résultat. Bien sûr, le résultat exact peut différer, en raison de la méthode de binarisation utilisée et d'autres opérations (comme les opérations effectuées après la binarisation), mais comme vous pouvez le constater dans d'autres réponses, il se situera dans la plage 2500-3500.
Cyriel
4

Une tentative avec php, pas la réponse la plus basse mais son code assez simple

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

Auto-marquant

95 est une valeur bleue qui semblait fonctionner lorsque les tests avec GIMP 2966 étaient une taille de grain moyenne

exussum
la source