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
B
C
D
E
F
G
H
I
J
(フルサイズバージョンの各画像をクリック)
このラベリング手順の後、私のプログラムは個々の粒子を見て、ピクセル数とピクセル/楕円面積比に基づいて推定します。
- シングルグレイン(+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ではラベルはほとんど正しいです
コンセプトは少し工夫されています:
私の主な問題の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)コードゴルフの答えとしては大きすぎて、他のソリューションの優雅さに欠けているため、このソリューションに少し戸惑います。
一方、私は穀物を数えるだけでなく、穀物のラベル付けで達成した進歩に非常に満足しています。