Show both unfiltered and filtered sinogram

This commit is contained in:
Peter Stockings
2025-05-18 11:49:37 +10:00
parent 9f12966b12
commit fe6c00c0fa
2 changed files with 99 additions and 23 deletions

View File

@@ -12,13 +12,14 @@ export const UploadImageComponent = {
imageUrl: imageUrl:
"https://upload.wikimedia.org/wikipedia/commons/e/e5/Shepp_logan.png", "https://upload.wikimedia.org/wikipedia/commons/e/e5/Shepp_logan.png",
sinogramUrl: null, sinogramUrl: null,
filteredSinogramUrl: null, // Added to store the filtered sinogram
reconstructedUrl: null, reconstructedUrl: null,
defaultImageUrl: defaultImageUrl:
"https://upload.wikimedia.org/wikipedia/commons/e/e5/Shepp_logan.png", "https://upload.wikimedia.org/wikipedia/commons/e/e5/Shepp_logan.png",
reconstructionFrames: [], reconstructionFrames: [],
currentFrameIndex: 0, currentFrameIndex: 0,
renderMode: "grayscale", // or "heatmap" renderMode: "grayscale", // or "heatmap"
useFBP: true, filterType: "ramp", // Added to manage selected filter
drawAngleOverlay(theta) { drawAngleOverlay(theta) {
const canvas = this.overlayCanvas; const canvas = this.overlayCanvas;
@@ -57,6 +58,7 @@ export const UploadImageComponent = {
async loadAndProcess(url, isUploaded = false) { async loadAndProcess(url, isUploaded = false) {
this.imageUrl = url; this.imageUrl = url;
this.sinogramUrl = "loading"; this.sinogramUrl = "loading";
this.filteredSinogramUrl = null; // Reset filtered sinogram
this.reconstructedUrl = null; this.reconstructedUrl = null;
m.redraw(); m.redraw();
@@ -78,18 +80,23 @@ export const UploadImageComponent = {
this.reconstructionFrames = []; this.reconstructionFrames = [];
this.currentFrameIndex = 0; this.currentFrameIndex = 0;
this.reconstructedUrl = await reconstructImageFromSinogram( const reconstructionResult = await reconstructImageFromSinogram(
this.sinogramUrl, this.sinogramUrl,
undefined, undefined,
(angle, frameUrl) => { (angle, frameUrl) => {
this.reconstructionFrames.push(frameUrl); this.reconstructionFrames.push(frameUrl);
this.currentFrameIndex = this.reconstructionFrames.length - 1; this.currentFrameIndex = this.reconstructionFrames.length - 1;
this.reconstructedUrl = frameUrl; this.reconstructedUrl = frameUrl; // This is the final reconstructed image frame
m.redraw(); m.redraw();
}, },
this.renderMode, this.renderMode,
this.useFBP this.filterType
); );
// After all frames are processed, the final reconstructedUrl and filteredSinogramUrl are available
this.reconstructedUrl = reconstructionResult.reconstructedUrl;
this.filteredSinogramUrl = reconstructionResult.filteredSinogramUrl;
m.redraw(); // Ensure UI updates with the final urls
}, },
oninit() { oninit() {
@@ -234,6 +241,22 @@ export const UploadImageComponent = {
}), }),
]), ]),
// Filtered Sinogram Display
this.filteredSinogramUrl &&
m("div", { class: "mt-10 w-full max-w-md text-center" }, [
m(
"h2",
{ class: "text-xl font-semibold text-gray-700 mb-4" },
"Filtered Sinogram"
),
m("img", {
src: this.filteredSinogramUrl,
alt: "Filtered Sinogram",
class: "rounded shadow max-w-full h-auto mx-auto",
style: "width: 100%; max-height: 300px;",
}),
]),
m(StepAccordion, { index: 3 }), m(StepAccordion, { index: 3 }),
// Reconstructed // Reconstructed
@@ -272,21 +295,30 @@ export const UploadImageComponent = {
]), ]),
m("div", { class: "mt-4 w-full max-w-md text-left" }, [ m("div", { class: "mt-4 w-full max-w-md text-left" }, [
m("label", [
m("input", {
type: "checkbox",
checked: this.useFBP,
onchange: (e) => {
this.useFBP = e.target.checked;
this.loadAndProcess(this.imageUrl); // regenerate with or without FBP
},
}),
m( m(
"span", "label",
{ class: "ml-2 text-gray-700" }, { class: "text-sm text-gray-600 mr-2" },
"Use Filtered Back Projection (Ramp)" "Filter Type:"
),
m(
"select",
{
class: "p-1 border rounded text-sm",
value: this.filterType,
onchange: (e) => {
this.filterType = e.target.value;
this.loadAndProcess(this.imageUrl);
},
},
[
m("option", { value: "ramp" }, "Ramp"),
m("option", { value: "shepp-logan" }, "Shepp-Logan"),
m("option", { value: "hann" }, "Hann"),
m("option", { value: "hamming" }, "Hamming"),
m("option", { value: "cosine" }, "Cosine"),
m("option", { value: "none" }, "None"),
]
), ),
]),
]), ]),
this.reconstructionFrames.length > 1 && this.reconstructionFrames.length > 1 &&

View File

@@ -1,4 +1,4 @@
import { applyRampFilter } from "./fbp.js"; import { applyFilter } from "./fbp.js";
export async function generateSinogram( export async function generateSinogram(
imageUrl, imageUrl,
@@ -77,7 +77,7 @@ export async function reconstructImageFromSinogram(
size = 256, size = 256,
onFrame = null, onFrame = null,
renderMode = "heatmap", renderMode = "heatmap",
useFBP = true filterType = "ramp"
) { ) {
const sinogramImage = await loadImage(sinogramUrl); const sinogramImage = await loadImage(sinogramUrl);
const canvas = Object.assign(document.createElement("canvas"), { const canvas = Object.assign(document.createElement("canvas"), {
@@ -113,6 +113,8 @@ export async function reconstructImageFromSinogram(
const accum = new Float32Array(size * size); const accum = new Float32Array(size * size);
const center = size / 2; const center = size / 2;
const allFilteredProjections = []; // To store filtered projections
for (let angle = 0; angle < angles; angle++) { for (let angle = 0; angle < angles; angle++) {
const theta = (angle * Math.PI) / angles; const theta = (angle * Math.PI) / angles;
@@ -125,9 +127,9 @@ export async function reconstructImageFromSinogram(
projection.push(sinogramData[i]); projection.push(sinogramData[i]);
} }
if (useFBP) { // Apply the selected filter
projection = applyRampFilter(projection); projection = applyFilter(projection, filterType);
} allFilteredProjections.push(projection); // Store the filtered projection
for (let y = 0; y < size; y++) { for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) { for (let x = 0; x < size; x++) {
@@ -170,7 +172,49 @@ export async function reconstructImageFromSinogram(
} }
} }
return outputCanvas.toDataURL(); // Create filtered sinogram image
const filteredSinogramCanvas = Object.assign(
document.createElement("canvas"),
{
width: isVertical ? angles : width, // Same dimensions as original sinogram
height: isVertical ? width : angles,
}
);
const filteredSinCtx = filteredSinogramCanvas.getContext("2d");
const filteredImgData = filteredSinCtx.createImageData(
filteredSinogramCanvas.width,
filteredSinogramCanvas.height
);
let maxFilteredVal = 0;
for (const proj of allFilteredProjections) {
for (const val of proj) {
if (val > maxFilteredVal) maxFilteredVal = val;
}
}
// Avoid division by zero if maxFilteredVal is 0
if (maxFilteredVal === 0) maxFilteredVal = 1;
for (let angle = 0; angle < angles; angle++) {
const currentProjection = allFilteredProjections[angle];
for (let x = 0; x < width; x++) {
const val = (currentProjection[x] / maxFilteredVal) * 255; // Normalize and scale
const px = isVertical ? angle : x;
const py = isVertical ? x : angle;
const i = (py * filteredSinogramCanvas.width + px) * 4;
filteredImgData.data[i + 0] = val;
filteredImgData.data[i + 1] = val;
filteredImgData.data[i + 2] = val;
filteredImgData.data[i + 3] = 255;
}
}
filteredSinCtx.putImageData(filteredImgData, 0, 0);
const filteredSinogramUrl = filteredSinogramCanvas.toDataURL();
return {
reconstructedUrl: outputCanvas.toDataURL(),
filteredSinogramUrl: filteredSinogramUrl,
};
} }
// Heatmap mapping: blue → green → yellow → red // Heatmap mapping: blue → green → yellow → red