Подсчет зерен риса


81

Рассмотрим эти 10 изображений различного количества сырых зерен белого риса.
ЭТО ТОЛЬКО ПУСТОЙ. Нажмите на изображение, чтобы просмотреть его в полном размере.

A: B: C: D: E:A В С D Е

F: G: H: I: J:F грамм ЧАС я 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
Конечно, кто-то должен попробовать научиться!

Отличный конкурс! :) Кстати, не могли бы вы рассказать нам что-нибудь о дате окончания этого конкурса?
Cyriel

1
@Lembik До 7 :)
Доктор Белизариус

5
Однажды, рисовый ученый собирается прийти и быть по уши счастливым, что этот вопрос существует.
Nit

Ответы:


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 *)

Здесь вы можете увидеть чувствительность счета к используемому размеру зерна:

Математическая графика


2
Намного понятнее, спасибо!
Увлечения Кэлвина

Может ли эта точная процедура быть скопирована в python или есть что-то особенное, что Mathematica делает здесь, чего не могут сделать библиотеки python?

@Lembik Не знаю. Здесь нет питона. Сожалею. (Тем не менее, я сомневаюсь , что точно такие же алгоритмы EdgeDetect[], DeleteSmallComponents[]и Dilation[]реализуются в другом месте)
доктор Велизарий

55

Python, оценка: 24 16

Это решение, как и решение Фалько, основано на измерении площади «переднего плана» и делении ее на среднюю площадь зерна.

Фактически, эта программа пытается обнаружить фон, а не передний план. Используя тот факт, что рисовые зерна никогда не касаются границы изображения, программа начинается с заливки белым цветом в верхнем левом углу. Алгоритм заливки закрашивает смежные пиксели, если разница между их и яркостью текущего пикселя находится в пределах определенного порога, таким образом приспосабливаясь к постепенному изменению цвета фона. В конце этого этапа изображение может выглядеть примерно так:

фигура 1

Как вы можете видеть, он неплохо справляется с обнаружением фона, но пропускает все области, которые «зажаты» между зернами. Мы обрабатываем эти области, оценивая яркость фона в каждом пикселе и размещая все пиксели, равные или более яркие. Эта оценка работает следующим образом: на этапе заливки мы рассчитываем среднюю яркость фона для каждой строки и каждого столбца. Расчетная яркость фона в каждом пикселе является средним значением яркости строк и столбцов в этом пикселе. Это производит что-то вроде этого:

фигура 2

РЕДАКТИРОВАТЬ: Наконец, площадь каждой непрерывной области переднего плана (то есть, не белого) делится на среднюю, предварительно рассчитанную, площадь зерна, что дает нам оценку количества зерна в указанной области. Сумма этих величин является результатом. Первоначально мы делали то же самое для всей области переднего плана в целом, но этот подход, в буквальном смысле, более мелкозернистый.


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

A В С D Е

F грамм ЧАС я J


2
Это действительно умное решение, хорошая работа!
Крис 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 A B В C С D D EЕ

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

}

Я немного смущен этим решением, потому что: а) я не уверен, соответствует ли оно духу этой задачи, и б) оно слишком велико для ответа Codegolf и не обладает элегантностью других решений.

С другой стороны, я очень доволен прогрессом, которого я достиг в маркировке зерен, а не просто в подсчете, так что это так.


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

Вероятно, но я не хотел еще больше запутывать это решение. Это слишком запутанно для моих вкусов, как это :)
HugoRune

+1 за усилие и потому что вы единственный, кто находит способ отображать индивидуально каждое зерно. К сожалению, код немного раздут и много зависит от жестко закодированных констант. Мне было бы любопытно увидеть, как алгоритм развертки, который я написал, работает на этом (на отдельных цветных зернах).
Tigrou

Я действительно думаю, что это правильный подход для такого типа проблем (+1 для вас), но мне интересно одно: почему вы «выбираете 10 случайных краевых пикселей», я думаю, что вы получите лучшую производительность, если выберете краевые точки с наименьшим количеством соседних краевых точек (то есть частей, которые торчат), я бы подумал (теоретически), что это сначала исключит "самые легкие" зерна, вы рассматривали это?
Дэвид Роджерс

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

17

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

Основная идея моего метода довольно проста - попробуйте стереть одиночные зерна (и «двойные зерна» - 2 (но не больше!) Зерна, близкие друг к другу) из изображения, а затем сосчитать остальные, используя метод, основанный на области (например, Falko, Элл и Велисарий). Использование этого подхода немного лучше, чем стандартный «метод области», потому что легче найти хорошее среднее значениеPixelsPerObject.

(1-й шаг) Прежде всего нам нужно использовать бинаризацию Оцу на S-канале изображения в HSV. Следующим шагом является использование оператора расширения для улучшения качества извлекаемого переднего плана. Чем нам нужно найти контуры. Конечно, некоторые контуры не являются рисовыми зернами - нам нужно удалить слишком маленькие контуры (с площадью, меньшей, чем AveragePixelsPerObject / 4. В моем случае, AveragePixelsPerObject равен 2855). Теперь, наконец, мы можем начать считать зерна :) (2-й шаг) Найти одинарные и двойные зерна довольно просто - просто посмотрите в списке контуров контуры с областью в определенных диапазонах - если область контура находится в диапазоне, удалите его из списка и добавьте 1 (или 2, если это было «двойное» зерно) к счетчику зерна. (3-ий шаг) Последний шаг - это, конечно, деление площади оставшихся контуров на значение AveragePixelsPerObject и добавление результата в счетчик зерен.

Изображения (для изображения F.jpg) должны показать эту идею лучше, чем слова:
1-й шаг (без маленьких контуров (шум)): 1st step (without small contours(noise))
2-й шаг - только простые контуры: 2nd step - only simple contours
3-й шаг - остальные контуры: 3rd step - remaining contours

Вот код, он довольно уродливый, но должен работать без проблем. Конечно 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

enter image description here -> enter image description here

Это будет чисто удалить фон.

Затем я дополнительно удалил тени зерен с переднего плана, применив фиксированный порог к каналу значений. (Не уверен, что это действительно очень помогает, но это было достаточно просто добавить)

enter image description here

Затем я применяю преобразование расстояния на переднем плане изображения.

enter image description here

и найти все локальные максимумы в этом преобразовании расстояния.

Вот где моя идея рушится. чтобы избежать получения нескольких локальных максимумов в пределах одного зерна, мне приходится много фильтровать. В настоящее время я держу только самый сильный максимум в радиусе 45 пикселей, что означает, что не у каждого зерна есть локальный максимум. И у меня нет никакого оправдания для радиуса 45 пикселей, это было просто значение, которое сработало.

enter image description here

(как видите, этих семян недостаточно для учета каждого зерна)

Затем я использую эти максимумы в качестве семян для алгоритма водораздела:

enter image description here

Результаты Мех . Я надеялся в основном на отдельные зерна, но сгустки все еще слишком велики.

Теперь я идентифицирую самые маленькие капли, подсчитываю их средний размер в пикселях, а затем оцениваю количество зерен из этого. Это не то, что я планировал сделать в начале, но это был единственный способ спасти это.

используя Систему ; 
используя систему . Коллекции . Универсальный ; 
используя систему . Линк ; 
используя систему . Текст ; 
используя OpenCvSharp ;

пространство имен GrainTest2 { класс 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 [] Ожидаемые Зерна

     
    
          
        
             
                               
                                     
                                     
                                      
                               
            = новый [] { 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 ); } } 
                    waterShedMarkers . SaveImage ( "08-watershed-seeds.png" );  
                        
                    


                    источник . Водораздел ( waterShedMarkers ); 
                    WaterShedMarkers . SaveImage ( "09-watershed-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, которая все еще больше, чем у большинства других решений.

enter image description here enter image description here enter image description here enter image description here


Я думаю, что вы можете использовать порог (операция эрозии также может быть полезна) с некоторым небольшим значением в результате преобразования расстояния - это должно разделить некоторые группы зерен на более мелкие группы (предпочтительно - только с 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
рисами

1
@ Kroltan Нет, когда вы используете полноразмерное изображение.
Увлечения Кэлвина

1
@ Calvin'sHobbies FF36 в Windows получает 0, в Ubuntu - 3 с полноразмерным изображением.
Kroltan

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.