2021-09-17 03:18:02 +00:00
|
|
|
|
using SixLabors.ImageSharp;
|
|
|
|
|
using SixLabors.ImageSharp.Processing;
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
using System.Drawing;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Runtime.InteropServices;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using Tesseract;
|
|
|
|
|
|
|
|
|
|
namespace MSFSPopoutPanelManager
|
|
|
|
|
{
|
|
|
|
|
public class AnalysisEngine
|
|
|
|
|
{
|
|
|
|
|
[DllImport("user32.dll")]
|
|
|
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
|
|
|
private static extern bool EnumChildWindows(IntPtr window, EnumWindowProc callback, IntPtr lParam);
|
|
|
|
|
|
|
|
|
|
[DllImport("user32.dll", CharSet = CharSet.Auto)]
|
|
|
|
|
public static extern Int32 GetClassName(IntPtr hWnd, StringBuilder StrPtrClassName, Int32 nMaxCount);
|
|
|
|
|
|
2021-09-21 00:19:46 +00:00
|
|
|
|
[DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
|
|
|
|
|
public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
|
|
|
|
|
|
|
|
|
|
[DllImport("user32.dll", ExactSpelling = false, CharSet = CharSet.Auto, SetLastError = true)]
|
|
|
|
|
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpWindowText, int nMaxCount);
|
|
|
|
|
|
|
|
|
|
[DllImport("user32.dll")]
|
|
|
|
|
public static extern bool SetWindowText(System.IntPtr hwnd, System.String lpString);
|
|
|
|
|
|
2021-09-17 03:18:02 +00:00
|
|
|
|
[DllImport("user32.dll")]
|
|
|
|
|
private static extern int GetWindowRect(IntPtr hwnd, out Rect lpRect);
|
|
|
|
|
|
2021-09-21 00:19:46 +00:00
|
|
|
|
[DllImport("user32.dll")]
|
|
|
|
|
static extern bool MoveWindow(IntPtr hWnd, int x, int y, int width, int height, bool repaint);
|
|
|
|
|
|
|
|
|
|
[DllImport("user32.dll")]
|
|
|
|
|
public static extern IntPtr SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int x, int y, int cx, int cy, uint wFlags);
|
2021-09-17 03:18:02 +00:00
|
|
|
|
|
2021-09-21 00:19:46 +00:00
|
|
|
|
[DllImport("user32.dll")]
|
2021-09-17 03:18:02 +00:00
|
|
|
|
static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
|
|
|
|
|
|
|
|
private delegate bool EnumWindowProc(IntPtr hwnd, IntPtr lParam);
|
2021-09-21 00:19:46 +00:00
|
|
|
|
const int SWP_SHOWWINDOW = 0x0040;
|
2021-09-17 03:18:02 +00:00
|
|
|
|
|
|
|
|
|
public AnalysisEngine() { }
|
|
|
|
|
|
|
|
|
|
public event EventHandler<EventArgs<string>> OnStatusUpdated;
|
|
|
|
|
public event EventHandler<EventArgs<Dictionary<string, string>>> OnOcrDebugged;
|
|
|
|
|
|
2021-09-23 13:34:10 +00:00
|
|
|
|
public void Analyze(ref MainWindow simWindow, OcrEvalData ocrEvaluationData)
|
2021-09-17 03:18:02 +00:00
|
|
|
|
{
|
|
|
|
|
MainWindow processZeroMainWindow = null;
|
|
|
|
|
|
|
|
|
|
// Get process with PID of zero (PID zero launches all the popout windows for MSFS)
|
|
|
|
|
foreach (var process in Process.GetProcesses())
|
|
|
|
|
{
|
|
|
|
|
if(process.Id == 0)
|
|
|
|
|
processZeroMainWindow = new MainWindow()
|
|
|
|
|
{
|
|
|
|
|
ProcessId = process.Id,
|
|
|
|
|
ProcessName = process.ProcessName,
|
|
|
|
|
Title = "Process Zero",
|
|
|
|
|
Handle = process.MainWindowHandle
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get all child windows
|
|
|
|
|
GetChildWindows(simWindow);
|
|
|
|
|
GetChildWindows(processZeroMainWindow);
|
|
|
|
|
|
|
|
|
|
// The popout windows such as PFD and MFD attached itself to main window for Process ID zero instead of the MSFS process.
|
|
|
|
|
// Moving these windows back into MSFS main window
|
|
|
|
|
if (processZeroMainWindow != null)
|
|
|
|
|
{
|
|
|
|
|
foreach (var child in processZeroMainWindow.ChildWindowsData)
|
|
|
|
|
{
|
|
|
|
|
int parentProcessId;
|
|
|
|
|
GetWindowThreadProcessId(child.Handle, out parentProcessId);
|
|
|
|
|
|
|
|
|
|
if (simWindow != null && parentProcessId == simWindow.ProcessId)
|
|
|
|
|
simWindow.ChildWindowsData.Add(child);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int classNameLength = 256;
|
|
|
|
|
foreach (var childWindow in simWindow.ChildWindowsData)
|
|
|
|
|
{
|
|
|
|
|
StringBuilder className = new StringBuilder(classNameLength);
|
|
|
|
|
GetClassName(childWindow.Handle, className, classNameLength);
|
|
|
|
|
childWindow.ClassName = className.ToString();
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-21 00:19:46 +00:00
|
|
|
|
// Remove all child windows where class name is not 'AceApp' and try to determine popout type
|
2021-09-17 03:18:02 +00:00
|
|
|
|
simWindow.ChildWindowsData.RemoveAll(x => x.ClassName != "AceApp" || (x.Title != null && x.Title.Contains("Microsoft Flight Simulator", StringComparison.CurrentCultureIgnoreCase)));
|
2021-09-21 00:19:46 +00:00
|
|
|
|
simWindow.ChildWindowsData = simWindow.ChildWindowsData.GroupBy(x => x.Handle).Select(g => g.First()).ToList();
|
|
|
|
|
|
|
|
|
|
simWindow.ChildWindowsData.ForEach(x => {
|
|
|
|
|
if (String.IsNullOrEmpty(x.Title) || x.Title.Contains("Custom - ") || x.Title.Contains("Failed Analysis"))
|
|
|
|
|
x.PopoutType = PopoutType.Custom;
|
|
|
|
|
else
|
|
|
|
|
x.PopoutType = PopoutType.BuiltIn;
|
|
|
|
|
});
|
2021-09-17 03:18:02 +00:00
|
|
|
|
|
|
|
|
|
if(simWindow.ChildWindowsData.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
var ocrImageScale = ocrEvaluationData.OCRImageScale;
|
|
|
|
|
Dictionary<string, string> debugInfo = new Dictionary<string, string>();
|
|
|
|
|
|
|
|
|
|
foreach (var childWindow in simWindow.ChildWindowsData)
|
|
|
|
|
{
|
2021-09-21 00:19:46 +00:00
|
|
|
|
if (childWindow.PopoutType == PopoutType.Custom)
|
2021-09-17 03:18:02 +00:00
|
|
|
|
{
|
2021-09-21 00:19:46 +00:00
|
|
|
|
// Figure out what windows is what?
|
2021-09-17 03:18:02 +00:00
|
|
|
|
// Theses are the windows with no system menu bar title (ie. PFD, MFD, FMS, etc)
|
|
|
|
|
// We need to take a screenshot and do OCR to try to figure them out
|
|
|
|
|
Rect rect = new Rect();
|
|
|
|
|
GetWindowRect(childWindow.Handle, out rect);
|
|
|
|
|
|
|
|
|
|
// Set window to be custom scale (eaiser to OCR) and set to foreground before taking screenshot image
|
|
|
|
|
var originalWidth = rect.Right - rect.Left;
|
|
|
|
|
var originalHeight = rect.Bottom - rect.Top;
|
|
|
|
|
|
|
|
|
|
var newWidth = Convert.ToInt32(originalWidth * ocrImageScale);
|
|
|
|
|
var newHeight = Convert.ToInt32(originalHeight * ocrImageScale);
|
|
|
|
|
rect.Right = rect.Left + newWidth;
|
|
|
|
|
rect.Bottom = rect.Top + newHeight;
|
2021-09-21 00:19:46 +00:00
|
|
|
|
MoveWindow(childWindow.Handle, rect.Left, rect.Top, newWidth, newHeight, true);
|
2021-09-17 03:18:02 +00:00
|
|
|
|
SetForegroundWindow(childWindow.Handle);
|
|
|
|
|
Thread.Sleep(500);
|
|
|
|
|
|
2021-09-23 13:34:10 +00:00
|
|
|
|
var image = TakeScreenShot(rect, childWindow.Handle.ToInt32());
|
2021-09-21 00:19:46 +00:00
|
|
|
|
MoveWindow(childWindow.Handle, rect.Left, rect.Top, originalWidth, originalHeight, true);
|
|
|
|
|
|
2021-09-17 03:18:02 +00:00
|
|
|
|
// OCR the image into text
|
|
|
|
|
var imageText = OcrImage(image);
|
|
|
|
|
var popoutName = EvaluateImageText(imageText, ocrEvaluationData);
|
2021-09-21 00:19:46 +00:00
|
|
|
|
childWindow.Title = popoutName == null ? $"Failed Analysis - {childWindow.Handle}" : $"Custom - {popoutName}";
|
|
|
|
|
childWindow.PopoutType = popoutName == null ? PopoutType.Undetermined : PopoutType.Custom;
|
|
|
|
|
|
|
|
|
|
SetWindowText(childWindow.Handle, childWindow.Title);
|
2021-09-17 03:18:02 +00:00
|
|
|
|
|
2021-09-21 00:19:46 +00:00
|
|
|
|
if (!debugInfo.TryAdd(childWindow.Title, imageText))
|
|
|
|
|
debugInfo.Add($"{childWindow.Title} - {childWindow.Handle}", imageText);
|
2021-09-17 03:18:02 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Output debug info
|
|
|
|
|
if(debugInfo.Count > 0)
|
|
|
|
|
OnOcrDebugged?.Invoke(this, new EventArgs<Dictionary<string, string>>(debugInfo));
|
|
|
|
|
|
|
|
|
|
// Remove all windows that cannot be identified
|
|
|
|
|
simWindow.ChildWindowsData.RemoveAll(x => x.Title == null);
|
2021-09-21 00:19:46 +00:00
|
|
|
|
|
|
|
|
|
OnStatusUpdated?.Invoke(this, new EventArgs<string>("Anaylsis completed."));
|
2021-09-17 03:18:02 +00:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
OnStatusUpdated?.Invoke(this, new EventArgs<string>("No pop out panels to analyze."));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void GetChildWindows(MainWindow mainWindow)
|
|
|
|
|
{
|
|
|
|
|
var childHandles = GetAllChildHandles(mainWindow.Handle);
|
|
|
|
|
|
|
|
|
|
childHandles.ForEach(childHandle =>
|
|
|
|
|
{
|
|
|
|
|
mainWindow.ChildWindowsData.Add(new ChildWindow
|
|
|
|
|
{
|
|
|
|
|
Handle = childHandle,
|
|
|
|
|
Title = GetWindowTitle(childHandle)
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string GetWindowTitle(IntPtr hWnd)
|
|
|
|
|
{
|
|
|
|
|
StringBuilder title = new StringBuilder(1024);
|
|
|
|
|
GetWindowText(hWnd, title, title.Capacity);
|
|
|
|
|
|
|
|
|
|
return String.IsNullOrEmpty(title.ToString()) ? null : title.ToString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private List<IntPtr> GetAllChildHandles(IntPtr parent)
|
|
|
|
|
{
|
|
|
|
|
var childHandles = new List<IntPtr>();
|
|
|
|
|
|
|
|
|
|
GCHandle gcChildhandlesList = GCHandle.Alloc(childHandles);
|
|
|
|
|
IntPtr pointerChildHandlesList = GCHandle.ToIntPtr(gcChildhandlesList);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
EnumWindowProc childProc = new EnumWindowProc(EnumWindow);
|
|
|
|
|
EnumChildWindows(parent, childProc, pointerChildHandlesList);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
gcChildhandlesList.Free();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return childHandles;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool EnumWindow(IntPtr hWnd, IntPtr lParam)
|
|
|
|
|
{
|
|
|
|
|
var gcChildhandlesList = GCHandle.FromIntPtr(lParam);
|
|
|
|
|
|
|
|
|
|
if (gcChildhandlesList.Target == null)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var childHandles = gcChildhandlesList.Target as List<IntPtr>;
|
|
|
|
|
childHandles.Add(hWnd);
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-23 13:34:10 +00:00
|
|
|
|
private byte[] TakeScreenShot(Rect rect, int imageId)
|
2021-09-17 03:18:02 +00:00
|
|
|
|
{
|
|
|
|
|
var bounds = new System.Drawing.Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top);
|
|
|
|
|
var bitmap = new Bitmap(bounds.Width, bounds.Height);
|
|
|
|
|
|
|
|
|
|
using (var g = Graphics.FromImage(bitmap))
|
|
|
|
|
{
|
|
|
|
|
g.CopyFromScreen(new System.Drawing.Point(bounds.Left, bounds.Top), System.Drawing.Point.Empty, bounds.Size);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var converter = new ImageConverter();
|
|
|
|
|
var imageBytes = (byte[])converter.ConvertTo(bitmap, typeof(byte[]));
|
|
|
|
|
|
|
|
|
|
// Convert image to grayscale and invert the color to give tesseract a better image to analyze
|
|
|
|
|
using(var img = SixLabors.ImageSharp.Image.Load(imageBytes))
|
|
|
|
|
{
|
|
|
|
|
img.Mutate(x => x.Invert().GaussianSharpen().Grayscale());
|
2021-09-23 13:34:10 +00:00
|
|
|
|
|
|
|
|
|
double imageRatio = 250 / 72.0; // change from default of 72 DPI to 250 DPI
|
|
|
|
|
|
|
|
|
|
img.Metadata.HorizontalResolution *= imageRatio;
|
|
|
|
|
img.Metadata.VerticalResolution *= imageRatio;
|
2021-09-17 03:18:02 +00:00
|
|
|
|
var memoryStream = new MemoryStream();
|
|
|
|
|
img.Save(memoryStream, new SixLabors.ImageSharp.Formats.Bmp.BmpEncoder());
|
2021-09-23 13:34:10 +00:00
|
|
|
|
#if DEBUG
|
|
|
|
|
Directory.CreateDirectory("imageDebug");
|
|
|
|
|
img.Save(@$"./imageDebug/test-{imageId}.jpg");
|
|
|
|
|
#endif
|
2021-09-17 03:18:02 +00:00
|
|
|
|
return memoryStream.ToArray();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string OcrImage(byte[] imageByteArray)
|
|
|
|
|
{
|
|
|
|
|
var ocrengine = new TesseractEngine(@".\tessdata", "eng", EngineMode.Default);
|
|
|
|
|
var iamge = Pix.LoadFromMemory(imageByteArray);
|
|
|
|
|
var result = ocrengine.Process(iamge);
|
|
|
|
|
|
|
|
|
|
return result.GetText();
|
|
|
|
|
}
|
2021-09-21 00:19:46 +00:00
|
|
|
|
|
2021-09-17 03:18:02 +00:00
|
|
|
|
private string EvaluateImageText(string imageText, OcrEvalData ocrEvaluationData)
|
|
|
|
|
{
|
|
|
|
|
if(ocrEvaluationData != null)
|
|
|
|
|
{
|
|
|
|
|
var popoutNames = ocrEvaluationData.EvalData.Select(x => x.PopoutName).Distinct();
|
|
|
|
|
|
|
|
|
|
foreach(var popoutName in popoutNames)
|
|
|
|
|
{
|
|
|
|
|
if (ocrEvaluationData.EvalData.Find(x => x.PopoutName == popoutName).Data.Any(s => imageText.Contains(s.ToLower(), StringComparison.CurrentCultureIgnoreCase)))
|
|
|
|
|
return popoutName;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|