การนับเม็ดข้าว


81

ลองพิจารณาภาพทั้ง 10 ของข้าวขาวที่ยังไม่ผ่านกระบวนการ
เหล่านี้เป็นเพียง THUMBNAILS คลิกที่ภาพเพื่อดูขนาดเต็ม

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

F: G: H: I: J:F G H ผม J

นับเม็ด: A: 3, B: 5, C: 12, D: 25, E: 50, F: 83, G: 120, H:150, I: 151, J: 200

สังเกตว่า ...

  • ธัญพืชอาจสัมผัสซึ่งกันและกัน แต่พวกเขาไม่เคยทับซ้อนกัน รูปแบบของธัญพืชไม่สูงมากกว่าหนึ่งเม็ด
  • ภาพมีขนาดแตกต่างกัน แต่ขนาดของข้าวในภาพทั้งหมดนั้นสอดคล้องกันเพราะกล้องและพื้นหลังนิ่ง
  • ธัญพืชไม่เคยออกนอกขอบเขตหรือสัมผัสกับขอบเขตของภาพ
  • พื้นหลังเป็นสีที่สอดคล้องกันของสีเหลืองอมขาวเสมอ
  • เมล็ดขนาดเล็กและขนาดใหญ่นับรวมกันเป็นหนึ่งเม็ด

5 จุดเหล่านี้รับประกันสำหรับภาพทั้งหมดของการจัดเรียงนี้

ท้าทาย

เขียนโปรแกรมที่ถ่ายภาพดังกล่าวและนับจำนวนเมล็ดข้าวอย่างแม่นยำที่สุดเท่าที่จะทำได้

โปรแกรมของคุณควรใช้ชื่อไฟล์ของภาพและพิมพ์จำนวนของธัญพืชที่คำนวณ โปรแกรมของคุณจะต้องใช้งานได้กับรูปแบบไฟล์ภาพเหล่านี้อย่างน้อยหนึ่งรูปแบบ: JPEG, Bitmap, PNG, GIF, TIFF (ขณะนี้ภาพทั้งหมดเป็น JPEG)

คุณอาจใช้การประมวลผลภาพและห้องสมุดวิสัยทัศน์คอมพิวเตอร์

คุณไม่สามารถ hardcode ผลลัพธ์ของภาพตัวอย่าง 10 ภาพ อัลกอริทึมของคุณควรใช้กับรูปภาพข้าวที่คล้ายกันทั้งหมด มันควรจะสามารถทำงานในเวลาน้อยกว่า 5 นาทีบนคอมพิวเตอร์ที่ทันสมัยถ้าพื้นที่ภาพน้อยกว่า 2,000 * 2,000 พิกเซลและมีข้าวน้อยกว่า 300 เม็ด

เกณฑ์การให้คะแนน

สำหรับแต่ละภาพ 10 ภาพใช้ค่าสัมบูรณ์ของจำนวนธัญพืชจริงลบด้วยจำนวนธัญพืชที่โปรแกรมของคุณทำนาย รวมค่าสัมบูรณ์เหล่านี้เพื่อรับคะแนนของคุณ คะแนนต่ำสุดชนะ คะแนน 0 นั้นสมบูรณ์แบบ

ในกรณีที่ผูกคำตอบที่ได้คะแนนสูงสุดจะเป็นฝ่ายชนะ ฉันอาจทดสอบโปรแกรมของคุณเกี่ยวกับภาพเพิ่มเติมเพื่อตรวจสอบความถูกต้องและความถูกต้องของมัน


1
แน่นอนว่ามีบางคนต้องลอง scikit เรียนรู้!

การประกวดที่ยอดเยี่ยม! :) Btw - บอกอะไรเราได้บ้างเกี่ยวกับวันที่สิ้นสุดของการท้าทายนี้?
cyriel

1
@Lembik ลงถึง 7 :)
ดร. เบลิซาเรี

5
อยู่มาวันหนึ่งนักวิทยาศาสตร์เกี่ยวกับข้าวกำลังจะมาพบกันและมีความสุขกับคำถามนี้
นิตย์

2
@Nit เพียงแค่บอกพวกเขาncbi.nlm.nih.gov/pmc/articles/PMC3510117 :)
ดร. เบลิซาเรี

คำตอบ:


22

Mathematica คะแนน: 7

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

im = Import /@ i;

ฉันคิดว่าชื่อฟังก์ชั่นนั้นมีความหมายเพียงพอ:

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

กำลังประมวลผลรูปภาพทั้งหมดในครั้งเดียว:

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

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

คะแนนคือ:

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

ที่นี่คุณสามารถดูความไวของคะแนนได้โดยใช้ขนาดเกรน:

กราฟิกทางคณิตศาสตร์


2
ชัดเจนยิ่งขึ้นขอบคุณ!
งานอดิเรกของ Calvin

สามารถคัดลอกโพรซีเดอร์ที่แน่นอนนี้ในไพ ธ อนหรือมีบางอย่างที่ Mathematica กำลังทำอยู่ที่ไลบรารี่ของไพ ธ อนไม่สามารถทำได้?

@ Lembik ไม่มีความคิด หลามไม่มีที่นี่ ขอโทษ ( แต่ผมสงสัยขั้นตอนวิธีการเดียวที่แน่นอนสำหรับEdgeDetect[], DeleteSmallComponents[]และDilation[]มีการดำเนินการอื่น ๆ )
ดร. เบลิซาเรี

55

Python คะแนน: 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

ใช้ชื่อไฟล์อินพุตผ่านบรรทัด comand

ผล

      Actual  Estimate  Abs. Error
A         3         3           0
B         5         5           0
C        12        12           0
D        25        25           0
E        50        48           2
F        83        83           0
G       120       116           4
H       150       145           5
I       151       156           5
J       200       200           0
                        ----------
                Total:         16

A B C D E

F G H ผม J


2
นี่เป็นทางออกที่ฉลาดจริงๆใช้งานได้ดี!
Chris Cirefice

1
ที่ไม่avg_grain_area = 3038.38;มาจากไหน?
njzk2

2
ไม่นับเป็นhardcoding the resultหรือ
njzk2

5
@ njzk2 ไม่ได้รับกฎThe images have different dimensions but the scale of the rice in all of them is consistent because the camera and background were stationary.นี่เป็นเพียงค่าที่แสดงถึงกฎนั้น อย่างไรก็ตามผลลัพธ์จะเปลี่ยนแปลงไปตามอินพุต หากคุณเปลี่ยนกฎค่านี้จะเปลี่ยน แต่ผลลัพธ์จะเหมือนกัน - ตามอินพุต
Adam Davis

6
ฉันสบายดีกับค่าเฉลี่ยของพื้นที่ พื้นที่เกรนเป็นค่าคงที่ (ประมาณ) สำหรับภาพ
งานอดิเรกของ Calvin

28

Python + OpenCV: คะแนน 27

การสแกนเส้นแนวนอน

แนวคิด: สแกนภาพทีละแถว สำหรับแต่ละบรรทัดให้นับจำนวนเมล็ดข้าวที่พบ (โดยการตรวจสอบว่าพิกเซลเปลี่ยนเป็นสีดำเป็นสีขาวหรือตรงข้าม) หากจำนวนธัญพืชสำหรับสายเพิ่มขึ้น (เทียบกับบรรทัดก่อนหน้า) นั่นหมายความว่าเราพบเมล็ดใหม่ ถ้าจำนวนนั้นลดลงนั่นหมายความว่าเราส่งผ่านธัญพืช ในกรณีนี้ให้เพิ่ม +1 ในผลรวมทั้งหมด

ป้อนคำอธิบายรูปภาพที่นี่

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

เนื่องจากวิธีการทำงานของอัลกอริธึมจึงเป็นสิ่งสำคัญที่จะต้องมีภาพที่สะอาดและขาวดำ เสียงดังมากให้ผลลัพธ์ที่ไม่ดี พื้นหลังหลักแรกถูกล้างทำความสะอาดโดยใช้ floodfill (โซลูชันคล้ายกับคำตอบของ Ell) จากนั้นจะใช้เกณฑ์เพื่อสร้างผลลัพธ์ขาวดำ

ป้อนคำอธิบายรูปภาพที่นี่

มันไกลจากความสมบูรณ์แบบ แต่มันให้ผลลัพธ์ที่ดีเกี่ยวกับความเรียบง่าย อาจมีหลายวิธีในการปรับปรุง (โดยให้ภาพ b / w ที่ดีขึ้นสแกนไปในทิศทางอื่น (เช่น: แนวตั้ง, แนวทแยงมุม) โดยเฉลี่ย ฯลฯ ... )

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 นาทีสำหรับภาพที่ใหญ่ที่สุด และฉันต้องขออภัยสำหรับรหัสยุ่ง

มีพารามิเตอร์และวิธีคิดมากมายในสิ่งนี้ที่ฉันกลัวที่จะ overfitting โปรแกรมของฉันสำหรับภาพตัวอย่าง 10 ภาพ คะแนนสุดท้ายของ 2 นั้นเกือบจะเป็นกรณีของการ overfitting แน่นอน: ฉันมีสองพารามิเตอร์average grain size in pixelและminimum ratio of pixel / elipse_areaและท้ายที่สุดฉันก็ใช้การผสมทั้งหมดของพารามิเตอร์ทั้งสองนี้จนหมดคะแนนต่ำสุด ฉันไม่แน่ใจว่านี่คือสิ่งที่โคเชอร์ทั้งหมดตามกฎของการท้าทายนี้หรือไม่

average_grain_size_in_pixel = 2530
pixel / elipse_area >= 0.73

แต่ถึงแม้จะไม่มีคลัตช์ที่ overfitting เหล่านี้ผลลัพธ์ก็ค่อนข้างดี หากไม่มีขนาดเกรนคงที่หรืออัตราส่วนพิกเซลเพียงแค่ประมาณขนาดเกรนเฉลี่ยจากภาพการฝึกอบรมคะแนนยังคงอยู่ที่ 27

และฉันได้รับเป็นผลผลิตไม่เพียง แต่จำนวน แต่ตำแหน่งที่จริงการวางแนวและรูปร่างของเมล็ดแต่ละ มีจำนวนฉลากที่ติดฉลากผิดเล็กน้อย แต่โดยรวมแล้วฉลากส่วนใหญ่นั้นตรงกับเมล็ดจริง:

A A B B C C D D EE

F F G G H H I ผม JJ

(คลิกที่ภาพแต่ละภาพสำหรับเวอร์ชันเต็ม)

หลังจากขั้นตอนการติดฉลากนี้โปรแกรมของฉันดูที่แต่ละเมล็ดพืชและประมาณการตามจำนวนพิกเซลและอัตราส่วนพิกเซล / วงรีวงรีพื้นที่ไม่ว่าจะเป็น

  • เม็ดเดียว (+1)
  • หลายธัญพืชติดฉลากผิดเป็นหนึ่งเดียว (+ X)
  • หยดเล็กเกินกว่าจะเป็นเกรน (+0)

คะแนนข้อผิดพลาดสำหรับแต่ละภาพคือ A:0; B:0; C:0; D:0; E:2; F:0; G:0 ; H:0; I:0, J:0

อย่างไรก็ตามข้อผิดพลาดจริงอาจสูงขึ้นเล็กน้อย ข้อผิดพลาดบางอย่างในภาพเดียวกันจะยกเลิกซึ่งกันและกัน โดยเฉพาะอย่างยิ่งภาพ H มีเมล็ดที่ติดฉลากไม่ถูกต้องบางส่วนในภาพ E ฉลากส่วนใหญ่จะถูกต้อง

แนวคิดมีการประดิษฐ์เล็กน้อย:

  • ก่อนอื่นโฟร์กราวน์จะถูกแยกผ่าน otsu-thresholding บนช่องทางอิ่มตัว (ดูรายละเอียดคำตอบก่อนหน้าของฉัน)

  • ทำซ้ำจนกระทั่งไม่มีพิกเซลเหลืออยู่:

    • เลือกหยดที่ใหญ่ที่สุด
    • เลือก 10 พิกเซลขอบสุ่มในหยดนี้เป็นตำแหน่งเริ่มต้นของธัญพืช

    • สำหรับแต่ละจุดเริ่มต้น

      • สมมติเกรนที่มีความสูงและความกว้าง 10 พิกเซลที่ตำแหน่งนี้

      • ทำซ้ำจนกระทั่งบรรจบกัน

        • ไปด้านนอกอย่างเร่าร้อนจากจุดนี้ในมุมที่แตกต่างกันจนกว่าคุณจะพบพิกเซลขอบ (ขาว - ดำ - ดำ)

        • หวังว่าพิกเซลที่พบควรเป็นพิกเซลขอบของเกรนเดียว ลองแยก inliers ออกจาก outliers โดยละทิ้งพิกเซลที่อยู่ห่างจากวงรีที่สันนิษฐานมากกว่าคนอื่น

        • พยายามใส่วงรีซ้ำ ๆ ผ่านชุดย่อยของ inliers ซ้ำ ๆ กันรักษาวงรีที่ดีที่สุด (RANSACK)

        • อัปเดตตำแหน่งเกรนการวางแนวความกว้างและความสูงด้วยวงรีที่พบ

        • หากตำแหน่งเกรนไม่เปลี่ยนแปลงอย่างมีนัยสำคัญให้หยุด

    • ในบรรดาเมล็ดธัญพืชที่ติดตั้ง 10 ชนิดให้เลือกเมล็ดที่ดีที่สุดตามรูปร่างจำนวนพิกเซลขอบ ละทิ้งคนอื่น ๆ

    • ลบพิกเซลทั้งหมดสำหรับเมล็ดพืชนี้จากภาพต้นฉบับจากนั้นทำซ้ำ

    • ในที่สุดให้ดูรายการของเมล็ดที่พบและนับแต่ละเม็ดเป็น 1 เม็ด 0 เม็ด (เล็กเกินไป) หรือ 2 เม็ด (ใหญ่เกินไป)

หนึ่งในปัญหาหลักของฉันคือฉันไม่ต้องการใช้ตัวชี้วัดระยะทางวงรีจุด - เต็มเนื่องจากการคำนวณว่าในตัวเองเป็นกระบวนการซ้ำที่ซับซ้อน ดังนั้นฉันจึงใช้วิธีแก้ไขปัญหาต่าง ๆ โดยใช้ฟังก์ชั่น OpenCV Ellipse2Poly และ FitEllipse และผลลัพธ์ไม่ได้สวยเกินไป

เห็นได้ชัดว่าฉันยัง จำกัด ขนาดของ codegolf

คำตอบนั้น จำกัด อยู่ที่ 30,000 ตัวอักษรตอนนี้ฉันอยู่ที่ 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) มันใหญ่เกินไปสำหรับคำตอบ codegolf และขาดความงดงามของโซลูชันอื่น ๆ

ในทางกลับกันฉันมีความสุขมากกับความก้าวหน้าที่ฉันได้รับในการติดฉลากธัญพืชไม่ใช่แค่นับพวกมันดังนั้นจึงมี


คุณรู้ว่าคุณสามารถลดความยาวโค้ดนั้นได้ตามขนาดโดยใช้ชื่อที่มีขนาดเล็กลงและใช้เทคนิคการตีกอล์ฟอื่น ๆ )
เครื่องมือเพิ่มประสิทธิภาพ

อาจเป็นไปได้ แต่ฉันไม่ต้องการทำให้งงงวยโซลูชันนี้ต่อไป มันสับสนเกินไปสำหรับรสนิยมของฉันอย่างที่มันเป็น :)
HugoRune

+1 สำหรับความพยายามและเพราะคุณเป็นคนเดียวที่ค้นหาวิธีการแสดงทีละเมล็ด น่าเสียดายที่โค้ดนั้นมีป่องเล็กน้อยและต้องพึ่งพาล็อตค่าคงที่จำนวนมาก ฉันอยากรู้ว่าขั้นตอนวิธีการสแกนไลน์ที่ฉันเขียนนั้นทำได้อย่างไร
tigrou

ฉันคิดว่านี่เป็นแนวทางที่เหมาะสมสำหรับปัญหาประเภทนี้ (+1 สำหรับคุณ) แต่สิ่งหนึ่งที่ฉันสงสัยว่าทำไมคุณ "เลือก 10 พิกเซลขอบภาพแบบสุ่ม" ฉันคิดว่าคุณจะได้ประสิทธิภาพที่ดีขึ้นหากคุณเลือก จุดขอบที่มีจำนวนจุดขอบใกล้เคียงต่ำสุด (เช่นชิ้นส่วนที่ยื่นออกมา) ฉันคิดว่า (ในทางทฤษฎี) สิ่งนี้จะกำจัดธัญพืชที่ "ง่ายที่สุด" ก่อนคุณเคยพิจารณาเรื่องนี้หรือไม่?
David Rogers

ฉันเคยคิดมาก่อน แต่ยังไม่ได้ลองเลย ตำแหน่งเริ่มต้นแบบสุ่ม '10 'เป็นการต่อท้ายซึ่งเพิ่มและง่ายต่อการขนาน ก่อนหน้านั้น 'หนึ่งตำแหน่งเริ่มต้นแบบสุ่ม' ดีกว่า 'มุมบนซ้ายเสมอ' ความเสี่ยงของการเลือกตำแหน่งเริ่มต้นด้วยกลยุทธ์เดียวกันในแต่ละครั้งคือเมื่อฉันลบแบบที่ดีที่สุดอาจจะเลือกอีก 9 ครั้งในครั้งต่อไปและเมื่อเวลาผ่านไปตำแหน่งที่แย่ที่สุดของตำแหน่งเริ่มต้นเหล่านั้นจะอยู่ข้างหลังและเลือกอีกครั้งและ อีกครั้ง ส่วนที่ยื่นออกมาอาจเป็นเพียงเศษเสี้ยวของเมล็ดก่อนหน้าที่ถูกเอาออกอย่างไม่สมบูรณ์
HugoRune

17

C ++, OpenCV, คะแนน: 9

แนวคิดพื้นฐานของวิธีการของฉันนั้นค่อนข้างง่าย - พยายามลบเมล็ดเดี่ยว (และ "เมล็ดคู่" - 2 เม็ด (แต่ไม่มาก!) ใกล้กัน) จากรูปภาพแล้วนับส่วนที่เหลือโดยใช้วิธีการตามพื้นที่ (เช่น Falko Ell และ belisarius) การใช้วิธีนี้ดีกว่า "วิธีการตามมาตรฐาน" เล็กน้อยเนื่องจากง่ายต่อการค้นหาค่าเฉลี่ยที่ดีของพิกเซล PerObject

(ขั้นตอนที่ 1) ก่อนอื่นเราต้องใช้ Otsu binarization กับ S channel ของภาพใน HSV ขั้นตอนต่อไปคือการใช้ตัวดำเนินการขยายเพื่อปรับปรุงคุณภาพของพื้นหน้าที่แยก กว่าที่เราต้องหารูปทรง แน่นอนว่ารูปทรงบางอย่างไม่ใช่เมล็ดข้าว - เราต้องลบรูปทรงที่มีขนาดเล็กเกินไป (ด้วยพื้นที่ที่มีขนาดเล็กกว่าค่าเฉลี่ยแล้วพิกเซล PerObject / 4 ค่าเฉลี่ยของพิกเซล PerObject คือ 2855 ในสถานการณ์ของฉัน) ตอนนี้ในที่สุดเราก็สามารถเริ่มนับธัญพืช :) (ขั้นตอนที่ 2) การค้นหาเมล็ดเดี่ยวและคู่นั้นค่อนข้างง่าย - เพียงแค่ดูรายการรูปทรงสำหรับรูปทรงที่มีพื้นที่ภายในช่วงที่ระบุ - หากพื้นที่รูปร่างอยู่ในช่วงลบออกจากรายการและเพิ่ม 1 (หรือ 2 ถ้าเป็น "double" grain) เพื่อนับธัญพืช (ขั้นตอนที่ 3) ขั้นตอนสุดท้ายคือการแบ่งพื้นที่ของรูปทรงที่เหลือโดยค่าเฉลี่ยพิกเซล PixPerObject และเพิ่มผลลัพธ์ให้กับตัวนับธัญพืช

รูปภาพ (สำหรับรูปภาพ 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 (.. , .. ) เพื่อเรียกใช้ฟังก์ชั่นและตั้งค่าตัวแปรการประมวลผลเร็วเป็นเท็จ รูปภาพ (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

ป้อนคำอธิบายรูปภาพที่นี่ -> ป้อนคำอธิบายรูปภาพที่นี่

การทำเช่นนี้จะเป็นการลบพื้นหลังอย่างหมดจด

จากนั้นฉันก็ลบเงาของเกรนออกจากส่วนหน้าโดยเพิ่มขีด จำกัด คงที่ให้กับแชแนลค่า (ไม่แน่ใจว่าจริงช่วยได้มาก แต่มันก็ง่ายพอที่จะเพิ่ม)

ป้อนคำอธิบายรูปภาพที่นี่

จากนั้นฉันก็ใช้การแปลงระยะทางกับภาพเบื้องหน้า

ป้อนคำอธิบายรูปภาพที่นี่

และค้นหา maxima ท้องถิ่นทั้งหมดในการแปลงระยะทางนี้

นี่คือที่ความคิดของฉันแตกสลาย เพื่อหลีกเลี่ยงการได้รับ maxima ท้องถิ่นมากมายภายในเกรนเดียวกันฉันต้องกรองเยอะ ปัจจุบันฉันรักษาระดับสูงสุดที่แข็งแกร่งที่สุดไว้ในรัศมี 45 พิกเซลเท่านั้นซึ่งหมายความว่าทุกเม็ดมีค่าสูงสุดในท้องถิ่น และฉันไม่ได้มีเหตุผลสำหรับรัศมี 45 พิกเซลมันเป็นเพียงค่าที่ใช้งานได้

ป้อนคำอธิบายรูปภาพที่นี่

(อย่างที่คุณเห็นว่าเมล็ดเหล่านั้นมีเมล็ดไม่พอที่จะอธิบายแต่ละเม็ด)

จากนั้นฉันใช้ maxima เหล่านั้นเป็นเมล็ดสำหรับอัลกอริทึมลุ่มน้ำ:

ป้อนคำอธิบายรูปภาพที่นี่

ผลลัพธ์ที่ได้จะMeh ฉันหวังว่าจะเป็นธัญพืชส่วนบุคคล แต่กระจุกยังคงใหญ่เกินไป

ตอนนี้ฉันระบุ blobs ที่เล็กที่สุดแล้วนับขนาดพิกเซลเฉลี่ยของพวกเขาจากนั้นประมาณจำนวนธัญพืชจากนั้น นี่ไม่ใช่สิ่งที่ฉันวางแผนไว้ว่าจะทำตั้งแต่เริ่มต้น แต่นี่เป็นวิธีเดียวที่จะกอบกู้สิ่งนี้

โดยใช้ระบบ; 
โดยใช้ระบบ คอลเลกชัน ทั่วไป; 
โดยใช้ระบบ Linq ; 
โดยใช้ระบบ ข้อความ; 
ใช้OpenCvSharp ;

เนมสเปซGrainTest2 { โปรแกรมคลาส{ โมฆะสแตติกหลัก( สตริง[] args ) { สตริง[] ไฟล์= ใหม่[] { "sourceA.jpg" , "sourceB.jpg" , "sourceC.jpg" , "sourceD.jpg" , " sourceE.jpg " , " sourceF.jpg " , " sourceG.jpg " , " sourceH.jpg " , " sourceI.jpg " , " sourceJ.jpg " , };int [] คาดว่าสายฝน

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

            int totalError = 0 ; int totalPixels = 0 ; 
             

            สำหรับ( int fileno = 0 ; fileno เครื่องหมาย= ใหม่รายการ(); 
                    ใช้( CvMemStorage จัดเก็บ= ใหม่CvMemStorage ()) 
                    โดยใช้( CvContourScanner สแกนเนอร์= ใหม่CvContourScanner ( localMaxima , การจัดเก็บ, CvContour . sizeof , ContourRetrieval . ภายนอก, ContourChain . ApproxNone ))         
                    { // ตั้งค่าสูงสุดแต่ละท้องถิ่นเป็นหมายเลขเมล็ด 25, 35, 45, ... // (ตัวเลขจริงไม่สำคัญเลือกสำหรับการมองเห็นที่ดีขึ้นใน png) int markerNo = 20 ; foreach ( CvSeq c ในเครื่องสแกน) { 
                            markerNo + = 5 ; เครื่องหมาย
                            เพิ่ม( markerNo ); waterShedMarkers
                            DrawContours ( c , CvScalar ใหม่( markerNo ), ใหม่
                        
                        
                         
                         
                             CvScalar ( markerNo ), 0 , - 1 ); } } 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 ซึ่งยังคงใหญ่กว่าโซลูชันอื่น ๆ ส่วนใหญ่

ป้อนคำอธิบายรูปภาพที่นี่ ป้อนคำอธิบายรูปภาพที่นี่ ป้อนคำอธิบายรูปภาพที่นี่ ป้อนคำอธิบายรูปภาพที่นี่


ฉันคิดว่าคุณสามารถใช้ threshold (การกัดเซาะอาจมีประโยชน์เช่นกัน) ด้วยค่าเล็กน้อยจากการแปลงระยะทาง - ควรแบ่งธัญพืชบางกลุ่มออกเป็นกลุ่มเล็ก ๆ (ควรมีเพียง 1 หรือ 2 เม็ด) ยิ่งกว่านั้นควรนับธัญพืชที่อ้างว้างเหล่านั้นง่ายกว่า กลุ่มใหญ่ที่คุณสามารถนับได้ว่าเป็นคนส่วนใหญ่ที่นี่ - การแบ่งพื้นที่โดยพื้นที่เฉลี่ยของเมล็ดเดียว
cyriel

9

HTML + Javascript: คะแนน 39

ค่าที่แน่นอนคือ:

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

มันแบ่งออก (ไม่ถูกต้อง) ในค่าที่มากขึ้น

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

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


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

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

คำอธิบาย: โดยทั่วไปนับจำนวนพิกเซลข้าวและหารด้วยจำนวนพิกเซลเฉลี่ยต่อธัญพืช


ใช้รูปข้าว 3 ตัวประมาณ 0 สำหรับฉัน ... : /
Kroltan

1
@ Kroltan ไม่ใช่เมื่อคุณใช้ภาพขนาดเต็ม
งานอดิเรกของ Calvin

1
@ Calvin'sHobbies FF36 บน Windows ได้รับ 0, บน Ubuntu ได้รับ 3 ด้วยขนาดภาพเต็ม
Kroltan

4
@ บ๊อบบี้แจ็คข้าวรับประกันว่าจะมากหรือน้อยกว่าขนาดเดียวกันในภาพ ฉันเห็นไม่มีปัญหากับมัน
งานอดิเรกของ Calvin

1
@githubphagocyte - คำอธิบายค่อนข้างชัดเจน - หากคุณนับพิกเซลสีขาวทั้งหมดจากผลการแยกภาพสองภาพแล้วหารจำนวนนี้ด้วยจำนวนธัญพืชในรูปภาพคุณจะได้ผลลัพธ์นี้ แน่นอนผลลัพธ์ที่แน่นอนอาจแตกต่างกันเนื่องจากวิธีการ binarization ใช้และสิ่งอื่น ๆ (เช่นการดำเนินการหลังจาก binarization) แต่อย่างที่คุณเห็นคำตอบอื่น ๆ มันจะอยู่ในช่วง 2500-3500
cyriel

4

ความพยายามกับ php ไม่ใช่คำตอบให้คะแนนต่ำสุด แต่เป็นรหัสที่ค่อนข้างง่าย

คะแนน: 31

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

การให้คะแนนตนเอง

95 คือค่าสีน้ำเงินซึ่งดูเหมือนว่าจะใช้งานได้เมื่อทดสอบกับ GIMP 2966 คือขนาดเกรนเฉลี่ย

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.