using System.Windows; using System.Windows.Controls; using System.Windows.Media; using CommunityToolkit.Mvvm.ComponentModel; using System.IO; using System.Windows.Media.Imaging; using Emgu.CV.CvEnum; using Emgu.CV; using System.Drawing; using Image = System.Windows.Controls.Image; using Rectangle = System.Windows.Shapes.Rectangle; using Size = System.Drawing.Size; using Ookii.Dialogs.Wpf; using Rect = System.Windows.Rect; using System.ComponentModel; using Newtonsoft.Json; using Xceed.Wpf.Toolkit.PropertyGrid.Attributes; using System.ComponentModel; using ClosedXML.Excel; using Colors = System.Windows.Media.Colors; using CtrEditor.FuncionesBase; using System.Drawing; using System.Windows.Shapes; using System.Drawing.Imaging; using Emgu.CV.Structure; namespace CtrEditor.ObjetosSim.Extraccion_Datos { /// /// Represents a template search control that identifies similar patterns in images and creates tag extraction clones. /// This class is designed to work with OCR extraction by finding visual patterns and creating copies of extraction tags /// at each matching location. /// /// /// Key functionalities: /// - Template matching using OpenCV /// - Automatic tag cloning at found locations /// - OCR text extraction from matched regions /// - Export capabilities to Excel /// /// Workflow: /// 1. User creates a search template by positioning and sizing the control over a pattern /// 2. Links extraction tags to this template using Id_Search_Templates /// 3. Activates search_templates to find similar patterns /// 4. The system automatically: /// - Searches for visual matches in the image /// - Creates clones of linked extraction tags at each match /// - Assigns incremental copy_Number to organize rows in exports /// - Performs OCR on each cloned tag location /// /// Properties: /// - search_templates: Triggers the pattern search process /// - threshold: Minimum similarity threshold for pattern matching /// - coincidencias: Number of matches found (readonly) /// - show_debug_ocr: Shows debug windows during OCR process /// - export_ocr: Triggers OCR text export for all matches /// /// Usage example: /// 1. Position the search template over a repeating pattern /// 2. Create extraction tags and link them to this template /// 3. Set threshold value (default usually works well) /// 4. Activate search_templates to find matches and create clones /// public partial class osBuscarCoincidencias : osBase, IosBase { [JsonIgnore] public float offsetY; [JsonIgnore] public float offsetX; [ObservableProperty] List search_rectangles; public static string NombreClase() { return "Search Templates"; } private string nombre = NombreClase(); public override string Nombre { get => nombre; set => SetProperty(ref nombre, value); } [ObservableProperty] [property: Category("Tag Extraction:")] bool search_templates; partial void OnSearch_templatesChanged(bool oldValue, bool newValue) { if (Search_templates) BuscarCoincidencias(); Search_templates = false; } [ObservableProperty] [property: Category("Tag Extraction:")] bool tomarClip; // En lugar de almacenar Mat directamente, guardaremos una representación serializable [ObservableProperty] [property: Category("Tag Extraction:")] byte[] capturedRegionData; [ObservableProperty] [property: Category("Tag Extraction:")] [property: ReadOnly(true)] bool regionCapturada; // Para uso interno (no serializado) [JsonIgnore] private Mat _capturedRegion; // Propiedades para almacenar las dimensiones de la captura [ObservableProperty] [property: Category("Tag Extraction:")] int capturedWidth; [ObservableProperty] [property: Category("Tag Extraction:")] int capturedHeight; [ObservableProperty] [property: Category("Tag Extraction:")] bool export_ocr; [ObservableProperty] [property: Category("Tag Extraction:")] string text_export_ocr; // Esta propiedad manejará la conversión entre Mat y datos serializables [JsonIgnore] public Mat CapturedRegion { get { if (_capturedRegion == null && CapturedRegionData != null && CapturedRegionData.Length > 0) { // Recrear Mat desde los datos almacenados _capturedRegion = BytesToMat(CapturedRegionData, CapturedWidth, CapturedHeight); } return _capturedRegion; } set { if (value != null) { // Convertir Mat a bytes para serialización CapturedRegionData = MatToBytes(value); CapturedWidth = value.Width; CapturedHeight = value.Height; _capturedRegion = value; } else { CapturedRegionData = null; CapturedWidth = 0; CapturedHeight = 0; _capturedRegion = null; } } } partial void OnTomarClipChanged(bool oldValue, bool newValue) { if (tomarClip) { CapturarRegionActual(); TomarClip = false; // Resetear el flag después de la captura } } // Método para capturar la región actual private void CapturarRegionActual() { Application.Current.Dispatcher.Invoke(() => { if (_mainViewModel?.MainCanvas.Children[0] is Image imagenDeFondo) { if (imagenDeFondo.Source is BitmapSource bitmapSource) { // Obtener los DPI de la imagen original float originalDpiX = (float)bitmapSource.DpiX; float originalDpiY = (float)bitmapSource.DpiY; // Estándar DPI en el que el Canvas renderiza la imagen float canvasDpiX = 96; float canvasDpiY = 96; // Calcular el ratio de escala entre el Canvas y la imagen original float scaleFactorX = originalDpiX / canvasDpiX; float scaleFactorY = originalDpiY / canvasDpiY; // Ajustar las coordenadas de recorte en función del ratio de escala int x = (int)MeterToPixels(Left * scaleFactorX); int y = (int)MeterToPixels(Top * scaleFactorY); int width = (int)MeterToPixels(Ancho * scaleFactorX); int height = (int)MeterToPixels(Alto * scaleFactorY); // Validar y ajustar el tamaño del recorte para que se mantenga dentro de los límites de la imagen if (x < 0) x = 0; if (y < 0) y = 0; if (x + width > bitmapSource.PixelWidth) width = bitmapSource.PixelWidth - x; if (y + height > bitmapSource.PixelHeight) height = bitmapSource.PixelHeight - y; // Recortar el área deseada utilizando las coordenadas ajustadas CroppedBitmap croppedBitmap = new CroppedBitmap(bitmapSource, new Int32Rect(x, y, width, height)); // Capturar la región y almacenarla if (_capturedRegion != null) { _capturedRegion.Dispose(); // Liberar recursos previos } // Usar la propiedad que maneja la serialización CapturedRegion = BitmapSourceToMat(croppedBitmap); // Actualizar el estado RegionCapturada = true; MessageBox.Show("Región capturada correctamente.", "Información", MessageBoxButton.OK, MessageBoxImage.Information); } } }); } // Métodos para convertir entre Mat y bytes private byte[] MatToBytes(Mat mat) { if (mat == null) return null; // Asegurar que tenemos un formato consistente para serialización Mat bgr = new Mat(); if (mat.NumberOfChannels != 3) { CvInvoke.CvtColor(mat, bgr, mat.NumberOfChannels == 1 ? ColorConversion.Gray2Bgr : ColorConversion.Bgra2Bgr); } else { bgr = mat.Clone(); } // Convertir a un formato que pueda ser serializado using (MemoryStream ms = new MemoryStream()) { // Convertir Mat a Bitmap Bitmap bitmap = bgr.ToBitmap(); // Guardar como PNG (sin pérdida de calidad) bitmap.Save(ms, ImageFormat.Png); // Liberar recursos bitmap.Dispose(); if (bgr != mat) bgr.Dispose(); return ms.ToArray(); } } private Mat BytesToMat(byte[] bytes, int width, int height) { if (bytes == null || bytes.Length == 0) return null; try { using (MemoryStream ms = new MemoryStream(bytes)) { // Cargar imagen desde bytes Bitmap bitmap = (Bitmap)System.Drawing.Image.FromStream(ms); // Convertir Bitmap a Mat Image img = bitmap.ToImage(); // Liberar recursos bitmap.Dispose(); // Si las dimensiones no coinciden, redimensionar if (img.Width != width || img.Height != height) { CvInvoke.Resize(img.Mat, img.Mat, new Size(width, height)); } return img.Mat; } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"Error al reconstruir Mat: {ex.Message}"); return null; } } // Sobrescribir los métodos de cambio de tamaño para limpiar la región capturada public override void AnchoChanged(float newValue) { base.AnchoChanged(newValue); //LimpiarRegionCapturada(); } public override void AltoChanged(float newValue) { base.AnchoChanged(newValue); // LimpiarRegionCapturada(); } private void LimpiarRegionCapturada() { if (_capturedRegion != null) { _capturedRegion.Dispose(); _capturedRegion = null; } // Usar la propiedad para manejar la serialización CapturedRegion = null; RegionCapturada = false; } partial void OnExport_ocrChanged(bool value) { if (Export_ocr) { Text_export_ocr = ""; if (!string.IsNullOrEmpty(Nombre) && _mainViewModel != null) { foreach (var objetoSimulable in _mainViewModel.ObjetosSimulables) { if (objetoSimulable != this && objetoSimulable.Group_Panel == Nombre) { if (objetoSimulable is osExtraccionTag osExtraccionTag) { osExtraccionTag.CaptureImageAreaAndDoOCR(); Text_export_ocr += osExtraccionTag.Tag_extract; } } } } } Export_ocr = false; } public override void TopChanging(float oldValue, float newValue) { offsetY = newValue - oldValue; } public override void LeftChanging(float oldValue, float newValue) { offsetX = newValue - oldValue; } [ObservableProperty] [property: Category("Tag Extraction:")] string tag_extract; [ObservableProperty] [property: Category("Tag Extraction:")] string clase; [ObservableProperty] [property: Category("Tag Extraction:")] string tag_name; [ObservableProperty] float opacity_oculto; [ObservableProperty] [property: Category("Tag Extraction:")] bool show_debug_ocr; [ObservableProperty] [property: Category("Tag Extraction:")] float threshold; [ObservableProperty] [property: Category("Tag Extraction:")] [property: ReadOnly(true)] int coincidencias; public osBuscarCoincidencias() { Ancho = 1; Alto = 1; Angulo = 0; Opacity_oculto = 0.1f; Threshold = 0.6f; } private void ShowPreviewWindow(Stream imageStream) { // Create a new window for preview Window previewWindow = new Window { Title = "Preview Captured Image", Width = 500, Height = 500, Content = new Image { Source = BitmapFrame.Create(imageStream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad), Stretch = Stretch.Uniform } }; previewWindow.ShowDialog(); } public async void BuscarCoincidencias() { var progressDialog = new ProgressDialog { WindowTitle = "Procesando", Text = "Buscando coincidencias...", ShowTimeRemaining = true, ShowCancelButton = false }; progressDialog.DoWork += (sender, e) => BuscarCoincidenciasAsync(progressDialog); progressDialog.RunWorkerCompleted += (sender, e) => { if (e.Error != null) { MessageBox.Show(e.Error.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); } }; progressDialog.Show(); } // Modificar el método BuscarCoincidenciasAsync para usar la región capturada si está disponible private void BuscarCoincidenciasAsync(ProgressDialog progressDialog) { // Reset the Canvas children Application.Current.Dispatcher.Invoke(() => { var clearShapes = _mainViewModel.MainCanvas.Children.OfType().Where(s => s.Tag as string == "BuscarCoincidencias").ToList(); foreach (var shape in clearShapes) { _mainViewModel.MainCanvas.Children.Remove(shape); } if (_mainViewModel?.MainCanvas.Children[0] is Image imagenDeFondo) { // Asegurarse de que la imagen origen está disponible if (imagenDeFondo.Source is BitmapSource bitmapSource) { progressDialog.ReportProgress(10); Mat templateMat; int width, height; float scaleFactorX, scaleFactorY; bool deleteTemplateMat = false; // Usar la región capturada si existe, de lo contrario capturar la región actual if (CapturedRegion != null && RegionCapturada) { // Usar la región almacenada (ya deserializada) templateMat = CapturedRegion.Clone(); deleteTemplateMat = true; width = templateMat.Width; height = templateMat.Height; // Para mantener la relación con la imagen original float originalDpiX = (float)bitmapSource.DpiX; float originalDpiY = (float)bitmapSource.DpiY; float canvasDpiX = 96; float canvasDpiY = 96; scaleFactorX = originalDpiX / canvasDpiX; scaleFactorY = originalDpiY / canvasDpiY; } else { // Obtener los DPI de la imagen original float originalDpiX = (float)bitmapSource.DpiX; float originalDpiY = (float)bitmapSource.DpiY; // Estándar DPI en el que el Canvas renderiza la imagen float canvasDpiX = 96; float canvasDpiY = 96; // Calcular el ratio de escala entre el Canvas y la imagen original scaleFactorX = originalDpiX / canvasDpiX; scaleFactorY = originalDpiY / canvasDpiY; // Ajustar las coordenadas de recorte en función del ratio de escala int x = (int)MeterToPixels(Left * scaleFactorX); int y = (int)MeterToPixels(Top * scaleFactorY); width = (int)MeterToPixels(Ancho * scaleFactorX); height = (int)MeterToPixels(Alto * scaleFactorY); // Validar y ajustar el tamaño del recorte para que se mantenga dentro de los límites de la imagen if (x < 0) x = 0; if (y < 0) y = 0; if (x + width > bitmapSource.PixelWidth) width = bitmapSource.PixelWidth - x; if (y + height > bitmapSource.PixelHeight) height = bitmapSource.PixelHeight - y; // Recortar el área deseada utilizando las coordenadas ajustadas CroppedBitmap croppedBitmap = new CroppedBitmap(bitmapSource, new Int32Rect(x, y, width, height)); // Convertir CroppedBitmap a Mat directamente templateMat = BitmapSourceToMat(croppedBitmap); deleteTemplateMat = true; } int scale = 4; // Convertir la plantilla a escala de grises y redimensionarla Mat templateGray = new Mat(); CvInvoke.CvtColor(templateMat, templateGray, ColorConversion.Bgr2Gray); Mat templateGrayResized = new Mat(); CvInvoke.Resize(templateGray, templateGrayResized, new Size(templateGray.Width / scale, templateGray.Height / scale), 0, 0, Inter.Linear); progressDialog.ReportProgress(20); // El resto del código permanece igual... // Cargar la imagen principal completa en un Mat Mat mainImageMat = BitmapSourceToMat(bitmapSource); // Convertir la imagen principal a escala de grises y redimensionarla Mat mainImageGray = new Mat(); CvInvoke.CvtColor(mainImageMat, mainImageGray, ColorConversion.Bgr2Gray); Mat mainImageGrayResized = new Mat(); CvInvoke.Resize(mainImageGray, mainImageGrayResized, new Size(mainImageGray.Width / scale, mainImageGray.Height / scale), 0, 0, Inter.Linear); progressDialog.ReportProgress(50); // Realizar la coincidencia de plantillas Mat result = new Mat(); CvInvoke.MatchTemplate(mainImageGray, templateGray, result, TemplateMatchingType.CcoeffNormed); // Establecer un umbral de coincidencia if (Threshold < 0.4) Threshold = 0.4f; double threshold = Threshold; int ConteoPositivos = 0; // Lista para mantener áreas ya aceptadas if (search_rectangles != null) search_rectangles.Clear(); else search_rectangles = new List(); // Obtener los puntos que superan el umbral float[] resultArray = result.GetData(false) as float[]; if (resultArray != null) { for (int i = 0; i < resultArray.Length; i++) { if (resultArray[i] >= threshold) { int row = i / result.Cols; int col = i % result.Cols; Rect newRect = new Rect(); newRect.X = col / scaleFactorX; newRect.Y = row / scaleFactorY; newRect.Width = width / scaleFactorX; newRect.Height = height / scaleFactorY; // Crear un rectángulo para la coincidencia actual Rectangle matchRect = new Rectangle { Stroke = new SolidColorBrush(Colors.Red), StrokeThickness = 2, Width = newRect.Width, Height = newRect.Height, Tag = "BuscarCoincidencias" }; Canvas.SetLeft(matchRect, newRect.X); Canvas.SetTop(matchRect, newRect.Y); // Verificar si la coincidencia actual está dentro de algún rectángulo aceptado bool isOverlap = search_rectangles.Any(r => r.IntersectsWith(newRect) ); // Si no hay superposición, agregar el rectángulo al Canvas y a la lista de aceptados if (!isOverlap) { Canvas.SetZIndex(matchRect, 40); _mainViewModel.MainCanvas.Children.Add(matchRect); search_rectangles.Add(newRect); ConteoPositivos++; Coincidencias = ConteoPositivos; progressDialog.ReportProgress(90); if (ConteoPositivos > 20) { // Liberar recursos antes de salir if (deleteTemplateMat) templateMat.Dispose(); templateGray.Dispose(); templateGrayResized.Dispose(); mainImageMat.Dispose(); mainImageGray.Dispose(); mainImageGrayResized.Dispose(); result.Dispose(); return; } } } } PopularTagExtraction(); } // Limpiar recursos if (deleteTemplateMat) templateMat.Dispose(); templateGray.Dispose(); templateGrayResized.Dispose(); mainImageMat.Dispose(); mainImageGray.Dispose(); mainImageGrayResized.Dispose(); result.Dispose(); } } }); } public void PopularTagExtraction() { var objetosSimulablesCopy = new List(_mainViewModel.ObjetosSimulables); foreach (var obj in objetosSimulablesCopy) if (obj is osExtraccionTag objExtraccionTag) if (objExtraccionTag.Id_Search_Templates == this.Nombre && objExtraccionTag.Cloned) _mainViewModel.RemoverObjetoSimulable(objExtraccionTag); var objetosSimulables2Copy = new List(_mainViewModel.ObjetosSimulables); // Saltar el primer rectángulo en el foreach int Row = 0; foreach (var rectangle in search_rectangles) //.Skip(1)) { float offsetX = PixelsToMeters((float)rectangle.X) - Left; float offsetY = PixelsToMeters((float)rectangle.Y) - Top; osExtraccionTag newObj = null; foreach (var eTag in objetosSimulables2Copy) { if (eTag is osExtraccionTag objExtraccionTag) { if (objExtraccionTag.Id_Search_Templates == this.Nombre) { newObj = (osExtraccionTag)_mainViewModel.DuplicarObjeto(objExtraccionTag, offsetX, offsetY); if (newObj != null) { newObj.Cloned = true; newObj.Cloned_from = objExtraccionTag.Id; newObj.Copy_Number = Row; newObj.Enable_On_All_Pages = false; if (newObj.Extraer) objExtraccionTag.CaptureImageAreaAndDoOCR(); } } } } Row++; } } public static int FindFirstEmptyRow(IXLWorksheet worksheet) { var lastRowUsed = worksheet.LastRowUsed(); return lastRowUsed == null ? 1 : lastRowUsed.RowNumber() + 1; } // Método para convertir BitmapSource a Mat private Mat BitmapSourceToMat(BitmapSource bitmapSource) { if (bitmapSource == null) throw new ArgumentNullException(nameof(bitmapSource)); // Convierte BitmapSource a Bitmap Bitmap bitmap; using (MemoryStream outStream = new MemoryStream()) { BitmapEncoder enc = new BmpBitmapEncoder(); enc.Frames.Add(BitmapFrame.Create(bitmapSource)); enc.Save(outStream); bitmap = new Bitmap(outStream); } // Convierte directamente a Mat usando Image Image image = bitmap.ToImage(); return image.Mat; } public override void ucLoaded() { // El UserControl ya se ha cargado y podemos obtener las coordenadas para // crear el objeto de simulacion base.ucLoaded(); } } public partial class ucBuscarCoincidencias : UserControl, IDataContainer { public osBase? Datos { get; set; } public int zIndex_fromFrames { get; set; } public ucBuscarCoincidencias() { InitializeComponent(); this.Loaded += OnLoaded; this.Unloaded += OnUnloaded; } private void OnLoaded(object sender, RoutedEventArgs e) { Datos?.ucLoaded(); } private void OnUnloaded(object sender, RoutedEventArgs e) { Datos?.ucUnLoaded(); } public void Highlight(bool State) { } public ZIndexEnum ZIndex_Base() { return ZIndexEnum.Estaticos; } } }