Show both unfiltered and filtered sinogram
This commit is contained in:
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user