import { applyRampFilter } from "./fbp.js"; export async function generateSinogram( imageUrl, angles = 180, drawAngleCallback = null ) { const image = await loadImage(imageUrl); const size = Math.max(image.width, image.height); const projections = []; const canvas = Object.assign(document.createElement("canvas"), { width: size, height: size, }); const ctx = canvas.getContext("2d"); for (let angle = 0; angle < angles; angle++) { const theta = (angle * Math.PI) / angles; // 🔁 Call visual overlay for this angle if (drawAngleCallback) drawAngleCallback(theta); // (Optional: add delay for animation) await new Promise((r) => setTimeout(r, 0.01)); // Clear canvas ctx.clearRect(0, 0, size, size); // Transform and draw rotated image ctx.save(); ctx.translate(size / 2, size / 2); ctx.rotate(theta); ctx.drawImage(image, -image.width / 2, -image.height / 2); ctx.restore(); // Read pixel data const { data } = ctx.getImageData(0, 0, size, size); // Sum brightness vertically (simulate X-ray projection) const projection = []; for (let x = 0; x < size; x++) { let sum = 0; for (let y = 0; y < size; y++) { const i = (y * size + x) * 4; const gray = data[i]; // red channel (since grayscale) sum += gray; } projection.push(sum / size); // normalize } projections.push(projection); } // Create sinogram canvas const sinogramCanvas = Object.assign(document.createElement("canvas"), { width: size, height: angles, }); const sinCtx = sinogramCanvas.getContext("2d"); const imgData = sinCtx.createImageData(size, angles); for (let y = 0; y < angles; y++) { for (let x = 0; x < size; x++) { const val = projections[y][x]; const i = (y * size + x) * 4; imgData.data[i + 0] = val; imgData.data[i + 1] = val; imgData.data[i + 2] = val; imgData.data[i + 3] = 255; } } sinCtx.putImageData(imgData, 0, 0); return sinogramCanvas.toDataURL(); } export async function reconstructImageFromSinogram( sinogramUrl, size = 256, onFrame = null, renderMode = "heatmap", useFBP = true ) { const sinogramImage = await loadImage(sinogramUrl); const canvas = Object.assign(document.createElement("canvas"), { width: sinogramImage.width, height: sinogramImage.height, }); const ctx = canvas.getContext("2d"); ctx.drawImage(sinogramImage, 0, 0); const sinogramData = ctx.getImageData( 0, 0, sinogramImage.width, sinogramImage.height ).data; size = sinogramImage.width; // match size to sinogram resolution const outputCanvas = Object.assign(document.createElement("canvas"), { width: size, height: size, }); const outputCtx = outputCanvas.getContext("2d"); const accum = new Float32Array(size * size); const center = size / 2; const angles = sinogramImage.height; const width = sinogramImage.width; for (let angle = 0; angle < angles; angle++) { const theta = (angle * Math.PI) / angles; let projection = []; for (let x = 0; x < width; x++) { const i = (angle * width + x) * 4; projection.push(sinogramData[i]); } if (useFBP) { projection = applyRampFilter(projection); } for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const x0 = x - center; const y0 = center - y; // flip y const s = Math.round( x0 * Math.cos(theta) + y0 * Math.sin(theta) + width / 2 ); if (s >= 0 && s < width) { accum[y * size + x] += projection[s]; } } } if (onFrame) { // normalize and draw current frame let maxVal = 0; for (let i = 0; i < accum.length; i++) { if (accum[i] > maxVal) maxVal = accum[i]; } const imageData = outputCtx.createImageData(size, size); for (let i = 0; i < accum.length; i++) { let val = accum[i] / maxVal; val = Math.min(1, Math.max(0, val)); let r, g, b; if (renderMode === "grayscale") { const gray = Math.round(val * 255); r = g = b = gray; } else { [r, g, b] = getHeatmapColor(val); } imageData.data[i * 4 + 0] = r; imageData.data[i * 4 + 1] = g; imageData.data[i * 4 + 2] = b; imageData.data[i * 4 + 3] = 255; } outputCtx.putImageData(imageData, 0, 0); await new Promise((r) => setTimeout(r, 1)); onFrame(angle, outputCanvas.toDataURL()); } } return outputCanvas.toDataURL(); } // Heatmap mapping: blue → green → yellow → red function getHeatmapColor(value) { const r = Math.min(255, Math.max(0, 255 * Math.min(1, 4 * (value - 0.75)))); const g = Math.min(255, Math.max(0, 255 * (4 * Math.abs(value - 0.5) - 1))); const b = Math.min(255, Math.max(0, 255 * (1 - 4 * value))); return [r, g, b]; } function loadImage(src) { return new Promise((resolve) => { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => resolve(img); img.src = src; }); } export async function convertToGrayscale(imageUrl) { const image = await loadImage(imageUrl); const canvas = Object.assign(document.createElement("canvas"), { width: image.width, height: image.height, }); const ctx = canvas.getContext("2d"); // Draw original image ctx.drawImage(image, 0, 0); // Get pixel data const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; // Convert to grayscale: set R, G, B to luminance for (let i = 0; i < data.length; i += 4) { const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; const luminance = 0.299 * r + 0.587 * g + 0.114 * b; data[i] = data[i + 1] = data[i + 2] = luminance; } ctx.putImageData(imageData, 0, 0); return canvas.toDataURL(); }