Segment-Leaf / index.html
Subh775's picture
Update index.html
81157db verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Leaf Segmentation with Unet</title>
<style>
:root {
--primary: #4CAF50;
--dark-bg: #121212;
--surface-bg: #1E1E2E;
--text-primary: #E0E0E0;
--text-secondary: #9E9E9E;
--border-color: rgba(255, 255, 255, 0.1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', sans-serif;
background: var(--dark-bg);
color: var(--text-primary);
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: var(--surface-bg);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
}
.header-note {
font-size: 0.9rem;
background-color: rgba(255, 107, 53, 0.1);
color: #F7931E; /* Orange color to stand out */
padding: 10px 15px;
border-radius: 6px;
max-width: 800px;
margin: 0 auto;
border: 1px solid rgba(255, 107, 53, 0.3);
}
.tool-instruction {
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 15px;
text-align: center;
}
h1 {
text-align: center;
color: #66BB6A; /* Brighter green for better visibility */
margin-bottom: 15px; /* Reduced bottom margin */
font-size: 2.5em;
font-weight: 600; /* Increased font weight to make it bold */
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
padding: 0 30px 30px 30px;
}
.column {
background: rgba(0,0,0,0.2);
border-radius: 12px;
padding: 25px;
display: flex;
flex-direction: column;
}
.column h2 {
color: var(--primary);
margin-bottom: 20px;
font-size: 1.5em;
font-weight: 500;
text-align: center;
}
.upload-area {
border: 2px dashed var(--primary);
border-radius: 8px;
padding: 30px;
text-align: center;
margin-bottom: 20px;
transition: all 0.3s ease;
cursor: pointer;
user-select: none;
}
.upload-area:hover, .upload-area.dragover {
background: rgba(76, 175, 80, 0.1);
border-color: #66BB6A;
}
.canvas-container {
background: #000;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
justify-content: center;
align-items: center;
height: 400px;
border: 1px solid var(--border-color);
flex-shrink: 0;
position: relative;
}
canvas {
max-width: 100%;
max-height: 100%;
object-fit: contain;
cursor: crosshair;
}
.tools-panel, .output-controls {
background: rgba(0,0,0,0.2);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.tool-group { margin-bottom: 15px; }
.tool-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--text-secondary);
}
.tool-buttons { display: flex; gap: 10px; }
.tool-btn {
background: rgba(76, 175, 80, 0.2);
color: var(--primary);
border: 1px solid var(--primary);
padding: 10px 16px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
flex: 1;
}
.tool-btn:hover { background: rgba(76, 175, 80, 0.3); }
.tool-btn.active { background: var(--primary); color: white; }
.predict-btn {
background: linear-gradient(135deg, #FF6B35, #F7931E);
color: white; border: none;
padding: 15px 30px; font-size: 18px; width: 100%;
border-radius: 8px; cursor: pointer; transition: all 0.3s ease;
}
.predict-btn:hover:not(:disabled) { transform: translateY(-2px); }
.predict-btn:disabled { background: #666; cursor: not-allowed; }
.output-tabs { display: flex; gap: 10px; margin-bottom: 20px; }
.download-btn {
background: linear-gradient(135deg, #9C27B0, #673AB7);
color: white; border: none; padding: 12px 24px;
border-radius: 8px; cursor: pointer; width: 100%;
}
.download-btn:disabled { background: #666; cursor: not-allowed; }
.hidden { display: none !important; }
/* visually-hidden allows programmatic click while keeping it out of sight */
.visually-hidden {
position: absolute !important;
left: -9999px !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
}
@media (max-width: 900px) {
.main-content { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="container">
<h1>Leaf Segmentation with Unet</h1>
<p class="header-note">
<strong>NOTE:</strong> The tool is well-suited only for single-leaf per image. It may fails for in-the-wild images.
</p>
<div class="main-content">
<div class="column">
<h2>Input & Controls</h2>
<div class="upload-area" id="upload-area" role="button" tabindex="0">
<p>Click or Drop Image Here</p>
</div>
<!-- keep file input in DOM but visually hidden (not display:none) so programmatic click works reliably -->
<input type="file" id="file-input" class="visually-hidden" accept="image/*">
<div class="canvas-container">
<canvas id="input-canvas"></canvas>
</div>
<div class="tools-panel">
<div class="tool-group">
<label>Annotation Tools</label>
<p class="tool-instruction">
Provide model a hint by making a dot or drawing a short line inside the target leaf.
</p>
<div class="tool-buttons">
<button class="tool-btn active" data-tool="point">Point (Dot)</button>
<button class="tool-btn" data-tool="line">Line/Drag</button>
</div>
</div>
<button class="tool-btn" id="clear-annotations" style="width:100%; margin-top:10px;">Clear Annotations</button>
</div>
<button class="predict-btn" id="predict-btn" disabled>Predict</button>
</div>
<div class="column">
<h2>Output</h2>
<div class="output-tabs">
<button class="tool-btn active" id="view-filled">Filled</button>
<button class="tool-btn" id="view-outline">Outline</button>
<button class="tool-btn" id="view-binary">Binary Mask</button>
</div>
<div class="canvas-container" id="output-container">
<canvas id="output-canvas"></canvas>
</div>
<div class="output-controls">
<div class="tool-group" id="transparency-control">
<label>Mask Transparency</label>
<input type="range" id="transparency" min="0" max="100" value="40" style="width:100%;">
</div>
<button class="download-btn" id="download-btn" disabled>Download Mask</button>
</div>
</div>
</div>
</div>
<script src="https://docs.opencv.org/4.8.0/opencv.js"></script>
<script>
// Robust init: wait for both DOMContentLoaded and cv runtime
let cvReady = false;
let domReady = false;
function tryInit() {
if (cvReady && domReady) {
// instantiate app
window.segApp = new SegmentationApp();
}
}
document.addEventListener('DOMContentLoaded', () => { domReady = true; tryInit(); });
cv['onRuntimeInitialized'] = () => { console.log('OpenCV.js is ready.'); cvReady = true; tryInit(); };
class SegmentationApp {
constructor() {
this.inputCanvas = document.getElementById('input-canvas');
this.inputCtx = this.inputCanvas.getContext('2d');
this.outputCanvas = document.getElementById('output-canvas');
this.outputCtx = this.outputCanvas.getContext('2d');
this.maskCanvas = document.createElement('canvas');
this.maskCtx = this.maskCanvas.getContext('2d');
this.currentTool = 'point';
this.brushSize = 8;
this.isDrawing = false;
this.uploadedImage = null;
this.predictedMask = null;
this.annotations = [];
this.currentView = 'filled';
this.predictBtn = document.getElementById('predict-btn');
this.downloadBtn = document.getElementById('download-btn');
this.transparencySlider = document.getElementById('transparency');
this.transparencyControl = document.getElementById('transparency-control');
this.initEventListeners();
}
initEventListeners() {
const fileInput = document.getElementById('file-input');
const uploadArea = document.getElementById('upload-area');
// clicking upload area triggers the hidden file input
uploadArea.addEventListener('click', () => fileInput.click());
// also support keyboard activation for accessibility
uploadArea.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); fileInput.click(); } });
fileInput.addEventListener('change', e => this.handleFile(e.target.files[0]));
uploadArea.addEventListener('dragover', e => { e.preventDefault(); uploadArea.classList.add('dragover'); });
uploadArea.addEventListener('dragleave', () => uploadArea.classList.remove('dragover'));
uploadArea.addEventListener('drop', e => {
e.preventDefault(); uploadArea.classList.remove('dragover');
const f = e.dataTransfer.files && e.dataTransfer.files[0];
if (f) this.handleFile(f);
});
this.inputCanvas.addEventListener('mousedown', e => this.startDrawing(e));
this.inputCanvas.addEventListener('mousemove', e => this.draw(e));
['mouseup', 'mouseout'].forEach(evt => this.inputCanvas.addEventListener(evt, () => this.stopDrawing()));
document.querySelectorAll('.tool-btn[data-tool]').forEach(btn => {
btn.onclick = (e) => {
document.querySelectorAll('.tool-btn[data-tool]').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
this.currentTool = e.target.dataset.tool;
};
});
document.getElementById('view-filled').onclick = () => this.setView('filled');
document.getElementById('view-outline').onclick = () => this.setView('outline');
document.getElementById('view-binary').onclick = () => this.setView('binary');
this.transparencySlider.oninput = () => this.renderOutput();
document.getElementById('clear-annotations').onclick = () => this.clearAnnotations();
this.predictBtn.onclick = () => this.predict();
this.downloadBtn.onclick = () => this.downloadMask();
// prevent accidental image drag from navigating away
window.addEventListener('dragover', (e) => e.preventDefault());
window.addEventListener('drop', (e) => e.preventDefault());
}
handleFile(file) {
if (!file || !file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = e => {
const img = new Image();
img.onload = () => {
this.uploadedImage = img;
this.reset();
this.drawImageScaled(this.inputCanvas, this.inputCtx, img);
this.outputCanvas.width = this.inputCanvas.width;
this.outputCanvas.height = this.inputCanvas.height;
this.maskCanvas.width = this.inputCanvas.width;
this.maskCanvas.height = this.inputCanvas.height;
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
reset() {
this.annotations = [];
this.predictedMask = null;
this.predictBtn.disabled = true;
this.downloadBtn.disabled = true;
this.outputCtx.clearRect(0,0, this.outputCanvas.width, this.outputCanvas.height);
}
drawImageScaled(canvas, ctx, img) {
const container = canvas.parentElement;
const hRatio = container.clientWidth / img.width;
const vRatio = container.clientHeight / img.height;
const ratio = Math.min(hRatio, vRatio);
// Guard against zero sizes
const finalRatio = (ratio && isFinite(ratio)) ? ratio : 1;
canvas.width = Math.round(img.width * finalRatio);
canvas.height = Math.round(img.height * finalRatio);
ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
}
getCanvasCoordinates(e) {
const rect = this.inputCanvas.getBoundingClientRect();
// Support touch events
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
return {
x: (clientX - rect.left) * (this.inputCanvas.width / rect.width),
y: (clientY - rect.top) * (this.inputCanvas.height / rect.height)
};
}
startDrawing(e) {
if (!this.uploadedImage) return;
this.isDrawing = true;
const coords = this.getCanvasCoordinates(e);
if (this.currentTool === 'point') this.drawOnCanvas(coords.x, coords.y);
else { this.lastX = coords.x; this.lastY = coords.y; }
}
draw(e) {
if (!this.isDrawing || this.currentTool !== 'line') return;
const coords = this.getCanvasCoordinates(e);
this.drawOnCanvas(this.lastX, this.lastY, coords.x, coords.y);
[this.lastX, this.lastY] = [coords.x, coords.y];
}
stopDrawing() { this.isDrawing = false; }
drawOnCanvas(x1, y1, x2 = null, y2 = null) {
[this.inputCtx, this.maskCtx].forEach((ctx, i) => {
ctx.beginPath();
ctx.lineWidth = this.brushSize * 2;
ctx.strokeStyle = (i === 0) ? '#4CAF50' : 'white';
ctx.fillStyle = (i === 0) ? '#4CAF50' : 'white';
ctx.lineCap = 'round';
if (x2 !== null && y2 !== null) {
ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
} else {
ctx.arc(x1, y1, this.brushSize, 0, Math.PI * 2); ctx.fill();
}
});
this.annotations.push(true);
this.predictBtn.disabled = false;
}
clearAnnotations() {
if (!this.uploadedImage) return;
this.reset();
this.drawImageScaled(this.inputCanvas, this.inputCtx, this.uploadedImage);
this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height);
}
async predict() {
if (!this.uploadedImage || this.annotations.length === 0) return;
this.predictBtn.disabled = true; this.predictBtn.innerText = "Processing...";
try {
const payload = {
image: this.inputCanvas.toDataURL('image/jpeg'),
scribble_mask: this.maskCanvas.toDataURL('image/png')
};
const response = await fetch('/predict', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) throw new Error(`Server error: ${response.status}`);
const result = await response.json();
const maskImg = new Image();
maskImg.onload = () => {
this.predictedMask = maskImg;
this.renderOutput();
this.downloadBtn.disabled = false;
};
maskImg.src = result.predicted_mask;
} catch (error) {
console.error('Prediction failed:', error);
alert('Prediction failed. Check the console for details.');
} finally {
this.predictBtn.innerText = "Predict";
this.predictBtn.disabled = this.annotations.length === 0;
}
}
setView(view) {
this.currentView = view;
document.querySelectorAll('.output-tabs .tool-btn').forEach(b => b.classList.remove('active'));
const el = document.getElementById(`view-${view}`);
if (el) el.classList.add('active');
this.transparencyControl.style.display = (view === 'filled') ? 'block' : 'none';
this.renderOutput();
}
renderOutput() {
if (!this.predictedMask) {
this.outputCtx.clearRect(0, 0, this.outputCanvas.width, this.outputCanvas.height);
return;
}
this.outputCtx.clearRect(0,0, this.outputCanvas.width, this.outputCanvas.height);
this.drawImageScaled(this.outputCanvas, this.outputCtx, this.uploadedImage);
if (this.currentView === 'binary') {
this.drawImageScaled(this.outputCanvas, this.outputCtx, this.predictedMask);
return;
}
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = this.outputCanvas.width;
tempCanvas.height = this.outputCanvas.height;
tempCtx.drawImage(this.predictedMask, 0, 0, tempCanvas.width, tempCanvas.height);
// if (this.currentView === 'filled') {
// tempCtx.globalCompositeOperation = 'source-in';
// tempCtx.fillStyle = '#FF00FF'; // Magenta
// tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
// this.outputCtx.globalAlpha = this.transparencySlider.value / 100;
// this.outputCtx.drawImage(tempCanvas, 0, 0);
// this.outputCtx.globalAlpha = 1.0;
if (this.currentView === 'filled') {
// --- FIX START: Correct way to create a colored overlay ---
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imageData.data;
// Iterate through each pixel
for (let i = 0; i < data.length; i += 4) {
// data[i] is red, data[i+3] is alpha (transparency)
// If the pixel in the mask is white (value > 128)
if (data[i] > 128) {
data[i] = 255; // R - Magenta
data[i + 1] = 0; // G
data[i + 2] = 255; // B
data[i + 3] = 255; // Alpha - make it fully opaque
} else {
// If the pixel is black, make it completely transparent
data[i + 3] = 0;
}
}
tempCtx.putImageData(imageData, 0, 0);
// Now draw this corrected overlay onto the main canvas with user-defined transparency
this.outputCtx.globalAlpha = this.transparencySlider.value / 100;
this.outputCtx.drawImage(tempCanvas, 0, 0);
this.outputCtx.globalAlpha = 1.0; // Reset alpha
// --- FIX END ---
} else if (this.currentView === 'outline') {
const src = cv.imread(tempCanvas);
cv.cvtColor(src, src, cv.COLOR_RGBA2GRAY, 0);
// Create a transparent mat to draw contours on
let contourImg = cv.Mat.zeros(src.rows, src.cols, cv.CV_8UC4);
const contours = new cv.MatVector();
const hierarchy = new cv.Mat();
cv.findContours(src, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);
const color = new cv.Scalar(255, 255, 255, 255); // White color
for (let i = 0; i < contours.size(); ++i) {
cv.drawContours(contourImg, contours, i, color, 2, cv.LINE_8, hierarchy, 0);
}
// Draw the contours over the original image
// draw contourImg into a temporary HTML canvas first
const contourCanvas = document.createElement('canvas');
contourCanvas.width = contourImg.cols;
contourCanvas.height = contourImg.rows;
cv.imshow(contourCanvas, contourImg);
this.outputCtx.drawImage(contourCanvas, 0, 0, this.outputCanvas.width, this.outputCanvas.height);
src.delete(); contours.delete(); hierarchy.delete(); contourImg.delete();
}
}
// downloadMask() {
// if (!this.predictedMask) return;
// const link = document.createElement('a');
// link.download = 'binary_mask.png';
// link.href = this.predictedMask.src;
// link.click();
// }
downloadMask() {
if (!this.predictedMask) return; // No mask, nothing to download
const link = document.createElement('a');
let filename = 'segmentation_result.png';
// Determine filename based on current view
if (this.currentView === 'filled') {
filename = 'filled_mask.png';
} else if (this.currentView === 'outline') {
filename = 'outline_mask.png';
} else if (this.currentView === 'binary') {
filename = 'binary_mask.png';
}
// Get the image data from the outputCanvas (which holds the current view)
link.download = filename;
link.href = this.outputCanvas.toDataURL('image/png'); // Gets the current content of the outputCanvas
link.click();
}
}
</script>
</body>
</html>