Add raw and filtered projection graphs
This commit is contained in:
160
public/ProjectionGraphComponent.js
Normal file
160
public/ProjectionGraphComponent.js
Normal file
@@ -0,0 +1,160 @@
|
||||
export const ProjectionGraphComponent = {
|
||||
view: function (vnode) {
|
||||
const {
|
||||
data,
|
||||
title,
|
||||
width = 300,
|
||||
height = 150,
|
||||
color = "steelblue",
|
||||
} = vnode.attrs;
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return m(
|
||||
"div.projection-graph-container",
|
||||
{
|
||||
style: {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
border: "1px solid #ccc",
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "center",
|
||||
"background-color": "#f9f9f9",
|
||||
"margin-bottom": "10px",
|
||||
},
|
||||
},
|
||||
m("p.text-gray-500", title ? `${title}: No data` : "No data available")
|
||||
);
|
||||
}
|
||||
|
||||
const padding = { top: 20, right: 20, bottom: 30, left: 40 };
|
||||
const chartWidth = width - padding.left - padding.right;
|
||||
const chartHeight = height - padding.top - padding.bottom;
|
||||
|
||||
const maxVal = Math.max(...data, 0); // Ensure maxVal is at least 0
|
||||
const minVal = Math.min(...data, 0); // Ensure minVal is at most 0
|
||||
|
||||
// Adjust scale if all values are 0 or very close to 0
|
||||
let yMax = maxVal;
|
||||
let yMin = minVal;
|
||||
|
||||
if (maxVal === 0 && minVal === 0) {
|
||||
yMax = 1; // Avoid division by zero if all data points are 0
|
||||
yMin = -1;
|
||||
} else if (maxVal === minVal) {
|
||||
// All values are the same but not zero
|
||||
yMax = maxVal + Math.abs(maxVal * 0.1) + 1; // Add some padding
|
||||
yMin = minVal - Math.abs(minVal * 0.1) - 1;
|
||||
}
|
||||
|
||||
const xScale = (index) => (index / (data.length - 1)) * chartWidth;
|
||||
const yScale = (value) =>
|
||||
chartHeight - ((value - yMin) / (yMax - yMin)) * chartHeight;
|
||||
|
||||
const linePath = data
|
||||
.map((d, i) => `${i === 0 ? "M" : "L"}${xScale(i)},${yScale(d)}`)
|
||||
.join(" ");
|
||||
|
||||
// Generate Y-axis ticks (e.g., 3 ticks: min, mid, max)
|
||||
const yTicks = [];
|
||||
if (yMin !== yMax) {
|
||||
yTicks.push({ value: yMin, y: yScale(yMin) });
|
||||
yTicks.push({ value: (yMin + yMax) / 2, y: yScale((yMin + yMax) / 2) });
|
||||
yTicks.push({ value: yMax, y: yScale(yMax) });
|
||||
} else {
|
||||
// Handle case where all values are the same
|
||||
yTicks.push({ value: yMin - 1, y: yScale(yMin - 1) });
|
||||
yTicks.push({ value: yMin, y: yScale(yMin) });
|
||||
yTicks.push({ value: yMin + 1, y: yScale(yMin + 1) });
|
||||
}
|
||||
|
||||
return m(
|
||||
"div.projection-graph-container",
|
||||
{
|
||||
style: {
|
||||
"margin-bottom": "15px",
|
||||
padding: "10px",
|
||||
border: "1px solid #e2e8f0",
|
||||
"border-radius": "0.375rem",
|
||||
"background-color": "#fff",
|
||||
},
|
||||
},
|
||||
[
|
||||
m(
|
||||
"h4.text-md.font-semibold.text-gray-700.mb-2.text-center",
|
||||
title || "Projection Data"
|
||||
),
|
||||
m("svg", { width: width, height: height }, [
|
||||
m("g", { transform: `translate(${padding.left}, ${padding.top})` }, [
|
||||
// Y-axis
|
||||
m("line", { x1: 0, y1: 0, x2: 0, y2: chartHeight, stroke: "#ccc" }),
|
||||
yTicks.map((tick) =>
|
||||
m("g", { transform: `translate(0, ${tick.y})` }, [
|
||||
m("line", { x1: -5, y1: 0, x2: 0, y2: 0, stroke: "#ccc" }),
|
||||
m(
|
||||
"text",
|
||||
{
|
||||
x: -10,
|
||||
y: 0,
|
||||
"font-size": "10px",
|
||||
"text-anchor": "end",
|
||||
dy: ".32em",
|
||||
},
|
||||
parseFloat(tick.value).toFixed(1)
|
||||
),
|
||||
])
|
||||
),
|
||||
|
||||
// X-axis
|
||||
m("line", {
|
||||
x1: 0,
|
||||
y1: chartHeight,
|
||||
x2: chartWidth,
|
||||
y2: chartHeight,
|
||||
stroke: "#ccc",
|
||||
}),
|
||||
// X-axis labels (optional, e.g., start, mid, end)
|
||||
m(
|
||||
"text",
|
||||
{
|
||||
x: 0,
|
||||
y: chartHeight + 15,
|
||||
"font-size": "10px",
|
||||
"text-anchor": "start",
|
||||
},
|
||||
"0"
|
||||
),
|
||||
m(
|
||||
"text",
|
||||
{
|
||||
x: chartWidth / 2,
|
||||
y: chartHeight + 15,
|
||||
"font-size": "10px",
|
||||
"text-anchor": "middle",
|
||||
},
|
||||
`${Math.floor(data.length / 2)}`
|
||||
),
|
||||
m(
|
||||
"text",
|
||||
{
|
||||
x: chartWidth,
|
||||
y: chartHeight + 15,
|
||||
"font-size": "10px",
|
||||
"text-anchor": "end",
|
||||
},
|
||||
`${data.length - 1}`
|
||||
),
|
||||
|
||||
// Data line
|
||||
m("path", {
|
||||
d: linePath,
|
||||
stroke: color,
|
||||
"stroke-width": 2,
|
||||
fill: "none",
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
]
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from "./sinogram.js";
|
||||
|
||||
import { StepAccordion } from "./CTAccordion .js";
|
||||
import { ProjectionGraphComponent } from "./ProjectionGraphComponent.js";
|
||||
|
||||
export const UploadImageComponent = {
|
||||
hasLoadedInitialImage: false,
|
||||
@@ -21,6 +22,10 @@ export const UploadImageComponent = {
|
||||
manualAngleIndex: 0, // For the new angle control slider
|
||||
sinogramImageElement: null, // To store the sinogram img DOM element
|
||||
sinogramHighlightCanvas: null, // Canvas for sinogram highlight
|
||||
rawProjections: [], // To store raw projections from generateSinogram
|
||||
filteredProjections: [], // To store filtered projections from reconstructImageFromSinogram
|
||||
currentRawProjectionData: null, // Data for the raw projection graph
|
||||
currentFilteredProjectionData: null, // Data for the filtered projection graph
|
||||
renderMode: "grayscale", // or "heatmap"
|
||||
filterType: "ramp", // Added to manage selected filter
|
||||
|
||||
@@ -134,6 +139,10 @@ export const UploadImageComponent = {
|
||||
this.filteredSinogramUrl = null;
|
||||
this.reconstructedUrl = "loading"; // Indicate reconstruction is also loading
|
||||
this.manualAngleIndex = 0;
|
||||
this.rawProjections = [];
|
||||
this.filteredProjections = [];
|
||||
this.currentRawProjectionData = null;
|
||||
this.currentFilteredProjectionData = null;
|
||||
m.redraw();
|
||||
|
||||
let finalUrl = url;
|
||||
@@ -151,11 +160,20 @@ export const UploadImageComponent = {
|
||||
if (img) this.imageElement = img;
|
||||
}
|
||||
|
||||
this.sinogramUrl = await generateSinogram(
|
||||
const sinogramResult = await generateSinogram(
|
||||
finalUrl,
|
||||
this.angleCount,
|
||||
this.drawAngleOverlay.bind(this)
|
||||
);
|
||||
this.sinogramUrl = sinogramResult.sinogramUrl;
|
||||
this.rawProjections = sinogramResult.projections; // Store raw projections
|
||||
if (
|
||||
this.rawProjections &&
|
||||
this.rawProjections.length > this.manualAngleIndex
|
||||
) {
|
||||
this.currentRawProjectionData =
|
||||
this.rawProjections[this.manualAngleIndex];
|
||||
}
|
||||
m.redraw();
|
||||
|
||||
this.reconstructionFrames = [];
|
||||
@@ -181,6 +199,20 @@ export const UploadImageComponent = {
|
||||
|
||||
this.reconstructedUrl = reconstructionResult.reconstructedUrl;
|
||||
this.filteredSinogramUrl = reconstructionResult.filteredSinogramUrl;
|
||||
// Assuming reconstructImageFromSinogram now returns allFilteredProjections
|
||||
this.filteredProjections =
|
||||
reconstructionResult.allFilteredProjections || [];
|
||||
if (
|
||||
this.filteredProjections &&
|
||||
this.filteredProjections.length > this.manualAngleIndex
|
||||
) {
|
||||
this.currentFilteredProjectionData =
|
||||
this.filteredProjections[this.manualAngleIndex];
|
||||
}
|
||||
// It seems rawProjectionsFromSinogram is also returned, let's decide which raw projections to use.
|
||||
// For now, sticking with the ones from generateSinogram for the "raw projection" graph.
|
||||
// If `reconstructionResult.rawProjectionsFromSinogram` is preferred, adjust here.
|
||||
|
||||
m.redraw();
|
||||
},
|
||||
|
||||
@@ -473,9 +505,87 @@ export const UploadImageComponent = {
|
||||
(this.manualAngleIndex * Math.PI) / (this.angleCount || 1);
|
||||
this.drawAngleOverlay(theta);
|
||||
this.drawSinogramHighlight(this.manualAngleIndex);
|
||||
|
||||
// Update current projection data for graphs
|
||||
if (
|
||||
this.rawProjections &&
|
||||
this.rawProjections.length > this.manualAngleIndex
|
||||
) {
|
||||
this.currentRawProjectionData =
|
||||
this.rawProjections[this.manualAngleIndex];
|
||||
} else {
|
||||
this.currentRawProjectionData = null;
|
||||
}
|
||||
if (
|
||||
this.filteredProjections &&
|
||||
this.filteredProjections.length > this.manualAngleIndex
|
||||
) {
|
||||
this.currentFilteredProjectionData =
|
||||
this.filteredProjections[this.manualAngleIndex];
|
||||
} else {
|
||||
this.currentFilteredProjectionData = null;
|
||||
}
|
||||
// Redraw will be handled by Mithril automatically if currentRawProjectionData/currentFilteredProjectionData are used in view
|
||||
},
|
||||
}),
|
||||
]),
|
||||
// Projection Graphs
|
||||
(this.currentRawProjectionData ||
|
||||
this.currentFilteredProjectionData) &&
|
||||
m("div.mt-6.flex.flex-col.space-y-4", [
|
||||
this.currentRawProjectionData &&
|
||||
m(ProjectionGraphComponent, {
|
||||
title: "Current Raw Projection",
|
||||
data: this.currentRawProjectionData,
|
||||
width: 450, // Adjusted width
|
||||
height: 200, // Adjusted height
|
||||
color: "dodgerblue",
|
||||
}),
|
||||
this.currentFilteredProjectionData &&
|
||||
m(ProjectionGraphComponent, {
|
||||
title: "Current Filtered Projection",
|
||||
data: this.currentFilteredProjectionData,
|
||||
width: 450, // Adjusted width
|
||||
height: 200, // Adjusted height
|
||||
color: "orangered",
|
||||
}),
|
||||
]),
|
||||
// Filter Type Selector
|
||||
m("div", { class: "mt-6" }, [
|
||||
// Using mt-6 for spacing
|
||||
m(
|
||||
"label",
|
||||
{
|
||||
for: "filterType",
|
||||
class: "block text-sm font-medium text-gray-700 mb-1",
|
||||
},
|
||||
"Filter Type:"
|
||||
),
|
||||
m(
|
||||
"select",
|
||||
{
|
||||
id: "filterType",
|
||||
class:
|
||||
"mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md",
|
||||
value: this.filterType,
|
||||
onchange: (e) => {
|
||||
this.filterType = e.target.value;
|
||||
// Reload and reprocess when filter changes
|
||||
this.loadAndProcessDebounced(
|
||||
this.imageUrl,
|
||||
this.imageUrl !== this.defaultImageUrl
|
||||
);
|
||||
},
|
||||
},
|
||||
[
|
||||
m("option", { value: "none" }, "None"),
|
||||
m("option", { value: "ramp" }, "Ramp (Shepp-Logan)"),
|
||||
m("option", { value: "cosine" }, "Cosine"),
|
||||
m("option", { value: "hamming" }, "Hamming"),
|
||||
m("option", { value: "hann" }, "Hann"),
|
||||
]
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
|
||||
@@ -665,7 +775,7 @@ export const UploadImageComponent = {
|
||||
m(
|
||||
"div",
|
||||
{
|
||||
class: "mt-6 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4",
|
||||
class: "mt-6",
|
||||
},
|
||||
[
|
||||
m("div", [
|
||||
@@ -698,40 +808,6 @@ export const UploadImageComponent = {
|
||||
]
|
||||
),
|
||||
]),
|
||||
m("div", [
|
||||
m(
|
||||
"label",
|
||||
{
|
||||
for: "filterTypeSelect",
|
||||
class: "block text-sm font-medium text-gray-700 mb-1",
|
||||
},
|
||||
"Filter Type (Reconstruction):"
|
||||
),
|
||||
m(
|
||||
"select",
|
||||
{
|
||||
id: "filterTypeSelect",
|
||||
class:
|
||||
"mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md shadow-sm",
|
||||
value: this.filterType,
|
||||
onchange: (e) => {
|
||||
this.filterType = e.target.value;
|
||||
this.loadAndProcessDebounced(
|
||||
this.imageUrl,
|
||||
this.imageUrl !== this.defaultImageUrl
|
||||
);
|
||||
},
|
||||
},
|
||||
[
|
||||
m("option", { value: "ramp" }, "Ramp (Ram-Lak)"),
|
||||
m("option", { value: "shepp-logan" }, "Shepp-Logan"),
|
||||
m("option", { value: "cosine" }, "Cosine"),
|
||||
m("option", { value: "hamming" }, "Hamming"),
|
||||
m("option", { value: "hann" }, "Hann"),
|
||||
m("option", { value: "none" }, "None (Unfiltered)"),
|
||||
]
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
|
||||
|
||||
@@ -69,7 +69,10 @@ export async function generateSinogram(
|
||||
}
|
||||
|
||||
sinCtx.putImageData(imgData, 0, 0);
|
||||
return sinogramCanvas.toDataURL();
|
||||
return {
|
||||
sinogramUrl: sinogramCanvas.toDataURL(),
|
||||
projections: projections, // Return the raw projections
|
||||
};
|
||||
}
|
||||
|
||||
export async function reconstructImageFromSinogram(
|
||||
@@ -113,23 +116,28 @@ export async function reconstructImageFromSinogram(
|
||||
const accum = new Float32Array(size * size);
|
||||
const center = size / 2;
|
||||
|
||||
const rawProjectionsFromSinogram = []; // To store raw projections extracted from sinogram image
|
||||
const allFilteredProjections = []; // To store filtered projections
|
||||
|
||||
for (let angle = 0; angle < angles; angle++) {
|
||||
const theta = (angle * Math.PI) / angles;
|
||||
|
||||
let projection = [];
|
||||
let currentRawProjection = [];
|
||||
for (let x = 0; x < width; x++) {
|
||||
const i = isVertical
|
||||
? (x * angles + angle) * 4 // transposed layout
|
||||
: (angle * width + x) * 4; // normal layout
|
||||
|
||||
projection.push(sinogramData[i]);
|
||||
currentRawProjection.push(sinogramData[i]);
|
||||
}
|
||||
rawProjectionsFromSinogram.push(currentRawProjection); // Store the raw projection
|
||||
|
||||
// Apply the selected filter
|
||||
projection = applyFilter(projection, filterType);
|
||||
allFilteredProjections.push(projection); // Store the filtered projection
|
||||
const filteredProjection = applyFilter(
|
||||
[...currentRawProjection],
|
||||
filterType
|
||||
); // Apply filter to a copy
|
||||
allFilteredProjections.push(filteredProjection); // Store the filtered projection
|
||||
|
||||
for (let y = 0; y < size; y++) {
|
||||
for (let x = 0; x < size; x++) {
|
||||
@@ -139,7 +147,7 @@ export async function reconstructImageFromSinogram(
|
||||
x0 * Math.cos(theta) + y0 * Math.sin(theta) + width / 2
|
||||
);
|
||||
if (s >= 0 && s < width) {
|
||||
accum[y * size + x] += projection[s];
|
||||
accum[y * size + x] += filteredProjection[s]; // Use the filtered projection for accumulation
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,6 +222,8 @@ export async function reconstructImageFromSinogram(
|
||||
return {
|
||||
reconstructedUrl: outputCanvas.toDataURL(),
|
||||
filteredSinogramUrl: filteredSinogramUrl,
|
||||
rawProjectionsFromSinogram: rawProjectionsFromSinogram,
|
||||
allFilteredProjections: allFilteredProjections,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user