/// <summary> /// 检查另一笔画是否“单方面”被认为和这一笔画重叠。这个检查不是对称关系。 /// </summary> /// <param name="other"></param> private bool CheckPosition(StrokeRecord other) { return((other.HorizontalStart < OverlayMaxStart) || (OverlayMinEnd < other.HorizontalEnd)); }
public bool OverlayWith(StrokeRecord other) { return(this.CheckPosition(other) || other.CheckPosition(this)); }
/// <summary> /// 手写区松开鼠标键,或手指离开屏幕时执行 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> 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(); } } } }