using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Threading; using System.Windows.Forms; namespace ScreenTranslator { /// Прогон всего конвейера без окна: захват -> OCR -> блоки -> перевод -> отрисовка в PNG. /// Нужен, чтобы проверять качество распознавания и вёрстки плашек, не гоняя оверлей поверх /// рабочего стола и не кликая по нему. /// /// RenderTest.exe [--in кадр.png] [--out каталог] [--scale 2.0] [--no-translate] /// static class RenderTest { [System.Runtime.InteropServices.DllImport("kernel32.dll")] static extern int GetConsoleProcessList(int[] buffer, int count); /// Запущено двойным кликом (своя консоль), а не из терминала — тогда после /// работы надо дождаться пользователя, иначе окно захлопнется вместе с выводом. static bool OwnsConsole() { try { return GetConsoleProcessList(new int[4], 4) <= 1; } catch { return false; } } [STAThread] static int Main(string[] args) { Console.OutputEncoding = System.Text.Encoding.UTF8; bool pause = OwnsConsole(); try { return Run(args); } catch (Exception ex) { Console.WriteLine("\nОшибка: " + ex.Message); return 1; } finally { if (pause) { Console.WriteLine("\nНажмите Enter, чтобы закрыть окно…"); Console.ReadLine(); } } } /// Проверка биграммной модели на реальных примерах из прошлых прогонов: /// слева то, что должно проходить, справа — то, что модель обязана отбраковывать. /// Порог берётся из этих чисел, а не из общих соображений. static int SelfTest() { string[] good = { "Profile", "Cancel", "Settings", "Search", "Mining", "Tailoring", "Engineering", "Leatherworking", "Consumable", "Trade Goods", "Not equippable", "All Subclasses", "Have Materials", "Network configuration", "Wireless adapter", "Save changes", "Connection established", "Restore defaults", "First Aid", "Storage almost full" }; string[] bad = { "npocbwlb", "C03AaTb", "Pa3Mep", "npocM0TpeTb", "COPTMPOBaTb", "Bxoaguvqe", "KOMnb}0Tep", "Meccetqxep", "KonnpoeaTb", "nepenaeH0BaTb", "Bblpe3aTb", "3TOT KOMnb0Tep", "6b1CTPb1i AOCTyn", "npoeKTb1 Crpeneu", "Pa60ti cron" }; Console.WriteLine("--- должно проходить ---"); double gMin = double.MaxValue; foreach (var s in good) { double v = EnglishBigrams.TextScore(s); Console.WriteLine(" {0,8:F3} {1}", v, s); if (!double.IsNaN(v) && v < gMin) gMin = v; } Console.WriteLine("--- должно отбраковываться ---"); double bMax = double.MinValue; foreach (var s in bad) { double v = EnglishBigrams.TextScore(s); Console.WriteLine(" {0,8:F3} {1}", v, s); if (!double.IsNaN(v) && v > bMax) bMax = v; } Console.WriteLine("\nхудший «хороший»: {0:F3} лучший «плохой»: {1:F3} зазор: {2:F3}", gMin, bMax, gMin - bMax); // Строка для сборщика — намеренно латиницей: раннер пишет шаги во временный // скрипт в кодировке, которая калечит кириллицу, и проверка по русскому тексту // там развалится. Console.WriteLine("RESULT worst_good={0:F3} best_bad={1:F3}", gMin.ToString("F3", System.Globalization.CultureInfo.InvariantCulture), bMax.ToString("F3", System.Globalization.CultureInfo.InvariantCulture)); return 0; } static int Run(string[] args) { if (args.Contains("--selftest")) return SelfTest(); string inPath = Arg(args, "--in", null); string outDir = Arg(args, "--out", Path.Combine(Path.GetTempPath(), "screen-translator-test")); double scale = double.Parse(Arg(args, "--scale", "2.0"), System.Globalization.CultureInfo.InvariantCulture); bool translate = !args.Contains("--no-translate"); Directory.CreateDirectory(outDir); string err; var cfg = AppConfig.Load(out err); cfg.OcrScale = scale; if (err != null) Console.WriteLine("config: " + err); var ocr = OcrService.Create(cfg.OcrLanguage); if (ocr == null) { Console.WriteLine("OCR недоступен"); return 2; } ocr.Grayscale = args.Contains("--gray"); ocr.Invert = Arg(args, "--invert", "never"); Console.WriteLine("OCR: {0} масштаб: {1} серый: {2} инверсия: {3}", ocr.LanguageTag, scale, ocr.Grayscale ? "да" : "нет", ocr.Invert); CapturedFrame frame = inPath != null ? FromFile(inPath) : ScreenCapture.Capture(Native.MonitorUnderCursor()); Console.WriteLine("кадр: {0}x{1}", frame.Width, frame.Height); var sw = Stopwatch.StartNew(); var lines = ocr.RecognizeAsync(frame, cfg.OcrScale).GetAwaiter().GetResult(); long tOcr = sw.ElapsedMilliseconds; var sampler = new FrameSampler(frame); int minWords = int.Parse(Arg(args, "--min-words", cfg.MinWords.ToString())); var blocks = Blocks.Group(lines, cfg.MinChars, cfg.SkipGarbage, sampler, minWords); Console.WriteLine("OCR {0} мс: строк {1} -> блоков {2}", tOcr, lines.Count, blocks.Count); if (cfg.DetectForeign) { var other = OcrService.CreateOther(ocr.LanguageTag); if (other == null) Console.WriteLine("второй модели OCR нет — проверка языка пропущена"); else { sw.Restart(); var otherLines = other.RecognizeAsync(frame, cfg.OcrScale).GetAwaiter().GetResult(); var before = new List(blocks); Blocks.DropForeign(blocks, otherLines, Blocks.IsCyrillicLanguage(cfg.SourceLang)); Console.WriteLine("проверка моделью «{0}» {1} мс: отброшено {2}, осталось {3}", other.LanguageTag, sw.ElapsedMilliseconds, before.Count - blocks.Count, blocks.Count); foreach (var dropped in before.Where(b => !blocks.Contains(b))) Console.WriteLine(" отброшено: " + Cut(dropped.Source, 70)); } } Blocks.ComputeGrowLimits(blocks, lines, new Size(frame.Width, frame.Height)); foreach (var b in blocks) { Color bg, fg; sampler.Sample(b.Rect, out bg, out fg); b.Bg = bg; b.Fg = fg; } long tTr = 0; if (translate && blocks.Count > 0) { var cache = cfg.Cache ? new TranslationCache(cfg) : null; var chain = new TranslationChain(cfg, cache); sw.Restart(); var res = chain.TranslateAsync(blocks.Select(b => b.Source).ToArray(), CancellationToken.None) .GetAwaiter().GetResult(); tTr = sw.ElapsedMilliseconds; for (int i = 0; i < blocks.Count; i++) { if (string.IsNullOrWhiteSpace(res[i])) { blocks[i].Failed = true; continue; } blocks[i].Translated = res[i]; } if (cache != null) cache.Save(); Console.WriteLine("перевод {0} мс движком «{1}»{2}", tTr, chain.LastUsed ?? "—", chain.LastError != null ? " (ошибки: " + chain.LastError + ")" : ""); } else foreach (var b in blocks) b.Translated = b.Source; // 1) как видит OCR using (var dbg = Clone(frame)) using (var g = Graphics.FromImage(dbg)) { foreach (var b in blocks) using (var p = new Pen(Color.FromArgb(200, 255, 60, 60))) g.DrawRectangle(p, b.Rect); Save(dbg, Path.Combine(outDir, "1-blocks.png")); } // 2) итог: перевод поверх оригинала using (var res = Clone(frame)) using (var g = Graphics.FromImage(res)) { g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; var fonts = new Dictionary(); var layout = new Dictionary(); foreach (var b in blocks) { if (b.Translated == null) continue; PlateRenderer.Draw(g, b, cfg, new Size(frame.Width, frame.Height), fonts, layout); } foreach (var f in fonts.Values) f.Dispose(); Save(res, Path.Combine(outDir, "2-translated.png")); } Console.WriteLine(); foreach (var b in blocks.OrderBy(b => b.Rect.Top).Take(15)) Console.WriteLine(" [{0,4},{1,4} {2,3}x{3,-3} h={4:0.0}] {5}\n -> {6}", b.Rect.X, b.Rect.Y, b.Rect.Width, b.Rect.Height, b.LineHeight, Cut(b.Source, 70), Cut(b.Translated ?? "—", 70)); // Замер качества распознавания: сколько заранее известных строк реально нашлось. // Без такой метрики любые правки препроцессинга оцениваются на глаз, а на глаз // одинаково убедительно выглядят и улучшение, и ухудшение. string expect = Arg(args, "--expect", null); if (expect != null) { var want = File.ReadAllLines(expect) .Select(l => l.Trim()).Where(l => l.Length > 0).ToList(); var all = string.Join(" | ", blocks.Select(b => b.Source)); int hit = 0; var missed = new List(); foreach (var w in want) { if (all.IndexOf(w, StringComparison.OrdinalIgnoreCase) >= 0) hit++; else missed.Add(w); } Console.WriteLine("\nРАСПОЗНАНО: {0} из {1} ({2:P0})", hit, want.Count, (double)hit / want.Count); if (missed.Count > 0) Console.WriteLine("не найдено: " + string.Join(", ", missed)); } // Итог латиницей и в фиксированном формате — по нему сборщик решает, не поехала ли // сегментация. Человеческую сводку выше он разбирать не может: кодировка временного // скрипта у раннера портит кириллицу, и любой шаблон с русским словом промахивается. Console.WriteLine("RESULT lines={0} blocks={1} translated={2}", lines.Count, blocks.Count, blocks.Count(b => b.Translated != null)); Console.WriteLine("\nкартинки: " + outDir); frame.Image.Dispose(); return 0; } static string Cut(string s, int n) { return s.Length <= n ? s : s.Substring(0, n - 1) + "…"; } static string Arg(string[] a, string name, string def) { int i = Array.IndexOf(a, name); return i >= 0 && i + 1 < a.Length ? a[i + 1] : def; } static Bitmap Clone(CapturedFrame f) { var b = new Bitmap(f.Width, f.Height, PixelFormat.Format32bppArgb); using (var g = Graphics.FromImage(b)) g.DrawImageUnscaled(f.Image, 0, 0); return b; } static CapturedFrame FromFile(string path) { using (var src = new Bitmap(path)) { var bmp = new Bitmap(src.Width, src.Height, PixelFormat.Format32bppArgb); using (var g = Graphics.FromImage(bmp)) g.DrawImageUnscaled(src, 0, 0); var tmp = new CapturedFrame { Image = bmp, Width = bmp.Width, Height = bmp.Height, Area = new Rectangle(0, 0, bmp.Width, bmp.Height) }; // тот же путь извлечения пикселей, что и у экранного захвата tmp.Pixels = ScreenCapture.Crop(tmp, new Rectangle(0, 0, bmp.Width, bmp.Height)).Pixels; return tmp; } } static void Save(Bitmap b, string path) { b.Save(path, ImageFormat.Png); Console.WriteLine(" сохранено: " + path); } } }