in #U5b9e#U8df5#U6848#U4f8b/B09-#U624b#U5199#U7b97#U5f0f#U8ba1#U7b97#U5668/src/extended_mnist_calculator/MNIST.App/MainWindow.cs [123:294]
private void writeArea_MouseUp(object sender, MouseEventArgs e)
{
// 鼠标事件较多,通过条件来仅在鼠标左键按下,或手指在屏幕上时,才执行。
if (e.Button == MouseButtons.Left)
{
// 必须确实发生了鼠标移动事件,即有线段被画出,我们才认为有笔画存在。
if (strokePoints.Any())
{
var thisStrokeRecord = new StrokeRecord(strokePoints);
allStrokes.Add(thisStrokeRecord);
// 将所有笔画按水平起点排序,然后按重叠与否进行分组。
allStrokes = allStrokes.OrderBy(s => s.HorizontalStart).ToList();
int[] strokeGroupIds = new int[allStrokes.Count];
int nextGroupId = 1;
for (int i = 0; i < allStrokes.Count; i++)
{
// 为了避免水平方向太多笔画被连在一起,我们采取一种简单的办法:
// 当1、2笔画重叠时,我们就不会在检查笔画2和更右侧笔画是否重叠。
if (strokeGroupIds[i] != 0)
{
continue;
}
strokeGroupIds[i] = nextGroupId;
nextGroupId++;
var s1 = allStrokes[i];
for (int j = 1; i + j < allStrokes.Count; j++)
{
var s2 = allStrokes[i + j];
if (s2.HorizontalStart < s1.OverlayMaxStart)
{
if (strokeGroupIds[i + j] == 0)
{
if (s1.OverlayWith(s2))
{
strokeGroupIds[i + j] = strokeGroupIds[i];
}
}
}
else
{
break;
}
}
}
bool enableDebug = visualizeSwitch.Checked;
if (enableDebug)
{
graphics.Clear(Color.White);
}
// 清除之前显式的推理结果
outputText.Text = "";
var batchInferInput = new List<IEnumerable<float>>();
Pen penStyle = new Pen(Color.Black, 20)
{
StartCap = LineCap.Round,
EndCap = LineCap.Round
};
List<IGrouping<int, StrokeRecord>> groups = allStrokes
.Zip(strokeGroupIds, Tuple.Create)
.GroupBy(tuple => tuple.Item2, tuple => tuple.Item1) // Item2是分组编号, Item1是StrokeRecord
.ToList();
foreach (IGrouping<int, StrokeRecord> group in groups)
{
int gid = group.Key;
var groupedStrokes = group.ToList(); // IGrouping<TKey, TElement>本质上也是一个可迭代的IEnumerable<TElement>
// 确定整个分组的所有笔画的范围。
int grpHorizontalStart = groupedStrokes.Min(s => s.HorizontalStart);
int grpHorizontalEnd = groupedStrokes.Max(s => s.HorizontalEnd);
int grpHorizontalLength = grpHorizontalEnd - grpHorizontalStart;
int canvasEdgeLen = writeArea.Height;
Bitmap canvas = new Bitmap(canvasEdgeLen, canvasEdgeLen);
Graphics canvasGraphics = Graphics.FromImage(canvas);
canvasGraphics.Clear(Color.White);
// 因为我们提取了每个笔画,就不能把长方形的绘图区直接当做输入了。
// 这里我们把宽度小于 writeArea.Height 的分组在 canvas 内居中。
int halfOffsetX = Math.Max(canvasEdgeLen - grpHorizontalLength, 0) / 2;
var grpClr = GetDebugColor(gid);
var rectClr = Color.FromArgb(120, grpClr);
foreach (var stroke in groupedStrokes)
{
if (enableDebug)
{
graphics.FillRectangle(
new SolidBrush(rectClr),
stroke.OverlayMinEnd,
0,
Math.Max(2, stroke.OverlayMaxStart - stroke.OverlayMinEnd), // At least width of 2px
30);
}
Point startPoint = stroke.Points[0];
foreach (var point in stroke.Points.Skip(1))
{
var from = startPoint;
var to = point;
// 因为每个分组都是在长方形的绘图区被记录的,所以在单一位图上,需要先减去相对于长方形绘图区的偏移量 grpHorizontalStart
from.X = from.X - grpHorizontalStart + halfOffsetX;
to.X = to.X - grpHorizontalStart + halfOffsetX;
canvasGraphics.DrawLine(penStyle, from, to);
/*
* 调试用。
* 取消注释后可以看到每一笔画,会按照其分组显示不同的颜色。
*/
if (enableDebug)
{
graphics.DrawLine(
new Pen(grpClr, 20)
{
StartCap = LineCap.Round,
EndCap = LineCap.Round
},
startPoint,
point);
}
startPoint = point;
}
}
// 1. 将分割出的笔画图片缩小至 28 x 28,与训练数据格式一致。
Bitmap clonedBmp = new Bitmap(canvas, ImageSize, ImageSize);
var image = new List<float>(ImageSize * ImageSize);
for (var x = 0; x < ImageSize; x++)
{
for (var y = 0; y < ImageSize; y++)
{
var color = clonedBmp.GetPixel(y, x);
image.Add((float)(0.5 - (color.R + color.G + color.B) / (3.0 * 255)));
}
}
// 将这一组笔画对应的矩阵保存下来,以备批量推理。
batchInferInput.Add(image);
}
// 2. 进行批量推理
// batchInferInput 是一个列表,它的每个元素都是一次推量的输入。
//IEnumerable<IEnumerable<long>> inferResult = model.Infer(batchInferInput);
var inferResult = batchInferInput.SelectMany(i => model.Infer(new List<IEnumerable<float>> { i })).ToList();
// 推量的结果是一个可枚举对象,它的每个元素代表了批量推理中一次推理的结果。我们用 仅一次.First() 将它们的结果都取出来,并格式化。
// outputText.Text = string.Join("", inferResult.Select(singleResult => singleResult.First().ToString()));
var recognizedLabels = inferResult.Select(singleResult => (int)singleResult.First()).ToList();
outputText.Text = EvaluateAndFormatExpression(recognizedLabels);
if (enableDebug)
{
// 这是调试用的。在上面的调试代码没有启用时,这句话没有特别作用。
writeArea.Invalidate();
}
}
}
}