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("\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);
}
}
}