welsonjs/WelsonJS.Toolkit/WelsonJS.Service/ScreenMatch.cs

665 lines
21 KiB
C#
Raw Normal View History

2024-08-12 03:47:19 +00:00
// ScreenMatching.cs
// https://github.com/gnh1201/welsonjs
// https://github.com/gnh1201/welsonjs/wiki/Screen-Time-Feature
2024-08-12 03:47:19 +00:00
using System;
2024-07-29 12:43:14 +00:00
using System.Collections.Generic;
2024-08-24 13:24:26 +00:00
using System.Diagnostics;
2024-07-29 12:43:14 +00:00
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using System.ServiceProcess;
2024-07-29 12:43:14 +00:00
using System.Text;
2024-09-04 07:18:05 +00:00
using System.Threading;
2024-07-29 12:43:14 +00:00
using System.Windows.Forms;
2024-09-06 07:10:58 +00:00
using System.Linq;
using Tesseract;
2024-07-29 12:43:14 +00:00
using WelsonJS.Service;
2024-08-24 12:32:44 +00:00
public class ScreenMatch
2024-07-29 12:43:14 +00:00
{
// User32.dll API 함수 선언
[DllImport("user32.dll")]
2024-08-13 02:21:48 +00:00
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
2024-07-29 12:43:14 +00:00
[DllImport("user32.dll")]
2024-08-13 02:21:48 +00:00
private static extern bool IsWindowVisible(IntPtr hWnd);
2024-07-29 12:43:14 +00:00
[DllImport("user32.dll")]
2024-08-13 02:21:48 +00:00
private static extern int GetWindowRect(IntPtr hWnd, out RECT lpRect);
2024-07-29 12:43:14 +00:00
[DllImport("user32.dll")]
2024-08-13 02:21:48 +00:00
private static extern IntPtr GetDC(IntPtr hWnd);
2024-07-29 12:43:14 +00:00
[DllImport("user32.dll")]
2024-08-13 02:21:48 +00:00
private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
2024-07-29 12:43:14 +00:00
[DllImport("user32.dll")]
2024-08-13 02:21:48 +00:00
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
2024-07-29 12:43:14 +00:00
[DllImport("user32.dll")]
2024-08-13 02:21:48 +00:00
private static extern int GetWindowTextLength(IntPtr hWnd);
2024-07-29 12:43:14 +00:00
[DllImport("gdi32.dll")]
private static extern bool BitBlt(IntPtr hDestDC, int x, int y, int nWidth, int nHeight, IntPtr hSrcDC, int xSrc, int ySrc, int dwRop);
2024-08-13 02:21:48 +00:00
2024-08-24 13:24:26 +00:00
[DllImport("user32.dll", SetLastError = true)]
2024-08-25 13:43:34 +00:00
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")]
private static extern bool EnumDisplaySettings(string lpszDeviceName, int iModeNum, ref DEVMODE lpDevMode);
// https://stackoverflow.com/questions/60872044/how-to-get-scaling-factor-for-each-monitor-e-g-1-1-25-1-5
[StructLayout(LayoutKind.Sequential)]
private struct DEVMODE
{
private const int CCHDEVICENAME = 0x20;
private const int CCHFORMNAME = 0x20;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
public string dmDeviceName;
public short dmSpecVersion;
public short dmDriverVersion;
public short dmSize;
public short dmDriverExtra;
public int dmFields;
public int dmPositionX;
public int dmPositionY;
public ScreenOrientation dmDisplayOrientation;
public int dmDisplayFixedOutput;
public short dmColor;
public short dmDuplex;
public short dmYResolution;
public short dmTTOption;
public short dmCollate;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
public string dmFormName;
public short dmLogPixels;
public int dmBitsPerPel;
public int dmPelsWidth;
public int dmPelsHeight;
public int dmDisplayFlags;
public int dmDisplayFrequency;
public int dmICMMethod;
public int dmICMIntent;
public int dmMediaType;
public int dmDitherType;
public int dmReserved1;
public int dmReserved2;
public int dmPanningWidth;
public int dmPanningHeight;
}
2024-08-24 13:24:26 +00:00
2024-07-29 12:43:14 +00:00
private const int SRCCOPY = 0x00CC0020;
// 델리게이트 선언
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
// RECT 구조체 선언
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
private ServiceMain parent;
2024-08-12 03:47:19 +00:00
private List<Bitmap> templateImages;
2024-08-12 12:05:17 +00:00
private string templateDirectoryPath;
2024-08-25 13:43:34 +00:00
private string outputDirectoryPath;
2024-08-12 12:05:17 +00:00
private int templateCurrentIndex = 0;
2024-08-24 05:57:01 +00:00
private double threshold = 0.4;
private string mode;
private bool busy = false;
2024-08-24 07:16:07 +00:00
private List<string> _params = new List<string>();
private bool isSearchFromEnd = false;
private bool isSaveToFile = false;
private bool isUseSampleClipboard = false;
private bool isUseSampleOCR = false;
2024-09-06 07:10:58 +00:00
private int sampleWidth = 128;
private int sampleHeight = 128;
private int sampleAdjustX = 0;
private int sampleAdjustY = 0;
private string sampleOnly = "";
private byte thresholdConvertToBinary = 191;
2024-09-02 05:37:45 +00:00
private string tesseractDataPath;
private string tesseractLanguage;
2024-07-29 12:43:14 +00:00
2024-08-24 12:32:44 +00:00
public ScreenMatch(ServiceBase parent, string workingDirectory)
2024-07-29 12:43:14 +00:00
{
2024-08-12 03:47:19 +00:00
this.parent = (ServiceMain)parent;
2024-08-12 12:05:17 +00:00
templateDirectoryPath = Path.Combine(workingDirectory, "app/assets/img/_templates");
2024-08-25 13:43:34 +00:00
outputDirectoryPath = Path.Combine(workingDirectory, "app/assets/img/_captured");
2024-08-12 04:00:04 +00:00
templateImages = new List<Bitmap>();
2024-08-24 05:57:01 +00:00
// Read values from configration file
string screen_time_mode;
string screen_time_params;
2024-08-24 05:57:01 +00:00
try
{
screen_time_mode = this.parent.GetSettingsFileHandler().Read("SCREEN_TIME_MODE", "Service");
screen_time_params = this.parent.GetSettingsFileHandler().Read("SCREEN_TIME_PARAMS", "Service");
2024-08-24 05:57:01 +00:00
}
catch (Exception ex)
{
screen_time_mode = null;
screen_time_params = null;
2024-08-24 05:57:01 +00:00
this.parent.Log($"Failed to read from configration file: {ex.Message}");
}
if (!String.IsNullOrEmpty(screen_time_params))
2024-08-24 07:13:06 +00:00
{
2024-09-06 07:10:58 +00:00
var screen_time_configs = screen_time_params
.Split(',')
.Select(pair => pair.Split('='))
.ToDictionary(
parts => parts[0],
parts => parts.Length > 1 ? parts[1] : parts[0]
);
foreach (var config in screen_time_configs)
{
switch (config.Key)
{
case "backward":
{
isSearchFromEnd = true;
this.parent.Log("Use the backward search when screen time");
break;
}
case "save":
{
isSaveToFile = true;
this.parent.Log("Will be save an image file when capture the screens");
break;
}
case "sample_clipboard":
{
isUseSampleClipboard = true;
this.parent.Log("Use Clipboard within a 128x128 pixel range around specific coordinates.");
break;
}
case "sample_ocr":
{
tesseractDataPath = Path.Combine(workingDirectory, "app/assets/tessdata_best");
tesseractLanguage = "eng";
isUseSampleOCR = true;
this.parent.Log("Use OCR within a 128x128 pixel range around specific coordinates.");
break;
}
case "sample_width":
{
int.TryParse(config.Value, out sampleWidth);
break;
}
case "sample_height":
{
int.TryParse(config.Value, out sampleHeight);
break;
}
case "sample_adjust_x":
{
int.TryParse(config.Value, out sampleAdjustX);
break;
}
case "sample_adjust_y":
{
int.TryParse(config.Value, out sampleAdjustY);
break;
}
case "sample_only":
{
sampleOnly = config.Value;
break;
}
}
2024-08-24 07:13:06 +00:00
}
}
SetMode(screen_time_mode);
LoadTemplateImages();
2024-07-29 12:43:14 +00:00
}
2024-08-24 04:44:48 +00:00
public void SetMode(string mode)
{
2024-08-24 05:57:01 +00:00
if (!String.IsNullOrEmpty(mode))
{
this.mode = mode;
}
else
{
this.mode = "screen";
}
}
2024-08-24 04:44:48 +00:00
public void SetThreshold(double threshold)
{
this.threshold = threshold;
}
2024-07-29 12:43:14 +00:00
public void LoadTemplateImages()
{
2024-08-24 06:40:15 +00:00
string[] files;
2024-08-12 04:00:04 +00:00
try
{
2024-08-12 12:05:17 +00:00
files = Directory.GetFiles(templateDirectoryPath, "*.png");
2024-08-12 04:00:04 +00:00
}
catch (Exception ex)
{
2024-08-24 06:40:15 +00:00
files = new string[]{};
2024-08-24 04:44:48 +00:00
parent.Log($"Failed to read the directory structure: {ex.Message}");
2024-08-12 04:00:04 +00:00
}
2024-07-29 12:43:14 +00:00
foreach (var file in files)
{
2024-08-26 06:24:38 +00:00
string filename = Path.GetFileName(file);
2024-08-24 04:44:48 +00:00
Bitmap bitmap = new Bitmap(file)
{
2024-08-26 06:24:38 +00:00
Tag = filename
2024-08-24 04:44:48 +00:00
};
2024-08-26 06:24:38 +00:00
if (filename.StartsWith("binary_"))
{
templateImages.Add(ConvertToBinary(bitmap, thresholdConvertToBinary));
}
else
{
templateImages.Add(bitmap);
}
2024-07-29 12:43:14 +00:00
}
}
// 캡쳐 및 템플릿 매칭 진행
public List<ScreenMatchResult> CaptureAndMatch()
{
2024-08-26 06:24:38 +00:00
List<ScreenMatchResult> results = new List<ScreenMatchResult>();
if (busy)
2024-08-26 06:24:38 +00:00
{
throw new Exception("Waiting done a previous job...");
}
2024-08-29 06:34:54 +00:00
if (templateImages.Count > 0)
{
toggleBusy();
2024-08-29 06:34:54 +00:00
switch (mode)
{
case "screen": // 화면 기준
results = CaptureAndMatchAllScreens();
toggleBusy();
2024-08-29 06:34:54 +00:00
break;
case "window": // 윈도우 핸들 기준
results = CaptureAndMatchAllWindows();
toggleBusy();
2024-08-29 06:34:54 +00:00
break;
default:
toggleBusy();
2024-08-29 06:34:54 +00:00
throw new Exception($"Unknown capture mode {mode}");
}
}
2024-08-26 06:24:38 +00:00
return results;
}
2024-07-29 12:43:14 +00:00
// 화면을 기준으로 찾기
public List<ScreenMatchResult> CaptureAndMatchAllScreens()
{
var results = new List<ScreenMatchResult>();
for (int i = 0; i < Screen.AllScreens.Length; i++)
{
Screen screen = Screen.AllScreens[i];
Bitmap mainImage = CaptureScreen(screen);
2024-08-26 06:24:38 +00:00
if (isSaveToFile)
2024-08-25 13:43:34 +00:00
{
2024-08-26 12:53:13 +00:00
string outputFilePath = Path.Combine(outputDirectoryPath, $"{DateTime.Now.ToString("yyyy-MM-dd hh mm ss")}.png");
2024-08-26 06:24:38 +00:00
((Bitmap)mainImage.Clone()).Save(outputFilePath);
2024-08-25 13:43:34 +00:00
parent.Log($"Screenshot saved: {outputFilePath}");
}
2024-08-12 12:05:17 +00:00
Bitmap image = templateImages[templateCurrentIndex];
2024-08-02 07:53:33 +00:00
parent.Log($"Trying match the template {image.Tag as string} on the screen {i}...");
2024-07-29 12:43:14 +00:00
2024-08-26 06:24:38 +00:00
string filename = image.Tag as string;
2024-09-02 06:30:20 +00:00
int imageWidth = image.Width;
int imageHeight = image.Height;
Bitmap _mainImage;
2024-08-26 06:24:38 +00:00
if (filename.StartsWith("binary_"))
{
_mainImage = ConvertToBinary((Bitmap)mainImage.Clone(), thresholdConvertToBinary);
}
else
{
_mainImage = mainImage;
2024-08-26 06:24:38 +00:00
}
Point matchPosition = FindTemplate(_mainImage, (Bitmap)image.Clone(), out double maxCorrelation);
2024-09-06 07:10:58 +00:00
string text = "";
if (String.IsNullOrEmpty(sampleOnly) || (!String.IsNullOrEmpty(sampleOnly) && sampleOnly == filename))
{
text = InspectSample((Bitmap)mainImage.Clone(), matchPosition.X, matchPosition.Y, imageWidth, imageHeight, sampleWidth, sampleHeight);
}
2024-08-26 06:24:38 +00:00
if (matchPosition != Point.Empty)
{
2024-08-26 06:24:38 +00:00
results.Add(new ScreenMatchResult
{
FileName = image.Tag.ToString(),
ScreenNumber = i,
Position = matchPosition,
MaxCorrelation = maxCorrelation,
2024-09-06 07:10:58 +00:00
Text = text
2024-08-26 06:24:38 +00:00
});
}
}
2024-08-26 08:53:21 +00:00
if (results.Count > 0)
{
parent.Log("Match found");
}
else
2024-08-26 06:24:38 +00:00
{
parent.Log($"No match found");
2024-07-29 12:43:14 +00:00
}
2024-08-12 12:05:17 +00:00
templateCurrentIndex = ++templateCurrentIndex % templateImages.Count;
2024-07-29 12:43:14 +00:00
return results;
}
public string InspectSample(Bitmap bitmap, int x, int y, int a, int b, int w, int h)
{
2024-09-02 06:30:20 +00:00
if (bitmap == null)
{
throw new ArgumentNullException(nameof(bitmap), "Bitmap cannot be null.");
}
// initial text
string text = "";
2024-09-02 06:30:20 +00:00
// Adjust coordinates
x = x + (a / 2);
y = y + (b / 2);
// Set range of crop image
2024-09-06 07:10:58 +00:00
int cropX = Math.Max(x - w / 2, 0) + sampleAdjustX;
int cropY = Math.Max(y - h / 2, 0) + sampleAdjustY;
int cropWidth = Math.Min(w, bitmap.Width - cropX);
int cropHeight = Math.Min(h, bitmap.Height - cropY);
Rectangle cropArea = new Rectangle(cropX, cropY, cropWidth, cropHeight);
2024-09-02 06:30:20 +00:00
// Crop image
Bitmap croppedBitmap = bitmap.Clone(cropArea, bitmap.PixelFormat);
// if use Clipboard
if (isUseSampleClipboard)
{
2024-09-04 07:18:05 +00:00
Thread th = new Thread(new ThreadStart(() =>
{
2024-09-04 07:18:05 +00:00
try
{
2024-09-04 07:20:48 +00:00
Clipboard.SetImage((Bitmap)croppedBitmap.Clone());
2024-09-04 07:18:05 +00:00
parent.Log($"Copied the image to Clipboard");
}
catch (Exception ex)
{
parent.Log($"Error in Clipboard: {ex.Message}");
}
}));
th.SetApartmentState(ApartmentState.STA);
th.Start();
}
// if use OCR
if (isUseSampleOCR)
{
try
{
using (var engine = new TesseractEngine(tesseractDataPath, tesseractLanguage, EngineMode.Default))
{
using (var page = engine.Process(croppedBitmap))
{
text = page.GetText();
parent.Log($"Mean confidence: {page.GetMeanConfidence()}");
parent.Log($"Text (GetText): {text}");
}
}
}
catch (Exception ex)
{
parent.Log($"Error in OCR: {ex.Message}");
}
}
return text;
}
public Bitmap CaptureScreen(Screen screen)
2024-07-29 12:43:14 +00:00
{
Rectangle screenSize = screen.Bounds;
2024-08-25 13:43:34 +00:00
DEVMODE dm = new DEVMODE();
dm.dmSize = (short)Marshal.SizeOf(typeof(DEVMODE));
EnumDisplaySettings(screen.DeviceName, -1, ref dm);
var scalingFactor = Math.Round(Decimal.Divide(dm.dmPelsWidth, screen.Bounds.Width), 2);
2024-08-26 06:24:38 +00:00
parent.Log($"Resolved the screen scale: {scalingFactor}");
2024-08-25 13:43:34 +00:00
int adjustedWidth = (int)(screenSize.Width * scalingFactor);
int adjustedHeight = (int)(screenSize.Height * scalingFactor);
Bitmap bitmap = new Bitmap(adjustedWidth, adjustedHeight);
using (Graphics bitmapGraphics = Graphics.FromImage(bitmap))
2024-07-29 12:43:14 +00:00
{
2024-08-25 13:43:34 +00:00
bitmapGraphics.CopyFromScreen(screenSize.Left, screenSize.Top, 0, 0, new Size(adjustedWidth, adjustedHeight));
2024-07-29 12:43:14 +00:00
}
2024-08-26 06:24:38 +00:00
return bitmap;
2024-07-29 12:43:14 +00:00
}
// 윈도우 핸들을 기준으로 찾기
public List<ScreenMatchResult> CaptureAndMatchAllWindows()
{
var results = new List<ScreenMatchResult>();
// 모든 윈도우 핸들을 열거
EnumWindows((hWnd, lParam) =>
{
if (IsWindowVisible(hWnd))
{
try
{
string windowTitle = GetWindowTitle(hWnd);
2024-08-24 13:24:26 +00:00
string processName = GetProcessName(hWnd);
GetWindowRect(hWnd, out RECT windowRect);
Point windowPosition = new Point(windowRect.Left, windowRect.Top); // 창 위치 계산
2024-07-29 12:43:14 +00:00
Bitmap windowImage = CaptureWindow(hWnd);
2024-08-24 13:24:26 +00:00
2024-07-29 12:43:14 +00:00
if (windowImage != null)
{
2024-08-12 12:05:17 +00:00
Bitmap image = templateImages[templateCurrentIndex];
2024-08-24 13:24:26 +00:00
Point matchPosition = FindTemplate(windowImage, image, out double maxCorrelation);
string templateFileName = image.Tag as string;
var result = new ScreenMatchResult
2024-07-29 12:43:14 +00:00
{
FileName = templateFileName,
WindowHandle = hWnd,
WindowTitle = windowTitle,
2024-08-24 13:24:26 +00:00
ProcessName = processName,
WindowPosition = windowPosition,
Position = matchPosition,
MaxCorrelation = maxCorrelation
};
results.Add(result);
2024-07-29 12:43:14 +00:00
}
}
catch { }
}
return true;
}, IntPtr.Zero);
2024-08-12 12:05:17 +00:00
templateCurrentIndex = ++templateCurrentIndex % templateImages.Count;
2024-07-29 12:43:14 +00:00
return results;
}
public string GetWindowTitle(IntPtr hWnd)
{
int length = GetWindowTextLength(hWnd);
StringBuilder sb = new StringBuilder(length + 1);
GetWindowText(hWnd, sb, sb.Capacity);
return sb.ToString();
}
2024-08-24 13:24:26 +00:00
public string GetProcessName(IntPtr hWnd)
{
uint processId;
GetWindowThreadProcessId(hWnd, out processId);
Process process = Process.GetProcessById((int)processId);
return process.ProcessName;
}
2024-07-29 12:43:14 +00:00
public Bitmap CaptureWindow(IntPtr hWnd)
{
GetWindowRect(hWnd, out RECT rect);
int width = rect.Right - rect.Left;
int height = rect.Bottom - rect.Top;
if (width <= 0 || height <= 0)
return null;
Bitmap bitmap = new Bitmap(width, height);
Graphics graphics = Graphics.FromImage(bitmap);
IntPtr hDC = graphics.GetHdc();
IntPtr windowDC = GetDC(hWnd);
bool success = BitBlt(hDC, 0, 0, width, height, windowDC, 0, 0, SRCCOPY);
ReleaseDC(hWnd, windowDC);
graphics.ReleaseHdc(hDC);
return success ? bitmap : null;
}
public Point FindTemplate(Bitmap mainImage, Bitmap templateImage, out double maxCorrelation)
{
int mainWidth = mainImage.Width;
int mainHeight = mainImage.Height;
int templateWidth = templateImage.Width;
int templateHeight = templateImage.Height;
Point bestMatch = Point.Empty;
maxCorrelation = 0;
int startX = isSearchFromEnd ? mainWidth - templateWidth : 0;
int endX = isSearchFromEnd ? -1 : mainWidth - templateWidth + 1;
int stepX = isSearchFromEnd ? -1 : 1;
2024-08-25 08:57:22 +00:00
int startY = isSearchFromEnd ? mainHeight - templateHeight : 0;
int endY = isSearchFromEnd ? -1 : mainHeight - templateHeight + 1;
int stepY = isSearchFromEnd ? -1 : 1;
2024-08-25 08:57:22 +00:00
for (int x = startX; x != endX; x += stepX)
2024-07-29 12:43:14 +00:00
{
2024-08-25 08:57:22 +00:00
for (int y = startY; y != endY; y += stepY)
2024-07-29 12:43:14 +00:00
{
2024-08-12 12:05:17 +00:00
if (IsTemplateMatch(mainImage, templateImage, x, y, threshold))
2024-07-29 12:43:14 +00:00
{
bestMatch = new Point(x, y);
2024-08-24 04:44:48 +00:00
maxCorrelation = 1.0;
2024-07-29 12:43:14 +00:00
return bestMatch;
}
}
}
return bestMatch;
}
private void toggleBusy()
2024-08-26 06:24:38 +00:00
{
busy = !busy;
2024-08-26 06:24:38 +00:00
}
2024-08-12 12:05:17 +00:00
private bool IsTemplateMatch(Bitmap mainImage, Bitmap templateImage, int offsetX, int offsetY, double threshold)
2024-07-29 12:43:14 +00:00
{
int templateWidth = templateImage.Width;
int templateHeight = templateImage.Height;
int totalPixels = templateWidth * templateHeight;
int requiredMatches = (int)(totalPixels * threshold);
// When the square root of the canvas size of the image to be matched is less than 10, a complete match is applied.
if (Math.Sqrt(templateWidth * templateHeight) < 10.0)
{
for (int y = 0; y < templateHeight; y++)
{
for (int x = 0; x < templateWidth; x++)
{
if (mainImage.GetPixel(x + offsetX, y + offsetY) != templateImage.GetPixel(x, y))
{
return false;
}
}
}
return true;
}
// Otherwise, randomness is used.
int matchedCount = 0;
Random rand = new Random();
while (matchedCount < requiredMatches)
2024-07-29 12:43:14 +00:00
{
int x = rand.Next(templateWidth);
int y = rand.Next(templateHeight);
if (mainImage.GetPixel(x + offsetX, y + offsetY) != templateImage.GetPixel(x, y))
2024-07-29 12:43:14 +00:00
{
return false;
2024-07-29 12:43:14 +00:00
}
matchedCount++;
2024-07-29 12:43:14 +00:00
}
return true;
}
private Bitmap ConvertToBinary(Bitmap image, byte threshold)
{
Bitmap binaryImage = new Bitmap(image.Width, image.Height);
2024-08-26 06:24:38 +00:00
if (image.Tag != null)
{
binaryImage.Tag = image.Tag;
}
for (int y = 0; y < image.Height; y++)
{
for (int x = 0; x < image.Width; x++)
{
// Convert the pixel to grayscale
Color pixelColor = image.GetPixel(x, y);
byte grayValue = (byte)((pixelColor.R + pixelColor.G + pixelColor.B) / 3);
// Apply threshold to convert to binary
2024-08-26 06:24:38 +00:00
Color binaryColor = grayValue >= threshold ? Color.White : Color.Black;
binaryImage.SetPixel(x, y, binaryColor);
}
}
return binaryImage;
}
}