Files
sinogram/public/sinogram.js
2025-05-17 09:37:05 +10:00

210 lines
6.0 KiB
JavaScript

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();
}