Refactor code structure for improved readability and maintainability

This commit is contained in:
Miguel 2025-09-08 22:10:19 +02:00
parent 4ef265705c
commit 59c1c0cd3e
7 changed files with 944 additions and 32 deletions

View File

@ -737,3 +737,14 @@ Esta corrección resuelve un problema crítico de estabilidad que impedía el us
* Se identificó y resolvió un problema crítico de sincronización en el servidor MCP durante la inicialización de CtrEditor. El error "MPC -32603: CtrEditor not available" ocurría porque aunque CtrEditor se iniciaba correctamente, el servidor MCP interno (puerto 5006) requiere 30-60 segundos adicionales para estar completamente operativo. El servidor se inicia automáticamente en el constructor del MainViewModel pero no acepta conexiones inmediatamente. La solución documentada es esperar este tiempo de inicialización antes de intentar operaciones MCP como `create_object`. Una vez inicializado, el método CreateObject funciona perfectamente para crear objetos como osHydPump con todas sus propiedades. Este conocimiento es crucial para el uso correcto del sistema MCP y evita falsas alarmas sobre conectividad.
## Problemas Críticos Identificados en ScreenshotManager
### Problema de Canvas Completo con Baja Resolución
El sistema genera imágenes masivas (3.4MB) con baja resolución útil debido al tamaño de canvas de 78x54 metros cuando los objetos están concentrados en área pequeña (39-46m x 19-21m). La densidad de píxeles por metro es insuficiente para ver detalles de los componentes.
### Problema de Captura de Objetos en Blanco
Las capturas de objetos específicos resultan en archivos prácticamente vacíos (2.5KB) por tres causas principales en ScreenshotManager.cs: (1) VisualBrush falla en WPF bajo ciertas condiciones (líneas 446-451), (2) factor de escala 2.0 problemático causa problemas de memoria (líneas 406-409), y (3) elementos con coordenadas NaN causan fallos en intersección de rectángulos (líneas 438-442).
### Soluciones Recomendadas
Se proponen tres mejoras: (1) implementar auto-zoom para canvas que detecte área con objetos visibles y capture solo esa región con padding adicional, (2) reemplazar VisualBrush con DrawingVisual para renderizado directo y reducir scaleFactor a 1.0, y (3) mejorar validación de posiciones de elementos verificando NaN, infinito y tamaños válidos antes del cálculo de intersección. Estas correcciones resolverían ambos problemas críticos del sistema de capturas.

160
README_Canvas_Reference.md Normal file
View File

@ -0,0 +1,160 @@
# Sistema de Imagen de Referencia para Canvas de CtrEditor
Este sistema permite generar y analizar imágenes de fondo de referencia para el canvas de CtrEditor, facilitando la detección de problemas de resolución y posicionamiento en las capturas de pantalla.
## 📁 Archivos del Sistema
- `generate_canvas_reference.py` - Generador de imagen de fondo con QR codes
- `analyze_canvas_screenshot.py` - Analizador de capturas con QR codes
- `requirements.txt` - Dependencias Python necesarias
- `canvas_reference_background.png` - Imagen generada (3915×2726 px)
## 🚀 Instalación y Uso
### 1. Instalar Dependencias
```bash
pip install -r requirements.txt
```
### 2. Generar Imagen de Referencia
```bash
python generate_canvas_reference.py
```
Esto crea `canvas_reference_background.png` con:
- **Dimensiones**: 3915×2726 píxeles (78.31×54.53 metros)
- **Resolución**: 50 píxeles por metro
- **8 QR codes** en posiciones estratégicas
- **Grilla de coordenadas** cada 1 metro (gruesas cada 5m)
- **4 zonas de color** para identificación visual
- **Área de objetos** marcada en rojo
### 3. Usar en CtrEditor
1. Cargar `canvas_reference_background.png` como imagen de fondo del canvas
2. Tomar capturas de pantalla (completas o parciales)
3. Analizar las capturas con el script analizador
### 4. Analizar Capturas
```bash
python analyze_canvas_screenshot.py screenshot.png
```
El analizador detecta automáticamente:
- **Posición real** de QR codes vs posición esperada
- **Factor de escala** actual vs original
- **Distorsiones** en resolución horizontal/vertical
- **Problemas de posicionamiento** en área de objetos
## 🎯 QR Codes y Sus Posiciones
| Etiqueta | Posición (metros) | Propósito |
|----------|-------------------|-----------|
| TL | (5, 5) | Esquina superior izquierda |
| TR | (70, 5) | Esquina superior derecha |
| BL | (5, 45) | Esquina inferior izquierda |
| BR | (70, 45) | Esquina inferior derecha |
| CENTER | (39, 27) | Centro del canvas |
| OBJECTS | (42, 20) | Área donde están los objetos |
| CAL1 | (20, 15) | Punto de calibración 1 |
| CAL2 | (55, 35) | Punto de calibración 2 |
## 📊 Información en QR Codes
Cada QR contiene datos JSON con:
```json
{
"position": [42, 20],
"label": "OBJECTS",
"canvas_size": [78.31, 54.53],
"image_size": [3915, 2726],
"pixels_per_meter": 50,
"timestamp": "2025-01-09T...",
"type": "CtrEditor_Canvas_Reference"
}
```
## 🔍 Ejemplos de Análisis
### Captura Normal
```bash
$ python analyze_canvas_screenshot.py debug_full_canvas.png
📸 Analizando imagen: debug_full_canvas.png
📐 Tamaño de imagen: 3915 × 2726 píxeles
🔍 QR codes encontrados: 8
🎯 QR #1: TL
📍 Posición esperada: [5, 5] metros
📱 Encontrado en píxeles: (250, 250)
📊 Escala original: 50.0 px/m
📊 Escala actual: 50.0 px/m
📊 Factor de escala: 1.000
```
### Captura con Problemas
```bash
🎯 QR #1: OBJECTS
📍 Posición esperada: [42, 20] metros
📱 Encontrado en píxeles: (1890, 850)
📊 Escala original: 50.0 px/m
📊 Escala actual: 25.2 px/m
📊 Factor de escala: 0.504
⚠️ PROBLEMA: Resolución reducida al 50.4%
```
## 🛠️ Resolución de Problemas Detectados
### Factor de Escala < 1.0
- **Causa**: Canvas se renderiza a menor resolución
- **Solución**: Ajustar factor de escala en ScreenshotManager.cs
### QR Codes Faltantes
- **Causa**: Área de captura muy pequeña o desplazada
- **Solución**: Verificar coordenadas de área de captura
### Distorsión Horizontal/Vertical
- **Causa**: Aspect ratio incorrecto
- **Solución**: Verificar cálculos de píxeles vs metros
## 🎨 Características de la Imagen
### Zonas de Color
- **Zona 1** (Superior izquierda): Rojo muy claro
- **Zona 2** (Superior derecha): Verde muy claro
- **Zona 3** (Inferior izquierda): Azul muy claro
- **Zona 4** (Inferior derecha): Amarillo muy claro
### Patrones de Referencia
- **Círculos concéntricos** en el centro (radios 2, 4, 6, 8, 10m)
- **Líneas finas** para detectar aliasing
- **Grilla precisa** con coordenadas etiquetadas
### Área de Objetos
Rectángulo rojo que marca la región donde están ubicados los objetos del análisis:
- **Coordenadas**: 39.92, 19.76 → 44.99, 20.47 metros
- **Tamaño**: 5.07 × 0.71 metros
## 📋 Flujo de Trabajo Recomendado
1. **Generar** imagen de referencia
2. **Cargar** como fondo en CtrEditor
3. **Tomar** capturas problemáticas
4. **Analizar** con script automático
5. **Identificar** problemas específicos
6. **Corregir** código ScreenshotManager.cs
7. **Validar** con nuevas capturas
## 🤖 Integración con LLM
El sistema está diseñado para que los LLMs puedan:
- **Leer automáticamente** los QR codes de las capturas
- **Detectar problemas** de escala y posicionamiento
- **Generar reportes** específicos de cada captura
- **Sugerir correcciones** basadas en los datos detectados
Este enfoque automatiza completamente la detección de problemas en el sistema de capturas de CtrEditor.

View File

@ -65,8 +65,8 @@ namespace CtrEditor.Services
if (!targetObjects.Any())
throw new ArgumentException($"No se encontraron objetos visibles con los IDs: {string.Join(", ", objectIds)}");
// Calcular bounding box usando la misma lógica que ObjectManipulationManager
var boundingBox = CalculateObjectsBoundingBox(targetObjects);
// SOLUCIÓN 2: Usar bounding box mejorado para objetos con dimensiones 0.0
var boundingBox = CalculateObjectsBoundingBoxImproved(targetObjects);
// Aplicar padding
var captureArea = new ScreenshotArea
@ -234,21 +234,53 @@ namespace CtrEditor.Services
}
/// <summary>
/// Captura screenshot de todo el canvas
/// Captura screenshot de todo el canvas con auto-zoom inteligente
/// </summary>
/// <param name="filename">Nombre del archivo (opcional)</param>
/// <param name="includeBackground">Si incluir imagen de fondo</param>
/// <param name="saveToFile">Si guardar el archivo</param>
/// <param name="returnBase64">Si retornar la imagen como base64</param>
/// <param name="autoZoom">Si usar auto-zoom a área con objetos (por defecto true)</param>
/// <returns>Resultado de la captura</returns>
public ScreenshotResult CaptureFullCanvas(
string filename = null,
bool includeBackground = true,
bool saveToFile = true,
bool returnBase64 = true)
bool returnBase64 = true,
bool autoZoom = true)
{
try
{
// SOLUCIÓN 1: Auto-zoom inteligente
if (autoZoom)
{
var visibleObjects = _mainViewModel.ObjetosSimulables
.Where(o => o.Show_On_This_Page).ToList();
if (visibleObjects.Any())
{
try
{
var bounds = CalculateObjectsBoundingBoxImproved(visibleObjects);
var padding = Math.Max(2.0f, Math.Max(bounds.Width, bounds.Height) * 0.2f);
Trace.WriteLine($"[ScreenshotManager] Auto-zoom detectado: área {bounds.Width:F1}×{bounds.Height:F1}m, padding {padding:F1}m");
return CaptureArea(
bounds.Left - padding,
bounds.Top - padding,
bounds.Width + (padding * 2),
bounds.Height + (padding * 2),
filename, includeBackground, saveToFile, returnBase64);
}
catch (Exception ex)
{
Trace.WriteLine($"[ScreenshotManager] Auto-zoom falló: {ex.Message}, usando canvas completo");
}
}
}
// Fallback a canvas completo
var bitmap = CaptureEntireCanvas(includeBackground);
var canvasWidthMeters = PixelToMeter.Instance.calc.PixelsToMeters((float)_canvas.ActualWidth);
@ -357,6 +389,88 @@ namespace CtrEditor.Services
};
}
/// <summary>
/// SOLUCIÓN 2: Calcula el bounding box mejorado que maneja objetos con dimensiones 0.0
/// </summary>
private ScreenshotArea CalculateObjectsBoundingBoxImproved(List<osBase> objects)
{
if (!objects.Any())
throw new ArgumentException("La lista de objetos no puede estar vacía");
float left = float.MaxValue;
float top = float.MaxValue;
float right = float.MinValue;
float bottom = float.MinValue;
foreach (var obj in objects)
{
// Obtener dimensiones reales del objeto
float objWidth = obj.Ancho;
float objHeight = obj.Alto;
// SOLUCIÓN 2: Manejar objetos con dimensiones 0.0 (como botellas)
if (objWidth <= 0.0f || objHeight <= 0.0f)
{
// Usar dimensiones por defecto basadas en el tipo de objeto
if (obj.GetType().Name == "osBotella")
{
objWidth = 0.2f; // 20cm por defecto para botellas
objHeight = 0.2f;
}
else
{
objWidth = Math.Max(objWidth, 0.1f); // Mínimo 10cm
objHeight = Math.Max(objHeight, 0.1f);
}
}
float objLeft = obj.Left - objWidth / 2; // Centrar el objeto
float objTop = obj.Top - objHeight / 2;
float objRight = obj.Left + objWidth / 2;
float objBottom = obj.Top + objHeight / 2;
left = Math.Min(left, objLeft);
top = Math.Min(top, objTop);
right = Math.Max(right, objRight);
bottom = Math.Max(bottom, objBottom);
Trace.WriteLine($"[ScreenshotManager] Object {obj.Id.Value} ({obj.GetType().Name}) - Original: {obj.Ancho}×{obj.Alto}, Used: {objWidth}×{objHeight}");
}
if (left == float.MaxValue)
throw new InvalidOperationException("No se encontraron objetos válidos para calcular el bounding box");
var finalWidth = right - left;
var finalHeight = bottom - top;
// Asegurar dimensiones mínimas
if (finalWidth < 0.5f)
{
var center = (left + right) / 2;
left = center - 0.25f;
right = center + 0.25f;
finalWidth = 0.5f;
}
if (finalHeight < 0.5f)
{
var center = (top + bottom) / 2;
top = center - 0.25f;
bottom = center + 0.25f;
finalHeight = 0.5f;
}
Trace.WriteLine($"[ScreenshotManager] Bounding box mejorado - L:{left:F2} T:{top:F2} W:{finalWidth:F2} H:{finalHeight:F2}");
return new ScreenshotArea
{
Left = left,
Top = top,
Width = finalWidth,
Height = finalHeight
};
}
/// <summary>
/// Captura un área específica del canvas
/// </summary>
@ -395,19 +509,21 @@ namespace CtrEditor.Services
}
/// <summary>
/// Captura un rectángulo específico del canvas en coordenadas de píxeles
/// SOLUCIÓN 3: Captura un rectángulo específico del canvas usando DrawingVisual (sin VisualBrush)
/// </summary>
private RenderTargetBitmap CaptureCanvasRect(Rect captureRect, bool includeBackground)
{
if (captureRect.Width <= 0 || captureRect.Height <= 0)
throw new ArgumentException("Las dimensiones de captura deben ser mayores que 0");
// Configurar escala para alta calidad
var scaleFactor = 2.0; // Factor de escala para mejor calidad
// SOLUCIÓN 3: Reducir scaleFactor de 2.0 a 1.0 para evitar problemas de memoria
var scaleFactor = 1.0; // Reducido para mayor estabilidad
var renderWidth = Math.Max(1, (int)(captureRect.Width * scaleFactor));
var renderHeight = Math.Max(1, (int)(captureRect.Height * scaleFactor));
var dpi = 96 * scaleFactor;
Trace.WriteLine($"[ScreenshotManager] Capturando {renderWidth}×{renderHeight} px (scaleFactor: {scaleFactor})");
var renderBitmap = new RenderTargetBitmap(
renderWidth,
renderHeight,
@ -415,7 +531,7 @@ namespace CtrEditor.Services
dpi,
PixelFormats.Pbgra32);
// Crear canvas temporal para renderizado
// SOLUCIÓN 3 CORREGIDA: Enfoque híbrido - Canvas temporal con VisualBrush mejorado
var tempCanvas = new Canvas()
{
Width = captureRect.Width * scaleFactor,
@ -425,52 +541,91 @@ namespace CtrEditor.Services
tempCanvas.RenderTransform = new ScaleTransform(scaleFactor, scaleFactor);
// Clonar elementos visibles que intersectan con el área de captura
// Forzar actualización de layout del canvas principal antes de capturar
_canvas.UpdateLayout();
_canvas.InvalidateVisual();
// Renderizar elementos con VisualBrush mejorado
int elementCount = 0;
foreach (UIElement child in _canvas.Children)
{
if (child.Visibility == Visibility.Visible)
if (ValidateElementForCapture(child, captureRect))
{
try
{
var left = Canvas.GetLeft(child);
var top = Canvas.GetTop(child);
var elementRect = new Rect(
double.IsNaN(left) ? 0 : left,
double.IsNaN(top) ? 0 : top,
child.RenderSize.Width,
child.RenderSize.Height);
// SOLUCIÓN 3: Validación mejorada de coordenadas
if (double.IsNaN(left)) left = 0;
if (double.IsNaN(top)) top = 0;
if (double.IsInfinity(left) || double.IsInfinity(top)) continue;
var elementRect = new Rect(left, top, child.RenderSize.Width, child.RenderSize.Height);
if (captureRect.IntersectsWith(elementRect))
{
var visualBrush = new VisualBrush(child)
// Forzar actualización del UserControl
child.UpdateLayout();
child.InvalidateVisual();
// Usar VisualBrush mejorado con manejo robusto de errores
try
{
Stretch = Stretch.None,
AlignmentX = AlignmentX.Left,
AlignmentY = AlignmentY.Top
};
var visualBrush = new VisualBrush(child)
{
Stretch = Stretch.None,
AlignmentX = AlignmentX.Left,
AlignmentY = AlignmentY.Top,
ViewboxUnits = BrushMappingMode.Absolute,
Viewbox = new Rect(0, 0, child.RenderSize.Width, child.RenderSize.Height)
};
var rect = new Rectangle()
var rect = new Rectangle()
{
Width = child.RenderSize.Width * scaleFactor,
Height = child.RenderSize.Height * scaleFactor,
Fill = visualBrush
};
Canvas.SetLeft(rect, (elementRect.X - captureRect.X) * scaleFactor);
Canvas.SetTop(rect, (elementRect.Y - captureRect.Y) * scaleFactor);
tempCanvas.Children.Add(rect);
elementCount++;
Trace.WriteLine($"[ScreenshotManager] Capturado UserControl: {child.GetType().Name} en ({elementRect.X:F1}, {elementRect.Y:F1})");
}
catch (Exception brushEx)
{
Width = child.RenderSize.Width * scaleFactor,
Height = child.RenderSize.Height * scaleFactor,
Fill = visualBrush
};
Canvas.SetLeft(rect, (elementRect.X - captureRect.X) * scaleFactor);
Canvas.SetTop(rect, (elementRect.Y - captureRect.Y) * scaleFactor);
tempCanvas.Children.Add(rect);
Trace.WriteLine($"[ScreenshotManager] Error con VisualBrush para {child.GetType().Name}: {brushEx.Message}");
// Fallback: Rectángulo de placeholder
var placeholder = new Rectangle()
{
Width = child.RenderSize.Width * scaleFactor,
Height = child.RenderSize.Height * scaleFactor,
Fill = Brushes.LightGray,
Stroke = Brushes.Red,
StrokeThickness = 2
};
Canvas.SetLeft(placeholder, (elementRect.X - captureRect.X) * scaleFactor);
Canvas.SetTop(placeholder, (elementRect.Y - captureRect.Y) * scaleFactor);
tempCanvas.Children.Add(placeholder);
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"[ScreenshotManager] Error procesando elemento: {ex.Message}");
Trace.WriteLine($"[ScreenshotManager] Error procesando elemento {child.GetType().Name}: {ex.Message}");
}
}
}
// Renderizar canvas temporal
Trace.WriteLine($"[ScreenshotManager] Renderizados {elementCount} elementos UserControl");
// Renderizar canvas temporal mejorado
var scaledSize = new Size(captureRect.Width * scaleFactor, captureRect.Height * scaleFactor);
tempCanvas.Measure(scaledSize);
tempCanvas.Arrange(new Rect(0, 0, scaledSize.Width, scaledSize.Height));
@ -480,6 +635,29 @@ namespace CtrEditor.Services
return renderBitmap;
}
/// <summary>
/// SOLUCIÓN 3: Valida si un elemento debe ser incluido en la captura
/// </summary>
private bool ValidateElementForCapture(UIElement element, Rect captureRect)
{
if (element.Visibility != Visibility.Visible)
return false;
if (element.RenderSize.Width <= 0 || element.RenderSize.Height <= 0)
return false;
var left = Canvas.GetLeft(element);
var top = Canvas.GetTop(element);
// Validación mejorada de posiciones
if (double.IsNaN(left) || double.IsNaN(top) ||
double.IsInfinity(left) || double.IsInfinity(top))
return false;
var elementRect = new Rect(left, top, element.RenderSize.Width, element.RenderSize.Height);
return captureRect.IntersectsWith(elementRect);
}
/// <summary>
/// Procesa la salida del screenshot (guardar archivo y/o generar base64)
/// </summary>

View File

@ -0,0 +1,220 @@
#!/usr/bin/env python3
"""
Analizador de capturas de pantalla del canvas con QR codes de referencia
Lee QR codes de las capturas para detectar problemas de resolución y posicionamiento.
Uso: python analyze_canvas_screenshot.py <imagen.png>
"""
import sys
import json
from PIL import Image
import cv2
import numpy as np
from pyzbar import pyzbar
import argparse
from datetime import datetime
def analyze_qr_codes(image_path):
"""Analiza los QR codes en una imagen y extrae información de posición"""
try:
# Cargar imagen
image = Image.open(image_path)
image_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
# Detectar QR codes
qr_codes = pyzbar.decode(image_cv)
if not qr_codes:
return {
"status": "error",
"message": "No se encontraron QR codes en la imagen",
}
results = {
"status": "success",
"image_path": image_path,
"image_size": image.size,
"analysis_timestamp": datetime.now().isoformat(),
"qr_codes_found": len(qr_codes),
"qr_data": [],
}
print(f"📸 Analizando imagen: {image_path}")
print(f"📐 Tamaño de imagen: {image.size[0]} × {image.size[1]} píxeles")
print(f"🔍 QR codes encontrados: {len(qr_codes)}")
print()
for i, qr in enumerate(qr_codes):
try:
# Decodificar datos JSON del QR
qr_json = json.loads(qr.data.decode("utf-8"))
# Información de posición del QR en la imagen
qr_rect = qr.rect
qr_center_pixels = (
qr_rect.left + qr_rect.width // 2,
qr_rect.top + qr_rect.height // 2,
)
# Información del QR
qr_info = {
"index": i + 1,
"label": qr_json.get("label", "Unknown"),
"expected_position_meters": qr_json.get("position", [0, 0]),
"found_position_pixels": qr_center_pixels,
"qr_size_pixels": (qr_rect.width, qr_rect.height),
"original_data": qr_json,
}
results["qr_data"].append(qr_info)
print(f"🎯 QR #{i+1}: {qr_info['label']}")
print(
f" 📍 Posición esperada: {qr_info['expected_position_meters']} metros"
)
print(f" 📱 Encontrado en píxeles: {qr_center_pixels}")
print(f" 📏 Tamaño QR: {qr_rect.width} × {qr_rect.height} px")
# Calcular escala si tenemos la información original
if "pixels_per_meter" in qr_json:
original_ppm = qr_json["pixels_per_meter"]
expected_canvas_size = qr_json.get("canvas_size", [78.31, 54.53])
# Calcular escala actual basada en el canvas
if image.size[0] > 0 and expected_canvas_size[0] > 0:
current_ppm = image.size[0] / expected_canvas_size[0]
scale_factor = current_ppm / original_ppm
print(f" 📊 Escala original: {original_ppm:.1f} px/m")
print(f" 📊 Escala actual: {current_ppm:.1f} px/m")
print(f" 📊 Factor de escala: {scale_factor:.3f}")
qr_info["scale_analysis"] = {
"original_pixels_per_meter": original_ppm,
"current_pixels_per_meter": current_ppm,
"scale_factor": scale_factor,
}
print()
except (json.JSONDecodeError, KeyError) as e:
print(f"❌ Error decodificando QR #{i+1}: {e}")
continue
# Análisis de distribución espacial
if len(results["qr_data"]) >= 2:
print("📏 Análisis de distribución espacial:")
analyze_spatial_distribution(results["qr_data"], results)
return results
except Exception as e:
return {"status": "error", "message": f"Error analizando imagen: {e}"}
def analyze_spatial_distribution(qr_data, results):
"""Analiza la distribución espacial de los QR codes para detectar distorsiones"""
# Buscar QR codes de esquinas conocidas para análisis de distorsión
corners = {}
for qr in qr_data:
label = qr["label"]
if label in ["TL", "TR", "BL", "BR"]: # Top-Left, Top-Right, etc.
corners[label] = qr
if len(corners) >= 2:
print(" 🔍 Detectando distorsión basada en esquinas...")
# Calcular distancias esperadas vs reales
if "TL" in corners and "TR" in corners:
tl_pos = corners["TL"]["found_position_pixels"]
tr_pos = corners["TR"]["found_position_pixels"]
width_pixels = abs(tr_pos[0] - tl_pos[0])
# Comparar con ancho esperado del canvas
if corners["TL"]["original_data"].get("canvas_size"):
expected_width_meters = corners["TL"]["original_data"]["canvas_size"][0]
current_ppm = width_pixels / expected_width_meters
print(
f" 📐 Ancho detectado: {width_pixels} px = {expected_width_meters}m"
)
print(f" 📊 Resolución horizontal: {current_ppm:.1f} px/m")
if "TL" in corners and "BL" in corners:
tl_pos = corners["TL"]["found_position_pixels"]
bl_pos = corners["BL"]["found_position_pixels"]
height_pixels = abs(bl_pos[1] - tl_pos[1])
if corners["TL"]["original_data"].get("canvas_size"):
expected_height_meters = corners["TL"]["original_data"]["canvas_size"][
1
]
current_ppm = height_pixels / expected_height_meters
print(
f" 📐 Alto detectado: {height_pixels} px = {expected_height_meters}m"
)
print(f" 📊 Resolución vertical: {current_ppm:.1f} px/m")
# Verificar área de objetos si existe el QR "OBJECTS"
objects_qr = next((qr for qr in qr_data if qr["label"] == "OBJECTS"), None)
if objects_qr:
obj_pos = objects_qr["found_position_pixels"]
print(f" 🎯 Área de objetos detectada en: {obj_pos} px")
# Calcular si está en la posición esperada relativa
expected_pos = objects_qr["expected_position_meters"]
print(f" 🎯 Posición esperada: {expected_pos} metros")
def main():
"""Función principal"""
parser = argparse.ArgumentParser(
description="Analiza capturas del canvas con QR de referencia"
)
parser.add_argument("image", help="Ruta a la imagen de captura a analizar")
parser.add_argument("--output", "-o", help="Archivo JSON de salida para resultados")
parser.add_argument("--verbose", "-v", action="store_true", help="Salida detallada")
args = parser.parse_args()
# Verificar que existe la imagen
try:
with open(args.image, "rb"):
pass
except FileNotFoundError:
print(f"❌ Error: No se encontró la imagen '{args.image}'")
return 1
# Analizar imagen
results = analyze_qr_codes(args.image)
if results["status"] == "error":
print(f"{results['message']}")
return 1
# Mostrar resumen
print("=" * 60)
print("📋 RESUMEN DEL ANÁLISIS:")
print(f"✅ QR codes válidos encontrados: {results['qr_codes_found']}")
if results["qr_data"]:
labels_found = [qr["label"] for qr in results["qr_data"]]
print(f"🏷️ Etiquetas detectadas: {', '.join(labels_found)}")
# Guardar resultados si se especifica
if args.output:
try:
with open(args.output, "w") as f:
json.dump(results, f, indent=2)
print(f"💾 Resultados guardados en: {args.output}")
except Exception as e:
print(f"❌ Error guardando resultados: {e}")
print("✅ Análisis completado")
return 0
if __name__ == "__main__":
exit(main())

View File

@ -0,0 +1,335 @@
#!/usr/bin/env python3
"""
Generador de imagen de fondo de referencia para CtrEditor Canvas
Crea una imagen PNG con QR codes, grillas y patrones para detectar problemas de resolución y posicionamiento.
Uso: python generate_canvas_reference.py
Salida: canvas_reference_background.png
"""
import qrcode
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import json
from datetime import datetime
import math
# Configuración del canvas (basado en análisis previo)
CANVAS_WIDTH_METERS = 78.31
CANVAS_HEIGHT_METERS = 54.53
# Configuración de imagen (alta resolución para detectar problemas de escalado)
PIXELS_PER_METER = 50 # 50 píxeles por metro = buena resolución para análisis
IMAGE_WIDTH = int(CANVAS_WIDTH_METERS * PIXELS_PER_METER)
IMAGE_HEIGHT = int(CANVAS_HEIGHT_METERS * PIXELS_PER_METER)
# Colores de referencia
BACKGROUND_COLOR = (240, 248, 255) # Alice Blue - fondo suave
GRID_COLOR = (200, 200, 200) # Gris claro para grilla
MAJOR_GRID_COLOR = (150, 150, 150) # Gris medio para grilla principal
TEXT_COLOR = (50, 50, 50) # Gris oscuro para texto
QR_BACKGROUND = (255, 255, 255) # Blanco para QR
ZONE_COLORS = [
(255, 240, 240), # Rojo muy claro
(240, 255, 240), # Verde muy claro
(240, 240, 255), # Azul muy claro
(255, 255, 240), # Amarillo muy claro
]
def create_qr_code(data, size_pixels):
"""Crea un QR code con los datos especificados"""
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=max(
1, size_pixels // 25
), # Ajustar tamaño de caja según el tamaño del QR
border=1,
)
qr.add_data(data)
qr.make(fit=True)
qr_img = qr.make_image(fill_color="black", back_color="white")
return qr_img.resize((size_pixels, size_pixels), Image.Resampling.NEAREST)
def meters_to_pixels(meters):
"""Convierte metros a píxeles"""
return int(meters * PIXELS_PER_METER)
def pixels_to_meters(pixels):
"""Convierte píxeles a metros"""
return pixels / PIXELS_PER_METER
def draw_grid(draw, image_width, image_height):
"""Dibuja la grilla de referencia"""
# Grilla menor cada 1 metro
for x_meters in range(0, int(CANVAS_WIDTH_METERS) + 1):
x_pixels = meters_to_pixels(x_meters)
if x_pixels < image_width:
color = MAJOR_GRID_COLOR if x_meters % 5 == 0 else GRID_COLOR
width = 2 if x_meters % 5 == 0 else 1
draw.line(
[(x_pixels, 0), (x_pixels, image_height)], fill=color, width=width
)
for y_meters in range(0, int(CANVAS_HEIGHT_METERS) + 1):
y_pixels = meters_to_pixels(y_meters)
if y_pixels < image_height:
color = MAJOR_GRID_COLOR if y_meters % 5 == 0 else GRID_COLOR
width = 2 if y_meters % 5 == 0 else 1
draw.line([(0, y_pixels), (image_width, y_pixels)], fill=color, width=width)
def draw_coordinate_labels(draw, font):
"""Dibuja etiquetas de coordenadas"""
# Etiquetas cada 5 metros
for x_meters in range(0, int(CANVAS_WIDTH_METERS) + 1, 5):
x_pixels = meters_to_pixels(x_meters)
if x_pixels < IMAGE_WIDTH - 50:
draw.text((x_pixels + 2, 2), f"{x_meters}m", fill=TEXT_COLOR, font=font)
draw.text(
(x_pixels + 2, IMAGE_HEIGHT - 20),
f"{x_meters}m",
fill=TEXT_COLOR,
font=font,
)
for y_meters in range(0, int(CANVAS_HEIGHT_METERS) + 1, 5):
y_pixels = meters_to_pixels(y_meters)
if y_pixels < IMAGE_HEIGHT - 20:
draw.text((2, y_pixels + 2), f"{y_meters}m", fill=TEXT_COLOR, font=font)
draw.text(
(IMAGE_WIDTH - 40, y_pixels + 2),
f"{y_meters}m",
fill=TEXT_COLOR,
font=font,
)
def create_zone_backgrounds(image):
"""Crea fondos de colores suaves en diferentes zonas para identificación visual"""
draw = ImageDraw.Draw(image)
# Dividir canvas en 4 zonas
zone_width = IMAGE_WIDTH // 2
zone_height = IMAGE_HEIGHT // 2
zones = [
(0, 0, zone_width, zone_height), # Superior izquierda
(zone_width, 0, IMAGE_WIDTH, zone_height), # Superior derecha
(0, zone_height, zone_width, IMAGE_HEIGHT), # Inferior izquierda
(zone_width, zone_height, IMAGE_WIDTH, IMAGE_HEIGHT), # Inferior derecha
]
for i, (x1, y1, x2, y2) in enumerate(zones):
# Crear overlay semi-transparente
overlay = Image.new("RGBA", (x2 - x1, y2 - y1), ZONE_COLORS[i] + (30,))
image.paste(overlay, (x1, y1), overlay)
def place_qr_codes(image):
"""Coloca QR codes en posiciones estratégicas con información de posición"""
qr_positions = [
# Esquinas
(5, 5, "TL"), # Top-Left
(70, 5, "TR"), # Top-Right
(5, 45, "BL"), # Bottom-Left
(70, 45, "BR"), # Bottom-Right
# Centro y puntos de interés
(39, 27, "CENTER"), # Centro del canvas
(42, 20, "OBJECTS"), # Área donde están los objetos según análisis
# Puntos de calibración adicionales
(20, 15, "CAL1"),
(55, 35, "CAL2"),
]
qr_size_pixels = meters_to_pixels(3) # QR de 3x3 metros
for x_meters, y_meters, label in qr_positions:
# Información del QR
qr_data = {
"position": [x_meters, y_meters],
"label": label,
"canvas_size": [CANVAS_WIDTH_METERS, CANVAS_HEIGHT_METERS],
"image_size": [IMAGE_WIDTH, IMAGE_HEIGHT],
"pixels_per_meter": PIXELS_PER_METER,
"timestamp": datetime.now().isoformat(),
"type": "CtrEditor_Canvas_Reference",
}
# Crear QR
qr_img = create_qr_code(json.dumps(qr_data), qr_size_pixels)
# Calcular posición en píxeles (centrado)
x_pixels = meters_to_pixels(x_meters) - qr_size_pixels // 2
y_pixels = meters_to_pixels(y_meters) - qr_size_pixels // 2
# Asegurar que el QR esté dentro de los límites
x_pixels = max(0, min(x_pixels, IMAGE_WIDTH - qr_size_pixels))
y_pixels = max(0, min(y_pixels, IMAGE_HEIGHT - qr_size_pixels))
# Fondo blanco para el QR
draw = ImageDraw.Draw(image)
draw.rectangle(
[
x_pixels - 5,
y_pixels - 5,
x_pixels + qr_size_pixels + 5,
y_pixels + qr_size_pixels + 5,
],
fill=QR_BACKGROUND,
outline=TEXT_COLOR,
width=2,
)
# Pegar QR
image.paste(qr_img, (x_pixels, y_pixels))
# Etiqueta debajo del QR
try:
font = ImageFont.truetype("arial.ttf", 20)
except:
font = ImageFont.load_default()
draw.text(
(x_pixels + 5, y_pixels + qr_size_pixels + 8),
f"{label}\n({x_meters}, {y_meters})m",
fill=TEXT_COLOR,
font=font,
)
def draw_scale_patterns(image):
"""Dibuja patrones de diferentes escalas para detectar problemas de resolución"""
draw = ImageDraw.Draw(image)
# Patrón de círculos concéntricos en el centro
center_x = meters_to_pixels(CANVAS_WIDTH_METERS / 2)
center_y = meters_to_pixels(CANVAS_HEIGHT_METERS / 2)
# Círculos cada 2 metros de radio
for radius_meters in range(2, 12, 2):
radius_pixels = meters_to_pixels(radius_meters)
draw.ellipse(
[
center_x - radius_pixels,
center_y - radius_pixels,
center_x + radius_pixels,
center_y + radius_pixels,
],
outline=TEXT_COLOR,
width=1,
)
# Etiqueta del radio
label_x = center_x + radius_pixels - 20
label_y = center_y - 10
draw.text((label_x, label_y), f"{radius_meters}m", fill=TEXT_COLOR)
# Patrones de líneas finas para detectar aliasing
for i in range(0, 10):
y = meters_to_pixels(10 + i * 0.2)
draw.line(
[(meters_to_pixels(60), y), (meters_to_pixels(75), y)],
fill=TEXT_COLOR,
width=1,
)
def create_reference_image():
"""Función principal que crea la imagen de referencia"""
print(f"Generando imagen de referencia del canvas...")
print(
f"Dimensiones del canvas: {CANVAS_WIDTH_METERS} × {CANVAS_HEIGHT_METERS} metros"
)
print(f"Dimensiones de imagen: {IMAGE_WIDTH} × {IMAGE_HEIGHT} píxeles")
print(f"Resolución: {PIXELS_PER_METER} píxeles por metro")
# Crear imagen base
image = Image.new("RGB", (IMAGE_WIDTH, IMAGE_HEIGHT), BACKGROUND_COLOR)
# Crear fondos de zona
create_zone_backgrounds(image)
# Crear objeto draw
draw = ImageDraw.Draw(image)
# Dibujar grilla
draw_grid(draw, IMAGE_WIDTH, IMAGE_HEIGHT)
# Fuente para texto
try:
font = ImageFont.truetype("arial.ttf", 16)
title_font = ImageFont.truetype("arial.ttf", 24)
except:
font = ImageFont.load_default()
title_font = ImageFont.load_default()
# Etiquetas de coordenadas
draw_coordinate_labels(draw, font)
# Título e información
title_text = "CtrEditor Canvas Reference Background"
info_text = f"{CANVAS_WIDTH_METERS}m × {CANVAS_HEIGHT_METERS}m | {PIXELS_PER_METER} px/m | {datetime.now().strftime('%Y-%m-%d %H:%M')}"
draw.text((20, 20), title_text, fill=TEXT_COLOR, font=title_font)
draw.text((20, 50), info_text, fill=TEXT_COLOR, font=font)
# Colocar QR codes
place_qr_codes(image)
# Patrones de escala
draw_scale_patterns(image)
# Información de los objetos conocidos (área de interés)
objects_area = {"left": 39.92, "top": 19.76, "right": 44.99, "bottom": 20.47}
# Dibujar rectángulo de área de objetos
x1 = meters_to_pixels(objects_area["left"])
y1 = meters_to_pixels(objects_area["top"])
x2 = meters_to_pixels(objects_area["right"])
y2 = meters_to_pixels(objects_area["bottom"])
draw.rectangle([x1, y1, x2, y2], outline=(255, 0, 0), width=3)
draw.text((x1, y1 - 25), "ÁREA DE OBJETOS", fill=(255, 0, 0), font=font)
return image
def main():
"""Función principal"""
try:
# Crear imagen
image = create_reference_image()
# Guardar imagen
filename = "canvas_reference_background.png"
image.save(filename, "PNG", optimize=True)
print(f"\n✅ Imagen generada exitosamente: {filename}")
print(f"📐 Tamaño de archivo: {image.size}")
print(f"🎯 QR codes contienen información de posición y calibración")
print(f"📏 Grilla cada 1m (líneas gruesas cada 5m)")
print(f"🎨 4 zonas de color para identificación visual")
print(f"📍 Área de objetos marcada en rojo")
# Información para uso
print(f"\n📋 Para usar en CtrEditor:")
print(f"1. Cargar '{filename}' como imagen de fondo del canvas")
print(f"2. Tomar capturas de pantalla y analizar los QR codes")
print(f"3. Verificar grilla y patrones para detectar problemas de escala")
print(f"4. Los QR codes contienen información JSON con coordenadas exactas")
except Exception as e:
print(f"❌ Error generando imagen: {e}")
return 1
return 0
if __name__ == "__main__":
exit(main())

8
requirements.txt Normal file
View File

@ -0,0 +1,8 @@
# Dependencias para el generador de imagen de referencia del canvas
Pillow>=9.0.0
qrcode[pil]>=7.0.0
numpy>=1.21.0
# Dependencias adicionales para el analizador de capturas
opencv-python>=4.5.0
pyzbar>=0.1.9

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB