Embryo-one-Final-Java-Script / src /ui /inlineDetection.js
Embryo-One's picture
Upload 49 files
ed9f15f verified
/**
* Inline Detection View - Show all detected embryos on the main image
*/
import { appState, setCurrentEmbryoIndex, getEmbryoRemark } from '../state.js';
import { loadImage } from '../utils/imageUtils.js';
import { showToast } from './toast.js';
import { showConfirmModal } from './modal.js';
let canvas, ctx;
let selectedEmbryoIndex = null;
let isDragging = false;
let dragType = null; // 'move', 'nw', 'ne', 'sw', 'se', 'n', 's', 'e', 'w'
let dragStart = { x: 0, y: 0 };
let originalBox = null;
let imageElement = null;
let scaleFactor = 1;
let offsetX = 0;
let offsetY = 0;
const HANDLE_SIZE = 10;
const COLORS = [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8',
'#F7DC6F', '#BB8FCE', '#85C1E2', '#F8B739', '#52B788'
];
/**
* Initialize the inline detection canvas
*/
export async function initializeInlineDetection(imageData, detections) {
canvas = document.getElementById('detectionCanvas');
if (!canvas) {
console.error('Detection canvas not found');
showToast('Failed to initialize detection view', 'error');
return;
}
ctx = canvas.getContext('2d');
if (!ctx) {
console.error('Failed to get canvas context');
showToast('Canvas not supported', 'error');
return;
}
// Load the original image
imageElement = await loadImage(imageData);
// Set canvas size to match image
canvas.width = imageElement.width;
canvas.height = imageElement.height;
// Calculate scale factor for display
const container = canvas.parentElement;
if (!container) {
console.error('Canvas container not found');
return;
}
const maxWidth = container.clientWidth - 40;
const maxHeight = 600;
scaleFactor = Math.min(
maxWidth / imageElement.width,
maxHeight / imageElement.height,
1
);
canvas.style.width = `${imageElement.width * scaleFactor}px`;
canvas.style.height = `${imageElement.height * scaleFactor}px`;
// Draw initial view
drawDetections();
// Add event listeners
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('mouseleave', handleMouseUp);
// Touch events for mobile
canvas.addEventListener('touchstart', handleTouchStart);
canvas.addEventListener('touchmove', handleTouchMove);
canvas.addEventListener('touchend', handleTouchEnd);
// Update embryo list
updateEmbryoList(detections);
// Setup detection info with manual add button
const info = document.getElementById('detectionInfo');
if (info) {
info.innerHTML = `
<p>Click on an embryo to adjust its crop area</p>
<button class="btn btn-secondary" id="addManualEmbryoBtn" style="margin-top: 10px;">
+ Add Embryo Manually
</button>
`;
}
// Setup manual add button
setupManualAddButton();
}
/**
* Draw all detections on the canvas
*/
function drawDetections() {
if (!imageElement || !ctx) return;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw the original image
ctx.drawImage(imageElement, 0, 0, canvas.width, canvas.height);
// Draw all bounding boxes
appState.croppedEmbryos.forEach((detection, index) => {
const isSelected = index === selectedEmbryoIndex;
const color = COLORS[index % COLORS.length];
drawBoundingBox(detection.box, color, isSelected, index);
});
}
/**
* Draw a single bounding box
*/
function drawBoundingBox(box, color, isSelected, index) {
const lineWidth = isSelected ? 3 : 2;
const alpha = isSelected ? 1 : 0.7;
// Draw box
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.globalAlpha = alpha;
ctx.strokeRect(box.x1, box.y1, box.x2 - box.x1, box.y2 - box.y1);
// Draw label background
const label = `Embryo ${index + 1}`;
ctx.font = '14px Arial';
const textWidth = ctx.measureText(label).width;
const labelHeight = 20;
ctx.fillStyle = color;
ctx.globalAlpha = 0.9;
ctx.fillRect(box.x1, box.y1 - labelHeight, textWidth + 10, labelHeight);
// Draw label text
ctx.fillStyle = 'white';
ctx.globalAlpha = 1;
ctx.fillText(label, box.x1 + 5, box.y1 - 5);
// Draw resize handles if selected
if (isSelected) {
drawResizeHandles(box, color);
}
ctx.globalAlpha = 1;
}
/**
* Draw resize handles on the selected box
*/
function drawResizeHandles(box, color) {
const handles = [
{ x: box.x1, y: box.y1, type: 'nw' },
{ x: box.x2, y: box.y1, type: 'ne' },
{ x: box.x1, y: box.y2, type: 'sw' },
{ x: box.x2, y: box.y2, type: 'se' },
{ x: (box.x1 + box.x2) / 2, y: box.y1, type: 'n' },
{ x: (box.x1 + box.x2) / 2, y: box.y2, type: 's' },
{ x: box.x1, y: (box.y1 + box.y2) / 2, type: 'w' },
{ x: box.x2, y: (box.y1 + box.y2) / 2, type: 'e' }
];
ctx.fillStyle = color;
handles.forEach(handle => {
ctx.fillRect(
handle.x - HANDLE_SIZE / 2,
handle.y - HANDLE_SIZE / 2,
HANDLE_SIZE,
HANDLE_SIZE
);
});
}
/**
* Get mouse position relative to canvas
*/
function getMousePos(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
return {
x: (e.clientX - rect.left) * scaleX,
y: (e.clientY - rect.top) * scaleY
};
}
/**
* Get handle at position
*/
function getHandleAtPosition(box, x, y) {
const handles = [
{ x: box.x1, y: box.y1, type: 'nw' },
{ x: box.x2, y: box.y1, type: 'ne' },
{ x: box.x1, y: box.y2, type: 'sw' },
{ x: box.x2, y: box.y2, type: 'se' },
{ x: (box.x1 + box.x2) / 2, y: box.y1, type: 'n' },
{ x: (box.x1 + box.x2) / 2, y: box.y2, type: 's' },
{ x: box.x1, y: (box.y1 + box.y2) / 2, type: 'w' },
{ x: box.x2, y: (box.y1 + box.y2) / 2, type: 'e' }
];
for (const handle of handles) {
const dist = Math.sqrt(
Math.pow(x - handle.x, 2) + Math.pow(y - handle.y, 2)
);
if (dist < HANDLE_SIZE) {
return handle.type;
}
}
return null;
}
/**
* Check if point is inside a box
*/
function isPointInBox(box, x, y) {
return x >= box.x1 && x <= box.x2 && y >= box.y1 && y <= box.y2;
}
/**
* Handle mouse down
*/
function handleMouseDown(e) {
const pos = getMousePos(e);
// If in manual draw mode, start drawing new box
if (isDrawingNewBox) {
newBoxStart = pos;
return;
}
// Check if clicking on selected box handles first
if (selectedEmbryoIndex !== null) {
const box = appState.croppedEmbryos[selectedEmbryoIndex].box;
const handle = getHandleAtPosition(box, pos.x, pos.y);
if (handle) {
isDragging = true;
dragType = handle;
dragStart = pos;
originalBox = { ...box };
return;
}
// Check if clicking inside selected box to move it
if (isPointInBox(box, pos.x, pos.y)) {
isDragging = true;
dragType = 'move';
dragStart = pos;
originalBox = { ...box };
return;
}
}
// Check if clicking on any box to select it
for (let i = appState.croppedEmbryos.length - 1; i >= 0; i--) {
const box = appState.croppedEmbryos[i].box;
if (isPointInBox(box, pos.x, pos.y)) {
selectEmbryo(i);
return;
}
}
// Clicked outside all boxes - deselect
selectedEmbryoIndex = null;
drawDetections();
updateEmbryoList(appState.croppedEmbryos);
// Reset detection info with button
const info = document.getElementById('detectionInfo');
if (info) {
info.innerHTML = `
<p>Click on an embryo to adjust its crop area</p>
<button class="btn btn-secondary" id="addManualEmbryoBtn" style="margin-top: 10px;">
+ Add Embryo Manually
</button>
`;
setupManualAddButton();
}
}
/**
* Handle mouse move
*/
function handleMouseMove(e) {
const pos = getMousePos(e);
// If drawing new box, show preview
if (isDrawingNewBox && newBoxStart) {
drawDetections();
// Draw preview box
ctx.strokeStyle = '#00B8D4';
ctx.lineWidth = 3;
ctx.setLineDash([5, 5]);
ctx.strokeRect(
newBoxStart.x,
newBoxStart.y,
pos.x - newBoxStart.x,
pos.y - newBoxStart.y
);
ctx.setLineDash([]);
return;
}
if (!isDragging || selectedEmbryoIndex === null) {
// Update cursor based on hover
updateCursor(pos);
return;
}
const dx = pos.x - dragStart.x;
const dy = pos.y - dragStart.y;
const box = appState.croppedEmbryos[selectedEmbryoIndex].box;
if (dragType === 'move') {
// Move the entire box
box.x1 = Math.max(0, Math.min(originalBox.x1 + dx, canvas.width - (originalBox.x2 - originalBox.x1)));
box.y1 = Math.max(0, Math.min(originalBox.y1 + dy, canvas.height - (originalBox.y2 - originalBox.y1)));
box.x2 = box.x1 + (originalBox.x2 - originalBox.x1);
box.y2 = box.y1 + (originalBox.y2 - originalBox.y1);
} else {
// Resize the box based on handle
resizeBox(box, originalBox, dx, dy, dragType);
}
drawDetections();
updateEmbryoCrop(selectedEmbryoIndex);
}
/**
* Resize box based on drag type
*/
function resizeBox(box, original, dx, dy, type) {
const minSize = 30;
switch (type) {
case 'nw':
box.x1 = Math.max(0, Math.min(original.x1 + dx, original.x2 - minSize));
box.y1 = Math.max(0, Math.min(original.y1 + dy, original.y2 - minSize));
break;
case 'ne':
box.x2 = Math.min(canvas.width, Math.max(original.x2 + dx, original.x1 + minSize));
box.y1 = Math.max(0, Math.min(original.y1 + dy, original.y2 - minSize));
break;
case 'sw':
box.x1 = Math.max(0, Math.min(original.x1 + dx, original.x2 - minSize));
box.y2 = Math.min(canvas.height, Math.max(original.y2 + dy, original.y1 + minSize));
break;
case 'se':
box.x2 = Math.min(canvas.width, Math.max(original.x2 + dx, original.x1 + minSize));
box.y2 = Math.min(canvas.height, Math.max(original.y2 + dy, original.y1 + minSize));
break;
case 'n':
box.y1 = Math.max(0, Math.min(original.y1 + dy, original.y2 - minSize));
break;
case 's':
box.y2 = Math.min(canvas.height, Math.max(original.y2 + dy, original.y1 + minSize));
break;
case 'w':
box.x1 = Math.max(0, Math.min(original.x1 + dx, original.x2 - minSize));
break;
case 'e':
box.x2 = Math.min(canvas.width, Math.max(original.x2 + dx, original.x1 + minSize));
break;
}
}
/**
* Update cursor based on position
*/
function updateCursor(pos) {
// If in drawing mode, show crosshair cursor
if (isDrawingNewBox) {
canvas.style.cursor = 'crosshair';
return;
}
if (selectedEmbryoIndex !== null) {
const box = appState.croppedEmbryos[selectedEmbryoIndex].box;
const handle = getHandleAtPosition(box, pos.x, pos.y);
if (handle) {
const cursors = {
'nw': 'nw-resize', 'ne': 'ne-resize',
'sw': 'sw-resize', 'se': 'se-resize',
'n': 'n-resize', 's': 's-resize',
'w': 'w-resize', 'e': 'e-resize'
};
canvas.style.cursor = cursors[handle];
return;
}
if (isPointInBox(box, pos.x, pos.y)) {
canvas.style.cursor = 'move';
return;
}
}
// Check if over any box
for (const detection of appState.croppedEmbryos) {
if (isPointInBox(detection.box, pos.x, pos.y)) {
canvas.style.cursor = 'pointer';
return;
}
}
canvas.style.cursor = 'default';
}
/**
* Handle mouse up
*/
function handleMouseUp(e) {
// If finishing drawing new box
if (isDrawingNewBox && newBoxStart) {
const pos = getMousePos(e);
const newBox = {
x1: newBoxStart.x,
y1: newBoxStart.y,
x2: pos.x,
y2: pos.y
};
completeManualCrop(newBox);
return;
}
if (isDragging && selectedEmbryoIndex !== null) {
// Finalize the crop update
updateEmbryoCrop(selectedEmbryoIndex);
showToast('Crop area updated', 'success');
}
isDragging = false;
dragType = null;
}
/**
* Handle touch events
*/
function handleTouchStart(e) {
e.preventDefault();
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousedown', {
clientX: touch.clientX,
clientY: touch.clientY
});
canvas.dispatchEvent(mouseEvent);
}
function handleTouchMove(e) {
e.preventDefault();
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousemove', {
clientX: touch.clientX,
clientY: touch.clientY
});
canvas.dispatchEvent(mouseEvent);
}
function handleTouchEnd(e) {
e.preventDefault();
// Get the last touch position or use changedTouches
const touch = e.changedTouches[0];
const mouseEvent = new MouseEvent('mouseup', {
clientX: touch.clientX,
clientY: touch.clientY
});
canvas.dispatchEvent(mouseEvent);
}
/**
* Select an embryo
*/
function selectEmbryo(index) {
selectedEmbryoIndex = index;
setCurrentEmbryoIndex(index);
drawDetections();
updateEmbryoList(appState.croppedEmbryos);
const info = document.getElementById('detectionInfo');
if (info) {
info.innerHTML = `
<p><strong>Embryo ${index + 1} selected</strong> - Drag to move, drag handles to resize</p>
<button class="btn btn-secondary" id="addManualEmbryoBtn" style="margin-top: 10px;">
+ Add Embryo Manually
</button>
`;
setupManualAddButton();
}
}
/**
* Update embryo crop after modification
*/
async function updateEmbryoCrop(index) {
const detection = appState.croppedEmbryos[index];
const box = detection.box;
// Create a temporary canvas to crop the image
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
const width = box.x2 - box.x1;
const height = box.y2 - box.y1;
tempCanvas.width = width;
tempCanvas.height = height;
// Draw the cropped portion
tempCtx.drawImage(
imageElement,
box.x1, box.y1, width, height,
0, 0, width, height
);
// Update the detection's imageData
detection.imageData = tempCanvas.toDataURL('image/png');
// Update the list
updateEmbryoList(appState.croppedEmbryos);
}
/**
* Update the embryo list in the sidebar
*/
function updateEmbryoList(detections) {
const listContainer = document.getElementById('embryoList');
if (!listContainer) return;
listContainer.innerHTML = '';
detections.forEach((detection, index) => {
const card = document.createElement('div');
card.className = 'embryo-card';
if (index === selectedEmbryoIndex) {
card.classList.add('selected');
}
const hasRemark = getEmbryoRemark(index).trim().length > 0;
card.innerHTML = `
<div class="embryo-card-thumbnail">
<img src="${detection.imageData}" alt="Embryo ${index + 1}">
</div>
<div class="embryo-card-info">
<div class="embryo-card-title">
Embryo ${index + 1}
</div>
<div class="embryo-card-confidence">
Confidence: ${(detection.confidence * 100).toFixed(1)}%
</div>
</div>
<div class="embryo-card-actions">
<button class="embryo-card-btn discard" data-index="${index}">
Discard
</button>
</div>
`;
// Click to select
card.addEventListener('click', (e) => {
if (!e.target.classList.contains('embryo-card-btn')) {
selectEmbryo(index);
}
});
// Discard button
const discardBtn = card.querySelector('.discard');
discardBtn.addEventListener('click', (e) => {
e.stopPropagation();
discardEmbryo(index);
});
listContainer.appendChild(card);
});
}
/**
* Discard an embryo
*/
async function discardEmbryo(index) {
if (appState.croppedEmbryos.length <= 1) {
showToast('Cannot discard the last embryo', 'warning');
return;
}
const confirmed = await showConfirmModal(
`Are you sure you want to discard Embryo ${index + 1}?`,
'Discard Embryo'
);
if (!confirmed) {
return;
}
// Remove embryo
appState.croppedEmbryos.splice(index, 1);
// Adjust selected index
if (selectedEmbryoIndex === index) {
selectedEmbryoIndex = Math.max(0, Math.min(selectedEmbryoIndex, appState.croppedEmbryos.length - 1));
} else if (selectedEmbryoIndex > index) {
selectedEmbryoIndex--;
}
// Redraw
drawDetections();
updateEmbryoList(appState.croppedEmbryos);
showToast('Embryo discarded', 'success');
}
/**
* Setup manual add embryo button
*/
function setupManualAddButton() {
const addBtn = document.getElementById('addManualEmbryoBtn');
if (!addBtn) return;
addBtn.addEventListener('click', startManualCrop);
}
let isDrawingNewBox = false;
let newBoxStart = null;
/**
* Start manual crop mode
*/
function startManualCrop() {
isDrawingNewBox = true;
selectedEmbryoIndex = null;
const detectionInfo = document.getElementById('detectionInfo');
if (detectionInfo) {
detectionInfo.innerHTML = `
<p style="color: var(--primary-color); font-weight: 600;">Draw a box around the embryo</p>
<p style="font-size: 0.9em;">Click and drag on the image to create a new bounding box</p>
<button class="btn btn-secondary" id="cancelManualAdd" style="margin-top: 10px;">Cancel</button>
`;
const cancelBtn = document.getElementById('cancelManualAdd');
if (cancelBtn) {
cancelBtn.addEventListener('click', cancelManualCrop);
}
}
drawDetections();
showToast('Draw a box around the embryo', 'info');
}
/**
* Cancel manual crop mode
*/
function cancelManualCrop() {
isDrawingNewBox = false;
newBoxStart = null;
const detectionInfo = document.getElementById('detectionInfo');
if (detectionInfo) {
detectionInfo.innerHTML = `
<p>Click on an embryo to adjust its crop area</p>
<button class="btn btn-secondary" id="addManualEmbryoBtn" style="margin-top: 10px;">
+ Add Embryo Manually
</button>
`;
setupManualAddButton();
}
drawDetections();
}
/**
* Complete manual crop and add embryo
*/
async function completeManualCrop(box) {
// Validate box size
const minSize = 50;
const boxWidth = Math.abs(box.x2 - box.x1);
const boxHeight = Math.abs(box.y2 - box.y1);
if (boxWidth < minSize || boxHeight < minSize) {
showToast('Box too small. Please draw a larger area.', 'warning');
return;
}
// Normalize box coordinates
const normalizedBox = {
x1: Math.min(box.x1, box.x2),
y1: Math.min(box.y1, box.y2),
x2: Math.max(box.x1, box.x2),
y2: Math.max(box.y1, box.y2)
};
// Crop the embryo from the original image
const croppedCanvas = document.createElement('canvas');
const croppedCtx = croppedCanvas.getContext('2d');
croppedCanvas.width = normalizedBox.x2 - normalizedBox.x1;
croppedCanvas.height = normalizedBox.y2 - normalizedBox.y1;
croppedCtx.drawImage(
imageElement,
normalizedBox.x1, normalizedBox.y1,
croppedCanvas.width, croppedCanvas.height,
0, 0,
croppedCanvas.width, croppedCanvas.height
);
const croppedImageData = croppedCanvas.toDataURL();
// Add to embryos array
const newEmbryo = {
imageData: croppedImageData,
box: normalizedBox,
confidence: 1.0, // Manual additions have 100% confidence
isManual: true
};
appState.croppedEmbryos.push(newEmbryo);
// Reset manual mode
isDrawingNewBox = false;
newBoxStart = null;
// Update UI
cancelManualCrop();
drawDetections();
updateEmbryoList(appState.croppedEmbryos);
// Enable next button if this is the first embryo
if (appState.croppedEmbryos.length > 0) {
import('./stepper.js').then(module => {
module.enableNextButton(2);
});
}
showToast('Embryo added successfully', 'success');
}
/**
* Export functions
*/
export function getSelectedEmbryoIndex() {
return selectedEmbryoIndex;
}
export function redrawDetections() {
drawDetections();
}