// ============================================ // Synthetic Generator — 2-Phase Single Screen // ============================================ const SyntheticGenerator = { currentPhase: 1, currentTool: 'polygon', brushSize: 10, maskOpacity: 0.5, zoomLevel: 1, // Phase 1: Source & Mask sourceImage: null, sourceImageData: null, maskCanvas: null, maskCtx: null, currentMask: null, isDrawing: false, polygonPoints: [], undoStack: [], redoStack: [], // Phase 1: Templates templates: [], // Phase 2: Target images targetImages: [], selectedTargetIndices: new Set(), activeSource: null, // Phase 2: ROI markers roiMarkers: [], selectedRoiIndex: null, // Phase 2: Target canvas targetCanvas: null, targetCtx: null, targetImage: null, targetImageData: null, targetZoomLevel: 1, // Phase 2: Transform settings transforms: { colorShift: 0, brightness: 0, rotation: 0, scale: 1.0, blur: 0, opacity: 1.0, randomColor: true, randomRotation: true, randomScale: true }, // Generation state generatedResults: [], init() { this.cacheElements(); this.populateSourceSelect(); this.populateTargetGallery(); this.bindEvents(); this.loadTemplates(); // Initialize mask canvas setTimeout(() => this.initMaskCanvas(), 100); }, cacheElements() { // Phase tabs this.phaseTabs = document.querySelectorAll('.synth-phase-tab'); this.phase1 = document.getElementById('synth-phase-1'); this.phase2 = document.getElementById('synth-phase-2'); // Phase 1 elements this.sourceSelect = document.getElementById('synth-source-select'); this.sourceCanvas = document.getElementById('synth-source-canvas'); this.sourceWrapper = document.getElementById('synth-source-wrapper'); this.sourceHint = document.getElementById('synth-source-hint'); this.sourceContainer = document.getElementById('synth-source-container'); this.templatesList = document.getElementById('synth-templates-list'); this.maskCanvas = document.getElementById('synth-mask-canvas'); this.templateName = document.getElementById('synth-template-name'); this.defectType = document.getElementById('synth-defect-type'); // Phase 2 elements this.targetGallery = document.getElementById('synth-target-gallery'); this.activeSource = document.getElementById('synth-active-source'); this.targetCanvas = document.getElementById('synth-target-canvas'); this.targetWrapper = document.getElementById('synth-target-wrapper'); this.targetHint = document.getElementById('synth-target-hint'); this.targetContainer = document.getElementById('synth-target-container'); this.roiCount = document.getElementById('synth-roi-count'); this.totalCount = document.getElementById('synth-total-count'); this.resultsGrid = document.getElementById('synth-results-grid'); this.progressOverlay = document.getElementById('synth-progress-overlay'); this.progressFill = document.getElementById('synth-progress-fill'); this.progressCurrent = document.getElementById('synth-progress-current'); this.progressTotal = document.getElementById('synth-progress-total'); }, bindEvents() { // Phase tabs this.phaseTabs.forEach(tab => { tab.addEventListener('click', () => this.switchPhase(parseInt(tab.dataset.phase))); }); // Source image select this.sourceSelect?.addEventListener('change', () => this.loadSourceImage()); // Zoom controls document.getElementById('synth-zoom-in')?.addEventListener('click', () => this.adjustZoom('source', 0.25)); document.getElementById('synth-zoom-out')?.addEventListener('click', () => this.adjustZoom('source', -0.25)); document.getElementById('synth-zoom-fit')?.addEventListener('click', () => this.zoomFit('source')); document.getElementById('synth-target-zoom-in')?.addEventListener('click', () => this.adjustZoom('target', 0.25)); document.getElementById('synth-target-zoom-out')?.addEventListener('click', () => this.adjustZoom('target', -0.25)); document.getElementById('synth-target-zoom-fit')?.addEventListener('click', () => this.zoomFit('target')); // Tool buttons document.querySelectorAll('.synth-tool-btn[data-tool]').forEach(btn => { btn.addEventListener('click', () => this.selectTool(btn.dataset.tool)); }); // Undo/Redo document.getElementById('synth-btn-undo')?.addEventListener('click', () => this.undo()); document.getElementById('synth-btn-redo')?.addEventListener('click', () => this.redo()); // Brush size & opacity sliders document.getElementById('synth-brush-size')?.addEventListener('input', (e) => { this.brushSize = parseInt(e.target.value); document.getElementById('synth-brush-size-val').textContent = this.brushSize + 'px'; }); document.getElementById('synth-mask-opacity')?.addEventListener('input', (e) => { this.maskOpacity = parseInt(e.target.value) / 100; document.getElementById('synth-opacity-val').textContent = Math.round(this.maskOpacity * 100) + '%'; }); // Template actions document.getElementById('synth-btn-clear-mask')?.addEventListener('click', () => this.clearMask()); document.getElementById('synth-btn-save-template')?.addEventListener('click', () => this.saveTemplate()); // Phase 1 → 2 button document.getElementById('synth-btn-to-phase2')?.addEventListener('click', () => this.switchToPhase2()); // Phase 2: Target gallery selection document.getElementById('synth-btn-select-good')?.addEventListener('click', () => this.selectAllGood()); // Phase 2: ROI actions document.getElementById('synth-btn-add-roi')?.addEventListener('click', () => this.addRoi()); document.getElementById('synth-btn-clear-rois')?.addEventListener('click', () => this.clearRois()); // Transform sliders ['color-shift', 'brightness', 'rotation', 'scale', 'blur', 'opacity-transform'].forEach(id => { document.getElementById('synth-' + id)?.addEventListener('input', (e) => { const value = parseFloat(e.target.value); const displayId = id === 'color-shift' ? 'synth-color-val' : id === 'opacity-transform' ? 'synth-opacity-transform-val' : 'synth-' + id.replace('-', '-') + '-val'; if (id === 'scale') { this.transforms.scale = value / 100; document.getElementById('synth-scale-val').textContent = (value / 100).toFixed(1); } else if (id === 'opacity-transform') { this.transforms.opacity = value / 100; document.getElementById('synth-opacity-transform-val').textContent = value + '%'; } else { this.transforms[id.replace('-transform', '')] = value; const valEl = document.getElementById(displayId); if (valEl) valEl.textContent = value; } }); }); // Copies per ROI document.getElementById('synth-copies-per-roi')?.addEventListener('input', () => this.updateTotalCount()); // Back to Phase 1 document.getElementById('synth-btn-back-to-phase1')?.addEventListener('click', () => this.switchPhase(1)); // Generate button document.getElementById('synth-btn-generate')?.addEventListener('click', () => this.generateSynthetic()); // Download / Add to Dataset document.getElementById('synth-btn-download-all')?.addEventListener('click', () => this.downloadAll()); document.getElementById('synth-btn-add-to-dataset')?.addEventListener('click', () => this.addToDataset()); }, // ==================== PHASE MANAGEMENT ==================== switchPhase(phase) { this.currentPhase = phase; this.phaseTabs.forEach(tab => { const tabPhase = parseInt(tab.dataset.phase); tab.classList.toggle('active', tabPhase === phase); }); this.phase1.classList.toggle('hidden', phase !== 1); this.phase2.classList.toggle('hidden', phase !== 2); if (phase === 2) { this.initTargetCanvas(); this.updateTotalCount(); } }, // ==================== PHASE 1: SOURCE & MASK ==================== populateSourceSelect() { if (!this.sourceSelect) return; // Get defect images from mock data const defectImages = MockData.images.filter(img => img.category.toLowerCase().includes('d02') || img.category.toLowerCase().includes('d07') || img.category.toLowerCase().includes('d08') ); this.sourceSelect.innerHTML = '' + defectImages.map(img => ``).join(''); // Store preview URLs for later use this.imagePreviewUrls = {}; MockData.images.forEach(img => { this.imagePreviewUrls[img.id] = Helpers.getImagePreviewUrl(img.id); }); }, loadSourceImage() { const imageId = this.sourceSelect?.value; if (!imageId) return; const image = MockData.images.find(img => img.id === imageId); if (!image) return; this.sourceImage = image; // Load image const img = new Image(); img.onload = () => { this.sourceImageData = img; this.sourceHint.style.display = 'none'; // Calculate canvas size for side-by-side view const container = this.sourceWrapper; const maxWidth = Math.min(container?.clientWidth || 300, 500); const maxHeight = Math.min(container?.clientHeight || 300, 400); let width = img.width; let height = img.height; // Scale down to fit container if (width > maxWidth || height > maxHeight) { const ratio = Math.min(maxWidth / width, maxHeight / height); width = width * ratio; height = height * ratio; } // Set minimum size width = Math.max(width, 150); height = Math.max(height, 150); // Set source canvas this.sourceCanvas.width = width; this.sourceCanvas.height = height; this.sourceCanvas.style.width = width + 'px'; this.sourceCanvas.style.height = height + 'px'; this.sourceCanvas.style.display = 'block'; this.sourceCanvas.style.position = 'absolute'; this.sourceCanvas.style.top = '0'; this.sourceCanvas.style.left = '0'; const ctx = this.sourceCanvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); // Initialize mask canvas (for the annotated view) this.initMaskCanvas(); // Initialize annotated canvas (composite view) this.initAnnotatedCanvas(); }; img.onerror = () => { // Create a placeholder image on error const canvas = document.createElement('canvas'); canvas.width = 200; canvas.height = 200; const ctx = canvas.getContext('2d'); ctx.fillStyle = '#ddd'; ctx.fillRect(0, 0, 200, 200); ctx.fillStyle = '#999'; ctx.font = '14px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('Image not found', 100, 100); const dataUrl = canvas.toDataURL(); const img = new Image(); img.onload = () => { this.sourceImageData = img; this.sourceCanvas.width = 200; this.sourceCanvas.height = 200; this.sourceCanvas.style.display = 'block'; const ctx = this.sourceCanvas.getContext('2d'); ctx.drawImage(img, 0, 0); this.initMaskCanvas(); this.initAnnotatedCanvas(); }; img.src = dataUrl; }; // Try to load from preview folder const previewPath = this.imagePreviewUrls?.[image.id] || Helpers.getImagePreviewUrl(image.id); img.src = previewPath || ''; }, initMaskCanvas() { if (!this.maskCanvas || !this.sourceCanvas) return; this.maskCanvas.width = this.sourceCanvas.width || 300; this.maskCanvas.height = this.sourceCanvas.height || 300; this.maskCanvas.style.width = this.sourceCanvas.style.width || '300px'; this.maskCanvas.style.height = this.sourceCanvas.style.height || '300px'; // Use addEventListener for better compatibility this.maskCanvas.addEventListener('mousedown', (e) => this.startDrawing(e)); this.maskCanvas.addEventListener('mousemove', (e) => this.draw(e)); this.maskCanvas.addEventListener('mouseup', () => this.stopDrawing()); this.maskCanvas.addEventListener('mouseleave', () => this.stopDrawing()); this.maskCanvas.addEventListener('dblclick', () => this.completePolygon()); // Ensure canvas is visible this.maskCanvas.style.display = 'block'; this.maskCanvas.style.position = 'absolute'; this.maskCanvas.style.top = '0'; this.maskCanvas.style.left = '0'; this.maskCanvas.style.pointerEvents = 'auto'; this.maskCanvas.style.cursor = 'crosshair'; }, initAnnotatedCanvas() { const annotatedCanvas = document.getElementById('synth-annotated-canvas'); const annotatedWrapper = document.getElementById('synth-annotated-wrapper'); if (!annotatedCanvas || !this.sourceCanvas) return; // Set same size as source canvas const width = this.sourceCanvas.width; const height = this.sourceCanvas.height; annotatedCanvas.width = width; annotatedCanvas.height = height; annotatedCanvas.style.width = width + 'px'; annotatedCanvas.style.height = height + 'px'; // Position the canvas annotatedCanvas.style.display = 'block'; annotatedCanvas.style.position = 'absolute'; annotatedCanvas.style.top = '0'; annotatedCanvas.style.left = '0'; // Draw the composite view this.updateAnnotatedView(); }, updateAnnotatedView() { const annotatedCanvas = document.getElementById('synth-annotated-canvas'); if (!annotatedCanvas || !this.sourceImageData) return; const ctx = annotatedCanvas.getContext('2d'); const width = this.sourceCanvas.width; const height = this.sourceCanvas.height; // Clear ctx.clearRect(0, 0, width, height); // Draw original image ctx.drawImage(this.sourceImageData, 0, 0, width, height); // Draw mask overlay if any if (this.maskCanvas && this.currentTool !== 'polygon') { ctx.globalAlpha = this.maskOpacity * 0.7; ctx.drawImage(this.maskCanvas, 0, 0); ctx.globalAlpha = 1; } }, completePolygon() { if (this.polygonPoints.length < 3) return; const ctx = this.maskCanvas.getContext('2d'); ctx.beginPath(); ctx.moveTo(this.polygonPoints[0].x, this.polygonPoints[0].y); for (let i = 1; i < this.polygonPoints.length; i++) { ctx.lineTo(this.polygonPoints[i].x, this.polygonPoints[i].y); } ctx.closePath(); ctx.fillStyle = `rgba(255,0,0,${this.maskOpacity * 0.5})`; ctx.fill(); this.polygonPoints = []; this.isDrawing = false; }, selectTool(tool) { if (tool === 'erase') { this.currentTool = 'erase'; } else if (tool === 'clear') { this.clearMask(); return; } else { this.currentTool = tool; } document.querySelectorAll('.synth-tool-btn[data-tool]').forEach(btn => { btn.classList.toggle('active', btn.dataset.tool === tool); }); }, saveState() { if (!this.maskCanvas) return; const imageData = this.maskCanvas.getContext('2d').getImageData( 0, 0, this.maskCanvas.width, this.maskCanvas.height ); this.undoStack.push(imageData); if (this.undoStack.length > 20) this.undoStack.shift(); this.redoStack = []; }, undo() { if (this.undoStack.length === 0) return; const state = this.undoStack.pop(); this.redoStack.push(state); const ctx = this.maskCanvas.getContext('2d'); ctx.putImageData(state, 0, 0); }, redo() { if (this.redoStack.length === 0) return; const state = this.redoStack.pop(); this.undoStack.push(state); const ctx = this.maskCanvas.getContext('2d'); ctx.putImageData(state, 0, 0); }, startDrawing(e) { e.preventDefault(); this.isDrawing = true; this.saveState(); if (this.currentTool === 'polygon') { const rect = this.maskCanvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; this.polygonPoints.push({ x, y }); this.drawPolygon(); } else { const rect = this.maskCanvas.getBoundingClientRect(); this.lastX = e.clientX - rect.left; this.lastY = e.clientY - rect.top; } }, draw(e) { if (!this.isDrawing) return; const ctx = this.maskCanvas.getContext('2d'); const rect = this.maskCanvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; if (this.currentTool === 'polygon') { // Just update last point for polygon this.lastX = x; this.lastY = y; return; } // Draw solid strokes (pencil/brush mode) ctx.beginPath(); ctx.lineWidth = this.brushSize; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; if (this.currentTool === 'erase') { ctx.globalCompositeOperation = 'destination-out'; ctx.strokeStyle = 'rgba(0,0,0,1)'; } else { ctx.globalCompositeOperation = 'source-over'; ctx.strokeStyle = `rgba(255, 0, 0, ${this.maskOpacity})`; } ctx.moveTo(this.lastX, this.lastY); ctx.lineTo(x, y); ctx.stroke(); this.lastX = x; this.lastY = y; // Update the annotated view after drawing this.updateAnnotatedView(); }, stopDrawing() { this.isDrawing = false; if (this.currentTool === 'polygon' && this.polygonPoints.length > 2) { // Complete polygon on double-click or when clicking near start } }, drawPolygon() { if (this.polygonPoints.length < 2) return; const ctx = this.maskCanvas.getContext('2d'); ctx.beginPath(); ctx.moveTo(this.polygonPoints[0].x, this.polygonPoints[0].y); for (let i = 1; i < this.polygonPoints.length; i++) { ctx.lineTo(this.polygonPoints[i].x, this.polygonPoints[i].y); } ctx.strokeStyle = `rgba(255,0,0,${this.maskOpacity})`; ctx.lineWidth = this.brushSize; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.stroke(); }, clearMask() { if (!this.maskCanvas) return; this.saveState(); const ctx = this.maskCanvas.getContext('2d'); ctx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height); this.polygonPoints = []; }, // ==================== TEMPLATES ==================== loadTemplates() { // Load from localStorage or use default const saved = localStorage.getItem('synth-templates'); this.templates = saved ? JSON.parse(saved) : []; this.renderTemplates(); }, saveTemplate() { if (!this.maskCanvas || !this.sourceImage) { Toast.show('Draw a mask first', 'warning'); return; } const name = this.templateName?.value?.trim() || 'Unnamed Template'; const type = this.defectType?.value || 'custom'; try { // Capture mask as data URL const maskDataUrl = this.maskCanvas.toDataURL('image/png'); // Capture source image as data URL (may fail if image is external) let sourceDataUrl = ''; try { sourceDataUrl = this.sourceCanvas.toDataURL('image/png'); } catch (e) { // If source canvas is tainted, create a placeholder sourceDataUrl = ''; } const template = { id: Date.now(), name: name, type: type, maskDataUrl: maskDataUrl, sourceDataUrl: sourceDataUrl, createdAt: new Date().toISOString() }; this.templates.unshift(template); localStorage.setItem('synth-templates', JSON.stringify(this.templates)); this.renderTemplates(); Toast.show('Template saved!', 'success'); } catch (e) { Toast.show('Could not save template (CORS issue)', 'warning'); } }, useTemplate(templateId) { const template = this.templates.find(t => t.id === templateId); if (!template) return; this.activeSource = template; // Create source canvas placeholder (use mask which is always safe) this.sourceCanvas.width = 300; this.sourceCanvas.height = 300; this.sourceCanvas.style.width = '300px'; this.sourceCanvas.style.height = '300px'; this.sourceCanvas.style.display = 'block'; const ctx = this.sourceCanvas.getContext('2d'); ctx.fillStyle = '#555'; ctx.fillRect(0, 0, 300, 300); ctx.fillStyle = '#fff'; ctx.font = '14px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('Source from template', 150, 150); // Create mask canvas this.maskCanvas.width = 300; this.maskCanvas.height = 300; this.maskCanvas.style.width = '300px'; this.maskCanvas.style.height = '300px'; this.maskCanvas.style.display = 'block'; this.maskCanvas.style.position = 'absolute'; this.maskCanvas.style.top = '0'; this.maskCanvas.style.left = '0'; this.maskCanvas.style.pointerEvents = 'auto'; // Load mask from template (always safe - it's from localStorage) const img = new Image(); img.onload = () => { const maskCtx = this.maskCanvas.getContext('2d'); maskCtx.drawImage(img, 0, 0, 300, 300); this.sourceHint.style.display = 'none'; Toast.show('Template applied!', 'success'); }; img.onerror = () => { Toast.show('Could not load template mask', 'warning'); }; img.src = template.maskDataUrl; this.sourceImageData = { width: 300, height: 300 }; }, renderTemplates() { if (!this.templatesList) return; if (this.templates.length === 0) { this.templatesList.innerHTML = `

No templates saved yet

`; return; } this.templatesList.innerHTML = this.templates.map(template => `
${template.name}
${template.name}
${template.type.toUpperCase()}
`).join(''); }, // ==================== PHASE 2 ==================== populateTargetGallery() { if (!this.targetGallery) return; // Get good images const goodImages = MockData.images.filter(img => img.category.toLowerCase().includes('good') ); this.targetImages = goodImages; this.targetGallery.innerHTML = goodImages.map((img, index) => `
${img.filename}
`).join(''); // Bind click events this.targetGallery.querySelectorAll('.synth-target-thumb').forEach(thumb => { thumb.addEventListener('click', () => { const index = parseInt(thumb.dataset.index); if (this.selectedTargetIndices.has(index)) { this.selectedTargetIndices.delete(index); thumb.classList.remove('selected'); } else { this.selectedTargetIndices.add(index); thumb.classList.add('selected'); } // Load first selected target image if (this.selectedTargetIndices.size > 0 && !this.targetImage) { const firstIndex = [...this.selectedTargetIndices][0]; this.loadTargetImage(firstIndex); } }); }); }, selectAllGood() { this.targetImages.forEach((_, index) => { this.selectedTargetIndices.add(index); }); this.targetGallery.querySelectorAll('.synth-target-thumb').forEach(thumb => { thumb.classList.add('selected'); }); if (this.targetImages.length > 0 && !this.targetImage) { this.loadTargetImage(0); } Toast.show(`${this.targetImages.length} good images selected`, 'info'); }, initTargetCanvas() { if (!this.targetCanvas || !this.targetWrapper) return; const container = this.targetWrapper; this.targetCanvas.width = container.clientWidth - 20; this.targetCanvas.height = container.clientHeight - 60; // Bind click for ROI placement this.targetCanvas.onclick = (e) => this.placeRoi(e); this.redrawTargetCanvas(); }, loadTargetImage(index) { const image = this.targetImages[index]; if (!image) return; const img = new Image(); img.onload = () => { this.targetImage = image; this.targetImageData = img; this.targetHint.style.display = 'none'; this.redrawTargetCanvas(); }; const previewPath = this.imagePreviewUrls?.[image.id] || Helpers.getImagePreviewUrl(image.id); img.src = previewPath || ''; }, redrawTargetCanvas() { if (!this.targetCanvas || !this.targetCtx) { this.targetCtx = this.targetCanvas?.getContext('2d'); } if (!this.targetCtx) return; const ctx = this.targetCtx; const width = this.targetCanvas.width; const height = this.targetCanvas.height; // Clear ctx.clearRect(0, 0, width, height); // Draw image if loaded if (this.targetImageData) { const img = this.targetImageData; const scale = Math.min(width / img.width, height / img.height) * 0.9; const w = img.width * scale; const h = img.height * scale; const x = (width - w) / 2; const y = (height - h) / 2; ctx.drawImage(img, x, y, w, h); } // Draw ROI markers this.roiMarkers.forEach((roi, index) => { this.drawRoiMarker(ctx, roi, index === this.selectedRoiIndex); }); }, drawRoiMarker(ctx, roi, selected) { const width = this.targetCanvas.width; const height = this.targetCanvas.height; // Normalize ROI position to canvas coordinates const x = roi.x * width; const y = roi.y * height; const size = roi.size * Math.min(width, height); ctx.strokeStyle = selected ? '#3b82f6' : '#a7002b'; ctx.lineWidth = selected ? 3 : 2; ctx.setLineDash([5, 5]); ctx.strokeRect(x - size/2, y - size/2, size, size); ctx.setLineDash([]); // Corner markers const cornerSize = 10; ctx.fillStyle = selected ? '#3b82f6' : '#a7002b'; ctx.fillRect(x - size/2 - 3, y - size/2 - 3, cornerSize, 3); ctx.fillRect(x - size/2 - 3, y - size/2 - 3, 3, cornerSize); ctx.fillRect(x + size/2 - cornerSize + 3, y - size/2 - 3, cornerSize, 3); ctx.fillRect(x + size/2 - 3, y - size/2 - 3, 3, cornerSize); ctx.fillRect(x - size/2 - 3, y + size/2 - 3, cornerSize, 3); ctx.fillRect(x - size/2 - 3, y + size/2 - cornerSize + 3, 3, cornerSize); ctx.fillRect(x + size/2 - cornerSize + 3, y + size/2 - 3, cornerSize, 3); ctx.fillRect(x + size/2 - 3, y + size/2 - cornerSize + 3, 3, cornerSize); // Center dot ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI * 2); ctx.fill(); // Label ctx.fillStyle = selected ? '#3b82f6' : '#a7002b'; ctx.font = '12px sans-serif'; ctx.fillText(`ROI ${this.roiMarkers.indexOf(roi) + 1}`, x + size/2 + 5, y - size/2 - 5); }, placeRoi(e) { const rect = this.targetCanvas.getBoundingClientRect(); const x = (e.clientX - rect.left) / this.targetCanvas.width; const y = (e.clientY - rect.top) / this.targetCanvas.height; this.roiMarkers.push({ x: x, y: y, size: 0.15 }); this.updateRoiCount(); this.redrawTargetCanvas(); this.updateTotalCount(); }, addRoi() { // Add at center this.roiMarkers.push({ x: 0.5, y: 0.5, size: 0.15 }); this.updateRoiCount(); this.redrawTargetCanvas(); this.updateTotalCount(); }, clearRois() { this.roiMarkers = []; this.selectedRoiIndex = null; this.updateRoiCount(); this.redrawTargetCanvas(); this.updateTotalCount(); }, updateRoiCount() { if (this.roiCount) { this.roiCount.textContent = this.roiMarkers.length; } }, updateTotalCount() { const copies = parseInt(document.getElementById('synth-copies-per-roi')?.value || 1); const total = this.roiMarkers.length * copies; if (this.totalCount) { this.totalCount.textContent = total; } document.getElementById('synth-btn-generate').disabled = total === 0; }, switchToPhase2() { if (!this.sourceImageData && this.templates.length === 0) { Toast.show('Load a source image or use a template first', 'warning'); return; } // Set active source if using source image if (this.sourceImageData) { let maskDataUrl = null; try { maskDataUrl = this.maskCanvas?.toDataURL('image/png') || null; } catch (e) { // Ignore CORS errors } this.activeSource = { maskDataUrl: maskDataUrl, name: this.sourceImage?.filename || 'Source' }; } this.switchPhase(2); // Update active source display const activeSourceEl = document.getElementById('synth-active-source'); if (this.activeSource && activeSourceEl) { activeSourceEl.innerHTML = `
Active source
${this.activeSource.name} Ready for placement
`; activeSourceEl.classList.add('synth-active-source--loaded'); } }, // ==================== GENERATION ==================== async generateSynthetic() { if (!this.activeSource || this.roiMarkers.length === 0) { Toast.show('Add ROI markers first', 'warning'); return; } const copies = parseInt(document.getElementById('synth-copies-per-roi')?.value || 1); const placementMode = document.querySelector('input[name="placement"]:checked')?.value || 'random'; // Show progress overlay this.progressOverlay.classList.remove('hidden'); const total = this.roiMarkers.length * copies; let current = 0; this.generatedResults = []; for (const roi of this.roiMarkers) { for (let i = 0; i < copies; i++) { await new Promise(resolve => setTimeout(resolve, 50)); current++; // Update progress const percent = (current / total) * 100; this.progressFill.style.width = percent + '%'; this.progressCurrent.textContent = current; this.progressTotal.textContent = total; // Generate synthetic image const synthetic = await this.generateSingleSynthetic(roi, placementMode, copies); this.generatedResults.push(synthetic); } } // Hide progress this.progressOverlay.classList.add('hidden'); // Render results this.renderResults(); // Enable buttons document.getElementById('synth-btn-download-all').disabled = false; document.getElementById('synth-btn-add-to-dataset').disabled = false; Toast.show(`${this.generatedResults.length} synthetic images generated!`, 'success'); }, async generateSingleSynthetic(roi, placementMode, copies) { // Create canvas for result const canvas = document.createElement('canvas'); canvas.width = this.targetImageData?.width || 800; canvas.height = this.targetImageData?.height || 600; const ctx = canvas.getContext('2d'); // Draw base image if (this.targetImageData) { ctx.drawImage(this.targetImageData, 0, 0); } // Calculate placement position let x, y; if (placementMode === 'random') { const roiSize = roi.size * Math.min(canvas.width, canvas.height); x = canvas.width * roi.x + (Math.random() - 0.5) * roiSize * 0.8; y = canvas.height * roi.y + (Math.random() - 0.5) * roiSize * 0.8; } else if (placementMode === 'grid') { // Grid placement within ROI const idx = this.generatedResults.length % copies; const cols = Math.ceil(Math.sqrt(copies)); const roiSize = roi.size * Math.min(canvas.width, canvas.height) * 0.5; const cellSize = roiSize / cols; x = canvas.width * roi.x - roiSize/2 + (idx % cols) * cellSize + cellSize/2; y = canvas.height * roi.y - roiSize/2 + Math.floor(idx / cols) * cellSize + cellSize/2; } else { x = canvas.width * roi.x; y = canvas.height * roi.y; } // Apply transforms const transforms = { ...this.transforms }; if (transforms.randomColor && Math.random() > 0.5) { transforms.colorShift = (Math.random() - 0.5) * 40; } if (transforms.randomRotation && Math.random() > 0.5) { transforms.rotation = (Math.random() - 0.5) * 60; } if (transforms.randomScale) { transforms.scale = 0.7 + Math.random() * 0.6; } // Draw defect if active source has mask if (this.activeSource?.maskDataUrl && this.targetImageData) { const defectImg = new Image(); defectImg.crossOrigin = 'anonymous'; await new Promise((resolve, reject) => { defectImg.onload = resolve; defectImg.onerror = () => { // If image fails to load, just skip this defect resolve(); }; defectImg.src = this.activeSource.maskDataUrl; }); // Check if image loaded successfully if (defectImg.complete && defectImg.naturalWidth > 0) { const scale = transforms.scale; const defectWidth = defectImg.width * scale; const defectHeight = defectImg.height * scale; ctx.save(); ctx.globalAlpha = transforms.opacity; // Apply rotation ctx.translate(x, y); ctx.rotate(transforms.rotation * Math.PI / 180); // Apply brightness/color adjustment if (transforms.brightness !== 0 || transforms.colorShift !== 0) { ctx.filter = `brightness(${1 + transforms.brightness / 100}) saturate(${1 + transforms.colorShift / 100})`; } // Apply blur if (transforms.blur > 0) { ctx.filter += ` blur(${transforms.blur}px)`; } ctx.drawImage(defectImg, -defectWidth/2, -defectHeight/2, defectWidth, defectHeight); ctx.restore(); } } // Generate a safe data URL (canvas might be tainted) let dataUrl = ''; try { dataUrl = canvas.toDataURL('image/png'); } catch (e) { // If canvas is tainted, create a placeholder dataUrl = this.createPlaceholderDataUrl(canvas.width, canvas.height); } return { id: Date.now() + Math.random(), dataUrl: dataUrl, roi: roi, transforms: transforms, timestamp: new Date().toISOString() }; }, createPlaceholderDataUrl(width, height) { const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); // Create a gradient background const gradient = ctx.createLinearGradient(0, 0, width, height); gradient.addColorStop(0, '#f0f0f0'); gradient.addColorStop(1, '#e0e0e0'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, width, height); // Add some text ctx.fillStyle = '#999'; ctx.font = '14px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('Synthetic Image', width / 2, height / 2); return canvas.toDataURL('image/png'); }, renderResults() { if (!this.resultsGrid) return; if (this.generatedResults.length === 0) { this.resultsGrid.innerHTML = `

Generate synthetic images

`; return; } this.resultsGrid.innerHTML = this.generatedResults.map((result, index) => `
Synthetic #${index + 1}
`).join(''); }, downloadAll() { if (this.generatedResults.length === 0) return; // Download all generated results one by one this.generatedResults.forEach((result, index) => { setTimeout(() => { const link = document.createElement('a'); link.href = result.dataUrl; link.download = `synthetic_${index + 1}_${Date.now()}.png`; link.click(); }, index * 200); }); Toast.show(`${this.generatedResults.length} images downloading!`, 'success'); }, addToDataset() { if (this.generatedResults.length === 0) return; Toast.show(`${this.generatedResults.length} images added to dataset!`, 'success'); }, // ==================== ZOOM CONTROLS ==================== adjustZoom(target, delta) { if (target === 'source') { this.zoomLevel = Math.max(0.25, Math.min(4, this.zoomLevel + delta)); document.getElementById('synth-zoom-level').textContent = Math.round(this.zoomLevel * 100) + '%'; if (this.sourceCanvas) { this.sourceCanvas.style.transform = `scale(${this.zoomLevel})`; } } else { this.targetZoomLevel = Math.max(0.25, Math.min(4, this.targetZoomLevel + delta)); document.getElementById('synth-target-zoom-level').textContent = Math.round(this.targetZoomLevel * 100) + '%'; if (this.targetCanvas) { this.targetCanvas.style.transform = `scale(${this.targetZoomLevel})`; } } }, zoomFit(target) { if (target === 'source') { this.zoomLevel = 1; document.getElementById('synth-zoom-level').textContent = '100%'; if (this.sourceCanvas) this.sourceCanvas.style.transform = 'scale(1)'; } else { this.targetZoomLevel = 1; document.getElementById('synth-target-zoom-level').textContent = '100%'; if (this.targetCanvas) this.targetCanvas.style.transform = 'scale(1)'; } } }; // Initialize when view becomes active window.SyntheticGenerator = SyntheticGenerator;