쌀 알갱이 계산


81

다양한 양의 생 쌀된 백미의 10 가지 이미지를 고려하십시오.
이들은 단지 THUMBNAILS입니다. 이미지를 클릭하면 전체 크기로 볼 수 있습니다.

A : B : C : D : E :에이 비 씨 디 이자형

F : G : H : I : J :에프 지 H 나는 제이

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

그것을주의해라...

  • 결은 서로 닿을 수 있지만 결코 겹치지 않습니다. 곡물의 레이아웃은 한 개 이상의 곡물보다 높지 않습니다.
  • 이미지의 크기는 다르지만 카메라와 배경이 고정되어 있기 때문에 모든 쌀 크기가 일정합니다.
  • 그레인은 결코 경계를 벗어나거나 이미지 경계에 닿지 않습니다.
  • 배경은 항상 똑같은 일관된 황백색입니다.
  • 작고 큰 곡물은 각각 하나의 곡물로 간주됩니다.

이 5 점은 이러한 종류의 모든 이미지를 보장합니다.

도전

그러한 이미지를 취하고 가능한 한 정확하게 쌀알의 수를 세는 프로그램을 작성하십시오.

프로그램은 이미지의 파일 이름을 가져 와서 그레인 수를 계산해야합니다. 프로그램은 JPEG, 비트 맵, PNG, GIF, TIFF 이미지 파일 형식 중 하나 이상에서 작동해야합니다 (현재 이미지는 모두 JPEG 임).

이미지 처리 및 컴퓨터 비전 라이브러리를 사용할 수 있습니다 .

10 개의 예제 이미지 출력을 하드 코딩 할 수 없습니다. 알고리즘은 모든 유사한 쌀알 이미지에 적용 할 수 있어야합니다. 이미지 영역이 2000 * 2000 픽셀 미만이고 쌀이 300 곡물 미만인 경우 괜찮은 현대 컴퓨터 에서 5 분 이내에 실행할 수 있어야합니다 .

채점

10 개의 이미지 각각에 대해 실제 입자 수의 절대 값에서 프로그램이 예측하는 입자 수를 뺀 값을 취하십시오. 이 절대 값을 합산하여 점수를 얻으십시오. 가장 낮은 점수가 이깁니다. 0 점 만점입니다.

동점 인 경우 가장 높은 투표 응답이 이깁니다. 추가 이미지에서 프로그램을 테스트하여 유효성과 정확성을 확인할 수 있습니다.


1
분명히 누군가는 scikit-learn을 시도해야합니다!

좋은 콘테스트! :) Btw-이 도전의 종료일에 대해 말씀해 주시겠습니까?
cyriel

1
@Lembik 아래로 7 :)
Dr. belisarius

5
언젠가 쌀 과학자가 와서이 질문이 존재한다는 것을 기쁘게 생각합니다.
Nit

2
@Nit 그냥 그들에게 ncbi.nlm.nih.gov/pmc/articles/PMC3510117를 알려주십시오 :)
Dr. belisarius

답변:


22

매스 매 티카, 7 점

i = {"http://i.stack.imgur.com/8T6W2.jpg",  "http://i.stack.imgur.com/pgWt1.jpg", 
     "http://i.stack.imgur.com/M0K5w.jpg",  "http://i.stack.imgur.com/eUFNo.jpg", 
     "http://i.stack.imgur.com/2TFdi.jpg",  "http://i.stack.imgur.com/wX48v.jpg", 
     "http://i.stack.imgur.com/eXCGt.jpg",  "http://i.stack.imgur.com/9na4J.jpg",
     "http://i.stack.imgur.com/UMP9V.jpg",  "http://i.stack.imgur.com/nP3Hr.jpg"};

im = Import /@ i;

함수의 이름은 충분히 설명 적이라고 생각합니다.

getSatHSVChannelAndBinarize[i_Image]             := Binarize@ColorSeparate[i, "HSB"][[2]]
removeSmallNoise[i_Image]                        := DeleteSmallComponents[i, 100]
fillSmallHoles[i_Image]                          := Closing[i, 1]
getMorphologicalComponentsAreas[i_Image]         := ComponentMeasurements[i, "Area"][[All, 2]]
roundAreaSizeToGrainCount[areaSize_, grainSize_] := Round[areaSize/grainSize]

모든 사진을 한 번에 처리 :

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

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

점수는 다음과 같습니다

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

여기에 사용 된 입자 크기의 점수 민감도를 볼 수 있습니다.

Mathematica 그래픽


2
훨씬 더 명확합니다, 감사합니다!
Calvin 's Hobbies

이 정확한 절차를 파이썬으로 복사 할 수 있습니까? 아니면 파이썬 라이브러리가 할 수없는 특별한 Mathematica가 여기에서하고 있습니까?

@Lembik 몰라요. 여기에 파이썬이 없습니다. 죄송합니다. (그러나, 나는에 대해 동일한 알고리즘을 의심 EdgeDetect[], DeleteSmallComponents[]그리고 Dilation[]다른 곳에서 구현)
박사 벨리 사리우스

55

파이썬, 점수 : 24 16

Falko의 솔루션과 마찬가지로이 솔루션은 "전경"영역을 측정하고 평균 곡물 영역으로 나눕니다.

실제로,이 프로그램이 감지하려고하는 것은 포 그라운드만큼이나 배경이 아닙니다. 쌀알이 이미지 경계에 절대로 닿지 않는다는 사실을 이용하여 프로그램은 왼쪽 상단 모서리에 흰색으로 물을 채 웁니다. 플러드 필 알고리즘은 현재 픽셀의 밝기와 현재 픽셀의 밝기 차이가 특정 임계 값 내에 있으면 인접한 픽셀을 페인트하여 배경색의 점진적인 변화에 맞춰 조정합니다. 이 단계가 끝나면 이미지가 다음과 같이 보일 수 있습니다.

그림 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

에이 비 씨 디 이자형

에프 지 H 나는 제이


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.이 주어지면 이는 해당 규칙 을 나타내는 값일뿐입니다. 그러나 입력에 따라 결과가 변경됩니다. 규칙을 변경하면이 값이 변경되지만 입력에 따라 결과는 동일합니다.
Adam Davis

6
나는 평균 지역 물건으로 괜찮습니다. 입자 영역은 이미지 전체에서 (거의) 일정합니다.
Calvin 's Hobbies

28

파이썬 + OpenCV : 27 점

가로줄 스캔

아이디어 : 이미지를 한 번에 한 줄씩 스캔합니다. 각 줄에 대해 발생하는 쌀알 수를 세십시오 (픽셀이 검은 색으로 바뀌는 지 또는 반대인지 확인하여). 선의 결 수가 증가하면 (이전 선과 비교하여) 새로운 결이 발견 된 것입니다. 그 숫자가 줄어들면, 우리는 곡물을 통과했음을 의미합니다. 이 경우 총 결과에 +1을 추가하십시오.

여기에 이미지 설명을 입력하십시오

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

알고리즘이 작동하는 방식 때문에 깨끗한 흑백 이미지를 갖는 것이 중요합니다. 소음이 많으면 결과가 좋지 않습니다. 첫 번째 기본 배경은 플러드 필 (Ell 답변과 유사한 솔루션)을 사용하여 청소 한 다음 임계 값을 적용하여 흑백 결과를 생성합니다.

여기에 이미지 설명을 입력하십시오

그것은 완벽하지는 않지만 단순성과 관련하여 좋은 결과를 낳습니다. 아마도 더 나은 흑백 이미지를 제공하고 다른 방향으로 스캔 (예 : 수직, 대각선)하여 평균을 취하는 등 여러 가지 방법이 있습니다.

import cv2
import numpy
import sys

filename = sys.argv[1]
I = cv2.imread(filename, 0)
h,w = I.shape[:2]
diff = (3,3,3)
mask = numpy.zeros((h+2,w+2),numpy.uint8)
cv2.floodFill(I,mask,(0,0), (255,255,255),diff,diff)
T,I = cv2.threshold(I,180,255,cv2.THRESH_BINARY)
I = cv2.medianBlur(I, 7)

totalrice = 0
oldlinecount = 0
for y in range(0, h):
    oldc = 0
    linecount = 0
    start = 0   
    for x in range(0, w):
        c = I[y,x] < 128;
        if c == 1 and oldc == 0:
            start = x
        if c == 0 and oldc == 1 and (x - start) > 10:
            linecount += 1
        oldc = c
    if oldlinecount != linecount:
        if linecount < oldlinecount:
            totalrice += oldlinecount - linecount
        oldlinecount = linecount
print totalrice

이미지 당 오류 : 0, 0, 0, 3, 0, 12, 4, 0, 7, 1


24

Python + OpenCV : 점수 84

첫 번째 순진한 시도가 있습니다. 수동으로 조정 된 매개 변수로 적응 임계 값을 적용하고 후속 침식 및 희석으로 일부 구멍을 닫고 전경 영역에서 곡물 수를 유도합니다.

import cv2
import numpy as np

filename = raw_input()

I = cv2.imread(filename, 0)
I = cv2.medianBlur(I, 3)
bw = cv2.adaptiveThreshold(I, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 101, 1)

kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (17, 17))
bw = cv2.dilate(cv2.erode(bw, kernel), kernel)

print np.round_(np.sum(bw == 0) / 3015.0)

중간 바이너리 이미지를 볼 수 있습니다 (검정색은 전경).

여기에 이미지 설명을 입력하십시오

이미지 당 오류는 0, 0, 2, 2, 4, 0, 27, 42, 0 및 7 그레인입니다.


20

C # + OpenCvSharp, 점수 : 2

이것은 나의 두 번째 시도입니다. 그것은 첫 번째 시도 와 는 상당히 다르 므로 훨씬 간단하므로 별도의 솔루션으로 게시하고 있습니다.

기본 아이디어는 반복적 인 타원 맞춤으로 각 개별 그레인을 식별하고 레이블을 지정하는 것입니다. 그런 다음 소스에서이 그레인에 대한 픽셀을 제거하고 모든 픽셀에 레이블이 지정 될 때까지 다음 그레인을 찾으십시오.

이것은 가장 예쁜 해결책이 아닙니다. 600 줄의 코드를 가진 거대한 돼지입니다. 가장 큰 이미지는 1.5 분이 소요됩니다. 지저분한 코드에 대해 사과드립니다.

이 문제에 대해 생각할 매개 변수와 방법이 너무 많아서 10 샘플 이미지에 대한 프로그램을 과도하게 맞추는 것을 두려워합니다. 2의 최종 점수는 거의 확실하게 과적 합의 경우입니다. 나는 두 개의 매개 변수가 있습니다 average grain size in pixel. 그리고 minimum ratio of pixel / elipse_area, 그리고 마지막으로 가장 낮은 점수를 얻을 때 까지이 두 매개 변수의 모든 조합을 소진했습니다. 이것이이 도전의 규칙에 정통한 것인지 확실하지 않습니다.

average_grain_size_in_pixel = 2530
pixel / elipse_area >= 0.73

그러나 이러한 과적 합 클러치가 없어도 결과는 매우 좋습니다. 고정 입자 크기 또는 픽셀 비율이 없으면 단순히 훈련 이미지에서 평균 입자 크기를 추정하면 점수는 여전히 27입니다.

그리고 숫자뿐만 아니라 각 그레인의 실제 위치, 방향 및 모양을 출력으로 얻습니다. 레이블이 잘못 지정된 입자는 적지 만 대부분의 레이블은 실제 그레인과 정확하게 일치합니다.

A 에이 B 비 C 씨 D 디 E이자형

F 에프 G 지 H H I 나는 J제이

(전체 크기 버전의 각 이미지를 클릭하십시오)

이 라벨링 단계 후에 내 프로그램은 각 개별 입자를보고 픽셀 수와 픽셀 / 타원 면적 비율을 기준으로 추정합니다.

  • 단일 그레인 (+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에서는 레이블이 대부분 정확합니다.

이 개념은 약간 고안되었습니다.

  • 먼저 채도 채널에서 otsu-thresholding을 통해 포 그라운드를 분리합니다 (자세한 내용은 이전 답변 참조).

  • 더 이상 픽셀이 남지 않을 때까지 반복하십시오.

    • 가장 큰 얼룩을 선택하십시오
    • 이 얼룩에서 입자의 시작 위치로 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);
    }

}

a)이 도전의 정신 내에 있는지 확실하지 않으며 b) 코드 골프 응답에 비해 너무 커서 다른 솔루션의 우아함이 부족하기 때문에이 솔루션에 조금 당황합니다.

다른 한편으로, 나는 곡물을 세는 것만이 아니라 곡물 에 라벨을 붙이는 과정에서 성취 한 것에 매우 만족 합니다.


더 작은 이름을 사용하고 다른 골프 기술을 적용하면 코드 길이를 크게 줄일 수 있습니다.)
Optimizer

아마도이 솔루션을 더 혼란스럽게하고 싶지 않았습니다. 그것은 내 취향으로 너무 난독 화됩니다 :)
HugoRune 20:48에

각 곡물을 개별적으로 표시하는 방법을 찾는 유일한 사람이기 때문에 노력 +1. 불행히도 코드는 약간 부풀어 오르며 하드 코딩 된 상수에 많이 의존합니다. 필자가 작성한 스캔 라인 알고리즘이 (개별 색상 그레인)에서 어떻게 수행되는지 궁금합니다.
tigrou

나는 이것이 이것이 이런 유형의 문제 (+1)에 대한 올바른 접근법이라고 생각하지만, 한 가지 궁금한 점은 왜 "랜덤 엣지 픽셀 10 개를 선택합니까?"를 선택하면 더 나은 성능을 얻을 것이라고 생각할 것입니다. 주변 에지 포인트 수가 가장 적은 에지 포인트 (즉, 튀어 나온 부분)는 (이론적으로) 이것이 가장 쉬운 곡물을 먼저 제거 할 것이라고 생각합니까?
David Rogers

나는 그것을 생각했지만 아직 시도하지 않았습니다. '10 개의 랜덤 시작 위치 '는 늦게 추가되었으며 추가 및 병렬화가 용이했습니다. 그 전에는 '하나의 임의 시작 위치'가 '항상 왼쪽 위 모서리'보다 훨씬 낫습니다. 매번 같은 전략으로 시작 위치를 선택할 때의 위험은 내가 가장 잘 맞는 것을 제거하면 다음에 다른 9 개가 다시 선택되고 시간이 지남에 따라 시작 위치 중 최악의 위치가 다시 유지되고 다시 선택됩니다. 다시. 튀어 나온 부분은 완전히 제거되지 않은 이전 곡물의 잔 유일 수 있습니다.
HugoRune

17

C ++, OpenCV, 9 점

내 방법의 기본 아이디어는 매우 간단합니다. 이미지에서 단일 그레인 (및 "더블 그레인"-2 개 (그러나 그 이상은 아닙니다!) 그레인)을 지우고 영역 (Falko, Ell and belisarius). 이 방법을 사용하면 standardPixelsPerObject 값을 쉽게 찾을 수 있기 때문에 표준 "영역 방법"보다 약간 낫습니다.

(1 단계) 우선 HSV에서 이미지의 S 채널에 Otsu 이진화를 사용해야합니다. 다음 단계는 확장 연산자를 사용하여 추출 된 전경의 품질을 향상시키는 것입니다. 우리는 윤곽을 찾아야합니다. 물론 일부 윤곽선은 쌀 알갱이가 아닙니다. 너무 작은 윤곽선을 삭제해야합니다 (averagePixelsPerObject / 4보다 작은 영역을 사용합니다. 내 상황에서는 averagePixelsPerObject가 2855 임). 이제 마지막으로 그레인 수를 계산할 수 있습니다 :) (2 단계) 단일 및 이중 그레인을 찾는 것은 매우 간단합니다. 특정 범위 내의 영역이있는 윤곽선의 윤곽선 목록에서 윤곽 영역이 범위 내에 있으면 목록에서 삭제하고 1을 추가하십시오 (또는 "이중"곡물 인 경우 2) 곡물 카운터에. (3 단계) 마지막 단계는 물론 남은 윤곽선의 면적을 averagePixelsPerObject 값으로 나누고 결과를 그레인 카운터에 추가하는 것입니다.

(이미지 F.jpg에 대한) 이미지는 말보다 더이 아이디어를 표시해야합니다
: 1 단계 (작은 윤곽 (소음) 제외) 1 단계 (작은 윤곽 (소음)없이)
- 단순한 윤곽 2 단계 : 2 단계-간단한 윤곽 만
3 단계 - 나머지 윤곽 : 3 단계-남은 윤곽선

여기 코드가 있지만, 추악하지만 문제없이 작동해야합니다. 물론 OpenCV가 필요합니다.

#include "stdafx.h"

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

using namespace cv;
using namespace std;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return 0;
}

모든 단계의 결과를 보려면 모든 imshow (......) 함수 호출을 주석 해제하고 fastProcessing 변수를 false로 설정하십시오. 이미지 (A.jpg, B.jpg, ...)는 디렉토리 이미지에 있어야합니다. 또는 명령 행에서 하나의 이미지 이름을 매개 변수로 지정할 수 있습니다.

물론 분명하지 않은 것이 있으면이를 설명하거나 이미지 / 정보를 제공 할 수 있습니다.


12

C # + OpenCvSharp, 점수 : 71

이것은 가장 까다 롭습니다 . 유역을 사용하여 각 곡물을 실제로 식별하는 솔루션을 얻으려고 했지만 그저 그저. 캔트. 가져 오기. 그것. 에. 작업.

나는 적어도 분리 솔루션을 정착 일부 개별 입자를 한 후 평균 입자 크기를 추정하는 곡물을 사용합니다. 그러나 지금까지 하드 코딩 된 입자 크기로 솔루션을 이길 수는 없습니다.

따라서이 솔루션의 주요 특징은 곡물의 고정 픽셀 크기를 가정하지 않으며 카메라를 움직이거나 쌀 유형이 변경 된 경우에도 작동해야합니다.

A.jpg; 곡물 수 : 3; 예상 3; 오류 0; 그레 인당 픽셀 : 2525,0;
B.jpg; 곡물 수 : 7; 예상 5; 오류 2; 입자 당 픽셀 수 : 1920,0;
C.jpg; 곡물 수 : 6; 예상 12; 오류 6; 그레 인당 픽셀 : 4242,5;
D.jpg; 곡물 수 : 23; 예상 25; 오류 2; 그레 인당 픽셀 : 2415,5;
E.jpg; 곡물 수 : 47; 예상 50; 오류 3; 그레 인당 픽셀 : 2729,9;
F.jpg; 곡물 수 : 65; 예상 83; 오류 18; 그레 인당 픽셀 : 2860,5;
G.jpg; 곡물 수 : 120; 예상 120; 오류 0; 그레 인당 픽셀 : 2552,3;
H.jpg; 곡물 수 : 159; 예상 150; 오류 9; 그레 인당 픽셀 : 2624,7;
I.jpg; 곡물 수 : 141; 예상 151; 오류 10; 그레 인당 픽셀 : 2697,4;
J.jpg; 곡물 수 : 179; 예상 200; 오류 21; 그레 인당 픽셀 : 2847,1;
총 오류 : 71

내 솔루션은 다음과 같이 작동합니다.

이미지를 HSV 로 변환 하고 채도 채널에서 Otsu 임계 값 을 적용 하여 전경을 분리하십시오 . 이것은 매우 간단하고 매우 잘 작동 하며이 도전을 시도하려는 다른 모든 사람들에게 권장합니다.

saturation channel                -->         Otsu thresholding

여기에 이미지 설명을 입력하십시오 -> 여기에 이미지 설명을 입력하십시오

배경이 깨끗하게 제거됩니다.

그런 다음 값 채널에 고정 임계 값을 적용하여 전경에서 그레인 그림자 를 추가로 제거했습니다 . (실제로 많은 도움이되는지 확실하지 않지만 추가하기에는 간단했습니다)

여기에 이미지 설명을 입력하십시오

그런 다음 전경 이미지에 거리 변환 을 적용합니다 .

여기에 이미지 설명을 입력하십시오

이 거리 변환에서 모든 로컬 최대 값을 찾으십시오.

이것은 내 생각이 무너지는 곳입니다. 같은 곡물 내에서 여러 로컬 최대 값을 얻지 않으려면 많이 필터링해야합니다. 현재 45 픽셀 반경 내에서 가장 강한 최대 값 만 유지하므로 모든 그레인에 로컬 최대 값이있는 것은 아닙니다. 그리고 나는 45 픽셀 반경에 대한 정당성을 가지고 있지 않습니다. 그것은 단지 효과가 있었던 값이었습니다.

여기에 이미지 설명을 입력하십시오

(보시다시피, 각 곡물을 설명하기에는 씨앗이 거의 없습니다)

그런 다음 유역 알고리즘의 씨앗으로 최대 값을 사용합니다.

여기에 이미지 설명을 입력하십시오

결과는 meh 입니다. 나는 주로 개별 곡물을 원했지만 덩어리는 여전히 큽니다.

이제 가장 작은 얼룩을 식별하고 평균 픽셀 크기를 계산 한 다음 그레인 수를 추정합니다. 입니다 하지 내가 시작 할 계획하지만, 문제는이를 구제 할 수있는 유일한 방법은 무엇인지.

시스템 사용 ; 시스템 
사용 . 컬렉션 . 일반 ; 시스템 
사용 . Linq ; 시스템 
사용 . 텍스트 ; OpenCvSharp 
사용 ;

namespace GrainTest2 { class Program { static void Main ( string [] args ) { string [] files = new [] { "sourceA.jpg" , "sourceB.jpg" , "sourceC.jpg" , "sourceD.jpg" , " sourceE.jpg " , "sourceF.jpg " , "sourceG.jpg " , "sourceH.jpg " , "sourceI.jpg " , "sourceJ.jpg " , };int [] expectGrains

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

            int totalError = 0 ; int totalPixels = 0 ; 
             

            for ( int fileNo = 0 ; fileNo markers = new List (); 
                    using ( CvMemStorage storage = new CvMemStorage ()) 
                    using ( CvContourScanner scanner = new CvContourScanner ( localMaxima , storage , CvContour . SizeOf , ContourRetrieval . External , ContourChain . ApproxNone ))         
                    { // 각 로컬 최대 값을 시드 번호 25, 35, 45, ...로 설정합니다. // (실제 숫자는 중요하지 않으며 png에서 더 잘 보이 도록 선택되었습니다.) int markerNo = 20 ; foreach는 ( CvSeq의 C 에서 스캐너 ) { 
                            markerNo가 + = 5 ; 
                            마커 . 추가 ( markerNo ); 
                            waterShedMarkers . 등고선 ( c , 새로운 CvScalar ( markerNo ), 새로운
                        
                        
                         
                         
                             CvScalar ( markerNo ), 0 , - 1 ); } } 
                    waterShedMarkers . SaveImage ( "08-watershed-seeds.png" );  
                        
                    


                    소스 . 유역 ( waterShedMarkers ); 
                    waterShedMarkers . SaveImage ( "09-watershed-result.png" );


                    List pixelsPerBlob = 목록 ();  

                    // Terrible hack because I could not get Cv2.ConnectedComponents to work with this openCv wrapper
                    // So I made a workaround to count the number of pixels per blob
                    waterShedMarkers.ConvertScale(waterShedThreshold);
                    foreach (int markerNo in markers)
                    {
                        using (CvMat tmp = new CvMat(waterShedMarkers.Rows, waterShedThreshold.Cols, MatrixType.U8C1))
                        {
                            waterShedMarkers.CmpS(markerNo, tmp, ArrComparison.EQ);
                            pixelsPerBlob.Add(tmp.CountNonZero());

                        }
                    }

                    // estimate the size of a single grain
                    // step 1: assume that the 10% smallest blob is a whole grain;
                    double singleGrain = pixelsPerBlob.OrderBy(p => p).ElementAt(pixelsPerBlob.Count/15);

                    // step2: take all blobs that are not much bigger than the currently estimated singel grain size
                    //        average their size
                    //        repeat until convergence (too lazy to check for convergence)
                    for (int i = 0; i  p  Math.Round(p/singleGrain)).Sum());

                    Console.WriteLine("input: {0}; number of grains: {1,4:0.}; expected {2,4}; error {3,4}; pixels per grain: {4:0.0}; better: {5:0.}", file, numGrains, expectedGrains[fileNo], Math.Abs(numGrains - expectedGrains[fileNo]), singleGrain, pixelsPerBlob.Sum() / 1434.9);

                    totalError += Math.Abs(numGrains - expectedGrains[fileNo]);
                    totalPixels += pixelsPerBlob.Sum();

                    // this is a terrible hack to visualise the estimated number of grains per blob.
                    // i'm too tired to clean it up
                    #region please ignore
                    using (CvMemStorage storage = new CvMemStorage())
                    using (CvMat tmp = waterShedThreshold.Clone())
                    using (CvMat tmpvisu = new CvMat(source.Rows, source.Cols, MatrixType.S8C3))
                    {
                        foreach (int markerNo in markers)
                        {
                            tmp.SetZero();
                            waterShedMarkers.CmpS(markerNo, tmp, ArrComparison.EQ);
                            double curGrains = tmp.CountNonZero() * 1.0 / singleGrain;
                            using (
                                CvContourScanner scanner = new CvContourScanner(tmp, storage, CvContour.SizeOf, ContourRetrieval.External,
                                                                                ContourChain.ApproxNone))
                            {
                                tmpvisu.Set(CvColor.Random(), tmp);
                                foreach (CvSeq c in scanner)
                                {
                                    //tmpvisu.DrawContours(c, CvColor.Random(), CvColor.DarkGreen, 0, -1);
                                    tmpvisu.PutText("" + Math.Round(curGrains, 1), c.First().Value, new CvFont(FontFace.HersheyPlain, 2, 2),
                                                    CvColor.Red);
                                }

                            }


                        }
                        tmpvisu.SaveImage("10-visu.png");
                        tmpvisu.SaveImage("10-visu" + file + ".png");
                    }
                    #endregion

                }

            }
            Console.WriteLine("total error: {0}, ideal Pixel per Grain: {1:0.0}", totalError, totalPixels*1.0/expectedGrains.Sum());

        }
    }
}

2544.4의 하드 코딩 된 픽셀 당 픽셀 크기를 사용한 소규모 테스트에서 총 오류 36이 나타 났으며 이는 대부분의 다른 솔루션보다 여전히 큽니다.

여기에 이미지 설명을 입력하십시오 여기에 이미지 설명을 입력하십시오 여기에 이미지 설명을 입력하십시오 여기에 이미지 설명을 입력하십시오


거리 변환 결과에 약간의 값으로 임계 값을 사용할 수 있다고 생각합니다 (일부 작업도 유용 할 수 있음). 그러면 일부 그레인 그룹을 더 작은 그룹 (바람직하게는 1 또는 2 그레인)으로 분할해야합니다. 외로운 곡식을 세는 것이 더 쉬워야합니다. 큰 그룹은 여기에서 대부분의 사람들로 계산할 수 있습니다-면적을 단일 입자의 평균 면적으로 나눕니다.
cyriel

9

HTML + 자바 스크립트 : 점수 39

정확한 값은 다음과 같습니다.

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

더 큰 값으로 분류됩니다 (정확하지 않음).

window.onload = function() {
  var $ = document.querySelector.bind(document);
  var canvas = $("canvas"),
    ctx = canvas.getContext("2d");

  function handleFileSelect(evt) {
    evt.preventDefault();
    var file = evt.target.files[0],
      reader = new FileReader();
    if (!file) return;
    reader.onload = function(e) {
      var img = new Image();
      img.onload = function() {
        canvas.width = this.width;
        canvas.height = this.height;
        ctx.drawImage(this, 0, 0);
        start();
      };
      img.src = e.target.result;
    };
    reader.readAsDataURL(file);
  }


  function start() {
    var imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height);
    var data = imgdata.data;
    var background = 0;
    var totalPixels = data.length / 4;
    for (var i = 0; i < data.length; i += 4) {
      var red = data[i],
        green = data[i + 1],
        blue = data[i + 2];
      if (Math.abs(red - 197) < 40 && Math.abs(green - 176) < 40 && Math.abs(blue - 133) < 40) {
        ++background;
        data[i] = 1;
        data[i + 1] = 1;
        data[i + 2] = 1;
      }
    }
    ctx.putImageData(imgdata, 0, 0);
    console.log("Pixels of rice", (totalPixels - background));
    // console.log("Total pixels", totalPixels);
    $("output").innerHTML = "Approximately " + Math.round((totalPixels - background) / 2670) + " grains of rice.";
  }

  $("input").onchange = handleFileSelect;
}
<input type="file" id="f" />
<canvas></canvas>
<output></output>

설명 : 기본적으로 쌀 픽셀 수를 세어 그레 인당 평균 픽셀로 나눕니다.


3 쌀 이미지를 사용하여, 그것은 나를 위해 0을 추정 ... : /
Kroltan

1
@Kroltan 전체 크기 이미지 를 사용할 때는 아닙니다 .
Calvin 's Hobbies

1
Windows의 @ Calvin'sHobbies FF36은 0이되고 Ubuntu는 전체 크기 이미지가 3이됩니다.
Kroltan

4
@BobbyJack 쌀은 이미지에서 거의 같은 스케일로 보장됩니다. 나는 아무런 문제가 없다.
Calvin 's Hobbies

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

자기 득점

김프 2966로 테스트 할 때 평균 입자 크기가 95 일 때 작동하는 것으로 보이는 파란색 값

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.