米の粒を数える


81

さまざまな量の未調理の白米のこれら10個の画像を考えてみてください。
これらは唯一のサンバイルです。画像をクリックしてフルサイズで表示します。

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

F: G:H:I:J:F G H I J

穀物カウント: A: 3, B: 5, C: 12, D: 25, E: 50, F: 83, G: 120, H:150, I: 151, J: 200

に注意してください...

  • 粒子は互いに接触する場合がありますが、重なることはありません。グレインのレイアウトが1グレイン以上になることはありません。
  • 画像の寸法は異なりますが、カメラと背景が静止しているため、すべての米のスケールは一貫しています。
  • 粒子が範囲外に出たり、画像の境界に触れたりすることはありません。
  • 背景は常に黄色がかった白の同じ一貫した色合いです。
  • 小粒と大粒はそれぞれ1粒としてカウントされます。

これらの5つのポイントは、この種のすべての画像の保証です。

チャレンジ

そのような画像を取り込み、可能な限り正確に米の粒数をカウントするプログラムを作成します。

プログラムは画像のファイル名を取得し、計算した粒子数を出力する必要があります。プログラムは、JPEG、ビットマップ、PNG、GIF、TIFF(現在はすべてJPEG)の画像ファイル形式の少なくとも1つで動作する必要があります。

あなたは可能画像処理とコンピュータビジョンライブラリを使用しています。

10個のサンプル画像の出力をハードコードすることはできません。アルゴリズムは、同様のすべての米粒画像に適用できる必要があります。まともな最新のコンピューターで 5分未満で実行できるはずです。画像領域が2000 * 2000ピクセル未満で、米粒が300未満の場合、です。

得点

10個の画像のそれぞれについて、実際の粒子数からプログラムが予測する粒子数を引いた絶対値を取ります。これらの絶対値を合計してスコアを取得します。最も低いスコアが勝ちます。スコア0は完璧です。

同点の場合、最も高い投票数の回答が勝ちます。追加の画像でプログラムをテストして、その有効性と正確性を検証する場合があります。


1
きっと誰かがscikit-learnを試さなければなりません!

素晴らしいコンテスト!:)ところで-このチャレンジの終了日について何か教えてもらえますか?
cyriel 14年

1
@Lembik 7まで:)
ベリサリウス博士14年

5
ある日、米の科学者がやって来て、この質問が存在することを喜んで真っ向から頭に浮かびます。
NIT

回答:


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

ここで、使用した粒度に対するスコアの感度を確認できます。

Mathematicaグラフィックス


2
より明確に、ありがとう!
カルビンの趣味14年

この正確な手順をpythonでコピーできますか、またはpythonライブラリではできない特別なMathematicaがここで行っていることはありますか?

@Lembikわかりません。Pythonはありません。ごめんなさい。(しかし、私は正確に同じアルゴリズムを疑うEdgeDetect[]DeleteSmallComponents[]そしてDilation[]他の場所で実装されている)
博士ベリサリウス

55

Python、スコア: 24 16

このソリューションは、ファルコのソリューションと同様に、「前景」面積を測定し、それを平均穀物面積で割ることに基づいています。

実際、このプログラムが検出しようとするのはバックグラウンドであり、フォアグラウンドほどではありません。米粒が画像の境界に決して接触しないという事実を利用して、プログラムは左上隅の白を塗りつぶすことから始めます。塗りつぶしアルゴリズムは、ピクセルと現在のピクセルの明るさとの差が特定のしきい値内にある場合、隣接するピクセルをペイントします。これにより、背景色の緩やかな変化に合わせて調整されます。この段階の最後では、画像は次のようになります。

図1

ご覧のとおり、バックグラウンドの検出は非常に優れていますが、粒子間に「閉じ込められた」領域はすべて除外されます。これらの領域を処理するには、各ピクセルで背景の明るさを推定し、すべての同等またはより明るいピクセルをペイントします。この推定は次のように機能します。塗りつぶし段階で、各行と各列の平均背景輝度を計算します。各ピクセルの推定背景輝度は、そのピクセルの行と列の輝度の平均です。これにより、次のようなものが生成されます。

図2

編集: 最後に、各連続した前景(つまり、非白)領域の面積を、事前に計算された平均の穀物面積で除算し、その地域の穀物数の推定値を提供します。これらの数量の合計が結果です。最初は、全体として前景領域全体に対して同じことを行いましたが、このアプローチは文字通り、よりきめ細かいです。


from sys import argv; from PIL import Image

# Init
I = Image.open(argv[1]); W, H = I.size; A = W * H
D = [sum(c) for c in I.getdata()]
Bh = [0] * H; Ch = [0] * H
Bv = [0] * W; Cv = [0] * W

# Flood-fill
Background = 3 * 255 + 1; S = [0]
while S:
    i = S.pop(); c = D[i]
    if c != Background:
        D[i] = Background
        Bh[i / W] += c; Ch[i / W] += 1
        Bv[i % W] += c; Cv[i % W] += 1
        S += [(i + o) % A for o in [1, -1, W, -W] if abs(D[(i + o) % A] - c) < 10]

# Eliminate "trapped" areas
for i in xrange(H): Bh[i] /= float(max(Ch[i], 1))
for i in xrange(W): Bv[i] /= float(max(Cv[i], 1))
for i in xrange(A):
    a = (Bh[i / W] + Bv[i % W]) / 2
    if D[i] >= a: D[i] = Background

# Estimate grain count
Foreground = -1; avg_grain_area = 3038.38; grain_count = 0
for i in xrange(A):
    if Foreground < D[i] < Background:
        S = [i]; area = 0
        while S:
            j = S.pop() % A
            if Foreground < D[j] < Background:
                D[j] = Foreground; area += 1
                S += [j - 1, j + 1, j - W, j + W]
        grain_count += int(round(area / avg_grain_area))

# Output
print grain_count

コマンドラインを介して入力ファイル名を取得します。

結果

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

A B C D E

F G H 私 J


2
これは本当に賢い解決策です、素晴らしい仕事です!
クリスクレフィス14年

1
どこavg_grain_area = 3038.38;から来たの?
njzk2 14年

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.これは、そのルールを表す単なる値です。ただし、結果は入力に応じて変化します。ルールを変更すると、この値は変更されますが、結果は入力に基づいて同じになります。
アダムデイビス14年

6
私は平均的な面積のもので大丈夫です。粒子面積は、画像全体で(ほぼ)一定です。
カルビンの趣味14年

28

Python + OpenCV:スコア27

水平線スキャン

アイデア:画像を一度に1行ずつスキャンします。各行について、遭遇した米粒の数を数えます(ピクセルが黒から白に変わるか、その反対になるかどうかを確認します)。ラインのグレインの数が(前のラインと比較して)増加する場合、新しいグレインに遭遇したことを意味します。その数が減少する場合、それは穀物を通過したことを意味します。この場合、合計結果に+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

これは私の2回目の試みです。私の最初の試みとはかなり異なりますが、これははるかに単純なので、別のソリューションとして投稿しています。

基本的な考え方は、反復楕円近似によって個々の粒子を識別してラベルを付けることです。次に、このグレインのピクセルをソースから削除し、すべてのピクセルにラベルが付けられるまで次のグレインを見つけようとします。

これは最もきれいな解決策ではありません。600行のコードを持つ巨大な豚です。最大の画像を表示するには1.5分かかります。厄介なコードを本当におaびします。

このことには非常に多くのパラメータと考え方があり、10個のサンプル画像でプログラムをオーバーフィットすることを非常に恐れています。最終スコア2は、ほぼ間違いなくオーバーフィッティングの場合です。2つのパラメーターaverage grain size in pixel、およびminimum ratio of pixel / elipse_areaがあり、最後に、スコアが最低になるまでこれら2つのパラメーターのすべての組み合わせを使い果たしました。これがこの挑戦のルールとコーシャであるかどうかはわかりません。

average_grain_size_in_pixel = 2530
pixel / elipse_area >= 0.73

しかし、これらのオーバーフィットクラッチがなくても、結果は非常に優れています。トレーニング画像から平均粒度を推定するだけで、固定粒度またはピクセル比がなくても、スコアは27のままです。

そして、出力として、数字だけでなく、各粒子の実際の位置、方向、および形状も取得します。誤ったラベルの付いたグレインは少数ですが、全体的にほとんどのラベルは実際のグレインと正確に一致しています。

A A B B C C D D EE

F F G G H H I 私 JJ

(フルサイズバージョンの各画像をクリック)

このラベリング手順の後、私のプログラムは個々の粒子を見て、ピクセル数とピクセル/楕円面積比に基づいて推定します。

  • シングルグレイン(+1)
  • 複数のグレインが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グレイン(大きすぎる)としてカウントします

私の主な問題の1つは、それ自体の計算自体が複雑な反復プロセスであるため、完全な楕円点距離メトリックを実装したくないということでした。そこで、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)コードゴルフの答えとしては大きすぎて、他のソリューションの優雅さに欠けているため、このソリューションに少し戸惑います。

一方、私は穀物を数えるだけでなく、穀物のラベル付けで達成した進歩に非常に満足しています。


小さい名前を使用し、他のゴルフテクニックを適用することで、コードの長さを大幅に短縮できることをご存じです;)
オプティマイザー14年

おそらく、しかし、私はこのソリューションをさらに難読化したくありませんでした。それは私の好みには
難解

努力のために+1します。これは、各穀物を個別に表示する方法を見つけるのはあなただけだからです。残念ながら、コードは少し肥大化しており、ハードコードされた定数に大きく依存しています。私が書いたスキャンラインアルゴリズムが(個々の色付きの粒子で)これをどのように実行するかを知りたいと思います。
tigrou 14

私は本当にこれがこのタイプの問題(あなたのために+1)の正しいアプローチだと思いますが、1つ疑問に思うのは、なぜ「10個のランダムエッジピクセルを選択する」のかを選ぶと、パフォーマンスが向上すると思います近くのエッジポイント(つまり、突き出ている部分)の数が最も少ないエッジポイント、(理論的に)これが最初に「最も簡単な」グレインを削除すると思いますか?
デビッドロジャース14

私はそれを考えましたが、まだ試していません。「10個のランダムな開始位置」は後から追加されたもので、簡単に追加でき、並列化も簡単でした。それ以前は、「1つのランダムな開始位置」は「常に左上隅」よりもずっと優れていました。毎回同じ戦略で開始位置を選択する危険性は、ベストフィットを削除すると、おそらく他の9つがおそらく再び選択され、時間が経つにつれて、それらの開始位置の最悪のものが遅れて再び選択されることです再び。突き出ている部分は、以前の穀物が不完全に除去されたままの場合があります。
HugoRune 14

17

C ++、OpenCV、スコア:9

私の方法の基本的な考え方は非常に簡単です-画像から単一の穀物(および「二重の穀物」-2つ(しかしそれ以上!)の穀物)を消去してから、面積に基づいた方法(ファルコ、エルとベリサリウス)。このアプローチを使用すると、標準的な「エリアメソッド」よりも少し優れています。これは、適切なaveragePixelsPerObject値を見つけるのが簡単だからです。

(第1ステップ)まず、HSVの画像のSチャンネルで大津二値化を使用する必要があります。次の手順では、膨張演算子を使用して、抽出された前景の品質を改善します。輪郭を見つける必要があるより。もちろん、一部の輪郭は米粒ではありません-小さすぎる輪郭を削除する必要があります(この場合、averagePixelsPerObject / 4よりも小さい面積です。averagePixelsPerObjectは私の状況では2855です)。最後に、穀物のカウントを開始できます:)(2番目のステップ)単一および二重の穀物を見つけるのは非常に簡単です-特定の範囲内の面積を持つ輪郭の輪郭リストを見るだけです-輪郭面積が範囲内にある場合、リストから削除して1を追加します(または「ダブル」グレインの場合は2)グレインカウンターに。(3番目のステップ)最後のステップは、もちろん、残りの輪郭の領域をaveragePixelsPerObject値で除算し、結果を穀物カウンターに追加することです。

(画像F.jpg用)画像言葉よりも優れたこの考えを示すべきである:
第一ステップ(小さな輪郭(ノイズ)なし): 最初のステップ(小さな輪郭(ノイズ)なし)
第二ステップ-だけの単純な輪郭: 2番目のステップ-単純な輪郭のみ
第三ステップ-残りの輪郭: 3番目のステップ-残りの輪郭

コードは次のとおりです。かなりratherいですが、問題なく動作するはずです。もちろん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、...)はディレクトリ画像にある必要があります。別の方法として、コマンドラインからパラメーターとして1つの画像の名前を指定することもできます。

もちろん、何か不明な点がある場合は、説明したり、画像や情報を提供したりできます。


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; 1粒あたりのピクセル:2729,9;
F.jpg; 穀物の数:65; 83が期待されます。エラー18; 粒あたりのピクセル:2860,5;
G.jpg; 穀物の数:120; 120が期待されます。エラー0; 粒あたりのピクセル:2552,3;
H.jpg; 穀物の数:159; 予想150 エラー9; 1粒あたりのピクセル:2624,7;
I.jpg; 穀物の数:141; 151が期待されます。エラー10; 1粒あたりのピクセル数:2697,4;
J.jpg; 穀物の数:179; 予想200; エラー21; 粒あたりのピクセル数:2847,1;
合計エラー:71

私のソリューションは次のように機能します:

画像をHSVに変換し、飽和チャンネルにOtsuしきい値を適用して、前景を分離します。これは非常に簡単で、非常にうまく機能します。このチャレンジを試してみたいすべての人にこれをお勧めします。

saturation channel                -->         Otsu thresholding

ここに画像の説明を入力してください -> ここに画像の説明を入力してください

これにより、背景がきれいに削除されます。

次に、値チャンネルに固定しきい値を適用することにより、フォアグラウンドから粒子の影をさらに削除しました。(それが実際に大いに役立つかどうかはわかりませんが、追加するのに十分簡単でした)

ここに画像の説明を入力してください

次に、前景画像に距離変換を適用します。

ここに画像の説明を入力してください

この距離変換ですべての極大値を見つけます。

これが私のアイデアが崩壊するところです。同じグレイン内で複数の極大値を取得しないようにするには、多くのフィルター処理を行う必要があります。現在、半径45ピクセルの範囲内で最も強い最大値のみを保持しています。これは、すべての粒子に局所的な最大値があるわけではないことを意味します。そして、45ピクセルの半径を正当化する理由は実際にはありません。それは機能する値でした。

ここに画像の説明を入力してください

(ご覧のとおり、これらは各穀物を説明するのに十分な種ではありません)

次に、これらの最大値を流域アルゴリズムのシードとして使用します。

ここに画像の説明を入力してください

結果はまあまあです。私は主に個々の穀物を望んでいましたが、塊はまだ大きすぎます。

次に、最小のBLOBを特定し、それらの平均ピクセルサイズをカウントし、それから粒子の数を推定します。これは最初に私が計画していたことではありませんが、これがこれを救う唯一の方法でした。

System を使用しますSystem 
を使用しますコレクションジェネリック; System 
を使用しますLinq ; System 
を使用しますテキスト; 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 [] expectedGrains

     
    
          
        
             
                               
                                     
                                     
                                      
                               
            = 新しい[] { 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" );


                    リストpixelsPerBlob = new List ();  

                    // 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 14年

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>

説明:基本的に、ライスピクセルの数をカウントし、1粒あたりの平均ピクセルで除算します。


3米の画像を使用して、私にとっては0と推定されました...:/
Kroltan 14年

1
@Kroltan フルサイズの画像を使用する場合は不要です。
カルビンの趣味14年

1
Windowsの@ Calvin'sHobbies FF36は0を取得し、Ubuntuでは3を取得し、フルサイズのイメージを取得します。
Kroltan 14年

4
@BobbyJackライスは、画像全体でほぼ同じスケールになることが保証されています。問題ありません。
カルビンの趣味14年

1
@githubphagocyte-説明は非常に明白です-画像の二値化の結果ですべての白いピクセルを数え、この数を画像の粒子の数で割ると、この結果が得られます。もちろん、使用されている二値化方法やその他のもの(二値化後に実行される操作など)のために正確な結果は異なる場合がありますが、他の答えでわかるように、範囲は2500〜3500になります。
cyriel 14年

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.