Підрахунок зерен рису


81

Розгляньте ці 10 зображень різної кількості сирого зерна білого рису.
ЦЕ Є ТОЛЬКО ДУМКИ. Клацніть зображення, щоб переглянути його в повному розмірі.

A: B: C: D: E:А Б С D Е

F: G: H: I: J:Ж Г Н Я J

Кількість зерна: A: 3, B: 5, C: 12, D: 25, E: 50, F: 83, G: 120, H:150, I: 151, J: 200

Зауважте, що ...

  • Зерна можуть торкатися один одного, але ніколи не перетинаються. Розкладка зерен ніколи не перевищує одного зерна.
  • Зображення мають різні розміри, але масштаб рису в усіх них відповідає тому, що камера та фон були нерухомими.
  • Зерна ніколи не виходять за межі або не торкаються меж зображення.
  • Фон завжди однаковий стійкий відтінок жовтувато-білого.
  • Дрібні та великі зерна зараховуються однаково до одного зерна.

Ці 5 балів - гарантії для всіх подібних зображень.

Виклик

Напишіть програму, яка знімає такі зображення і, як можна точніше, підраховує кількість зерен рису.

Ваша програма повинна взяти назву файла зображення та надрукувати кількість обчислених зерен. Ваша програма повинна працювати принаймні в одному з таких форматів файлів зображень: JPEG, Bitmap, PNG, GIF, TIFF (зараз зображення - це всі JPEG).

Ви можете використовувати для обробки зображень та бібліотеки комп'ютерного зору.

Ви не можете жорстко кодувати висновки 10-ти прикладних зображень. Ваш алгоритм повинен бути застосовний до всіх подібних зображень рисового зерна. На пристойному сучасному комп’ютері він повинен працювати менш ніж за 5 хвилин, якщо площа зображення менше 2000 * 2000 пікселів і є менше 300 зерен рису.

Оцінка балів

Для кожного з 10 зображень приймайте абсолютне значення фактичної кількості зерен за вирахуванням кількості зерен, яку передбачає ваша програма. Підсумуйте ці абсолютні значення, щоб отримати свій результат. Виграє найнижчий бал. Оцінка 0 досконала.

У разі зв’язків виграє найвища відповідь, яка проголосується. Я можу перевірити вашу програму на додаткових зображеннях, щоб перевірити її достовірність та точність.


1
Звичайно, хтось повинен спробувати scikit-learn!

Чудовий конкурс! :) Btw - могли б нам сказати щось про дату закінчення цього виклику?
cyriel

1
@Lembik Вниз до 7 :)
доктор belisarius

5
Одного разу рисознавець збирається приїхати разом і стане щасливим, що це питання існує.
Ніт

2
@Nit Просто скажи їм ncbi.nlm.nih.gov/pmc/articles/PMC3510117 :)
Dr. belisarius

Відповіді:


22

Математика, оцінка: 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;

Я думаю, що назви функції досить описові:

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]

Обробка всіх зображень одночасно:

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

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

Оцінка:

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

Тут ви можете побачити показник чутливості wrt розміру зерна:

Графіка математики


2
Набагато чіткіше, дякую!
Захоплення Кальвіна

Чи можна цю точну процедуру скопіювати в python чи щось там робить Mathematica, що бібліотеки python не можуть зробити?

@Lembik Не маю ідеї. Тут немає жодного пітона. Вибачте. (Однак я сумніваюсь в точно таких же алгоритмах EdgeDetect[], DeleteSmallComponents[]і Dilation[]вони реалізовані в іншому місці)
Dr. belisarius

55

Python, оцінка: 24 16

Це рішення, як і Фалько, базується на вимірюванні площі переднього плану та поділі його на середню площу зерна.

Насправді те, що ця програма намагається виявити, - це не стільки, як передній план. Використовуючи той факт, що рисові зерна ніколи не торкаються межі зображення, програма починається з заливного білого кольору в верхньому лівому куті. Алгоритм заливки заливає фарбою суміжні пікселі, якщо різниця між їх яскравістю та поточною яскравістю пікселя знаходиться в межах певного порогу, таким чином, підлаштовуючись до поступової зміни кольору фону. В кінці цього етапу зображення може виглядати приблизно так:

Фігура 1

Як бачимо, це робить досить непогану роботу при виявленні фону, але він залишає будь-які ділянки, які "потрапляють" між зернами. Ми обробляємо ці області, оцінюючи яскравість фону на кожному пікселі та вкладаючи всі рівні або світлі пікселі. Ця оцінка працює так: під час етапу заливки ми обчислюємо середню яскравість фону для кожного рядка та кожного стовпця. Орієнтовна яскравість фону на кожному пікселі - це середнє значення яскравості рядка та стовпця для цього пікселя. Це дає щось подібне:

Малюнок 2

EDIT: Нарешті, площа кожного безперервного переднього плану (тобто небілого) регіону ділиться на середню, попередньо обчислену, зернову площу, що дає нам оцінку кількості зерна у зазначеному регіоні. Сума цих величин - результат. Спочатку ми зробили те ж саме для всієї передньої частини в цілому, але цей підхід, буквально, більш тонкий.


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

Приймає ім'я вхідного файла через командний рядок.

Результати

      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

А Б С D Е

Ж Г Н Я J


2
Це дійсно розумне рішення, приємна робота!
Chris Cirefice

1
звідки береться avg_grain_area = 3038.38;?
njzk2

2
чи не вважається це hardcoding the result?
njzk2

5
@ njzk2 Ні. Дано правило The images have different dimensions but the scale of the rice in all of them is consistent because the camera and background were stationary.Це лише значення, яке представляє це правило. Результат, однак, змінюється відповідно до введених даних. Якщо ви зміните правило, то це значення зміниться, але результат буде таким же - на основі введених даних.
Адам Девіс

6
Я добре з середньою площею. Площа зерна (приблизно) постійна для зображень.
Захоплення Кальвіна

28

Python + OpenCV: оцінка 27

Сканування по горизонтальній лінії

Ідея: сканувати зображення, по одному рядку. Для кожного рядка підраховуйте кількість зустрічаються зерен рису (перевіряючи, чи піксель стає чорним до білим чи навпаки). Якщо кількість зерен для лінії збільшиться (порівняно з попереднім рядком), це означає, що ми зустріли нове зерно. Якщо ця кількість зменшиться, це означає, що ми перебрали зерно. У цьому випадку додайте +1 до загального результату.

введіть тут опис зображення

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

Через те, як працює алгоритм, важливо мати чисте, ч / б зображення. Багато шуму дають погані результати. Спочатку основний фон очищається за допомогою заливки (розчин, подібний до відповіді Ell), після чого застосовується поріг для отримання чорно-білого результату.

введіть тут опис зображення

Це далеко не ідеально, але це дає хороші результати щодо простоти. Напевно, існує багато способів поліпшити його (надання кращого ж / б зображення, сканування в інших напрямках (наприклад: вертикальний, діагональний), беручи середнє значення тощо).

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

Похибки на зображення: 0, 0, 0, 3, 0, 12, 4, 0, 7, 1


24

Python + OpenCV: Оцінка 84

Ось перша наївна спроба. Він застосовує адаптивний поріг із налаштованими вручну параметрами, закриває деякі отвори з подальшим розмиванням та розведенням та отримує кількість зерен із переднього плану.

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)

Тут ви можете побачити проміжні бінарні зображення (чорний колір - передній план):

введіть тут опис зображення

Похибки кожного зображення - 0, 0, 2, 2, 4, 0, 27, 42, 0 і 7 зерен.


20

C # + OpenCvSharp, Оцінка: 2

Це моя друга спроба. Він сильно відрізняється від моєї першої спроби , що набагато простіше, тому я розміщую це як окреме рішення.

Основна ідея полягає у визначенні та маркуванні кожного окремого зерна за допомогою ітеративного пристосування еліпса. Потім вийміть пікселі для цього зерна з джерела та спробуйте знайти наступне зерно, поки кожен піксель не буде маркований.

Це не найприємніше рішення. Це гігантська свиня з 600 рядками коду. На найбільше зображення потрібно 1,5 хвилини. І я дуже вибачаюся за безладний код.

У цій штуці так багато параметрів і способів роздумувати, що я дуже боюся переповнювати свою програму для 10 зразкових зображень. Підсумковий бал 2 майже напевно є вигідним примірником: у мене два параметри, average grain size in pixelі minimum ratio of pixel / elipse_area, і в кінці я просто вичерпав усі комбінації цих двох параметрів, поки не отримав найнижчу оцінку. Я не впевнений, чи все це кошерно з правилами цього виклику.

average_grain_size_in_pixel = 2530
pixel / elipse_area >= 0.73

Але навіть без цих непоганих кладок результати є досить приємними. Без фіксованого розміру зерна або співвідношення пікселів, просто оцінивши середній розмір зерна з навчальних зображень, оцінка все ще становить 27.

І я отримую як вихід не тільки кількість, але фактичне положення, орієнтацію та форму кожного зерна. є невелика кількість мічених зерен, але загалом більшість етикеток точно відповідають справжнім зернам:

A А B Б C С D D EЕ

F Ж G Г H Н I Я JJ

(натисніть на кожне зображення для повнорозмірної версії)

Після цього кроку маркування моя програма розглядає кожне окреме зерно і оцінює, виходячи з кількості пікселів та співвідношення площі піксель / еліпс, чи це

  • одне зерно (+1)
  • кілька зерен, неправильно позначених як одне (+ X)
  • крапля занадто мала, щоб бути зерном (+0)

Оцінки помилок для кожного зображення є A:0; B:0; C:0; D:0; E:2; F:0; G:0 ; H:0; I:0, J:0

Однак фактична помилка, ймовірно, трохи вище. Деякі помилки в одному зображенні скасовують одна одну. Зокрема, зображення H має деякі неправильно розмічені зерна, тоді як на зображенні E етикетки здебільшого правильні

Концепція дещо надумана:

  • Спочатку передній план відокремлюється через відхилення порогу на каналі насичення (детальну інформацію див. У попередній відповіді)

  • повторити, поки не залишилося більше пікселів:

    • виберіть найбільшу краплину
    • виберіть 10 випадкових крайових пікселів на цій крапці як вихідні позиції для зерна

    • для кожної відправної точки

      • припустіть зерно висотою та шириною 10 пікселів у цьому положенні.

      • повторити до зближення

        • йдіть радіально назовні від цієї точки під різними кутами, поки не зустрінете крайовий піксель (біло-чорний)

        • знайдені пікселі мають сподіватися, що крайові пікселі одного зерна. Спробуйте відокремити інлайєри від інших людей, відкинувши пікселі, віддаленіші від передбачуваного еліпса, ніж інші

        • кілька разів намагайтеся помістити еліпс через підмножину інлайєрів, зберігайте найкращий еліпс (RANSACK)

        • оновити положення зерна, орієнтацію, ширину та висоту знайденим затемненням

        • якщо положення зерна істотно не зміниться, зупиніть

    • серед 10 пристосованих зерен виберіть найкраще зерно за формою, кількістю крайових пікселів. Відмовтесь від інших

    • видаліть усі пікселі для цього зерна із вихідного зображення, а потім повторіть

    • нарешті, перегляньте список знайдених зерен і порахуйте кожне зерно як 1 зерно, 0 зерна (занадто мало) або 2 зерна (занадто велике)

Однією з моїх головних проблем було те, що я не хотів реалізувати повну метрику відстані в точці еліпса, оскільки обчислення того, що саме по собі є складним ітераційним процесом. Тому я використав різні обхідні шляхи за допомогою функцій OpenCV Ellipse2Poly та FitEllipse, і результати не надто гарні.

Мабуть, я також порушив обмеження розміру для codegolf.

Відповідь обмежена 30000 символами, зараз я на 34000. Тож мені доведеться дещо скоротити код нижче.

Повний код можна побачити на веб-сайті http://pastebin.com/RgM7hMxq

Вибачте за це, мені не було відомо про обмеження розміру.

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

}

Я трохи збентежений цим рішенням, тому що а) я не впевнений, чи відповідає це дух цієї проблеми, і б) він занадто великий для відповіді на кодегольф і не вистачає елегантності інших рішень.

З іншого боку, я цілком задоволений прогресом, досягнутим у маркуванні зерен, а не просто їх підрахунком, тому є таке.


Ви знаєте, що можете зменшити довжину коду на величини, використовуючи менші імена та застосувавши деякі інші методи для гольфу;)
Оптимізатор

Напевно, але я не хотів більше придушувати це рішення. Це занадто заплутано для моїх смаків, як це :)
HugoRune

+1 за зусилля і тому, що ви єдиний, хто знаходить спосіб індивідуально відобразити кожне зерно. На жаль, код трохи роздутий і багато покладається на твердо кодовані константи. Мені було б цікаво побачити, як алгоритм сканування, який я написав, виконує на цьому (на наочних кольорових зернах).
tigrou

Я дійсно вважаю, що це правильний підхід для подібного типу проблем (+1 для вас), але одне мені цікаво, чому ви "вибираєте 10 випадкових крайових пікселів", я б подумав, що ви отримаєте кращу ефективність, якби вибрали крайові точки з найменшою кількістю сусідніх крайових точок (тобто частин, що стирчать), я б подумав (теоретично), це спочатку усуне "найпростіші" зерна, ви це вважали?
Девід Роджерс

Я про це думав, але ще не пробував. "10 випадкових вихідних позицій" було пізнім доповненням, яке було легко додати та легко паралелізувати. До цього "одна випадкова вихідна позиція" була набагато кращою, ніж "завжди лівий верхній кут". Небезпека вибору вихідних позицій із однаковою стратегією кожного разу полягає в тому, що коли я зніму найкращу підгонку, інші 9, ймовірно, будуть обрані знову наступного разу, і з часом найгірші з цих вихідних позицій залишаться позаду і знову вибираються і знову. Частина, яка стирчить, може бути просто залишками повністю вилученого попереднього зерна.
HugoRune

17

C ++, OpenCV, оцінка: 9

Основна ідея мого методу досить проста - спробуйте стерти з зображення поодинокі зерна (і «подвійні зерна» - 2 (але не більше!) Зерна, близькі одне до одного), а потім порахуйте відпочинок, використовуючи метод на основі площі (наприклад, Falko, Ell і belisarius). Використання такого підходу трохи краще, ніж стандартний "метод області", тому що простіше знайти хороше середнєPixelsPerObject значення.

(1-й крок) Перш за все нам потрібно використовувати бінаризацію Otsu на S каналі зображення у HSV. Наступним кроком є ​​використання оператора розширення для поліпшення якості видобутого переднього плану. Чим нам потрібно знайти контури. Звичайно, деякі контури не є зернами рису - нам потрібно видалити занадто малі контури (з площею меншою за середнюPixelsPerObject / 4. У моїй ситуації середнєPixelsPerObject становить 2855). Тепер нарешті ми можемо почати підрахунок зерен :) (2-й крок) Пошук одинарних та подвійних зерен досить простий - просто подивіться у контурний список контурів із площею в певних діапазонах - якщо область контуру знаходиться в діапазоні, видаліть його зі списку та додайте 1 (або 2, якщо це було "подвійне" зерно) до лічильника зерен. (3-й крок) Останній крок - це, звичайно, розділення площі решти контурів на середнє значення PixelsPerObject і додавання результату до лічильника зерен.

Зображення (для зображення F.jpg) повинні відображати цю ідею краще, ніж слова:
1-й крок (без малих контурів (шум)): 1-й крок (без малих контурів (шум))
2-й крок - лише прості контури: 2-й крок - лише прості контури
3-й крок - контури, що залишилися: 3-й крок - залишилися контури

Ось код, він досить некрасивий, але повинен працювати без проблем. Звичайно, OpenCV потрібен.

#include "stdafx.h"

#include <cv.hpp>
#include <cxcore.h>
#include <highgui.h>
#include <vector>

using namespace cv;
using namespace std;

//A: 3, B: 5, C: 12, D: 25, E: 50, F: 83, G: 120, H:150, I: 151, J: 200
const int goodResults[] = {3, 5, 12, 25, 50, 83, 120, 150, 151, 200};
const float averagePixelsPerObject = 2855.0;

const int singleObjectPixelsCountMin = 2320;
const int singleObjectPixelsCountMax = 4060;

const int doubleObjectPixelsCountMin = 5000;
const int doubleObjectPixelsCountMax = 8000;

float round(float x)
{
    return x >= 0.0f ? floorf(x + 0.5f) : ceilf(x - 0.5f);
}

Mat processImage(Mat m, int imageIndex, int &error)
{
    int objectsCount = 0;
    Mat output, thresholded;
    cvtColor(m, output, CV_BGR2HSV);
    vector<Mat> channels;
    split(output, channels);
    threshold(channels[1], thresholded, 0, 255, CV_THRESH_OTSU | CV_THRESH_BINARY);
    dilate(thresholded, output, Mat()); //dilate to imporove quality of binary image
    imshow("thresholded", thresholded);
    int nonZero = countNonZero(output); //not realy important - just for tests
    if (imageIndex != -1)
        cout << "non zero: " << nonZero << ", average pixels per object: " << nonZero/goodResults[imageIndex] << endl;
    else
        cout << "non zero: " << nonZero << endl;

    vector<vector<Point>> contours, contoursOnlyBig, contoursWithoutSimpleObjects, contoursSimple;
    findContours(output, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); //find only external contours
    for (int i=0; i<contours.size(); i++)
        if (contourArea(contours[i]) > averagePixelsPerObject/4.0)
            contoursOnlyBig.push_back(contours[i]); //add only contours with area > averagePixelsPerObject/4 ---> skip small contours (noise)

    Mat bigContoursOnly = Mat::zeros(output.size(), output.type());
    Mat allContours = bigContoursOnly.clone();
    drawContours(allContours, contours, -1, CV_RGB(255, 255, 255), -1);
    drawContours(bigContoursOnly, contoursOnlyBig, -1, CV_RGB(255, 255, 255), -1);
    //imshow("all contours", allContours);
    output = bigContoursOnly;

    nonZero = countNonZero(output); //not realy important - just for tests
    if (imageIndex != -1)
        cout << "non zero: " << nonZero << ", average pixels per object: " << nonZero/goodResults[imageIndex] << " objects: "  << goodResults[imageIndex] << endl;
    else
        cout << "non zero: " << nonZero << endl;

    for (int i=0; i<contoursOnlyBig.size(); i++)
    {
        double area = contourArea(contoursOnlyBig[i]);
        if (area >= singleObjectPixelsCountMin && area <= singleObjectPixelsCountMax) //is this contours a single grain ?
        {
            contoursSimple.push_back(contoursOnlyBig[i]);
            objectsCount++;
        }
        else
        {
            if (area >= doubleObjectPixelsCountMin && area <= doubleObjectPixelsCountMax) //is this contours a double grain ?
            {
                contoursSimple.push_back(contoursOnlyBig[i]);
                objectsCount+=2;
            }
            else
                contoursWithoutSimpleObjects.push_back(contoursOnlyBig[i]); //group of grainss
        }
    }

    cout << "founded single objects: " << objectsCount << endl;
    Mat thresholdedImageMask = Mat::zeros(output.size(), output.type()), simpleContoursMat = Mat::zeros(output.size(), output.type());
    drawContours(simpleContoursMat, contoursSimple, -1, CV_RGB(255, 255, 255), -1);
    if (contoursWithoutSimpleObjects.size())
        drawContours(thresholdedImageMask, contoursWithoutSimpleObjects, -1, CV_RGB(255, 255, 255), -1); //draw only contours of groups of grains
    imshow("simpleContoursMat", simpleContoursMat);
    imshow("thresholded image mask", thresholdedImageMask);
    Mat finalResult;
    thresholded.copyTo(finalResult, thresholdedImageMask); //copy using mask - only pixels whc=ich belongs to groups of grains will be copied
    //imshow("finalResult", finalResult);
    nonZero = countNonZero(finalResult); // count number of pixels in all gropus of grains (of course without single or double grains)
    int goodObjectsLeft = goodResults[imageIndex]-objectsCount;
    if (imageIndex != -1)
        cout << "non zero: " << nonZero << ", average pixels per object: " << (goodObjectsLeft ? (nonZero/goodObjectsLeft) : 0) << " objects left: " << goodObjectsLeft <<  endl;
    else
        cout << "non zero: " << nonZero << endl;
    objectsCount += round((float)nonZero/(float)averagePixelsPerObject);

    if (imageIndex != -1)
    {
        error = objectsCount-goodResults[imageIndex];
        cout << "final objects count: " << objectsCount << ", should be: " << goodResults[imageIndex] << ", error is: " << error <<  endl;
    }
    else
        cout << "final objects count: " << objectsCount << endl; 
    return output;
}

int main(int argc, char* argv[])
{
    string fileName = "A";
    int totalError = 0, error;
    bool fastProcessing = true;
    vector<int> errors;

    if (argc > 1)
    {
        Mat m = imread(argv[1]);
        imshow("image", m);
        processImage(m, -1, error);
        waitKey(-1);
        return 0;
    }

    while(true)
    {
        Mat m = imread("images\\" + fileName + ".jpg");
        cout << "Processing image: " << fileName << endl;
        imshow("image", m);
        processImage(m, fileName[0] - 'A', error);
        totalError += abs(error);
        errors.push_back(error);
        if (!fastProcessing && waitKey(-1) == 'q')
            break;
        fileName[0] += 1;
        if (fileName[0] > 'J')
        {
            if (fastProcessing)
                break;
            else
                fileName[0] = 'A';
        }
    }
    cout << "Total error: " << totalError << endl;
    cout << "Errors: " << (Mat)errors << endl;
    cout << "averagePixelsPerObject:" << averagePixelsPerObject << endl;

    return 0;
}

Якщо ви хочете побачити результати всіх кроків, скасуйте всі виклики функцій imshow (.., ..) і встановіть зміну fastProcessing на false. Зображення (A.jpg, B.jpg, ...) повинні бути в образах каталогів. Крім того, ви можете вказати ім'я одному зображенню як параметр із командного рядка.

Звичайно, якщо щось незрозуміле, я можу пояснити це та / або надати деякі зображення / інформацію.


12

C # + OpenCvSharp, оцінка: 71

Це найбільше неприємно, я намагався знайти рішення, яке фактично ідентифікує кожне зерно за допомогою вододілу , але я просто. не може. дістати. це. до. робота.

Я зупинився на рішення , яке по крайней мере , що розділяє деякі окремі зерна , а потім використовує ці зерна для оцінки середнього розміру зерна. Однак поки що я не можу перемогти розчини з твердим розміром зерна.

Отже, головна родзинка цього рішення: воно не передбачає фіксованого розміру пікселів для зерен, і воно має працювати навіть у тому випадку, якщо камера переміщена або тип рису змінено.

A.jpg; кількість зерен: 3; очікувані 3; похибка 0; пікселів на зерно: 2525,0;
B.jpg; кількість зерен: 7; очікувані 5; помилка 2; пікселів на зерно: 1920,0;
C.jpg; кількість зерен: 6; очікувані 12; помилка 6; пікселів на зерно: 4242,5;
D.jpg; кількість зерен: 23; очікувано 25; помилка 2; пікселів на зерно: 2415,5;
E.jpg; кількість зерен: 47; очікувані 50; помилка 3; пікселів на зерно: 2729,9;
F.jpg; кількість зерен: 65; очікувані 83; помилка 18; пікселів на зерно: 2860,5;
G.jpg; кількість зерен: 120; очікувана 120; похибка 0; пікселів на зерно: 2552,3;
H.jpg; кількість зерен: 159; очікуване 150; помилка 9; пікселів на зерно: 2624,7;
I.jpg; кількість зерен: 141; очікувана 151; помилка 10; пікселів на зерно: 2697,4;
J.jpg; кількість зерен: 179; очікуване 200; помилка 21; пікселів на зерно: 2847,1;
загальна помилка: 71

Моє рішення працює так:

Розділіть передній план, перетворивши зображення на HSV та застосувавши порогове значення Otsu на каналі насичення. Це дуже просто, працює надзвичайно добре, і я рекомендував би це всім, хто хоче спробувати цю проблему:

saturation channel                -->         Otsu thresholding

введіть тут опис зображення -> введіть тут опис зображення

Це дозволить чисто видалити фон.

Потім я додатково видалив зернисті тіні з переднього плану, застосувавши фіксований поріг до каналу значення. (Не впевнений, чи насправді це дуже допомагає, але додати це було досить просто)

введіть тут опис зображення

Потім я застосовую перетворення відстані на передньому плані зображення.

введіть тут опис зображення

і знайти всі локальні максимуми в цій трансформації відстані.

Ось де моя ідея розпадається. щоб уникнути отримання тупих місцевих максимумів в межах одного зерна, мені доведеться багато фільтрувати. В даний час я тримаю лише найсильніший максимум в радіусі 45 пікселів, а це означає, що не кожне зерно має локальний максимум. І я насправді не маю виправдання для радіусу 45 пікселів, це було просто значення, яке спрацювало.

введіть тут опис зображення

(як ви бачите, це не майже достатньо насіння для обліку кожного зерна)

Тоді я використовую ці максимуми як насіння для алгоритму вододілу:

введіть тут опис зображення

Результати - мех . Я сподівався на переважно окремі зерна, але скупчення все ще занадто великі.

Тепер я визначаю найменші краплі, підраховую їх середній розмір пікселів, а потім оцінюю кількість зерен від цього. Це не те, що я планував зробити на старті, але це був єдиний спосіб врятувати це.

за допомогою системи ; 
за допомогою системи . Колекції . Родовий ; 
за допомогою системи . Linq ; 
за допомогою системи . Текст ; 
використовуючи OpenCvSharp ;

простір імен GrainTest2 { class Program { static void Main ( string [] args ) { string [] files = new [] { "sourceA.jpg" , "sourceB.jpg" , "sourceC.jpg" , "sourceD.jpg" , " sourceE.jpg " , " sourceF.jpg " , " sourceG.jpg " , " sourceH.jpg " , " sourceI.jpg " , " sourceJ.jpg " , };int [] очікуваніGrains

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

            int totalError = 0 ; int totalPixels = 0 ; 
             

            для ( INT fileNo = 0 ; fileNo маркерами = новий список (); 
                    використовуючи ( CvMemStorage зберігання = новий CvMemStorage ()) з 
                    використанням ( CvContourScanner сканер = новий CvContourScanner ( localMaxima , зберігання , CvContour . SizeOf , ContourRetrieval . Зовнішня , ContourChain . ApproxNone ))         
                    { // встановити кожен локальний максимум як насінне число 25, 35, 45, ... // (фактичні числа не мають значення, вибрано для кращої видимості в png) int markerNo = 20 ; foreach ( CvSeq c у сканері ) { 
                            markerNo + = 5 ; 
                            маркери . Додати ( markerNo ); 
                            waterShedMarkers . DrawContours ( c , новий CvScalar ( markerNo ), новий
                        
                        
                         
                         
                             CvScalar ( markerNo ), 0 , - 1 ); } } 
                    Водяні метки . SaveImage ( "08-waterhed-seed.png" );  
                        
                    


                    джерело . Вододіл ( waterShedMarkers ); 
                    waterShedMarkers . SaveImage ( "09-waterhed-result.png" );


                    Список пікселівPerBlob = новий Список ();  

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

        }
    }
}

Невеликий тест із твердим кодуванням пікселів на зерно розміром 2544,4 показав загальну помилку 36, яка все ще більша за більшість інших рішень.

введіть тут опис зображення введіть тут опис зображення введіть тут опис зображення введіть тут опис зображення


Я думаю, що ви можете використовувати поріг (еродова операція також може бути корисною) з невеликим значенням в результаті перетворення відстані - це повинно розділити деякі групи зерен на менші групи (бажано - лише 1 або 2 зерна). Тоді як легше порахувати ці самотні зерна. Великі групи, яких ви можете вважати більшістю людей тут - ділення площі на середню площу одного зерна.
cyriel

9

HTML + Javascript: Оцінка 39

Точні значення:

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

Він розпадається (не точно) на більші значення.

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>

Пояснення: В основному підраховує кількість рисових пікселів і ділить його на середні пікселі на зерно.


Використовуючи 3-рисове зображення, воно оцінюється 0 для мене ...: /
Kroltan

1
@Kroltan Не використовується при повному розмірі зображення.
Захоплення Кальвіна

1
@ Calvin'sHobbies FF36 на Windows отримує 0, на Ubuntu отримує 3, із зображенням у повному розмірі.
Кролтан

4
@BobbyJack Рис гарантовано матиме більш-менш однаковий масштаб у зображеннях. Я не бачу проблем з цим.
Захоплення Кальвіна

1
@githubphagocyte - пояснення цілком очевидно - якщо порахувати всі білі пікселі в результаті бінаризації зображення і розділити це число на кількість зерен зображення, ви отримаєте цей результат. Звичайно, точний результат може відрізнятися через застосований метод бінаризації та інші речі (наприклад, операції, що виконуються після бінаризації), але, як ви бачите в інших відповідях, він буде в діапазоні 2500-3500.
cyriel

4

Спроба php, не найнижча відповідь, але досить простий код

ШКОЛА: 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);
}

Самооцінка

95 - синє значення, яке, здається, працює при тестуванні на GIMP 2966 - це середній розмір зерна

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.