Đếm hạt gạo


81

Hãy xem xét 10 hình ảnh này về số lượng khác nhau của hạt gạo trắng chưa nấu chín.
NHỮNG ĐIỀU NÀY CHỈ CÓ NHIỀU. Nhấp vào hình ảnh để xem nó ở kích thước đầy đủ.

A: B: C: D: E:Một B C CƯỜI MỞ MIỆNG E

F: G: H: Tôi: J:ĐỤ G H Tôi J

Số hạt: A: 3, B: 5, C: 12, D: 25, E: 50, F: 83, G: 120, H:150, I: 151, J: 200

Thông báo rằng...

  • Các hạt có thể chạm vào nhau nhưng chúng không bao giờ chồng lên nhau. Bố cục của các hạt không bao giờ cao hơn một hạt.
  • Các hình ảnh có kích thước khác nhau nhưng quy mô của gạo trong tất cả chúng là phù hợp vì máy ảnh và hậu cảnh là đứng yên.
  • Các hạt không bao giờ đi ra khỏi giới hạn hoặc chạm vào ranh giới hình ảnh.
  • Nền luôn có cùng một sắc thái nhất quán của màu trắng vàng.
  • Các hạt nhỏ và lớn được tính như nhau mỗi hạt.

5 điểm này là đảm bảo cho tất cả các hình ảnh của loại này.

Thử thách

Viết một chương trình lấy những hình ảnh như vậy và, càng chính xác càng tốt, đếm số hạt gạo.

Chương trình của bạn nên lấy tên tệp của hình ảnh và in số lượng hạt mà nó tính toán. Chương trình của bạn phải hoạt động với ít nhất một trong các định dạng tệp hình ảnh: JPEG, Bitmap, PNG, GIF, TIFF (ngay bây giờ các hình ảnh đều là JPEG).

Bạn có thể sử dụng xử lý hình ảnh và thư viện thị giác máy tính.

Bạn không thể mã hóa đầu ra của 10 hình ảnh ví dụ. Thuật toán của bạn nên được áp dụng cho tất cả các hình ảnh hạt gạo tương tự. Nó có thể chạy trong chưa đầy 5 phút trên một máy tính hiện đại đàng hoàng nếu vùng hình ảnh nhỏ hơn 2000 * 2000 pixel và có ít hơn 300 hạt gạo.

Chấm điểm

Đối với mỗi hình ảnh trong số 10 hình ảnh, hãy lấy giá trị tuyệt đối của số lượng hạt thực tế trừ đi số lượng hạt mà chương trình của bạn dự đoán. Tính tổng các giá trị tuyệt đối này để có được điểm số của bạn. Điểm thấp nhất sẽ thắng. Điểm 0 là hoàn hảo.

Trong trường hợp quan hệ, câu trả lời được bình chọn cao nhất sẽ thắng. Tôi có thể kiểm tra chương trình của bạn trên các hình ảnh bổ sung để xác minh tính hợp lệ và chính xác của nó.


1
Chắc chắn ai đó phải thử scikit-learn!

Cuộc thi tuyệt vời! :) Btw - có thể cho chúng tôi biết điều gì về ngày kết thúc của thử thách này?
cyriel

1
@Lembik Xuống đến 7 :)
Tiến sĩ belisarius

5
Một ngày nọ, một nhà khoa học lúa gạo sẽ xuất hiện và vui mừng vì câu hỏi này tồn tại.
Nit

2
@Nit Chỉ cần nói với họ ncbi.nlm.nih.gov/pmc/articles/PMC3510117 :)
Tiến sĩ belisarius

Câu trả lời:


22

Toán, điểm: 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;

Tôi nghĩ tên của hàm đủ để mô tả:

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]

Xử lý tất cả các hình ảnh cùng một lúc:

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

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

Điểm số là:

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

Ở đây bạn có thể thấy độ nhạy điểm ghi kích thước hạt được sử dụng:

Đồ họa toán học


2
Rõ ràng hơn nhiều, cảm ơn!
Sở thích của Calvin

Thủ tục chính xác này có thể được sao chép trong python hay có điều gì đặc biệt Mathicala đang làm ở đây mà các thư viện python không thể làm được không?

@Lembik Không có ý kiến. Không có con trăn nào ở đây. Lấy làm tiếc. (Tuy nhiên, tôi nghi ngờ các thuật toán chính xác tương tự cho EdgeDetect[], DeleteSmallComponents[]Dilation[]được thực hiện ở nơi khác)
Tiến sĩ Belisarius

55

Python, Điểm: 24 16

Giải pháp này, giống như giải pháp của Falko, dựa trên việc đo diện tích "tiền cảnh" và chia cho diện tích hạt trung bình.

Trong thực tế, những gì chương trình này cố gắng phát hiện là nền, không nhiều như tiền cảnh. Sử dụng thực tế là các hạt gạo không bao giờ chạm vào ranh giới hình ảnh, chương trình bắt đầu bằng cách lấp đầy màu trắng ở góc trên bên trái. Thuật toán lấp đầy vẽ các pixel liền kề nếu chênh lệch giữa độ sáng của chúng và độ sáng của pixel hiện tại nằm trong một ngưỡng nhất định, do đó điều chỉnh để thay đổi dần dần màu nền. Ở cuối giai đoạn này, hình ảnh có thể trông giống như thế này:

Hình 1

Như bạn có thể thấy, nó làm một công việc khá tốt trong việc phát hiện hậu cảnh, nhưng nó để lại bất kỳ khu vực nào bị "mắc kẹt" giữa các hạt. Chúng tôi xử lý các khu vực này bằng cách ước tính độ sáng nền ở mỗi pixel và tạo ra tất cả các pixel bằng hoặc sáng hơn. Ước tính này hoạt động như vậy: trong giai đoạn lấp đầy lũ, chúng tôi tính toán độ sáng nền trung bình cho mỗi hàng và mỗi cột. Độ sáng nền ước tính ở mỗi pixel là mức trung bình của độ sáng hàng và cột tại pixel đó. Điều này tạo ra một cái gì đó như thế này:

Hình 2

EDIT: Cuối cùng, diện tích của từng vùng tiền cảnh liên tục (tức là không phải màu trắng) được chia cho diện tích hạt trung bình, được tính trước, cho chúng ta ước tính số lượng hạt trong vùng nói trên. Tổng của các đại lượng này là kết quả. Ban đầu, chúng tôi đã làm điều tương tự cho toàn bộ khu vực tiền cảnh, nhưng cách tiếp cận này, theo nghĩa đen, chi tiết hơn.


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

Lấy tên tệp đầu vào thông qua dòng comand.

Các kết quả

      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

Một B C CƯỜI MỞ MIỆNG E

ĐỤ G H Tôi J


2
Đây là một giải pháp thực sự thông minh, công việc tốt đẹp!
Chris Cirefice

1
không avg_grain_area = 3038.38;đến từ đâu
njzk2

2
không được tính là hardcoding the result?
njzk2

5
@ njzk2 Không. Đưa ra quy tắc The images have different dimensions but the scale of the rice in all of them is consistent because the camera and background were stationary.Đây chỉ là một giá trị đại diện cho quy tắc đó. Kết quả, tuy nhiên, thay đổi theo đầu vào. Nếu bạn thay đổi quy tắc, thì giá trị này sẽ thay đổi, nhưng kết quả sẽ giống nhau - dựa trên đầu vào.
Adam Davis

6
Tôi ổn với điều trung bình khu vực. Diện tích hạt là (khoảng) không đổi trên các hình ảnh.
Sở thích của Calvin

28

Python + OpenCV: Điểm 27

Quét ngang

Ý tưởng: quét hình ảnh, mỗi lần một hàng. Đối với mỗi dòng, hãy đếm số hạt gạo gặp phải (bằng cách kiểm tra xem pixel chuyển sang màu đen sang màu trắng hay ngược lại). Nếu số lượng hạt cho dòng tăng (so với dòng trước), điều đó có nghĩa là chúng ta đã gặp một hạt mới. Nếu con số đó giảm, điều đó có nghĩa là chúng ta đã vượt qua một hạt. Trong trường hợp này, thêm +1 vào tổng kết quả.

nhập mô tả hình ảnh ở đây

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

Do cách thức hoạt động của thuật toán, điều quan trọng là phải có hình ảnh sạch, b / w. Nhiều tiếng ồn tạo ra kết quả xấu. Nền chính đầu tiên được làm sạch bằng cách sử dụng vùng lũ (giải pháp tương tự như câu trả lời Ell) sau đó ngưỡng được áp dụng để tạo ra kết quả đen trắng.

nhập mô tả hình ảnh ở đây

Nó là xa hoàn hảo, nhưng nó tạo ra kết quả tốt về sự đơn giản. Có lẽ có nhiều cách để cải thiện nó (bằng cách cung cấp hình ảnh b / w tốt hơn, quét theo các hướng khác (ví dụ: dọc, chéo) lấy mức trung bình, v.v ...)

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

Các lỗi trên mỗi hình ảnh: 0, 0, 0, 3, 0, 12, 4, 0, 7, 1


24

Python + OpenCV: Điểm 84

Đây là một nỗ lực ngây thơ đầu tiên. Nó áp dụng một ngưỡng thích ứng với các tham số được điều chỉnh thủ công, đóng một số lỗ với sự xói mòn và pha loãng tiếp theo và lấy được số lượng hạt từ khu vực phía trước.

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)

Ở đây bạn có thể thấy hình ảnh nhị phân trung gian (màu đen là tiền cảnh):

nhập mô tả hình ảnh ở đây

Các lỗi trên mỗi hình ảnh là 0, 0, 2, 2, 4, 0, 27, 42, 0 và 7 hạt.


20

C # + OpenCvSharp, Điểm: 2

Đây là nỗ lực thứ hai của tôi. Nó khá khác so với lần thử đầu tiên của tôi , đơn giản hơn rất nhiều, vì vậy tôi đang đăng nó dưới dạng một giải pháp riêng biệt.

Ý tưởng cơ bản là xác định và dán nhãn cho từng hạt riêng lẻ bằng một hình elip lặp. Sau đó xóa các pixel cho hạt này khỏi nguồn và cố gắng tìm hạt tiếp theo, cho đến khi mọi pixel được dán nhãn.

Đây không phải là giải pháp đẹp nhất. Nó là một con lợn khổng lồ với 600 dòng mã. Nó cần 1,5 phút cho hình ảnh lớn nhất. Và tôi thực sự xin lỗi vì mã lộn xộn.

Có rất nhiều thông số và cách để suy nghĩ trong vấn đề này đến nỗi tôi khá sợ việc quá tải chương trình của mình cho 10 hình ảnh mẫu. Điểm cuối cùng của 2 gần như chắc chắn là một trường hợp thừa: Tôi có hai tham số average grain size in pixel, và minimum ratio of pixel / elipse_areacuối cùng, tôi chỉ đơn giản là sử dụng hết tất cả các kết hợp của hai tham số này cho đến khi tôi đạt điểm thấp nhất. Tôi không chắc liệu đây có phải là tất cả những gì nghiêm trọng hơn với các quy tắc của thử thách này.

average_grain_size_in_pixel = 2530
pixel / elipse_area >= 0.73

Nhưng ngay cả khi không có các ly hợp quá mức này, kết quả khá tốt đẹp. Nếu không có kích thước hạt cố định hoặc tỷ lệ pixel, chỉ cần ước tính kích thước hạt trung bình từ các hình ảnh đào tạo, điểm số vẫn là 27.

Và tôi nhận được như là đầu ra không chỉ số lượng, mà cả vị trí, định hướng và hình dạng thực tế của từng hạt. có một số lượng nhỏ các loại ngũ cốc dán nhãn sai, nhưng nhìn chung hầu hết các nhãn đều khớp chính xác với các loại ngũ cốc thật:

A Một B B C C D CƯỜI MỞ MIỆNG EE

F ĐỤ G G H H I Tôi JJ

(nhấp vào từng hình ảnh cho phiên bản kích thước đầy đủ)

Sau bước ghi nhãn này, chương trình của tôi sẽ xem xét từng hạt riêng lẻ và ước tính dựa trên số lượng pixel và tỷ lệ diện tích pixel / hình elip, cho dù đây là

  • một hạt (+1)
  • nhiều hạt bị gắn nhãn sai là một (+ X)
  • một đốm quá nhỏ để trở thành hạt (+0)

Điểm số lỗi cho mỗi hình ảnh là A:0; B:0; C:0; D:0; E:2; F:0; G:0 ; H:0; I:0, J:0

Tuy nhiên, lỗi thực tế có lẽ cao hơn một chút. Một số lỗi trong cùng một hình ảnh triệt tiêu lẫn nhau. Hình ảnh H đặc biệt có một số hạt bị dán nhãn sai, trong khi ở hình E, nhãn hầu hết là chính xác

Khái niệm này là một chút giả định:

  • Đầu tiên, tiền cảnh được phân tách thông qua ngưỡng otsu trên kênh bão hòa (xem câu trả lời trước của tôi để biết chi tiết)

  • lặp lại cho đến khi không còn pixel nào nữa:

    • chọn blob lớn nhất
    • chọn 10 pixel cạnh ngẫu nhiên trên blob này làm vị trí bắt đầu cho hạt

    • cho mỗi điểm bắt đầu

      • giả sử một hạt có chiều cao và chiều rộng 10 pixel tại vị trí này.

      • lặp lại cho đến khi hội tụ

        • đi thẳng ra ngoài từ điểm này, ở các góc khác nhau, cho đến khi bạn bắt gặp một pixel cạnh (trắng sang đen)

        • các pixel được tìm thấy hy vọng sẽ là các pixel cạnh của một hạt. Cố gắng tách các ngoại lệ khỏi các ngoại lệ, bằng cách loại bỏ các pixel ở xa hình elip giả định hơn các hình khác

        • nhiều lần cố gắng để phù hợp với một hình elip thông qua một tập hợp con của các phần tử bên trong, giữ cho hình elip tốt nhất (RANSACK)

        • cập nhật vị trí hạt, hướng, chiều rộng và chiều cao với hình elip được tìm thấy

        • nếu vị trí hạt không thay đổi đáng kể, dừng lại

    • Trong số 10 hạt được trang bị, chọn hạt tốt nhất theo hình dạng, số pixel cạnh. Vứt bỏ những người khác

    • xóa tất cả pixel cho hạt này khỏi ảnh nguồn, sau đó lặp lại

    • cuối cùng, đi qua danh sách các loại ngũ cốc được tìm thấy và đếm từng hạt là 1 hạt, 0 hạt (quá nhỏ) hoặc 2 hạt (quá lớn)

Một trong những vấn đề chính của tôi là tôi không muốn thực hiện một thước đo khoảng cách điểm elip đầy đủ, vì tính toán rằng bản thân nó là một quá trình lặp phức tạp. Vì vậy, tôi đã sử dụng nhiều cách giải quyết khác nhau bằng cách sử dụng các hàm OpenCV Ellipse2Poly và FitEllipse và kết quả không quá đẹp.

Rõ ràng tôi cũng đã phá vỡ giới hạn kích thước cho codegolf.

Một câu trả lời được giới hạn ở 30000 ký tự, tôi hiện ở mức 34000. Vì vậy, tôi sẽ phải rút ngắn phần nào mã bên dưới.

Mã đầy đủ có thể được nhìn thấy tại http://pastebin.com/RgM7hMxq

Xin lỗi vì điều này, tôi đã không biết rằng có giới hạn kích thước.

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

}

Tôi hơi lúng túng với giải pháp này bởi vì a) Tôi không chắc liệu nó có nằm trong tinh thần của thử thách này không, và b) nó quá lớn đối với câu trả lời của codegolf và thiếu sự thanh lịch của các giải pháp khác.

Mặt khác, tôi khá hài lòng với những tiến bộ tôi đạt được trong việc dán nhãn các loại ngũ cốc, không chỉ đơn thuần là đếm chúng, vì vậy có điều đó.


Bạn biết rằng bạn có thể giảm độ dài mã đó theo độ lớn bằng cách sử dụng tên nhỏ hơn và áp dụng một số kỹ thuật chơi gôn khác;)
Trình tối ưu hóa

Có thể, nhưng tôi không muốn làm xáo trộn thêm giải pháp này. Nó quá khó hiểu với sở thích của tôi vì nó là như vậy :)
HugoRune

+1 cho nỗ lực và bởi vì bạn là người duy nhất tìm ra cách hiển thị riêng lẻ từng hạt. Thật không may, mã là một chút cồng kềnh và phụ thuộc rất nhiều vào các hằng số được mã hóa cứng. Tôi sẽ tò mò muốn xem thuật toán scanline tôi đã viết thực hiện như thế nào về điều này (trên các hạt màu bất biến).
tigrou

Tôi thực sự nghĩ rằng đây là cách tiếp cận phù hợp cho loại vấn đề này (+1 cho bạn), nhưng có một điều tôi tự hỏi, tại sao bạn "chọn 10 pixel cạnh ngẫu nhiên", tôi sẽ nghĩ rằng bạn sẽ có hiệu suất tốt hơn nếu bạn chọn các điểm cạnh có số điểm cạnh gần nhất thấp nhất (nghĩa là các phần dính ra), tôi nghĩ (về mặt lý thuyết) điều này sẽ loại bỏ các hạt "dễ nhất" trước tiên, bạn đã xem xét điều này chưa?
David Rogers

Tôi đã nghĩ về nó, nhưng chưa thử nó. '10 vị trí bắt đầu ngẫu nhiên 'là một bổ sung muộn, dễ thêm và dễ song song. Trước đó, "một vị trí bắt đầu ngẫu nhiên" tốt hơn nhiều so với "luôn luôn là góc trên cùng bên trái". Nguy hiểm của việc chọn vị trí bắt đầu với cùng một chiến lược mỗi lần là khi tôi loại bỏ vị trí phù hợp nhất, 9 người còn lại có thể sẽ được chọn lại vào lần tới và theo thời gian, những vị trí bắt đầu tồi tệ nhất sẽ ở lại và được chọn lại và lần nữa. Một phần nhô ra có thể chỉ là phần còn lại của hạt bị loại bỏ hoàn toàn trước đó.
HugoRune

17

C ++, OpenCV, điểm: 9

Ý tưởng cơ bản về phương pháp của tôi khá đơn giản - cố gắng xóa các hạt đơn (và "hạt kép" - 2 hạt (nhưng không nhiều hơn!), Gần nhau) khỏi hình ảnh và sau đó đếm phần còn lại bằng phương pháp dựa trên diện tích (như Falko, Ell và belisarius). Sử dụng phương pháp này tốt hơn một chút so với "phương pháp diện tích" tiêu chuẩn, bởi vì dễ dàng tìm thấy giá trị AveragePixelsPerObject tốt.

(Bước 1) Trước hết, chúng ta cần sử dụng nhị phân Otsu trên kênh hình ảnh S trong HSV. Bước tiếp theo là sử dụng toán tử giãn để cải thiện chất lượng của tiền cảnh được trích xuất. Hơn chúng ta cần tìm đường viền. Tất nhiên một số đường viền không phải là hạt gạo - chúng ta cần xóa các đường viền quá nhỏ (với diện tích nhỏ hơn thì AveragePixelsPerObject / 4. AveragePixelsPerObject là 2855 trong tình huống của tôi). Bây giờ cuối cùng chúng ta cũng có thể bắt đầu đếm hạt :) (bước 2) Tìm hạt đơn và hạt khá đơn giản - chỉ cần tìm trong danh sách đường viền cho các đường viền có diện tích trong phạm vi cụ thể - nếu vùng đường viền nằm trong phạm vi, hãy xóa nó khỏi danh sách và thêm 1 (hoặc 2 nếu đó là hạt "gấp đôi") với hạt truy cập. (Bước 3) Bước cuối cùng dĩ nhiên là chia diện tích của các đường viền còn lại theo giá trị AveragePixelsPerObject và thêm kết quả vào bộ đếm hạt.

Hình ảnh (đối với hình ảnh F.jpg) sẽ hiển thị ý tưởng này tốt hơn từ ngữ:
Bước 1 (không có đường viền nhỏ (nhiễu)): Bước 1 (không có đường viền nhỏ (nhiễu))
Bước 2 - chỉ các đường viền đơn giản: Bước 2 - chỉ các đường viền đơn giản
Bước 3 - các đường viền còn lại: Bước 3 - các đường viền còn lại

Đây là mã, nó khá xấu, nhưng sẽ hoạt động mà không gặp vấn đề gì. Tất nhiên OpenCV là bắt buộc.

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

Nếu bạn muốn xem kết quả của tất cả các bước, hãy bỏ qua tất cả các lệnh gọi hàm imshow (.., ..) và đặt biến fastProcessing thành false. Hình ảnh (A.jpg, B.jpg, ...) phải có trong hình ảnh thư mục. Ngoài ra, bạn có thể đặt tên của một hình ảnh dưới dạng tham số từ dòng lệnh.

Tất nhiên nếu một cái gì đó không rõ ràng tôi có thể giải thích nó và / hoặc cung cấp một số hình ảnh / thông tin.


12

C # + OpenCvSharp, điểm: 71

Đây là điều khó chịu nhất, tôi đã cố gắng để có được một giải pháp thực sự xác định từng hạt bằng cách sử dụng lưu vực , nhưng tôi chỉ. không thể được. nó đến. công việc.

Tôi giải quyết cho một giải pháp ít nhất là tách một số hạt riêng lẻ và sau đó sử dụng những hạt đó để ước tính kích thước hạt trung bình. Tuy nhiên, cho đến nay tôi không thể đánh bại các giải pháp với kích thước hạt cứng.

Vì vậy, điểm nổi bật chính của giải pháp này: nó không giả định kích thước pixel cố định cho các hạt và sẽ hoạt động ngay cả khi máy ảnh được di chuyển hoặc loại gạo bị thay đổi.

A.jpg; số hạt: 3; dự kiến ​​3; lỗi 0; pixel mỗi hạt: 2525,0;
B.jpg; số lượng hạt: 7; dự kiến ​​5; lỗi 2; pixel mỗi hạt: 1920,0;
C.jpg; số hạt: 6; dự kiến ​​12; lỗi 6; pixel mỗi hạt: 4242,5;
D.jpg; số hạt: 23; dự kiến ​​25; lỗi 2; pixel mỗi hạt: 2415,5;
E.jpg; số hạt: 47; dự kiến ​​50; lỗi 3; pixel mỗi hạt: 2729,9;
F.jpg; số lượng hạt: 65; dự kiến ​​83; lỗi 18; pixel mỗi hạt: 2860,5;
G.jpg; số lượng hạt: 120; dự kiến ​​120; lỗi 0; pixel mỗi hạt: 2552,3;
H.jpg; số lượng hạt: 159; dự kiến ​​150; lỗi 9; pixel mỗi hạt: 2624,7;
I.jpg; số hạt: 141; dự kiến ​​151; lỗi 10; pixel mỗi hạt: 2697,4;
J.jpg; số lượng hạt: 179; dự kiến ​​200; lỗi 21; pixel mỗi hạt: 2847,1;
tổng lỗi: 71

Giải pháp của tôi hoạt động như thế này:

Tách nền trước bằng cách chuyển đổi hình ảnh thành HSV và áp dụng ngưỡng Otsu trên kênh bão hòa. Điều này rất đơn giản, hoạt động cực kỳ tốt và tôi muốn giới thiệu nó cho những người khác muốn thử thách này:

saturation channel                -->         Otsu thresholding

nhập mô tả hình ảnh ở đây -> nhập mô tả hình ảnh ở đây

Điều này sẽ xóa sạch nền.

Sau đó, tôi cũng loại bỏ các bóng hạt từ nền trước, bằng cách áp dụng một ngưỡng cố định cho kênh giá trị. (Không chắc điều đó có thực sự giúp ích nhiều không, nhưng nó đủ đơn giản để thêm vào)

nhập mô tả hình ảnh ở đây

Sau đó, tôi áp dụng một biến đổi khoảng cách trên hình ảnh nền trước.

nhập mô tả hình ảnh ở đây

và tìm tất cả các cực đại cục bộ trong biến đổi khoảng cách này.

Đây là nơi ý tưởng của tôi bị phá vỡ. để tránh bị cực đại cục bộ trong cùng một hạt, tôi phải lọc rất nhiều. Hiện tại tôi chỉ giữ mức tối đa mạnh nhất trong bán kính 45 pixel, điều đó có nghĩa là không phải hạt nào cũng có mức tối đa cục bộ. Và tôi thực sự không có lý do nào cho bán kính 45 pixel, đó chỉ là một giá trị hoạt động.

nhập mô tả hình ảnh ở đây

(như bạn có thể thấy, những hạt đó gần như không đủ để tính cho mỗi hạt)

Sau đó, tôi sử dụng các cực đại đó làm hạt giống cho thuật toán đầu nguồn:

nhập mô tả hình ảnh ở đây

Kết quả là meh . Tôi đã hy vọng cho hầu hết các hạt riêng lẻ, nhưng các khối vẫn còn quá lớn.

Bây giờ tôi xác định các đốm màu nhỏ nhất, đếm kích thước pixel trung bình của chúng và sau đó ước tính số lượng hạt từ đó. Đây không phải là những gì tôi dự định làm lúc đầu, nhưng đây là cách duy nhất để cứu vãn điều này.

sử dụng hệ thống ; 
sử dụng hệ thống . Bộ sưu tập . Chung chung ; 
sử dụng hệ thống . Linq ; 
sử dụng hệ thống . Văn bản ; 
sử dụng OpenCvSharp ;

không gian tên GrainTest2 { lớp Chương trình { 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 [] kỳ vọng

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

            int tổngError = 0 ; int TotalPixels = 0 ; 
             

            cho ( int fileNo = 0 ; fileNo dấu = mới Danh sách (); 
                    sử dụng ( CvMemStorage lưu trữ = mới CvMemStorage ()) 
                    sử dụng ( CvContourScanner quét = mới CvContourScanner ( localMaxima , lưu trữ , CvContour . sizeof , ContourRetrieval . External , ContourChain . ApproxNone ))         
                    { // đặt từng mức tối đa cục bộ là số hạt giống 25, 35, 45, ... // (số thực tế không quan trọng, được chọn để hiển thị tốt hơn trong png) int MarkerNo = 20 ; foreach ( CvSeq c trong máy quét ) { 
                            MarkerNo + = 5 ; 
                            đánh dấu . Thêm ( đánh dấu Không ); 
                            WaterShedMarkers . DrawContours ( c , CvScalar mới ( đánh dấu ), mới
                        
                        
                         
                         
                             CvScalar ( đánh dấu Không ), 0 , - 1 ); } } 
                    WaterShedMarkers . SaveImage ( "08-watershed-seed.png" );  
                        
                    


                    nguồn . Đầu nguồn ( waterShedMarkers ); 
                    WaterShedMarkers . SaveImage ( "09-watershed-result.png" );


                    Danh sách pixelPerBlob = Danh sách mới ();  

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

        }
    }
}

Một thử nghiệm nhỏ sử dụng kích thước pixel trên mỗi hạt được mã hóa cứng là 2544,4 cho thấy tổng lỗi là 36, vẫn lớn hơn hầu hết các giải pháp khác.

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây


Tôi nghĩ bạn có thể sử dụng ngưỡng (hoạt động ăn mòn cũng có thể hữu ích) với một số giá trị nhỏ do kết quả của biến đổi khoảng cách - điều này sẽ chia một số nhóm hạt thành các nhóm nhỏ hơn (tốt nhất là - chỉ với 1 hoặc 2 hạt). Hơn nó sẽ dễ dàng hơn để đếm những hạt cô đơn. Các nhóm lớn bạn có thể tính là hầu hết mọi người ở đây - chia diện tích cho diện tích trung bình của hạt đơn.
cyriel

9

HTML + Javascript: Điểm 39

Các giá trị chính xác là:

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

Nó phá vỡ (không chính xác) trên các giá trị lớn hơn.

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>

Giải thích: Về cơ bản, đếm số pixel gạo và chia nó cho các pixel trung bình trên mỗi hạt.


Sử dụng hình ảnh 3 hạt gạo, nó ước tính 0 cho tôi ...: /
Kroltan

1
@Kroltan Không phải khi bạn sử dụng hình ảnh kích thước đầy đủ .
Sở thích của Calvin

1
@ Calvin'sHob sở thích FF36 trên Windows được 0, trên Ubuntu được 3, với hình ảnh kích thước đầy đủ.
Kroltan

4
@BulkJack Gạo được đảm bảo ở cùng một tỷ lệ trên các hình ảnh. Tôi thấy không có vấn đề với nó.
Sở thích của Calvin

1
@githubphagocyte - một lời giải thích khá rõ ràng - nếu bạn đếm tất cả các pixel trắng trên kết quả của quá trình nhị phân hình ảnh và chia số này cho số hạt trong ảnh bạn sẽ nhận được kết quả này. Tất nhiên kết quả chính xác có thể khác nhau, bởi vì phương pháp nhị phân đã sử dụng và các công cụ khác (như các thao tác được thực hiện sau khi thực hiện nhị phân), nhưng như bạn có thể thấy trong các câu trả lời khác, nó sẽ nằm trong phạm vi 2500-3500.
cyriel

4

Một nỗ lực với php, Không phải là câu trả lời cho điểm thấp nhất nhưng mã khá đơn giản của nó

ĐIỂM: 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);
}

Tự chấm điểm

95 là giá trị màu xanh dường như hoạt động khi thử nghiệm với GIMP 2966 là cỡ hạt trung bình

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.