Menghitung Butir Beras

81

Perhatikan 10 gambar ini dari berbagai jumlah beras putih yang belum dimasak.
INI HANYA THUMBNAIL. Klik gambar untuk melihatnya dalam ukuran penuh.

A: B: C: D: E:SEBUAH B C D E

F: G: H: I: J:F G H saya J

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

Perhatikan itu...

  • Biji-bijian dapat saling menyentuh tetapi mereka tidak pernah tumpang tindih. Tata letak biji-bijian tidak pernah lebih dari satu butir.
  • Gambar memiliki dimensi yang berbeda tetapi skala beras di semuanya konsisten karena kamera dan latar belakangnya diam.
  • Butir tidak pernah keluar dari batas atau menyentuh batas gambar.
  • Latar belakang selalu sama dengan warna kekuningan-putih.
  • Biji-bijian kecil dan besar masing-masing dihitung sebagai satu butir.

5 poin ini adalah jaminan untuk semua gambar semacam ini.

Tantangan

Tulis sebuah program yang menghasilkan gambar-gambar tersebut dan, seakurat mungkin, menghitung jumlah butiran beras.

Program Anda harus mengambil nama file gambar dan mencetak jumlah butir yang dihitungnya. Program Anda harus bekerja untuk setidaknya satu dari format file gambar ini: JPEG, Bitmap, PNG, GIF, TIFF (sekarang semua gambar adalah JPEG).

Anda dapat menggunakan pemrosesan gambar dan perpustakaan visi komputer.

Anda tidak boleh membuat hardcode output dari 10 contoh gambar. Algoritme Anda harus berlaku untuk semua gambar padi-padian yang serupa. Seharusnya dapat berjalan dalam waktu kurang dari 5 menit pada komputer modern yang layak jika area gambar kurang dari 2000 * 2000 piksel dan ada kurang dari 300 butir beras.

Mencetak gol

Untuk masing-masing dari 10 gambar, ambil nilai absolut dari jumlah butir aktual dikurangi jumlah butir yang diprediksi program Anda. Jumlahkan nilai absolut ini untuk mendapatkan skor Anda. Skor terendah menang. Skor 0 sempurna.

Dalam hal ikatan, jawaban dengan suara terbanyak menang. Saya dapat menguji program Anda pada gambar tambahan untuk memverifikasi validitas dan akurasinya.

Hobi Calvin
sumber
1
Tentunya seseorang harus mencoba scikit-belajar!
Kontes hebat! :) Btw - bisa memberi tahu kami sesuatu tentang tanggal akhir dari tantangan ini?
cyriel
1
@Lembik Down to 7 :)
Dr. belisarius
5
Suatu hari, seorang ilmuwan padi akan datang dan merasa jengkel karena pertanyaan ini ada.
Nit
2
@Nit Katakan saja kepada mereka ncbi.nlm.nih.gov/pmc/articles/PMC3510117 :)
Dr. belisarius

Jawaban:

22

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

Saya pikir nama fungsi cukup deskriptif:

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]

Memproses semua gambar sekaligus:

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

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

Skornya adalah:

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

Di sini Anda dapat melihat sensitivitas skor wrt ukuran butir yang digunakan:

Grafik Mathematica

Belisarius
sumber
2
Jauh lebih jelas, terima kasih!
Hobi Calvin
Bisakah prosedur yang tepat ini disalin dalam python atau adakah sesuatu yang khusus Mathematica lakukan di sini yang tidak bisa dilakukan oleh perpustakaan python?
@Lembik Tidak tahu. Tidak ada python di sini. Maaf. (Namun, saya meragukan algoritma yang sama persis untuk EdgeDetect[], DeleteSmallComponents[]dan Dilation[]diterapkan di tempat lain)
Dr. belisarius
55

Python, Skor: 24 16

Solusi ini, seperti solusi Falko, didasarkan pada pengukuran area "foreground" dan membaginya dengan area butiran rata-rata.

Sebenarnya, yang coba dideteksi oleh program ini adalah latar belakangnya, tidak sebanyak latar depan. Menggunakan fakta bahwa butiran beras tidak pernah menyentuh batas gambar, program dimulai dengan mengisi putih di sudut kiri atas. Algoritme banjir-isi mengecat piksel yang berdekatan jika perbedaannya dan kecerahan piksel saat ini berada dalam ambang tertentu, sehingga menyesuaikan dengan perubahan bertahap pada warna latar belakang. Pada akhir tahap ini, gambar mungkin terlihat seperti ini:

Gambar 1

Seperti yang Anda lihat, ini cukup baik dalam mendeteksi latar belakang, tetapi tidak ada area yang "terperangkap" di antara butir. Kami menangani area ini dengan memperkirakan kecerahan latar belakang pada setiap piksel dan membuat semua piksel sama atau lebih cerah. Estimasi ini berfungsi seperti itu: selama tahap pengisian banjir, kami menghitung kecerahan latar belakang rata-rata untuk setiap baris dan setiap kolom. Perkiraan kecerahan latar belakang pada setiap piksel adalah rata-rata kecerahan baris dan kolom pada piksel tersebut. Ini menghasilkan sesuatu seperti ini:

Gambar 2

EDIT: Akhirnya, area masing-masing daerah latar depan kontinu (yaitu non-putih) dibagi dengan rata-rata, pra-perhitungan, area biji-bijian, memberi kami perkiraan jumlah biji-bijian di wilayah tersebut. Jumlah dari jumlah ini adalah hasilnya. Awalnya, kami melakukan hal yang sama untuk seluruh area latar depan secara keseluruhan, tetapi pendekatan ini, secara harfiah, lebih berbutir halus.


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

Mengambil nama file input melalui baris perintah.

Hasil

      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

SEBUAH B C D E

F G H saya J

Elo
sumber
2
Ini adalah solusi yang sangat pintar, kerja bagus!
Chris Cirefice
1
mana avg_grain_area = 3038.38;berasal?
njzk2
2
tidakkah itu diperhitungkan hardcoding the result?
njzk2
5
@ njzk2 Tidak. Diberi aturan The images have different dimensions but the scale of the rice in all of them is consistent because the camera and background were stationary.Ini hanyalah nilai yang mewakili aturan itu. Hasilnya, bagaimanapun, berubah sesuai dengan input. Jika Anda mengubah aturan, maka nilai ini akan berubah, tetapi hasilnya akan sama - berdasarkan input.
Adam Davis
6
Saya baik-baik saja dengan hal bidang rata-rata. Area butiran (kira-kira) konstan di seluruh gambar.
Calvin Hobbies
28

Python + OpenCV: Skor 27

Pemindaian garis horizontal

Ide: memindai gambar, satu baris setiap kali. Untuk setiap baris, hitung jumlah butiran beras yang ditemui (dengan memeriksa apakah piksel berubah menjadi hitam menjadi putih atau sebaliknya). Jika jumlah butir untuk baris meningkat (dibandingkan dengan baris sebelumnya), itu berarti kami mengalami butir baru. Jika angka itu berkurang, itu berarti kita melewati sebutir biji-bijian. Dalam hal ini, tambahkan +1 ke hasil total.

masukkan deskripsi gambar di sini

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

Karena cara algoritma bekerja, penting untuk memiliki gambar yang bersih, b / w. Banyak kebisingan menghasilkan hasil yang buruk. Latar belakang utama pertama dibersihkan menggunakan flofill (solusi mirip dengan jawaban Ell) kemudian ambang diterapkan untuk menghasilkan hasil hitam dan putih.

masukkan deskripsi gambar di sini

Ini jauh dari sempurna, tetapi menghasilkan hasil yang baik mengenai kesederhanaan. Mungkin ada banyak cara untuk memperbaikinya (dengan memberikan gambar b / w yang lebih baik, memindai ke arah lain (misalnya: vertikal, diagonal) dengan mengambil rata-rata dll ...)

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

Kesalahan per gambar: 0, 0, 0, 3, 0, 12, 4, 0, 7, 1

tigrou
sumber
24

Python + OpenCV: Skor 84

Ini adalah upaya naif pertama. Ini menerapkan ambang adaptif dengan parameter yang disetel secara manual, menutup beberapa lubang dengan erosi dan pengenceran berikutnya dan memperoleh jumlah butir dari area latar depan.

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)

Di sini Anda dapat melihat gambar biner menengah (hitam untuk latar):

masukkan deskripsi gambar di sini

Kesalahan per gambar adalah 0, 0, 2, 2, 4, 0, 27, 42, 0 dan 7 butir.

Falko
sumber
20

C # + OpenCvSharp, Nilai: 2

Ini adalah usaha kedua saya. Ini sangat berbeda dari upaya pertama saya , yang jauh lebih sederhana, jadi saya mempostingnya sebagai solusi terpisah.

Ide dasarnya adalah untuk mengidentifikasi dan memberi label setiap butir individu dengan fit elips iteratif. Kemudian hapus piksel untuk butiran ini dari sumber, dan coba temukan butir berikutnya, hingga setiap piksel dilabeli.

Ini bukan solusi yang paling cantik. Ini adalah babi raksasa dengan 600 baris kode. Perlu 1,5 menit untuk gambar terbesar. Dan saya benar-benar minta maaf atas kode yang berantakan.

Ada begitu banyak parameter dan cara untuk berpikir dalam hal ini sehingga saya cukup takut overfitting program saya untuk 10 sampel gambar. Skor akhir 2 hampir pasti adalah kasus overfitting: Saya memiliki dua parameter average grain size in pixel, dan minimum ratio of pixel / elipse_area, dan pada akhirnya saya hanya menghabiskan semua kombinasi dari dua parameter ini sampai saya mendapatkan skor terendah. Saya tidak yakin apakah ini halal dengan aturan tantangan ini.

average_grain_size_in_pixel = 2530
pixel / elipse_area >= 0.73

Tetapi bahkan tanpa cengkeraman overfitting ini, hasilnya cukup bagus. Tanpa ukuran butir tetap atau rasio piksel, hanya dengan memperkirakan ukuran butir rata-rata dari gambar pelatihan, skor masih 27.

Dan saya mendapatkan sebagai output tidak hanya angka, tetapi posisi aktual, orientasi dan bentuk setiap butir. ada sejumlah kecil biji-bijian berlabel salah, tetapi secara keseluruhan sebagian besar label secara akurat cocok dengan biji-bijian asli:

A SEBUAH B B C C D D EE

F F G G H H I saya JJ

(klik pada setiap gambar untuk versi ukuran penuh)

Setelah langkah pelabelan ini, program saya melihat setiap butir individu, dan perkiraan berdasarkan jumlah piksel dan rasio piksel / ellipse-area, apakah ini

  • satu butir (+1)
  • banyak butir diberi label salah satu (+ X)
  • gumpalan terlalu kecil untuk menjadi biji-bijian (+0)

Skor kesalahan untuk setiap gambar adalah A:0; B:0; C:0; D:0; E:2; F:0; G:0 ; H:0; I:0, J:0

Namun kesalahan yang sebenarnya mungkin sedikit lebih tinggi. Beberapa kesalahan dalam gambar yang sama membatalkan satu sama lain. Gambar H khususnya memiliki beberapa butir yang salah label, sedangkan pada gambar E sebagian besar label benar

Konsepnya sedikit dibuat-buat:

  • Pertama latar depan dipisahkan melalui otsu-thresholding pada saluran saturasi (lihat jawaban saya sebelumnya untuk detail)

  • ulangi hingga tidak ada lagi piksel yang tersisa:

    • pilih gumpalan terbesar
    • pilih 10 piksel tepi acak pada gumpalan ini sebagai posisi awal untuk sebuah butir

    • untuk setiap titik awal

      • anggap sebutir dengan tinggi dan lebar 10 piksel pada posisi ini.

      • ulangi sampai konvergensi

        • pergi secara radial ke luar dari titik ini, pada sudut yang berbeda, hingga Anda menemukan piksel tepi (putih-ke-hitam)

        • piksel yang ditemukan semoga menjadi piksel tepi dari satu butir. Cobalah untuk memisahkan inliers dari outlier, dengan membuang piksel yang lebih jauh dari elips yang diasumsikan daripada yang lain

        • berulang kali mencoba menyesuaikan elips melalui subset inliers, jaga elips terbaik (RANSACK)

        • perbarui posisi butir, orientasi, lebar dan tinggi dengan elips yang ditemukan

        • jika posisi butiran tidak berubah secara signifikan, hentikan

    • di antara 10 butir pas, pilih butir terbaik sesuai dengan bentuk, jumlah piksel tepi. Buang yang lain

    • hapus semua piksel untuk butir ini dari gambar sumber, lalu ulangi

    • akhirnya, lihat daftar butir yang ditemukan, dan hitung setiap butir baik sebagai 1 butir, 0 butir (terlalu kecil) atau 2 butir (terlalu besar)

Salah satu masalah utama saya adalah bahwa saya tidak ingin menerapkan metrik jarak elips penuh, karena menghitung itu sendiri merupakan proses berulang yang rumit. Jadi saya menggunakan berbagai solusi menggunakan fungsi OpenCV Ellipse2Poly dan FitEllipse, dan hasilnya tidak terlalu cantik.

Rupanya saya juga melanggar batas ukuran untuk codegolf.

Jawabannya terbatas pada 30000 karakter, saya saat ini di 34000. Jadi saya harus mempersingkat kode di bawah ini.

Kode lengkap dapat dilihat di http://pastebin.com/RgM7hMxq

Maaf untuk ini, saya tidak tahu bahwa ada batas ukuran.

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

}

Saya agak malu dengan solusi ini karena a) Saya tidak yakin apakah ini sesuai dengan semangat tantangan ini, dan b) terlalu besar untuk jawaban codegolf dan tidak memiliki keanggunan dari solusi lain.

Di sisi lain, saya cukup senang dengan kemajuan yang saya capai dalam pelabelan biji-bijian, tidak hanya menghitungnya, jadi ada itu.

HugoRune
sumber
Anda tahu Anda dapat mengurangi panjang kode itu dengan menggunakan nama yang lebih kecil dan menerapkan beberapa teknik bermain golf lainnya;)
Pengoptimal
Mungkin, tapi saya tidak ingin mengaburkan solusi ini. Itu terlalu membingungkan untuk seleraku seperti itu :)
HugoRune
Memberi +1 untuk upaya dan karena Anda adalah satu-satunya yang menemukan cara untuk menampilkan setiap butir secara individual. Sayangnya kode sedikit membengkak dan mengandalkan banyak pada konstanta hardcoded. Saya akan penasaran untuk melihat bagaimana algoritma scanline saya tulis melakukan ini (pada butir berwarna invidual).
tigrou
Saya benar-benar berpikir bahwa ini adalah pendekatan yang tepat untuk jenis masalah ini (+1 untuk Anda), tetapi satu hal yang saya heran, mengapa Anda "memilih 10 piksel tepi acak", saya akan berpikir bahwa Anda akan mendapatkan kinerja yang lebih baik jika Anda memilih titik tepi dengan jumlah titik tepi terdekat terendah (yaitu bagian yang menonjol), saya akan berpikir (secara teoritis) ini akan menghilangkan butiran "termudah" terlebih dahulu, sudahkah Anda mempertimbangkan ini?
David Rogers
Saya sudah memikirkannya, tetapi belum mencobanya. '10 posisi awal acak 'adalah tambahan terlambat, yang mudah ditambahkan dan mudah diparalelkan. Sebelum itu, 'satu posisi awal acak' jauh lebih baik daripada 'selalu sudut kiri atas'. Bahaya memilih posisi awal dengan strategi yang sama setiap kali adalah ketika saya menghapus yang paling cocok, 9 lainnya mungkin akan dipilih lagi di lain waktu, dan seiring waktu yang terburuk dari posisi awal akan tetap di belakang dan dipilih lagi dan lagi. Bagian yang menonjol mungkin hanya sisa-sisa dari butir sebelumnya yang tidak sepenuhnya dihapus.
HugoRune
17

C ++, OpenCV, skor: 9

Ide dasar metode saya cukup sederhana - cobalah untuk menghapus butiran tunggal (dan "butiran ganda" - butir 2 (tetapi tidak lebih!), Berdekatan satu sama lain) dari gambar dan kemudian menghitung sisanya menggunakan metode berdasarkan area (seperti Falko, Ell dan belisarius). Menggunakan pendekatan ini sedikit lebih baik daripada "metode area" standar, karena lebih mudah untuk menemukan nilai rata-rataPixelsPerObject yang baik.

(Langkah 1) Pertama-tama kita perlu menggunakan binarisasi Otsu pada saluran S gambar di HSV. Langkah selanjutnya adalah menggunakan operator dilatasi untuk meningkatkan kualitas latar depan yang diekstraksi. Daripada kita perlu menemukan kontur. Tentu saja beberapa kontur bukanlah butiran beras - kita perlu menghapus kontur yang terlalu kecil (dengan luas yang lebih kecil dari rata-rataPixelsPerObject / 4.. Sekarang akhirnya kita dapat mulai menghitung butiran :) (langkah ke-2) Menemukan butir tunggal dan ganda cukup sederhana - lihat saja daftar kontur untuk kontur dengan area dalam rentang tertentu - jika area kontur berada dalam jangkauan, hapus dari daftar dan tambahkan 1 (atau 2 jika itu butir "ganda") ke counter butir. (Langkah 3) Langkah terakhir tentu saja membagi area kontur yang tersisa dengan nilai rata-rataPixelsPerObject dan menambahkan hasil ke penghitung butir.

Gambar (untuk gambar F.jpg) harus menunjukkan ide ini lebih baik daripada kata-kata:
langkah pertama (tanpa kontur kecil (noise)): Langkah 1 (tanpa kontur kecil (noise))
langkah kedua - hanya kontur sederhana: Langkah 2 - hanya kontur sederhana
langkah ketiga - kontur yang tersisa: Langkah 3 - kontur yang tersisa

Ini kodenya, ini agak jelek, tetapi harus bekerja tanpa masalah. Tentu saja OpenCV diperlukan.

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

Jika Anda ingin melihat hasil dari semua langkah, batalkan komentar semua fungsi panggilan imshow (.., ..) dan setel variabel fastProcessing menjadi false. Gambar (A.jpg, B.jpg, ...) harus dalam gambar direktori. Atau tentu saja Anda dapat memberikan nama satu gambar sebagai parameter dari baris perintah.

Tentu saja jika ada sesuatu yang tidak jelas saya dapat menjelaskannya dan / atau memberikan beberapa gambar / informasi.

cyriel
sumber
12

C # + OpenCvSharp, skor: 71

Ini sangat menjengkelkan, saya mencoba untuk mendapatkan solusi yang benar-benar mengidentifikasi setiap butir menggunakan DAS , tetapi saya hanya. tidak bisa. mendapatkan. saya t. untuk. kerja.

Saya memilih solusi yang setidaknya memisahkan beberapa butir individu dan kemudian menggunakan biji-bijian itu untuk memperkirakan ukuran butir rata-rata. Namun sejauh ini saya tidak bisa mengalahkan solusi dengan ukuran butir hardcoded.

Jadi, sorotan utama dari solusi ini: tidak mengandaikan ukuran piksel yang tetap untuk biji-bijian, dan harus bekerja bahkan jika kamera dipindahkan atau jenis beras diubah.

A.jpg; jumlah biji-bijian: 3; diharapkan 3; kesalahan 0; piksel per butir: 2525,0;
B.jpg; jumlah biji-bijian: 7; diharapkan 5; kesalahan 2; piksel per butir: 1920,0;
C.jpg; jumlah biji-bijian: 6; diharapkan 12; kesalahan 6; piksel per butir: 4242,5;
D.jpg; jumlah biji-bijian: 23; diharapkan 25; kesalahan 2; piksel per butir: 2415,5;
E.jpg; jumlah biji-bijian: 47; diharapkan 50; kesalahan 3; piksel per butir: 2729,9;
F.jpg; jumlah biji-bijian: 65; diharapkan 83; kesalahan 18; piksel per butir: 2860,5;
G.jpg; jumlah biji-bijian: 120; diharapkan 120; kesalahan 0; piksel per butir: 2552,3;
H.jpg; jumlah biji-bijian: 159; diharapkan 150; kesalahan 9; piksel per butir: 2624,7;
I.jpg; jumlah biji-bijian: 141; diharapkan 151; kesalahan 10; piksel per butir: 2697,4;
J.jpg; jumlah biji-bijian: 179; diharapkan 200; kesalahan 21; piksel per butir: 2847,1;
kesalahan total: 71

Solusi saya berfungsi seperti ini:

Pisahkan latar depan dengan mengubah gambar menjadi HSV dan menerapkan ambang batas Otsu pada saluran saturasi. Ini sangat sederhana, berfungsi sangat baik, dan saya akan merekomendasikan ini untuk semua orang yang ingin mencoba tantangan ini:

saturation channel                -->         Otsu thresholding

masukkan deskripsi gambar di sini -> masukkan deskripsi gambar di sini

Ini dengan bersih akan menghapus latar belakang.

Saya kemudian juga menghapus bayangan gandum dari latar depan, dengan menerapkan ambang batas tetap ke saluran nilai. (Tidak yakin apakah itu benar-benar banyak membantu, tapi itu cukup sederhana untuk ditambahkan)

masukkan deskripsi gambar di sini

Lalu saya menerapkan transformasi jarak pada gambar latar depan.

masukkan deskripsi gambar di sini

dan temukan semua maxima lokal dalam transformasi jarak ini.

Di sinilah ide saya rusak. untuk menghindari mendapatkan maxima lokal mutiple dalam butir yang sama, saya harus banyak menyaring. Saat ini saya hanya menyimpan maksimum terkuat dalam radius 45 piksel, yang berarti tidak setiap butir memiliki maksimum lokal. Dan saya tidak benar-benar memiliki pembenaran untuk radius 45 piksel, itu hanya nilai yang berfungsi.

masukkan deskripsi gambar di sini

(seperti yang Anda lihat, itu hampir tidak cukup biji untuk menjelaskan setiap butir)

Lalu saya menggunakan maxima tersebut sebagai seed untuk algoritma DAS:

masukkan deskripsi gambar di sini

Hasilnya meh . Saya berharap sebagian besar biji-bijian individu, tetapi rumpunnya masih terlalu besar.

Sekarang saya mengidentifikasi gumpalan terkecil, menghitung ukuran piksel rata-rata, dan kemudian memperkirakan jumlah butir dari itu. Ini bukan apa yang saya rencanakan untuk dilakukan di awal, tetapi ini adalah satu-satunya cara untuk menyelamatkan ini.

menggunakan Sistem ; 
menggunakan Sistem . Koleksi . Generik ; 
menggunakan Sistem . Linq ; 
menggunakan Sistem . Teks ; 
menggunakan OpenCvSharp ;

namespace GrainTest2 { Program kelas { static void Main ( string [] args ) { string [] file = baru [] { "sourceA.jpg" , "sourceB.jpg" , "sourceC.jpg" , "sourceD.jpg" , "sourceD.jpg" , " sourceE.jpg " , " sourceF.jpg " , " sourceG.jpg " , " sourceH.jpg " , " sourceI.jpg " , " sourceJ.jpg " , };int [] diharapkanGain

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

            int totalError = 0 ; int totalPixels = 0 ; 
             

            untuk ( int Fileno = 0 ; Fileno spidol = baru Daftar (); 
                    menggunakan ( CvMemStorage penyimpanan = baru CvMemStorage ()) 
                    menggunakan ( CvContourScanner scanner = baru CvContourScanner ( localMaxima , penyimpanan , CvContour . sizeof , ContourRetrieval . Eksternal , ContourChain . ApproxNone ))         
                    { // tetapkan setiap maksimum lokal sebagai nomor benih 25, 35, 45, ... // (angka aktual tidak masalah, dipilih untuk visibilitas yang lebih baik di png) int markerNo = 20 ; foreach ( CvSeq c dalam pemindai ) { 
                            markerNo + = 5 ; 
                            spidol . Tambah ( penanda Tidak ); 
                            waterShedMarkers . DrawContours ( c , CvScalar baru ( markerTidak ), baru
                        
                        
                         
                         
                             CvScalar ( markerNo ), 0 , - 1 ); } } 
                    waterShedMarkers . SaveImage ( "08-watershed-seeds.png" );  
                        
                    


                    sumber . DAS ( waterShedMarkers ); 
                    waterShedMarkers . SaveImage ( "09-watershed-result.png" );


                    Daftar pikselPerBlob = Daftar baru ();  

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

        }
    }
}

Sebuah tes kecil menggunakan pixel-per-grain ukuran hard-coded dari 2544,4 menunjukkan total kesalahan 36, yang masih lebih besar dari kebanyakan solusi lainnya.

masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini

HugoRune
sumber
Saya pikir Anda dapat menggunakan ambang (operasi erode mungkin berguna juga) dengan nilai kecil pada hasil transformasi jarak - ini harus membagi beberapa kelompok butir menjadi kelompok yang lebih kecil (lebih disukai - dengan hanya 1 atau 2 butir). Daripada seharusnya lebih mudah untuk menghitung biji-bijian yang kesepian itu. Kelompok besar dapat Anda hitung karena sebagian besar orang di sini - membagi wilayah dengan luas rata-rata biji tunggal.
cyriel
9

HTML + Javascript: Skor 39

Nilai pastinya adalah:

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

Itu rusak (tidak akurat) pada nilai yang lebih besar.

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>

Penjelasan: Pada dasarnya, menghitung jumlah piksel beras dan membaginya dengan rata-rata piksel per butir.

soktinpk
sumber
Menggunakan gambar 3-beras, diperkirakan 0 untuk saya ...: /
Kroltan
1
@Kroltan Tidak saat Anda menggunakan gambar ukuran penuh .
Hobi Calvin
1
@ Calvin'sHobbies FF36 di Windows mendapat 0, di Ubuntu mendapat 3, dengan gambar ukuran penuh.
Kroltan
4
@ BobbyJack Beras dijamin pada skala kurang lebih sama di seluruh gambar. Saya tidak melihat masalah dengan itu.
Hobi Calvin
1
@githubphagocyte - penjelasannya cukup jelas - jika Anda menghitung semua piksel putih pada hasil binarisasi gambar dan membagi angka ini dengan jumlah butir dalam gambar, Anda akan mendapatkan hasil ini. Tentu saja hasil yang tepat mungkin berbeda, karena metode binarisasi yang digunakan dan hal-hal lain (seperti operasi yang dilakukan setelah binarisasi), tetapi seperti yang Anda lihat dalam jawaban lain, itu akan berada di kisaran 2500-3500.
cyriel
4

Upaya dengan php, Bukan jawaban skor terendah tetapi kode yang cukup sederhana

SCORE: 31

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

Penilaian diri

95 adalah nilai biru yang sepertinya berfungsi saat pengujian dengan GIMP 2966 adalah ukuran butir rata-rata

exussum
sumber