From 83a02ff667fa05cc5ed17f1171232224fba23c05 Mon Sep 17 00:00:00 2001 From: m34-singh <milan2.singh@live.uwe.ac.uk> Date: Tue, 29 Apr 2025 16:01:01 +0000 Subject: [PATCH] initial commit --- Digital systems project/templates/index.html | 852 +++++++++++++++++++ 1 file changed, 852 insertions(+) create mode 100644 Digital systems project/templates/index.html diff --git a/Digital systems project/templates/index.html b/Digital systems project/templates/index.html new file mode 100644 index 0000000..9e8ef60 --- /dev/null +++ b/Digital systems project/templates/index.html @@ -0,0 +1,852 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8" /> + <title>MRI Detection, Classification, and Segmentation</title> + <style> + /* Using custom 'Roboto' font */ + @font-face { + font-family: 'Roboto'; + src: url('static/fonts/Roboto/Roboto-Regular.ttf') format('truetype'); + } + + /* Zero out margin/padding and ensure box-sizing for consistency */ + * { + font-family: 'Roboto', sans-serif; + box-sizing: border-box; + margin: 0; + padding: 0; + } + + /* Body takes a dark background and white text for contrast */ + body { + font-family: 'Roboto', sans-serif; + background-color: #0c0e1a; + color: #ffffff; + margin: 0; + padding: 0; + } + + /* Header region at the top, with some spacing and bottom border */ + header { + font-family: 'Roboto', sans-serif; + background-color: #0c0e1a; + padding: 1rem 2rem; + border-bottom: 1px solid #2c2c2c; + } + header h1 { + font-family: 'Roboto', sans-serif; + margin: 0; + font-size: 1.5rem; + color: #ffffff; + display: inline-block; + } + + /* Nav bar with flexbox for spacing links and logout */ + nav { + font-family: 'Roboto', sans-serif; + display: flex; + align-items: center; + justify-content: space-between; + background-color: #06070d; + padding: 1rem 2rem; + border-bottom: 1px solid #2c2c2c; + } + .nav-links a { + font-family: 'Roboto', sans-serif; + margin-left: 1.5rem; + text-decoration: none; + color: #ffffff; + font-weight: 500; + font-size: 0.95rem; + } + .nav-links a:hover { + font-family: 'Roboto', sans-serif; + text-decoration: underline; + } + + /* Styling for the logout button on the right side */ + .logout-button { + font-family: 'Roboto', sans-serif; + text-decoration: none; + color: #0c0e1a; + background: linear-gradient(to right, #a64071, #72274a); + padding: 0.5rem 1rem; + border-radius: 4px; + font-weight: bold; + font-size: 0.9rem; + } + .logout-button:hover { + font-family: 'Roboto', sans-serif; + background: linear-gradient(to right, #cd4f8c, #963361); + } + + /* Main container holding sidebar and viewer, with some gap */ + .container { + font-family: 'Roboto', sans-serif; + max-width: 1400px; + margin: 1rem auto; + display: flex; + gap: 1rem; + padding: 0 1rem; + } + + /* Sidebar for file input, controls, and histogram */ + .sidebar { + font-family: 'Roboto', sans-serif; + width: 280px; + background-color: #06070d; + border: 1px solid #2c2c2c; + border-radius: 6px; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + } + + .sidebar h2 { + font-family: 'Roboto', sans-serif; + margin-top: 0; + font-size: 1.1rem; + color: #FFFFFF; + } + + .sidebar button { + font-family: 'Roboto', sans-serif; + border: none; + border-radius: 4px; + background: linear-gradient(to right, #a64071, #72274a); + color: #0c0e1a; + font-size: 0.9rem; + padding: 0.6rem 0.8rem; + cursor: pointer; + font-weight: bold; + } + + .sidebar button:hover { + font-family: 'Roboto', sans-serif; + background: linear-gradient(to right, #cd4f8c, #963361); + } + + label { + font-family: 'Roboto', sans-serif; + display: block; + margin-bottom: 0.2rem; + font-weight: 500; + color: #ffffff; + } + + input[type="file"] { + font-family: 'Roboto', sans-serif; + margin-bottom: 0.5rem; + background-color: #06070d; + color: #FFFFFF; + border: 1px solid #333; + border-radius: 4px; + padding: 0.3rem; + } + + .range-controls label { + font-family: 'Roboto', sans-serif; + color: #ffffff; + } + + .range-controls input[type="range"] { + font-family: 'Roboto', sans-serif; + width: 100%; + } + + /* Box for the histogram with a background and border */ + .histogram-container { + font-family: 'Roboto', sans-serif; + background: #06070d; + border: 1px solid #2c2c2c; + padding: 0.5rem; + border-radius: 4px; + text-align: center; + } + + .histogram-container h3 { + font-family: 'Roboto', sans-serif; + margin: 0 0 0.5rem; + font-size: 1rem; + color: #FFFFFF; + } + + .histogram-container canvas { + font-family: 'Roboto', sans-serif; + width: 100%; + height: 150px; + background: #1c1c1c; + display: block; + margin: 0 auto; + } + + /* Viewer that displays original and processed images */ + .viewer { + font-family: 'Roboto', sans-serif; + flex: 1; + background-color: #06070d; + border: 1px solid #2c2c2c; + border-radius: 6px; + padding: 1rem; + position: relative; + display: flex; + flex-direction: column; + gap: 1rem; + } + + .viewer h2 { + font-family: 'Roboto', sans-serif; + margin-top: 0; + font-size: 1.1rem; + color: #FFFFFF; + } + + /* Layout for the two images side by side */ + .viewer-images { + font-family: 'Roboto', sans-serif; + width: 100%; + display: flex; + gap: 1rem; + justify-content: space-evenly; + align-items: flex-start; + flex-wrap: wrap; + } + + .image-wrapper { + font-family: 'Roboto', sans-serif; + position: relative; + max-width: 48%; + } + + .image-wrapper img { + font-family: 'Roboto', sans-serif; + width: 100%; + max-height: 70vh; + object-fit: contain; + background: #000; + border: 2px solid #333; + display: none; + } + + .annotation-canvas { + font-family: 'Roboto', sans-serif; + position: absolute; + top: 0; + left: 0; + z-index: 10; + pointer-events: auto; + cursor: crosshair; + } + + .mask-overlay { + font-family: 'Roboto', sans-serif; + position: absolute; + top: 0; + left: 0; + z-index: 10; + pointer-events: none; + } + + /* Buttons for applying various color maps */ + .colormap-buttons { + font-family: 'Roboto', sans-serif; + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + justify-content: center; + } + + .colormap-buttons button { + font-family: 'Roboto', sans-serif; + padding: 0.4rem 0.6rem; + border: none; + border-radius: 4px; + cursor: pointer; + background: #a64071; + color: #0c0e1a; + font-size: 0.8rem; + font-weight: bold; + } + + .colormap-buttons button:hover { + font-family: 'Roboto', sans-serif; + background: #cd4f8c; + } + + /* Panel for classification results */ + .analysis-result { + font-family: 'Roboto', sans-serif; + padding: 0.5rem; + border: 1px solid #2c2c2c; + border-radius: 6px; + background-color: #0c0e1a; + text-align: center; + font-size: 1rem; + color: #ffffff; + } + + /* Footer */ + footer { + font-family: 'Roboto', sans-serif; + text-align: center; + background: #0c0e1a; + border-top: 1px solid #2c2c2c; + padding: 1rem; + margin-top: 1rem; + font-size: 0.85rem; + color: #ffffff; + } + + /* Overlay for showing any error messages */ + .modal-overlay { + font-family: 'Roboto', sans-serif; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + display: none; + justify-content: center; + align-items: center; + z-index: 9999; + } + + .modal-content { + font-family: 'Roboto', sans-serif; + background: #2d0e1a; + color: #ffffff; + width: 300px; + padding: 1.5rem; + border-radius: 8px; + text-align: center; + border: 2px solid #d04f5c; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); + } + + .modal-content h2 { + font-family: 'Roboto', sans-serif; + margin-top: 0; + margin-bottom: 0.5rem; + font-size: 1.2rem; + color: #d04f5c; + } + + .modal-content p { + font-family: 'Roboto', sans-serif; + margin-bottom: 1rem; + color: #d04f5c; + } + + .modal-content button { + font-family: 'Roboto', sans-serif; + border: none; + border-radius: 4px; + background: linear-gradient(to right, #a64071, #72274a); + color: #0c0e1a; + font-size: 0.9rem; + padding: 0.5rem 1rem; + cursor: pointer; + font-weight: bold; + } + + .modal-content button:hover { + font-family: 'Roboto', sans-serif; + background: linear-gradient(to right, #cd4f8c, #963361); + } + </style> +</head> +<body> + <!-- Header area with main title --> + <header> + <h1>MRI Detection, Classification, and Segmentation</h1> + </header> + + <!-- Navigation bar with page links --> + <nav> + <div class="nav-links"> + <a href="/">Detection Tools</a> + <a href="/segmentation">Segmentation Tools</a> + </div> + <a href="/logout" class="logout-button">Logout</a> + </nav> + + <!-- Main container with the sidebar on the left and viewer on the right --> + <div class="container"> + <div class="sidebar"> + <h2>Controls</h2> + <label>Select MRI (PNG/JPG)</label> + <input type="file" id="fileInput" accept=".png,.jpg,.jpeg" /> + <button id="analyzeBtn">Analyse Tumor</button> + + <div class="range-controls"> + <label for="brightnessRange">Brightness</label> + <input type="range" id="brightnessRange" min="-100" max="100" value="0"> + <label for="contrastRange">Contrast</label> + <input type="range" id="contrastRange" min="0" max="200" value="100"> + </div> + + <div class="histogram-container"> + <h3>Histogram</h3> + <canvas id="histogramCanvas"></canvas> + </div> + + <button id="exportBtn">Export Annotated Image</button> + </div> + + <div class="viewer"> + <h2>MRI Viewer</h2> + <div class="viewer-images"> + <div class="image-wrapper" id="leftWrapper"> + <img id="mriImage" alt="Original MRI"> + <canvas id="annotationCanvas" class="annotation-canvas"></canvas> + </div> + <div class="image-wrapper" id="rightWrapper"> + <img id="colorMapImage" alt="Processed/ColorMap MRI"> + <canvas id="maskOverlay" class="mask-overlay"></canvas> + </div> + </div> + + <div id="analysisResult" class="analysis-result" style="display: none;"></div> + + <div class="colormap-buttons"> + <button onclick="requestColorMap('jet')">JET</button> + <button onclick="requestColorMap('hot')">HOT</button> + <button onclick="requestColorMap('rainbow')">RAINBOW</button> + <button onclick="requestColorMap('bone')">BONE</button> + <button onclick="requestColorMap('ocean')">OCEAN</button> + <button onclick="requestColorMap('winter')">WINTER</button> + <button onclick="requestColorMap('parula')">PARULA</button> + <button onclick="requestColorMap('HSV')">HSV</button> + </div> + </div> + </div> + + <!-- Footer --> + <footer> + <p>© Digital Systems Project</p> + </footer> + + <!-- For showing errors --> + <div class="modal-overlay" id="errorModal"> + <div class="modal-content"> + <h2>Error</h2> + <p id="errorMsg">Something went wrong</p> + <button id="errorOkBtn">OK</button> + </div> + </div> + + <script> + // I keep references to our current file, image data, and various elements + let currentFile = null; + let originalPixels = null; + let mriValid = true; // Flag for whether the uploaded image is a valid MRI + + const fileInput = document.getElementById('fileInput'); + const analyzeBtn = document.getElementById('analyzeBtn'); + const exportBtn = document.getElementById('exportBtn'); + + // The two main <img> elements + const mriImage = document.getElementById('mriImage'); + const colorMapImage = document.getElementById('colorMapImage'); + + // Container for tumor analysis results + const analysisResultDiv = document.getElementById('analysisResult'); + + // Left canvas for annotations + const annotationCanvas = document.getElementById('annotationCanvas'); + let annCtx = null; + + // Right canvas for segmentation masks + const maskOverlay = document.getElementById('maskOverlay'); + let maskCtx = null; + + // Sliders for brightness and contrast + const brightnessRange = document.getElementById('brightnessRange'); + const contrastRange = document.getElementById('contrastRange'); + + // Histogram canvas + const histogramCanvas = document.getElementById('histogramCanvas'); + const histCtx = histogramCanvas.getContext('2d'); + + // Variables for drawing a selection rectangle on the left image + let drawing = false; + let startX = 0, startY = 0; + let currentX = 0, currentY = 0; + + // Error references + const errorModal = document.getElementById('errorModal'); + const errorMsg = document.getElementById('errorMsg'); + const errorOkBtn = document.getElementById('errorOkBtn'); + + // Close the error box when "OK" is clicked + errorOkBtn.addEventListener('click', () => { + errorModal.style.display = 'none'; + }); + + // Show any error message in the box + function showError(message) { + errorMsg.textContent = message; + errorModal.style.display = 'flex'; + } + + // Function to validate the uploaded image without processing segmentation + // This uses the /segment endpoint with a query parameter "validate=true" + async function validateMRI(file) { + const formData = new FormData(); + formData.append('file', file); + try { + const response = await fetch('/segment?validate=true', { + method: 'POST', + body: formData + }); + const data = await response.json(); + if (!response.ok) { + if (data.error && data.error.toLowerCase().includes("mri")) { + mriValid = false; + showError("The uploaded image is not a valid MRI."); + } else { + mriValid = true; + } + } else { + mriValid = true; + } + } catch (err) { + console.error(err); + showError("Server error during MRI validation."); + } + } + + // When a file is selected, read it in and display the original on the left, + // then immediately validate whether it's a valid MRI or not + fileInput.addEventListener('change', (e) => { + currentFile = e.target.files[0]; + if (!currentFile) return; + if (!currentFile.type.match('image.*')) { + showError("Please select a valid image file (PNG/JPG)."); + currentFile = null; + return; + } + + const reader = new FileReader(); + reader.onload = (evt) => { + // Put the image onto the left <img> + mriImage.src = evt.target.result; + mriImage.style.display = 'block'; + + // Clear out any old colormap image + colorMapImage.src = ''; + colorMapImage.style.display = 'none'; + + // Reset the analysis result area + analysisResultDiv.style.display = 'none'; + analysisResultDiv.innerHTML = ''; + }; + reader.readAsDataURL(currentFile); + + // Immediately validate whether the uploaded image is a valid MRI or not + validateMRI(currentFile); + }); + + // Once the left image is loaded, we size up the annotation canvas + mriImage.onload = () => { + annotationCanvas.width = mriImage.clientWidth; + annotationCanvas.height = mriImage.clientHeight; + annCtx = annotationCanvas.getContext('2d'); + annCtx.clearRect(0, 0, annotationCanvas.width, annotationCanvas.height); + + // Reset brightness/contrast to defaults + brightnessRange.value = "0"; + contrastRange.value = "100"; + + // I grab the raw pixels from the original image so I can manipulate them + loadOriginalPixels(mriImage).then((pixels) => { + autoRescale(pixels); + originalPixels = pixels; + drawHistogram(originalPixels); + }); + }; + + // When a colormap image loads, this code displays it and preps the mask canvas + colorMapImage.onload = () => { + colorMapImage.style.display = 'block'; + maskOverlay.width = colorMapImage.clientWidth; + maskOverlay.height = colorMapImage.clientHeight; + maskCtx = maskOverlay.getContext('2d'); + maskCtx.clearRect(0, 0, maskOverlay.width, maskOverlay.height); + }; + + // Trigger the tumor analysis (classification) + analyzeBtn.addEventListener('click', async () => { + if (!currentFile) { + showError("Please select an MRI image before analysing."); + return; + } + if (!mriValid) { + showError("The uploaded image is not a valid MRI."); + return; + } + const formData = new FormData(); + formData.append('file', currentFile); + + try { + const response = await fetch('/analyze', { + method: 'POST', + body: formData + }); + const data = await response.json(); + if (!response.ok) { + showError(data.error || "Unknown error occurred."); + return; + } + + // Extract the classification info and show it + const { tumor_type, probability } = data; + const confidence = (probability * 100).toFixed(2); + analysisResultDiv.style.display = 'block'; + analysisResultDiv.innerHTML = ` + <h3>Analysis Results</h3> + <p><strong>Tumor Type:</strong> ${tumor_type}</p> + <p><strong>Confidence:</strong> ${confidence}%</p> + `; + } catch (error) { + console.error(error); + showError("Server error during analysis."); + } + }); + + // Auto rescale function to stretch min/max intensities + function autoRescale(imageData) { + const data = imageData.data; + let minVal = 255; + let maxVal = 0; + + for (let i = 0; i < data.length; i += 4) { + const val = data[i]; + if (val < minVal) minVal = val; + if (val > maxVal) maxVal = val; + } + if (maxVal <= minVal) { + console.warn("No intensity variation found. Skipping rescale."); + return; + } + const range = maxVal - minVal; + for (let i = 0; i < data.length; i += 4) { + let r = data[i], g = data[i + 1], b = data[i + 2]; + r = ((r - minVal) * 255) / range; + g = ((g - minVal) * 255) / range; + b = ((b - minVal) * 255) / range; + data[i] = clamp(r, 0, 255); + data[i+1] = clamp(g, 0, 255); + data[i+2] = clamp(b, 0, 255); + } + } + + // This adjusts brightness and contrast whenever sliders move + brightnessRange.addEventListener('input', applyBrightnessContrast); + contrastRange.addEventListener('input', applyBrightnessContrast); + + function applyBrightnessContrast() { + if (!originalPixels) return; + + const brightnessVal = parseInt(brightnessRange.value, 10); + const contrastVal = parseInt(contrastRange.value, 10); + + const offCanvas = document.createElement('canvas'); + offCanvas.width = originalPixels.width; + offCanvas.height = originalPixels.height; + const offCtx = offCanvas.getContext('2d'); + offCtx.putImageData(originalPixels, 0, 0); + + const imageData = offCtx.getImageData(0, 0, offCanvas.width, offCanvas.height); + const data = imageData.data; + + // A formula for brightness and contrast + const contrastFactor = contrastVal / 100.0; + const brightnessOffset = brightnessVal * 2.55; + + for (let i = 0; i < data.length; i += 4) { + let r = data[i] - 128; + let g = data[i + 1] - 128; + let b = data[i + 2] - 128; + + r = r * contrastFactor + 128 + brightnessOffset; + g = g * contrastFactor + 128 + brightnessOffset; + b = b * contrastFactor + 128 + brightnessOffset; + + data[i] = clamp(r, 0, 255); + data[i+1] = clamp(g, 0, 255); + data[i+2] = clamp(b, 0, 255); + } + offCtx.putImageData(imageData, 0, 0); + + colorMapImage.src = offCanvas.toDataURL('image/png'); + colorMapImage.style.display = 'block'; + + // This clears any previously drawn mask + if (maskCtx) { + maskCtx.clearRect(0, 0, maskOverlay.width, maskOverlay.height); + } + drawHistogram(imageData); + } + + // This clamps a value into a valid range of 0-255 + function clamp(val, min, max) { + return Math.max(min, Math.min(max, val)); + } + + // This sets up mouse events to draw a rectangle on the left image + annotationCanvas.addEventListener('mousedown', (e) => { + drawing = true; + const rect = annotationCanvas.getBoundingClientRect(); + startX = e.clientX - rect.left; + startY = e.clientY - rect.top; + }); + + annotationCanvas.addEventListener('mousemove', (e) => { + if (!drawing) return; + const rect = annotationCanvas.getBoundingClientRect(); + currentX = e.clientX - rect.left; + currentY = e.clientY - rect.top; + + annCtx.clearRect(0, 0, annotationCanvas.width, annotationCanvas.height); + annCtx.strokeStyle = 'lime'; + annCtx.lineWidth = 2; + annCtx.strokeRect(startX, startY, currentX - startX, currentY - startY); + }); + + annotationCanvas.addEventListener('mouseup', () => { + drawing = false; + }); + + // If a user need to request a segmentation mask overlay + async function overlayMask() { + if (!currentFile) return; + const formData = new FormData(); + formData.append('file', currentFile); + + try { + const response = await fetch('/segment', { method: 'POST', body: formData }); + const data = await response.json(); + if (!response.ok) { + showError(data.error || "Segmentation failed."); + return; + } + const maskImg = new Image(); + maskImg.onload = () => { + maskOverlay.width = colorMapImage.clientWidth; + maskOverlay.height = colorMapImage.clientHeight; + maskCtx.clearRect(0, 0, maskOverlay.width, maskOverlay.height); + maskCtx.globalAlpha = 0.4; + maskCtx.drawImage(maskImg, 0, 0, maskOverlay.width, maskOverlay.height); + maskCtx.globalAlpha = 1.0; + }; + maskImg.src = "data:image/png;base64," + data.segmentation; + } catch (err) { + console.error(err); + showError("Server error during segmentation."); + } + } + + // Sends the image and selected colormap to server, updates the right image + // Now before applying a color map, it checks whether the uploaded image is a valid MRI or not + async function requestColorMap(colormap) { + if (!currentFile) { + showError("Please select an MRI image before applying a color map."); + return; + } + if (!mriValid) { + showError("The uploaded image is not a valid MRI."); + return; + } + const formData = new FormData(); + formData.append('file', currentFile); + formData.append('colormap', colormap); + + try { + const response = await fetch('/colormap', { + method: 'POST', + body: formData + }); + const data = await response.json(); + if (!response.ok) { + showError(data.error || "Unknown error occurred."); + return; + } + colorMapImage.src = "data:image/png;base64," + data.colormapped; + colorMapImage.style.display = 'block'; + + // Clear mask if there is any + if (maskCtx) { + maskCtx.clearRect(0, 0, maskOverlay.width, maskOverlay.height); + } + } catch (err) { + console.error(err); + showError("Server error applying colormap."); + } + } + + // Draw a histogram of the image intensities + function drawHistogram(imageDataOrPixels) { + const data = imageDataOrPixels.data; + const hist = new Array(256).fill(0); + + // Counting frequencies for each gray level in red channel + for (let i = 0; i < data.length; i += 4) { + const val = data[i]; + hist[val]++; + } + + histCtx.clearRect(0, 0, histogramCanvas.width, histogramCanvas.height); + const maxCount = Math.max(...hist); + const barWidth = histogramCanvas.width / 256; + + histCtx.fillStyle = "#fff"; + for (let i = 0; i < 256; i++) { + const barHeight = (hist[i] / maxCount) * histogramCanvas.height; + histCtx.fillRect(i * barWidth, histogramCanvas.height - barHeight, barWidth, barHeight); + } + } + + // Reads raw pixel data from an <img> by drawing it onto a canvas + async function loadOriginalPixels(img) { + const offCanvas = document.createElement('canvas'); + offCanvas.width = img.naturalWidth; + offCanvas.height = img.naturalHeight; + const offCtx = offCanvas.getContext('2d'); + offCtx.drawImage(img, 0, 0); + return offCtx.getImageData(0, 0, offCanvas.width, offCanvas.height); + } + + // Code below exports the left image along with any annotation drawn on top + // Now checks if the uploaded image is a valid MRI before exporting + exportBtn.addEventListener('click', () => { + // If no image is selected, the user can't export anything + if (!currentFile || !mriImage.src) { + showError("No MRI image loaded to export."); + return; + } + if (!mriValid) { + showError("The uploaded image is not a valid MRI."); + return; + } + // Merge the MRI image and annotation canvas. + const comboCanvas = document.createElement('canvas'); + comboCanvas.width = mriImage.width; + comboCanvas.height = mriImage.height; + const cCtx = comboCanvas.getContext('2d'); + + cCtx.drawImage(mriImage, 0, 0, comboCanvas.width, comboCanvas.height); + cCtx.drawImage(annotationCanvas, 0, 0, comboCanvas.width, comboCanvas.height); + + // This code triggers the download + const link = document.createElement('a'); + link.download = 'annotated_mri.png'; + link.href = comboCanvas.toDataURL('image/png'); + link.click(); + }); + </script> +</body> +</html> \ No newline at end of file -- GitLab