using System; using System.IO; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using AutoFocusTool.Imaging; namespace AutoFocusTool { /// /// BMP→小PNG 诊断转换器(不依赖相机/串口,纯 System.Drawing)。 /// 把相机 15MB 的 24bpp BMP 缩成小 PNG 供肉眼/AI 视觉检查,并: /// - 跑 WellDetector 把"检测器眼里的圆"画上去(绿圆+圆心十字) /// - 画画面中心十字(白)+ 居中容差带(±12% 高度,黄色虚拟区,用上下两条线表示) /// - stdout 打印检测数值(圆心/半径/偏移/完整/拒绝原因) /// 用法:ToPng.exe <输入.bmp 或 目录> [缩放宽度=900] [--nodetect] /// 注意:磁盘上的 BMP 已被 FlipY 翻正(人眼朝向),WellDetector 在此朝向上跑, /// 得到的圆位置即人眼所见;Y偏移符号与引擎内部(未翻转)相反,但位置/半径一致。 /// internal class ToPng { static void Main(string[] args) { Console.OutputEncoding = System.Text.Encoding.UTF8; if (args.Length == 0) { Console.WriteLine("用法: ToPng [宽度=900] [--nodetect]"); return; } string input = args[0]; int targetW = 900; bool detect = true; for (int i = 1; i < args.Length; i++) { if (args[i] == "--nodetect") detect = false; else if (int.TryParse(args[i], out int w)) targetW = w; } if (Directory.Exists(input)) { foreach (var f in Directory.GetFiles(input, "*.bmp")) Convert(f, targetW, detect); } else if (File.Exists(input)) { Convert(input, targetW, detect); } else Console.WriteLine($"找不到: {input}"); } static void Convert(string bmpPath, int targetW, bool detect) { try { using var src = new Bitmap(bmpPath); int W = src.Width, H = src.Height; string outPath = Path.ChangeExtension(bmpPath, ".png"); if (bmpPath.EndsWith("_v.png", StringComparison.OrdinalIgnoreCase)) return; // 跳过已生成的 // 取 BGR24 原始字节供 WellDetector WellCircle c = null; if (detect) { byte[] bgr = ToBgr24(src, W, H); c = WellDetector.Detect(bgr, W, H); Console.WriteLine($"{Path.GetFileName(bmpPath)}: " + (c.Found ? $"圆心({c.Cx:F0},{c.Cy:F0}) R={c.Radius:F0} 偏移X={c.OffsetXPct:F1}% Y={c.OffsetYPct:F1}% 完整={c.Complete} 面积={c.AreaPct:F1}% aspect={c.Aspect:F2} boxFill={c.BoxFill:F2} rConsist={c.RConsist:F2}" : $"未检出圆 [{c.RejectReason}] 面积={c.AreaPct:F1}%")); } // 叠加标注(在全分辨率上画,再缩放,线条清晰) using (var g = Graphics.FromImage(src)) { // 画面中心十字(白) var cxp = W / 2f; var cyp = H / 2f; using var wp = new Pen(Color.White, 3); g.DrawLine(wp, cxp - 40, cyp, cxp + 40, cyp); g.DrawLine(wp, cxp, cyp - 40, cxp, cyp + 40); // 居中容差带(±12% 高度)黄色横线 using var yp = new Pen(Color.Yellow, 2) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dash }; float tol = 0.12f * H; g.DrawLine(yp, 0, cyp - tol, W, cyp - tol); g.DrawLine(yp, 0, cyp + tol, W, cyp + tol); if (c != null && c.Found) { using var gp = new Pen(Color.Lime, 4); g.DrawEllipse(gp, (float)(c.Cx - c.Radius), (float)(c.Cy - c.Radius), (float)(c.Radius * 2), (float)(c.Radius * 2)); // 检测圆心(红十字) using var rp = new Pen(Color.Red, 3); g.DrawLine(rp, (float)c.Cx - 30, (float)c.Cy, (float)c.Cx + 30, (float)c.Cy); g.DrawLine(rp, (float)c.Cx, (float)c.Cy - 30, (float)c.Cx, (float)c.Cy + 30); } } int targetH = (int)((long)targetW * H / W); using var dst = new Bitmap(targetW, targetH); using (var g = Graphics.FromImage(dst)) { g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; g.DrawImage(src, 0, 0, targetW, targetH); } dst.Save(outPath, ImageFormat.Png); Console.WriteLine($" → {Path.GetFileName(outPath)} ({targetW}x{targetH})"); } catch (Exception ex) { Console.WriteLine($"{bmpPath} 转换失败: {ex.Message}"); } } /// System.Drawing.Bitmap → 24bpp BGR 行优先字节(WellDetector 输入格式)。 static byte[] ToBgr24(Bitmap bmp, int W, int H) { var clone = bmp.PixelFormat == PixelFormat.Format24bppRgb ? bmp : bmp.Clone(new Rectangle(0, 0, W, H), PixelFormat.Format24bppRgb); var data = clone.LockBits(new Rectangle(0, 0, W, H), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); byte[] buf = new byte[W * H * 3]; int rowBytes = W * 3; for (int y = 0; y < H; y++) Marshal.Copy(data.Scan0 + y * data.Stride, buf, y * rowBytes, rowBytes); clone.UnlockBits(data); if (!ReferenceEquals(clone, bmp)) clone.Dispose(); return buf; } } }