Pirinç taneleri sayma


81

Bu 10 değişik görüntüyü, bir miktar pişmemiş beyaz pirinç taneciği alın.
BU SADECE THUMBNAILS ARE. Resmi tam boyutlu görmek için üzerine tıklayın.

A: B: C: D: E:bir B C D E

F: G: H: I: J:F G, 'H ben J

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

Dikkat edin ki ...

  • Taneler birbirine dokunabilir, ancak asla üst üste gelemezler. Tanelerin yerleşimi asla bir taneden fazla değildir.
  • Görüntüler farklı boyutlara sahip ancak kamera ve arka plan durağan olduğu için hepsinde pirincin ölçeği tutarlı.
  • Taneler hiçbir zaman sınırların dışına çıkmaz veya görüntü sınırlarına dokunmaz.
  • Arka plan her zaman sarımsı-beyazın aynı tutarlı tonundadır.
  • Küçük ve büyük taneler, her biri bir tanecik olarak sayılır.

Bu 5 puan, bu türdeki tüm görüntüler için garantidir.

Meydan okuma

Bu tür görüntüleri alan ve mümkün olduğunca doğru bir şekilde pirinç taneleri sayısını sayan bir program yazın.

Programınız görüntünün dosya adını almalı ve hesapladığı tane sayısını yazdırmalıdır. Programınız şu resim dosyası formatlarından en az biri için çalışmalıdır: JPEG, Bitmap, PNG, GIF, TIFF (şu anda tüm resimler JPEG.)

Sen olabilir görüntü işleme ve bilgisayar görme kütüphaneleri kullanın.

10 örnek görüntünün çıktılarını sabit kodlayamayabilirsiniz. Algoritmanız benzer tüm pirinç taneli görüntülere uygulanabilir olmalıdır. Görüntü alanı 2000 x 2000 pikselden azsa ve 300 taneden az pirinç tanesi varsa, iyi bir modern bilgisayarda 5 dakikadan daha az bir sürede çalışabilmelidir .

puanlama

10 görüntünün her biri için gerçek tahıl sayısının mutlak değerini alarak eksi programınızın tahmin ettiği tahıl sayısını alır. Puanınızı almak için bu mutlak değerleri toplayın. En düşük puan kazanır. 0 puan mükemmeldir.

Kravat durumunda en yüksek oyu alan cevap kazanır. Geçerlilik ve doğruluğunu onaylamak için programınızı ek görüntüler üzerinde test edebilirim.


1
Elbette birileri scikit-learn'ü denemek zorundadır!

Büyük yarışma! :) Btw - Bize bu mücadelenin bitiş tarihi hakkında bir şeyler söyleyebilir misiniz?
cyriel

1
@Lembik 7 to 7 :)
Dr. belisarius

5
Bir gün, bir pirinç bilimcisi gelecek ve bu sorunun var olduğu için mutlu olacaklar.
Nit

Yanıtlar:


22

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

Fonksiyonun isimleri yeterince açıklayıcı olduğunu düşünüyorum:

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]

Tüm fotoğrafları bir kerede işlemek:

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

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

Puan:

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

Burada kullanılan tane büyüklüğüne göre puan hassasiyetini görebilirsiniz:

Mathematica grafikleri


2
Çok daha net, teşekkürler!
Calvin'in Hobileri

Bu işlem tam olarak python'a kopyalanabilir mi, yoksa burada python kütüphanelerinin yapamayacağı özel bir matematik var mı?

@Lembik Fikrim yok. Burada piton yok. Üzgünüm. (Ancak, ben tam aynı algoritmaları şüphe EdgeDetect[], DeleteSmallComponents[]ve Dilation[]başka yerlerde uygulanmaktadır)
Dr. Belisarius

55

Python, Puan: 24 16

Falko'nunki gibi bu çözüm, "ön plan" alanını ölçmeye ve onu ortalama tahıl alanına bölmeye dayanıyor.

Aslında, bu programın tespit etmeye çalıştığı şey, ön plan kadar değil, arka plandır. Pirinç taneleri hiçbir zaman görüntü sınırına temas etmiyorsa, program sol üst köşedeki beyazı suyla doldurmaya başlar. Taşma dolgusu algoritması, aralarındaki fark ile mevcut pikselin parlaklığı arasındaki fark belli bir eşik değerindeyse bitişik pikselleri boyar, böylece arka plan renginde kademeli değişime ayarlanır. Bu aşamanın sonunda, görüntü şöyle görünebilir:

Şekil 1

Gördüğünüz gibi, arka planı tespit etmek oldukça iyi bir iş çıkarsa da, tahıllar arasında "sıkışmış" olan herhangi bir alanı dışlar. Bu alanları, her bir pikseldeki arka plan parlaklığını tahmin ederek ve tüm eşit veya daha parlak pikselleri tarayarak ele alıyoruz. Bu tahmin şöyle çalışır: sel doldurma aşamasında, her satır ve her sütun için ortalama arka plan parlaklığını hesaplarız. Her pikseldeki tahmini arka plan parlaklığı, o pikseldeki satır ve sütun parlaklığının ortalamasıdır. Bu böyle bir şey üretir:

şekil 2

EDIT: Son olarak, her bir sürekli ön planın (yani beyaz olmayan) bölgenin alanı, ortalama, önceden hesaplanmış, tane alanına bölünerek bize, söz konusu bölgedeki tane sayısının bir tahminini verir. Bu miktarların toplamı sonuçtur. Başlangıçta, ön planın tamamı için bir bütün olarak aynı şeyi yaptık, ancak bu yaklaşım tam anlamıyla daha iyi tanımlanmış durumda.


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

Giriş dosya adını komut satırından alır.

Sonuçlar

      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

bir B C D E

F G, 'H ben J


2
Bu gerçekten akıllı bir çözüm, iyi iş!
Chris Cirefice

1
nereden avg_grain_area = 3038.38;geliyor
njzk2

2
Bu sayılmaz hardcoding the resultmı?
njzk2

5
@ njzk2 Hayır. Kural verilmiştir. The images have different dimensions but the scale of the rice in all of them is consistent because the camera and background were stationary.Bu sadece bu kuralı temsil eden bir değerdir. Ancak sonuç girdilere göre değişir. Kuralı değiştirirseniz, o zaman bu değer değişecektir ancak sonuç, girdiye bağlı olarak aynı olacaktır.
Adam Davis,

6
Ortalama alan konusunda iyiyim. Tahıl alanı (kabaca) görüntüler arasında sabittir.
Calvin'in Hobileri

28

Python + OpenCV: Puan 27

Yatay çizgi tarama

Fikir: görüntüyü, her seferinde bir satır tarayın. Her satır için, karşılaşılan pirinç tanelerinin sayısını sayın (pikselin siyah beyaz mı yoksa tam tersi mi olduğunu kontrol ederek). Hattaki tahıl sayısı artarsa ​​(önceki satıra kıyasla), yeni bir taneyle karşılaştığımız anlamına gelir. Bu sayı azalırsa, bir tahılın üstünden geçiyoruz demektir. Bu durumda, toplam sonuç için +1 ekleyin.

görüntü tanımını buraya girin

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

Algoritmanın çalışması nedeniyle, temiz, siyah / beyaz bir görüntünün olması önemlidir. Çok fazla gürültü kötü sonuçlar veriyor. İlk ana arka plan taşma dolgusu kullanılarak temizlenir (Ell cevabına benzer bir çözüm), ardından siyah beyaz sonuç elde etmek için eşik uygulanır.

görüntü tanımını buraya girin

Mükemmel olmaktan uzak, ancak basitlik konusunda iyi sonuçlar veriyor. Bunu iyileştirmenin birçok yolu vardır (daha iyi siyah / beyaz görüntü sunarak, başka yönlere tararken (örneğin: dikey, çapraz) ortalama vb. Alarak).

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

Resimdeki hata sayısı: 0, 0, 0, 3, 0, 12, 4, 0, 7, 1


24

Python + OpenCV: Puan 84

İşte ilk saf girişim. Manuel olarak ayarlanmış parametrelerle uyarlanabilir bir eşik uygular, daha sonra erozyon ve seyreltme ile bazı delikleri kapatır ve tahıl alanını ön alandan türetir.

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)

Burada ortadaki ikili görüntüleri görebilirsiniz (siyah ön plandadır):

görüntü tanımını buraya girin

Resim başına hata sayısı 0, 0, 2, 2, 4, 0, 27, 42, 0 ve 7 tanedir.


20

C # + OpenCvSharp, Puan: 2

Bu benim ikinci denemem. Benim oldukça farklıdır ilk denemede çok basittir, bu yüzden ayrı bir çözüm olarak bunu ilanıyla.

Temel fikir, her bir taneyi, yinelemeli bir elips biçimine göre tanımlamak ve etiketlemektir. Ardından bu tanecik için pikselleri kaynaktan kaldırın ve her piksel etiketlenene kadar bir sonraki taneciği bulmaya çalışın.

Bu en güzel çözüm değil. 600 satırlık kod içeren dev bir domuz. En büyük görüntü için 1,5 dakika gerekir. Ve ben gerçekten dağınık kod için özür dilerim.

Bu konuda düşünmek için pek çok parametre ve yol var, ki bu 10 örnek resim için programımı fazla kullanmaktan korkuyorum. Ben iki parametre vardır: 2'nin son puan neredeyse kesinlikle overfitting bir durumdur average grain size in pixelve minimum ratio of pixel / elipse_area, ve ben en düşük puanı oluncaya kadar sonunda ben sadece bu iki parametre her kombinasyonlarını tükenmiş. Bunun, bu meydan okuma kurallarına uygun olan tek şey olup olmadığından emin değilim.

average_grain_size_in_pixel = 2530
pixel / elipse_area >= 0.73

Ancak, bu şık kavramalar olmadan bile, sonuçlar oldukça iyi. Sabit bir tane büyüklüğü veya piksel oranı olmadan, sadece eğitim görüntülerinden ortalama tane büyüklüğü tahmin edilerek, puan hala 27'dir.

Ve çıktı olarak sadece sayıyı değil aynı zamanda her bir tanenin gerçek konumunu, yönünü ve şeklini de alıyorum. az sayıda yanlış etiketlenmiş tane var, ancak etiketlerin çoğu gerçek tahıllarla doğru şekilde eşleşiyor:

A bir B B C C D D EE

F F G G, H 'H İ ben JJ

(tam boyutlu versiyon için her bir resme tıklayın)

Bu etiketleme adımından sonra, programım her bir taneye bakar ve piksel sayısına ve piksel / elips alanı oranına dayalı olarak tahmin eder;

  • tek bir tane (+1)
  • tek olarak yanlış etiketlenmiş çoklu tahıllar (+ X)
  • tahıl olamayacak kadar küçük bir damla (+0)

Her resim için hata puanları A:0; B:0; C:0; D:0; E:2; F:0; G:0 ; H:0; I:0, J:0

Ancak asıl hata muhtemelen biraz daha yüksektir. Aynı görüntüdeki bazı hatalar birbirini iptal eder. Özellikle Resim H bazı kötü etiketlenmemiş tanelere sahipken, resim E'de etiketler çoğunlukla doğrudur.

Konsept biraz tartışılır:

  • İlk olarak ön plan doygunluk kanalında otsu-eşik değeriyle ayrılır (ayrıntılar için önceki cevabıma bakınız).

  • başka piksel kalmayıncaya kadar tekrarlayın:

    • en büyük blob'u seç
    • Tahıl için başlangıç ​​pozisyonu olarak bu blokta 10 rastgele kenar pikseli seçin

    • her başlangıç ​​noktası için

      • Bu konumda yüksekliği ve genişliği 10 piksel olan bir tane varsayalım.

      • yakınsama kadar tekrarlayın

        • Bir kenar pikseliyle karşılaşana kadar (beyazdan siyaha) bu noktadan farklı açılarla radyal olarak dışarı doğru git

        • Bulunan pikseller umarım tek bir tanenin kenar pikselleri olmalıdır. Varsayılan elipsten daha uzak olan pikselleri diğerlerinden daha uzak olan pikselleri atarak, inli'leri aykırı değerlerden ayırmaya çalışın.

        • defalarca inlier'in bir alt kümesine bir elips sığdırmaya çalışın, en iyi elipsin (RANSACK) kalmasını sağlayın

        • Tahıl konumu, yönlendirme, genişlik ve yüksekliği bulunan elips ile güncelleyin

        • Tahıl konumu önemli ölçüde değişmezse, dur

    • Takılan 10 tane tanenin arasından, şekline, kenar piksel sayısına göre en iyi tahılı seçin. Diğerlerini at

    • Kaynaktaki görüntüden bu tanenin tüm piksellerini kaldırın, sonra tekrarlayın.

    • Son olarak, bulunan taneler listesine göz atın ve her taneyi 1 taneli, 0 taneli (çok küçük) veya 2 taneli (çok büyük) olarak sayın

Başlıca sorunlarımdan biri, tam bir elips nokta uzaklık ölçüsü uygulamak istemiyor olmamdı, çünkü bunun kendi başına karmaşık bir yinelemeli süreç olduğunu hesaplamıştım. Bu yüzden Ellipse2Poly ve FitEllipse OpenCV işlevlerini kullanarak çeşitli geçici çözümler kullandım ve sonuçlar çok hoş değil.

Görünüşe göre ben de codegolf için boyut sınırını bozdum.

Bir cevap 30000 karakterle sınırlıdır, şu anda 34000 yaşındayım. Bu yüzden aşağıdaki kodu biraz kısaltmam gerekecek.

Kodun tamamı http://pastebin.com/RgM7hMxq adresinde görülebilir.

Bunun için üzgünüm, bir boyut sınırı olduğunu bilmiyordum.

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

}

Bu çözümden biraz utanıyorum çünkü a) bu zorluğun ruhuna uygun olup olmadığından emin değilim ve b) bir kod yazarı yanıtı için çok büyük ve diğer çözümlerin şıklığına sahip değil.

Öte yandan, tahılları etiketlemede kaydettiğim ilerlemeden oldukça mutluyum , sadece onları saymakla kalmıyorum, bu yüzden böyle.


Bu kod uzunluğunu daha küçük isimler kullanarak ve bazı diğer golf tekniklerini kullanarak azaltabileceğinizi biliyorsunuz;)
Optimizer

Muhtemelen, ama bu çözümü daha fazla karıştırmak istemedim. Zevklerim için olduğu kadar
şaşkın

Çaba için +1 ve çünkü her bir taneyi ayrı ayrı göstermenin bir yolunu bulan tek kişi sizsiniz. Maalesef kod biraz şişirilmiş ve sabit kodlanmış sabitlere çok güveniyor. Yazdığım tarama çizgisi algoritmasının bu konuda nasıl performans gösterdiğini merak ediyorum (bireysel renkli tahıllarda).
tigrou

Bunun, bu tür bir sorun için doğru yaklaşım olduğunu düşünüyorum (sizin için +1), ama merak ettiğim bir şey, neden "10 rastgele kenar pikseli seçtiğini" seçersiniz, eğer seçerseniz daha iyi performans alacağınızı düşünürdüm yakındaki en az sayıda uç noktaya sahip kenar noktaları (yani dışarı çıkan kısımlar), bence (teorik olarak) bunun ilk önce "en kolay" tahılları yok edeceğini düşünürdüm, bunu düşündünüz mü?
David Rogers

Düşündüm ama henüz denemedim. '10 Rastgele başlangıç ​​pozisyonu ', eklenmesi kolay ve paralel hale getirilmesi kolay olan, geç bir ekleme idi. Ondan önce, 'rastgele bir başlangıç ​​pozisyonu', 'her zaman sol üst köşeden' çok daha iyiydi. Her seferinde aynı stratejiyle başlangıç ​​pozisyonlarını seçme tehlikesi, en iyi uyumu kaldırdığımda, diğer 9 muhtemelen bir dahaki sefere tekrar seçilecek ve zamanla bu başlangıç ​​pozisyonlarının en kötüsü geride kalacak ve tekrar seçilecektir. tekrar. Ortaya çıkan parça, tamamen çıkarılmış bir önceki tahılın kalıntıları olabilir.
HugoRune 11:14

17

C ++, OpenCV, puan: 9

Yöntemin temel fikri oldukça basittir - tek taneleri (ve "çift taneler" - 2 (ancak daha fazla değil!) Taneleri görüntüden silmeye çalışın ve sonra alana dayalı yöntemi (Falko gibi) kullanarak istirahati sayın Ell ve belisarius). İyi bir ortalamaPixelsPerObject değeri bulmak daha kolay olduğundan, bu yaklaşımı kullanmak standart "alan yöntemi" den biraz daha iyidir.

(1. adım) Öncelikle HSV'de S görüntü kanalında Otsu ikilileştirmesini kullanmamız gerekir. Bir sonraki adım, çıkarılan ön planın kalitesini artırmak için dilate operatörünü kullanmaktır. Kontürleri bulmamız gerek. Elbette bazı konturlar pirinç taneleri değildir - çok küçük konturları silmemiz gerekir (alanın ortalama alanından küçük olan PixelsPerObject / 4. AveragePixelsPerObject benim durumumda 2855). Şimdi nihayet tahılları saymaya başlayabiliriz :) (2. adım) Tek ve çift tanelerin bulunması oldukça basittir - sadece belirli aralıktaki alanlara sahip kontürler için konturlar listesine bakın - kontur alanı aralıktaysa listeden silin ve ekleyin. (ya da "çift" tahıl ise 2) tahıl sayacına. (3. adım) Son adım, kalan konturların alanını ortalamaPixelsPerObject değerine bölmek ve tahıl sayacına sonuç eklemek.

Resimler (resim F.jpg için) bu fikri kelimelerden daha iyi göstermelidir:
1. adım (küçük konturlar olmadan (gürültü)): 1. adım (küçük konturlar olmadan (gürültü))
2. adım - sadece basit kontürler: 2. adım - sadece basit konturlar
3. adım - kalan kontürler: 3. adım - kalan kontürler

İşte kod, oldukça çirkin, ama sorunsuz çalışmalı. Tabii ki OpenCV gereklidir.

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

Tüm adımların sonuçlarını görmek istiyorsanız, tüm imshow (.., ..) işlevinin çağrıları uncomment yapın ve fastProcessing değişkenini false olarak ayarlayın. Görüntüler (A.jpg, B.jpg, ...) dizin resimlerinde yer almalıdır. Alternatif olarak, bir komutun ismini komut satırından parametre olarak verebilirsiniz.

Elbette bir şey net değilse, onu açıklayabilirim ve / veya bazı görüntüler / bilgiler sağlayabilirim.


12

C # + OpenCvSharp, puan: 71

Bu çok can sıkıcı, havza kullanarak her tahılı gerçekten tanımlayan bir çözüm bulmaya çalıştım , ama ben sadece. olamaz. almak. o. için. iş.

Bazı bireysel taneleri en azından ayıran ve daha sonra ortalama tane büyüklüğünü tahmin etmek için bu taneleri kullanan bir çözüm buldum . Bununla birlikte, bugüne kadar kodlanmış tane büyüklüğündeki çözümleri yenemiyorum.

Bu nedenle, bu çözümün ana özelliği: Tahıllar için sabit bir piksel boyutu varsaymıyor ve kamera hareket etse veya pirinç türü değişse bile çalışmalı.

A.jpg; tane sayısı: 3; beklenen 3; hata 0; tane başına piksel: 2525,0;
b.jpg; tane sayısı: 7; beklenen 5; hata 2; tane başına piksel: 1920,0;
c.jpg; tane sayısı: 6; beklenen 12; hata 6; tane başına piksel: 4242,5;
d.jpg; tane sayısı: 23; beklenen 25; hata 2; tane başına piksel: 2415,5;
E.jpg; tane sayısı: 47; beklenen 50; hata 3; tane başına piksel: 2729,9;
F.jpg; tane sayısı: 65; beklenen 83; hata 18; tane başına piksel: 2860,5;
G.jpg; tane sayısı: 120; beklenen 120; hata 0; tane başına piksel: 2552,3;
H.jpg; tane sayısı: 159; beklenen 150; hata 9; tane başına piksel: 2624,7;
i.jpg; tane sayısı: 141; beklenen 151; hata 10; tane başına piksel: 2697,4;
J.jpg; tane sayısı: 179; beklenen 200; hata 21; tane başına piksel: 2847,1;
toplam hata: 71

Benim çözümüm bu şekilde çalışıyor:

Görüntüyü HSV'ye dönüştürerek ve doygunluk kanalına Otsu eşiği uygulayarak ön planı ayırın . Bu çok basit, son derece iyi çalışıyor ve bu zorluğu denemek isteyen herkese bunu tavsiye ederim:

saturation channel                -->         Otsu thresholding

görüntü tanımını buraya girin -> görüntü tanımını buraya girin

Bu temiz bir şekilde arka planı kaldıracak.

Daha sonra , değer kanalına sabit bir eşik uygulayarak tahıl gölgelerini önden kaldırdım . (Bunun gerçekten çok yardımcı olup olmadığından emin değilim, ancak eklemek için yeterince basitti)

görüntü tanımını buraya girin

Sonra ön plan görüntüsüne bir mesafe dönüşümü uygularım .

görüntü tanımını buraya girin

ve tüm yerel maksimaları bu mesafe dönüşümünde bulun.

Bu benim fikrimin bozulduğu yer. aynı tane içinde yerel maksimuma mutiple olmamasını önlemek için, çok fazla filtrelemek zorundayım. Şu anda sadece 45 piksel yarıçapındaki en güçlü maksimum değeri tutuyorum, bu da her tanenin yerel maksimum değerine sahip olmadığı anlamına geliyor. Ve ben 45 piksel yarıçapı için bir gerekçeye sahip değilim, sadece işe yarayan bir değerdi.

görüntü tanımını buraya girin

(Gördüğünüz gibi, bunlar her taneyi hesaba katacak kadar tohum değil)

Sonra bu maxima'yı havza algoritması için tohum olarak kullanıyorum:

görüntü tanımını buraya girin

Sonuçlar meh . Çoğunlukla tek tek tahıllar için ümit ediyordum, ama topaklar hala çok büyük.

Şimdi en küçük blobları tespit ediyorum, ortalama piksel boyutlarını sayıyorum ve ondan tahıl sayısını tahmin ediyorum. Bu başlangıçta yapmayı planladığım şey değildi , ama bunu kurtarmanın tek yolu buydu.

kullanarak System ; Sistemini 
kullanarak . Koleksiyonlar . Jenerik ; Sistemini 
kullanarak . Linq ; Sistemini 
kullanarak . Metin ; OpenCvSharp 
kullanarak ;

ad alanı GrainTest2 { sınıfı Program { statik geçersiz Ana ( string [] args ) { string [] dosya = yeni [] { "sourceA.jpg" , "sourceB.jpg" , "sourceC.jpg" , "sourceD.jpg" , " sourceE.jpg " , " sourceF.jpg " , " sourceG.jpg " , " sourceH.jpg " , " sourceI.jpg " , " sourceJ.jpg " , };int [] expectedGrains

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

            int totalError = 0 ; int totalPixels = 0 ; 
             

            için ( int Fileno = 0 ; Fileno belirteçleri = Yeni Listesi (;) 
                    kullanılarak ( CvMemStorage depolama = yeni CvMemStorage ()) 
                    kullanılarak ( CvContourScanner tarayıcı = Yeni CvContourScanner ( localMaxima , depolama , CvContour . sizeOf , ContourRetrieval . Harici , ContourChain . ApproxNone ))         
                    { // her yerel maksimum değeri tohum numarası 25, 35, 45, ... // olarak ayarlayın (gerçek sayılar önemli değil, png'de daha iyi görünürlük için seçilmiş) int markerNo = 20 ; foreach ( CvSeq C de tarayıcı ) { 
                            markerNo + = 5 ; 
                            belirteçler . Ekleme ( markerNo ); 
                            waterShedMarkers . DrawContours ( c , yeni CvScalar ( markerNo ), yeni
                        
                        
                         
                         
                             CvScalar ( markerNo ), 0 , - 1 ); } } 
                    waterShedMarkers . SaveImage ( "08-havza-seeds.png" );  
                        
                    


                    kaynak . Watershed ( waterShedMarkers ); 
                    waterShedMarkers . SaveImage ( "09-havza-result.png" );


                    Liste pikselleriPerBlob = 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());

        }
    }
}

Tane başına 2544,4 boyutunda kodlanmış bir piksel kullanan küçük bir testte, diğer birçok çözümden daha büyük olan toplam 36 hata oluştu.

görüntü tanımını buraya girin görüntü tanımını buraya girin görüntü tanımını buraya girin görüntü tanımını buraya girin


Uzaklık dönüşümünün sonucu olarak bazı küçük değerlerle eşiği (aşındırıcı işlem de yararlı olabilir) kullanabileceğinizi düşünüyorum - bu, bazı tahıl gruplarını daha küçük gruplara bölmelidir (tercihen - sadece 1 veya 2 tane ile). Daha ziyade bu yalnız taneleri saymak daha kolay olmalı. Burada çoğu insan olarak sayılabilecek büyük gruplar - bölgeyi ortalama tek tahıl alanına ayırarak.
cyriel

9

HTML + Javascript: Puan 39

Kesin değerler:

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

Daha büyük değerlerde bozuluyor (doğru değil).

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>

Açıklama: Temel olarak, pirinç piksellerinin sayısını sayar ve onu tahıl başına ortalama piksellere böler.


3-pirinç görüntüyü kullanarak, benim için 0 tahmin etti ...: /
Kroltan

1
@Kroltan Tam boyutlu görüntüyü kullandığınızda değil .
Calvin'in Hobileri

1
@ Calvin'sHobbies Windows'daki FF36 0, Ubuntu'daki 3, tam boyutlu görüntü ile.
Kroltan

4
@BobbyJack Pirinci görüntüler arasında hemen hemen aynı ölçekte garanti edilmektedir. Hiçbir problem görmüyorum.
Calvin'in Hobileri

1
@githubphagocyte - bir açıklama oldukça açıktır - görüntünün binarizasyonu sonucu tüm beyaz pikselleri sayıyorsanız ve bu sayıyı görüntüdeki tanelerin sayısına bölerseniz, bu sonucu elde edersiniz. Elbette kesin sonuç, kullanılan ikilileştirme yöntemi ve diğer şeyler (ikilileştirme sonrası gerçekleştirilen işlemler gibi) nedeniyle farklı olabilir, ancak diğer yanıtlarda da görebileceğiniz gibi, 2500-3500 aralığında olacaktır.
cyriel

4

PHP ile bir girişim, En düşük puanlama cevabı değil, oldukça basit bir kod

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

Kendi kendine puanlama

95, GIMP 2966 ile test edilirken işlenen ortalama mavi renkte bir değerdir

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.