Contando grãos de arroz

81

Considere estas 10 imagens de várias quantidades de grãos não cozidos de arroz branco.
ESTES SÃO SOMENTE POLEGARES. Clique na imagem para vê-la em tamanho real.

A: B: C: D: E:UMA B C D E

F: G: H: I: J:F G H Eu J

Contagem de grãos: A: 3, B: 5, C: 12, D: 25, E: 50, F: 83, G: 120, H:150, I: 151, J: 200

Notar que...

  • Os grãos podem se tocar, mas nunca se sobrepõem. O layout dos grãos nunca tem mais do que um grão de altura.
  • As imagens têm dimensões diferentes, mas a escala do arroz em todas elas é consistente porque a câmera e o fundo estavam estacionários.
  • Os grãos nunca saem dos limites ou tocam nos limites da imagem.
  • O fundo é sempre o mesmo tom consistente de branco amarelado.
  • Grãos pequenos e grandes são contados como um grão cada.

Esses 5 pontos são garantias para todas as imagens desse tipo.

Desafio

Escreva um programa que capte essas imagens e, com a maior precisão possível, conte o número de grãos de arroz.

Seu programa deve pegar o nome do arquivo da imagem e imprimir o número de grãos que calcula. Seu programa deve funcionar para pelo menos um destes formatos de arquivo de imagem: JPEG, Bitmap, PNG, GIF, TIFF (agora as imagens são todas JPEGs).

Você pode usar as bibliotecas de processamento de imagem e visão computacional.

Você não pode codificar as saídas das 10 imagens de exemplo. Seu algoritmo deve ser aplicável a todas as imagens semelhantes de grãos de arroz. Ele deve ser executado em menos de 5 minutos em um computador moderno decente se a área da imagem for menor que 2000 * 2000 pixels e houver menos de 300 grãos de arroz.

Pontuação

Para cada uma das 10 imagens, pegue o valor absoluto do número real de grãos menos o número de grãos que seu programa prevê. Soma esses valores absolutos para obter sua pontuação. A pontuação mais baixa vence. Uma pontuação de 0 é perfeita.

Em caso de empate, a resposta mais votada vence. Posso testar seu programa em imagens adicionais para verificar sua validade e precisão.

Passatempos de Calvin
fonte
1
Certamente alguém tem que tentar aprender scikit!
Grande concurso! :) Btw - poderia nos dizer algo sobre a data de término deste desafio?
Cyriel 5/11
1
@Lembik Down to 7 :)
Dr. belisarius
5
Um dia, um cientista do arroz vai aparecer e ficar muito feliz por essa pergunta existir.
Nit
2
@Nit Basta dizer-lhes ncbi.nlm.nih.gov/pmc/articles/PMC3510117 :)
Dr. belisarius

Respostas:

22

Mathematica, pontuação: 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;

Eu acho que os nomes das funções são bastante descritivos:

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]

Processando todas as imagens de uma vez:

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

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

A pontuação é:

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

Aqui você pode ver a sensibilidade da pontuação no tamanho do grão usado:

Gráficos do Mathematica

Dr. belisarius
fonte
2
Muito mais claro, obrigado!
Hobbies de Calvin
Esse procedimento exato pode ser copiado em python ou há algo especial que o Mathematica está fazendo aqui que as bibliotecas python não podem fazer?
@Lembik Não faço ideia. Aqui não há python. Desculpa. (No entanto, duvido que os mesmos algoritmos exatos para EdgeDetect[], DeleteSmallComponents[]e Dilation[]são implementadas em outros lugares)
Dr. belisarius
55

Python, Pontuação: 24 16

Essa solução, como a de Falko, baseia-se na medição da área "em primeiro plano" e na divisão pela área média de grãos.

De fato, o que este programa tenta detectar é o plano de fundo, não tanto quanto o primeiro plano. Usando o fato de que os grãos de arroz nunca tocam o limite da imagem, o programa começa preenchendo a inundação em branco no canto superior esquerdo. O algoritmo de preenchimento pinta pixels adjacentes se a diferença entre o brilho deles e o pixel atual estiver dentro de um determinado limite, ajustando-se assim à mudança gradual na cor de fundo. No final deste estágio, a imagem pode ser algo como isto:

figura 1

Como você pode ver, ele faz um bom trabalho na detecção do fundo, mas deixa de fora quaisquer áreas "presas" entre os grãos. Lidamos com essas áreas estimando o brilho do plano de fundo em cada pixel e paitando todos os pixels iguais ou mais brilhantes. Essa estimativa funciona da seguinte maneira: durante o estágio de preenchimento, calculamos o brilho médio do plano de fundo para cada linha e cada coluna. O brilho estimado do plano de fundo em cada pixel é a média do brilho da linha e da coluna nesse pixel. Isso produz algo como isto:

Figura 2

EDIT: Por fim, a área de cada região contínua de primeiro plano (ou seja, não branca) é dividida pela área média pré-calculada de grãos, fornecendo uma estimativa da contagem de grãos nessa região. A soma dessas quantidades é o resultado. Inicialmente, fizemos o mesmo para toda a área de primeiro plano como um todo, mas essa abordagem é, literalmente, mais refinada.


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

Leva o nome do arquivo de entrada pela linha de comando.

Resultados

      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

UMA B C D E

F G H Eu J

Ell
fonte
2
Esta é uma solução realmente inteligente, bom trabalho!
precisa saber é o seguinte
1
de onde avg_grain_area = 3038.38;vem?
Njzk2
2
isso não conta como hardcoding the result?
Njzk2
5
@ njzk2 Não. Dada a regra The images have different dimensions but the scale of the rice in all of them is consistent because the camera and background were stationary.Este é apenas um valor que representa essa regra. O resultado, no entanto, muda de acordo com a entrada. Se você alterar a regra, esse valor será alterado, mas o resultado será o mesmo - com base na entrada.
Adam Davis
6
Eu estou bem com a coisa da área média. A área do grão é (aproximadamente) constante nas imagens.
Hobbies de Calvin
28

Python + OpenCV: pontuação 27

Digitalização de linha horizontal

Idéia: digitalize a imagem, uma linha de cada vez. Para cada linha, conte o número de grãos de arroz encontrados (verificando se o pixel fica preto em branco ou o oposto). Se o número de grãos para a linha aumentar (em comparação com a linha anterior), significa que encontramos um novo grão. Se esse número diminuir, significa que passamos por cima de um grão. Nesse caso, adicione +1 ao resultado total.

insira a descrição da imagem aqui

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

Devido à maneira como o algoritmo funciona, é importante ter uma imagem limpa, em preto e branco. Muito barulho produz resultados ruins. O primeiro plano de fundo principal é limpo usando o aterro (solução semelhante à resposta de Ell) e o limiar é aplicado para produzir resultados em preto e branco.

insira a descrição da imagem aqui

Está longe de ser perfeito, mas produz bons resultados em relação à simplicidade. Provavelmente, há muitas maneiras de melhorá-lo (fornecendo uma melhor imagem em preto e branco, digitalizando em outras direções (por exemplo: vertical, diagonal), medindo a média 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

Os erros por imagem: 0, 0, 0, 3, 0, 12, 4, 0, 7, 1

tigrou
fonte
24

Python + OpenCV: pontuação 84

Aqui está uma primeira tentativa ingênua. Aplica um limiar adaptável com parâmetros ajustados manualmente, fecha alguns furos com subsequente erosão e diluição e deriva o número de grãos da área do primeiro plano.

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)

Aqui você pode ver as imagens binárias intermediárias (o preto é o primeiro plano):

insira a descrição da imagem aqui

Os erros por imagem são 0, 0, 2, 2, 4, 0, 27, 42, 0 e 7 grãos.

Falko
fonte
20

C # + OpenCvSharp, Pontuação: 2

Esta é a minha segunda tentativa. É bem diferente da minha primeira tentativa , que é muito mais simples, por isso estou publicando como uma solução separada.

A idéia básica é identificar e rotular cada grão individual por um ajuste de elipse iterativo. Em seguida, remova os pixels desse grão da origem e tente encontrar o próximo grão, até que cada pixel tenha sido rotulado.

Esta não é a solução mais bonita. É um porco gigante com 600 linhas de código. Ele precisa de 1,5 minutos para a maior imagem. E realmente peço desculpas pelo código confuso.

Existem tantos parâmetros e maneiras de pensar nisso que tenho muito medo de ajustar meu programa para as 10 imagens de amostra. A pontuação final de 2 é quase definitivamente um caso de sobreajuste: eu tenho dois parâmetros,, average grain size in pixele minimum ratio of pixel / elipse_area, e no final simplesmente exaurei todas as combinações desses dois parâmetros até obter a pontuação mais baixa. Não tenho certeza se isso é tudo o que kosher com as regras deste desafio.

average_grain_size_in_pixel = 2530
pixel / elipse_area >= 0.73

Mas mesmo sem essas embreagens, os resultados são bastante bons. Sem um tamanho fixo de grão ou proporção de pixels, simplesmente estimando o tamanho médio de grão a partir das imagens de treinamento, a pontuação ainda é 27.

E recebo como resultado não apenas o número, mas a posição, orientação e forma reais de cada grão. há um pequeno número de grãos com etiquetas incorretas, mas no geral a maioria dos rótulos corresponde exatamente aos grãos reais:

A UMA B B C C D D EE

F F G G H H I Eu JJ

(clique em cada imagem para obter a versão em tamanho normal)

Após esta etapa de rotulagem, meu programa analisa cada granulação individual e estima com base no número de pixels e na proporção pixel / elipse-área, se isso é

  • um único grão (+1)
  • vários grãos classificados incorretamente como um (+ X)
  • um blob muito pequeno para ser um grão (+0)

As pontuações de erro para cada imagem são A:0; B:0; C:0; D:0; E:2; F:0; G:0 ; H:0; I:0, J:0

No entanto, o erro real é provavelmente um pouco maior. Alguns erros na mesma imagem se cancelam. A imagem H, em particular, possui alguns grãos mal rotulados, enquanto na imagem E os rótulos estão na maioria corretos

O conceito é um pouco artificial:

  • Primeiro, o primeiro plano é separado via otsu-thresholding no canal de saturação (veja minha resposta anterior para detalhes)

  • repita até que não haja mais pixels:

    • selecione o maior blob
    • escolha 10 pixels de borda aleatórios neste blob como posições iniciais de um grão

    • para cada ponto de partida

      • assuma um grão com altura e largura de 10 pixels nessa posição.

      • repita até a convergência

        • vá radialmente para fora a partir deste ponto, em diferentes ângulos, até encontrar um pixel de borda (branco para preto)

        • esperamos que os pixels encontrados sejam os pixels da borda de uma única granulação. Tente separar inliers de outliers, descartando pixels que estão mais distantes da elipse assumida do que os outros

        • tente repetidamente ajustar uma elipse através de um subconjunto dos inliers, mantenha a melhor elipse (RANSACK)

        • atualize a posição, orientação, largura e altura do grão com a elipse encontrada

        • se a posição do grão não mudar significativamente, pare

    • Entre os 10 grãos ajustados, escolha o melhor grão de acordo com a forma, número de pixels da aresta. Descartar os outros

    • remova todos os pixels desse grão da imagem de origem e repita

    • por fim, leia a lista de grãos encontrados e conte cada grão como 1 grão, 0 grão (muito pequeno) ou 2 grãos (muito grande)

Um dos meus principais problemas era que eu não queria implementar uma métrica de distância de ponto de elipse completa, pois calcular isso por si só é um processo iterativo complicado. Então, usei várias soluções alternativas usando as funções OpenCV Ellipse2Poly e FitEllipse, e os resultados não são muito bonitos.

Aparentemente, eu também quebrei o limite de tamanho para o codegolf.

Uma resposta é limitada a 30000 caracteres, atualmente estou em 34000. Portanto, vou ter que diminuir um pouco o código abaixo.

O código completo pode ser visto em http://pastebin.com/RgM7hMxq

Desculpe por isso, eu não sabia que havia um limite de tamanho.

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

}

Estou um pouco envergonhado com esta solução porque a) não tenho certeza se está dentro do espírito desse desafio eb) é muito grande para uma resposta do codegolf e não tem a elegância das outras soluções.

Por outro lado, estou muito feliz com o progresso que obtive na rotulagem dos grãos, não apenas contando-os, então é isso.

HugoRune
fonte
Você sabe que pode reduzir esse tamanho de código em magnitudes usando nomes menores e aplicando algumas outras técnicas de golfe;)
Optimizer
Provavelmente, mas não queria ofuscar ainda mais esta solução. É muito ofuscado para o meu gosto como é :)
HugoRune
+1 pelo esforço e porque você é o único que encontra uma maneira de exibir individualmente cada grão. Infelizmente, o código é um pouco inchado e depende muito de constantes codificadas. Eu ficaria curioso para ver como o algoritmo da linha de varredura que escrevi funciona sobre isso (nos grãos coloridos inviduais).
usar o seguinte
Eu realmente acho que essa é a abordagem correta para esse tipo de problema (+1 para você), mas uma coisa é: por que você "escolhe 10 pixels de borda aleatórios"? Eu pensaria que você obteria melhor desempenho se escolhesse os pontos de borda com o menor número de pontos de borda próximos (ou seja, partes que se destacam), eu pensaria (teoricamente) que isso eliminaria primeiro os grãos "mais fáceis", você já considerou isso?
David Rogers
Eu pensei nisso, mas ainda não tentei. A '10 posição inicial aleatória 'foi uma adição tardia, fácil de adicionar e fácil de paralelizar. Antes disso, 'uma posição inicial aleatória' era muito melhor do que 'sempre no canto superior esquerdo'. O perigo de escolher as posições iniciais com a mesma estratégia de cada vez é que, quando eu remover o melhor ajuste, os outros 9 provavelmente serão escolhidos novamente na próxima vez, e com o tempo a pior dessas posições iniciais ficará para trás e será escolhida novamente. novamente. Uma parte que sobressai pode ser apenas os restos de um grão anterior incompletamente removido.
HugoRune 11/11/14
17

C ++, OpenCV, pontuação: 9

A idéia básica do meu método é bastante simples - tente apagar grãos únicos (e "grãos duplos" - 2 (mas não mais!) Grãos, próximos um do outro) da imagem e, em seguida, conte o resto usando o método baseado na área (como Falko, Ell e Belisarius). O uso dessa abordagem é um pouco melhor que o "método de área" padrão, porque é mais fácil encontrar um bom valor averagePixelsPerObject.

(1º passo) Antes de tudo, precisamos usar a binarização Otsu no canal S da imagem em HSV. O próximo passo é usar o operador dilatado para melhorar a qualidade do primeiro plano extraído. Do que precisamos encontrar contornos. É claro que alguns contornos não são grãos de arroz - precisamos excluir contornos muito pequenos (com área menor que a médiaPixelsPerObject / 4. averagePixelsPerObject é 2855 na minha situação). Agora, finalmente, podemos começar a contar grãos :) (2º passo) Encontrar grãos simples e duplos é bastante simples - basta procurar na lista de contornos contornos com área dentro de intervalos específicos - se a área de contorno estiver no intervalo, exclua-a da lista e adicione 1 (ou 2, se for grão "duplo") para o contador de grãos. (3º passo) É claro que o último passo é dividir a área dos contornos restantes pelo valor averagePixelsPerObject e adicionar resultado ao contador de grãos.

As imagens (na imagem F.jpg) devem mostrar essa ideia melhor do que as palavras:
1º passo (sem pequenos contornos (ruído)): 1º passo (sem pequenos contornos (ruído))
2º passo - apenas contornos simples: 2º passo - apenas contornos simples
3º passo - contornos restantes: 3º passo - contornos restantes

Aqui está o código, é bastante feio, mas deve funcionar sem nenhum problema. Claro que o OpenCV é necessário.

#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 você quiser ver os resultados de todas as etapas, remova o comentário de todas as chamadas de função imshow (.., ..) e defina a variável fastProcessing como false. As imagens (A.jpg, B.jpg, ...) devem estar em imagens de diretório. Como alternativa, é possível dar o nome de uma imagem como parâmetro na linha de comando.

Claro que, se algo não estiver claro, eu posso explicar e / ou fornecer algumas imagens / informações.

cyriel
fonte
12

C # + OpenCvSharp, pontuação: 71

Isso é muito irritante, tentei obter uma solução que realmente identificasse cada grão usando a bacia hidrográfica , mas eu apenas. não posso. pegue. isto. para. trabalhos.

Eu decidi por uma solução que pelo menos separa alguns grãos individuais e depois os usa para estimar o tamanho médio dos grãos. No entanto, até agora não posso superar as soluções com tamanho de grão codificado.

Portanto, o principal destaque desta solução: ela não pressupõe um tamanho fixo de pixel para grãos e deve funcionar mesmo se a câmera for movida ou se o tipo de arroz for alterado.

A.jpg; número de grãos: 3; esperado 3; erro 0; pixels por grão: 2525,0;
B.jpg; número de grãos: 7; esperado 5; erro 2; pixels por grão: 1920,0;
C.jpg; número de grãos: 6; esperado 12; erro 6; pixels por grão: 4242,5;
D.jpg; número de grãos: 23; esperado 25; erro 2; pixels por grão: 2415,5;
E.jpg; número de grãos: 47; esperado 50; erro 3; pixels por grão: 2729,9;
F.jpg; número de grãos: 65; esperado 83; erro 18; pixels por grão: 2860,5;
G.jpg; número de grãos: 120; esperado 120; erro 0; pixels por grão: 2552,3;
H.jpg; número de grãos: 159; esperado 150; erro 9; pixels por grão: 2624,7;
I.jpg; número de grãos: 141; esperado 151; erro 10; pixels por grão: 2697,4;
J.jpg; número de grãos: 179; 200 esperado; erro 21; pixels por grão: 2847,1;
erro total: 71

Minha solução funciona assim:

Separe o primeiro plano transformando a imagem em HSV e aplicando o limiar de Otsu no canal de saturação. Isso é muito simples, funciona extremamente bem e eu recomendaria isso para todos os outros que queiram experimentar este desafio:

saturation channel                -->         Otsu thresholding

insira a descrição da imagem aqui -> insira a descrição da imagem aqui

Isso removerá o plano de fundo de maneira limpa.

Em seguida, removi adicionalmente as sombras de granulação do primeiro plano, aplicando um limite fixo ao canal de valor. (Não tenho certeza se isso realmente ajuda muito, mas foi simples o suficiente para adicionar)

insira a descrição da imagem aqui

Em seguida, aplico uma transformação de distância na imagem em primeiro plano.

insira a descrição da imagem aqui

e encontre todos os máximos locais nessa transformação de distância.

É aqui que minha ideia se desdobra. para evitar obter vários máximos locais dentro do mesmo grão, tenho que filtrar bastante. Atualmente, mantenho apenas o máximo mais forte em um raio de 45 pixels, o que significa que nem todo grão tem um máximo local. E eu realmente não tenho uma justificativa para o raio de 45 pixels, foi apenas um valor que funcionou.

insira a descrição da imagem aqui

(como você pode ver, essas sementes não são suficientes para dar conta de cada grão)

Então eu uso esses máximos como sementes para o algoritmo da bacia hidrográfica:

insira a descrição da imagem aqui

Os resultados são meh . Eu esperava principalmente grãos individuais, mas os pedaços ainda são grandes demais.

Agora identifico os menores blobs, conto seu tamanho médio de pixel e, em seguida, estimo o número de grãos a partir disso. Não era isso que eu planejava fazer no início, mas essa era a única maneira de salvar isso.

using System ; 
usando o sistema . Coleções . Genérico ; 
usando o sistema . Linq ; 
usando o sistema . Texto ; 
using OpenCvSharp ;

namespace GrainTest2 { programa de classe { static void Main ( string [] args ) { string [] files = new [] { "sourceA.jpg" , "sourceB.jpg" , "sourceC.jpg" , "sourceD.jpg" , " sourceE.jpg " , " sourceF.jpg " , " sourceG.jpg " , " sourceH.jpg " , " sourceI.jpg " , " sourceJ.jpg " , };int [] pectedGrains

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

            int totalError = 0 ; int totalPixels = 0 ; 
             

            for ( int fileNo = 0 ; fileNo markers = new List (); 
                    using ( CvMemStorage storage = new CvMemStorage ()) 
                    using ( CvContourScanner scanner = new CvContourScanner ( localMaxima , storage , CvContour . SizeOf , ContourRetrieval . External , ContourChain . ApproxNone ))         
                    { // defina cada máximo local como número inicial 25, 35, 45, ... // (os números reais não importam, escolhidos para melhor visibilidade no png) int markerNo = 20 ; foreach ( CvSeq c no scanner ) { 
                            markerNo + = 5 ; 
                            marcadores . Adicionar ( markerNo ); 
                            waterShedMarkers . DrawContours ( c , novo CvScalar ( markerNo ), novo
                        
                        
                         
                         
                             CvScalar ( marcador No ), 0 , - 1 ); } } 
                    waterShedMarkers . SaveImage ( "08-watershed-seeds.png" );  
                        
                    


                    fonte . Bacias Hidrográficas ( waterShedMarkers ); 
                    waterShedMarkers . SaveImage ( "09-watershed-result.png" );


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

        }
    }
}

Um pequeno teste usando um tamanho de pixel por grão codificado de 2544,4 mostrou um erro total de 36, ainda maior do que a maioria das outras soluções.

insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui

HugoRune
fonte
Eu acho que você pode usar o limiar (a operação de erosão também pode ser útil) com algum valor pequeno no resultado da transformação de distância - isso deve dividir alguns grupos de grãos em grupos menores (de preferência - com apenas 1 ou 2 grãos). Do que deveria ser mais fácil contar esses grãos solitários. Grupos grandes, você pode contar com a maioria das pessoas aqui - dividindo a área pela área média de um grão.
Cyriel #
9

HTML + Javascript: Pontuação 39

Os valores exatos são:

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

Ele divide (não é preciso) nos valores maiores.

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>

Explicação: Basicamente, conta o número de pixels de arroz e o divide pela média de pixels por grão.

soktinpk
fonte
Usando a imagem 3-arroz, estima-0 para mim ...: /
Kroltan
1
@ Kroltan Não quando você usa a imagem em tamanho real .
Calvin's Hobbies
1
@ Calvin'sHobbies O FF36 no Windows obtém 0, no Ubuntu obtém 3, com a imagem em tamanho real.
Kroltan
4
@BobbyJack É garantido que o arroz tem mais ou menos a mesma escala nas imagens. Não vejo problemas com isso.
Calvin's Hobbies
1
@githubphagocyte - uma explicação é bastante óbvia - se você contar todos os pixels brancos no resultado da binarização da imagem e dividir esse número pelo número de grãos na imagem, obterá esse resultado. É claro que o resultado exato pode diferir, devido ao método de binarização usado e outras coisas (como operações executadas após a binarização), mas como você pode ver em outras respostas, ele estará no intervalo 2500-3500.
Cyriel 5/11
4

Uma tentativa com php, não é a resposta de pontuação mais baixa, mas seu código bastante simples

PONTUAÇÃO: 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-pontuação

95 é um valor azul que parecia funcionar ao testar com o GIMP 2966 o tamanho médio dos grãos

exussum
fonte