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;
}
}
}