diff --git a/public/UploadImageComponent.js b/public/UploadImageComponent.js index 3f1266e..8872700 100644 --- a/public/UploadImageComponent.js +++ b/public/UploadImageComponent.js @@ -12,13 +12,14 @@ export const UploadImageComponent = { imageUrl: "https://upload.wikimedia.org/wikipedia/commons/e/e5/Shepp_logan.png", sinogramUrl: null, + filteredSinogramUrl: null, // Added to store the filtered sinogram reconstructedUrl: null, defaultImageUrl: "https://upload.wikimedia.org/wikipedia/commons/e/e5/Shepp_logan.png", reconstructionFrames: [], currentFrameIndex: 0, renderMode: "grayscale", // or "heatmap" - useFBP: true, + filterType: "ramp", // Added to manage selected filter drawAngleOverlay(theta) { const canvas = this.overlayCanvas; @@ -57,6 +58,7 @@ export const UploadImageComponent = { async loadAndProcess(url, isUploaded = false) { this.imageUrl = url; this.sinogramUrl = "loading"; + this.filteredSinogramUrl = null; // Reset filtered sinogram this.reconstructedUrl = null; m.redraw(); @@ -78,18 +80,23 @@ export const UploadImageComponent = { this.reconstructionFrames = []; this.currentFrameIndex = 0; - this.reconstructedUrl = await reconstructImageFromSinogram( + const reconstructionResult = await reconstructImageFromSinogram( this.sinogramUrl, undefined, (angle, frameUrl) => { this.reconstructionFrames.push(frameUrl); this.currentFrameIndex = this.reconstructionFrames.length - 1; - this.reconstructedUrl = frameUrl; + this.reconstructedUrl = frameUrl; // This is the final reconstructed image frame m.redraw(); }, 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() { @@ -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 }), // Reconstructed @@ -272,21 +295,30 @@ export const UploadImageComponent = { ]), m("div", { class: "mt-4 w-full max-w-md text-left" }, [ - m("label", [ - m("input", { - type: "checkbox", - checked: this.useFBP, + m( + "label", + { class: "text-sm text-gray-600 mr-2" }, + "Filter Type:" + ), + m( + "select", + { + class: "p-1 border rounded text-sm", + value: this.filterType, onchange: (e) => { - this.useFBP = e.target.checked; - this.loadAndProcess(this.imageUrl); // regenerate with or without FBP + this.filterType = e.target.value; + this.loadAndProcess(this.imageUrl); }, - }), - m( - "span", - { class: "ml-2 text-gray-700" }, - "Use Filtered Back Projection (Ramp)" - ), - ]), + }, + [ + 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 && diff --git a/public/sinogram.js b/public/sinogram.js index 98165d3..85f1fc3 100644 --- a/public/sinogram.js +++ b/public/sinogram.js @@ -1,4 +1,4 @@ -import { applyRampFilter } from "./fbp.js"; +import { applyFilter } from "./fbp.js"; export async function generateSinogram( imageUrl, @@ -77,7 +77,7 @@ export async function reconstructImageFromSinogram( size = 256, onFrame = null, renderMode = "heatmap", - useFBP = true + filterType = "ramp" ) { const sinogramImage = await loadImage(sinogramUrl); const canvas = Object.assign(document.createElement("canvas"), { @@ -113,6 +113,8 @@ export async function reconstructImageFromSinogram( const accum = new Float32Array(size * size); const center = size / 2; + const allFilteredProjections = []; // To store filtered projections + for (let angle = 0; angle < angles; angle++) { const theta = (angle * Math.PI) / angles; @@ -125,9 +127,9 @@ export async function reconstructImageFromSinogram( projection.push(sinogramData[i]); } - if (useFBP) { - projection = applyRampFilter(projection); - } + // Apply the selected filter + projection = applyFilter(projection, filterType); + allFilteredProjections.push(projection); // Store the filtered projection for (let y = 0; y < size; y++) { 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