Draw the angle overlay on sinogram

This commit is contained in:
Peter Stockings
2025-05-18 12:07:57 +10:00
parent fe6c00c0fa
commit 41c3c76218

View File

@@ -18,6 +18,9 @@ export const UploadImageComponent = {
"https://upload.wikimedia.org/wikipedia/commons/e/e5/Shepp_logan.png",
reconstructionFrames: [],
currentFrameIndex: 0,
manualAngleIndex: 0, // For the new angle control slider
sinogramImageElement: null, // To store the sinogram img DOM element
sinogramHighlightCanvas: null, // Canvas for sinogram highlight
renderMode: "grayscale", // or "heatmap"
filterType: "ramp", // Added to manage selected filter
@@ -46,6 +49,87 @@ export const UploadImageComponent = {
ctx.stroke();
},
drawSinogramHighlight(angleIndex) {
if (
!this.sinogramHighlightCanvas ||
!this.sinogramImageElement ||
!this.sinogramImageElement.complete ||
this.sinogramImageElement.naturalWidth === 0
) {
if (this.sinogramHighlightCanvas) {
const ctx = this.sinogramHighlightCanvas.getContext("2d");
ctx.clearRect(
0,
0,
this.sinogramHighlightCanvas.width,
this.sinogramHighlightCanvas.height
);
}
return;
}
const canvas = this.sinogramHighlightCanvas;
const ctx = canvas.getContext("2d");
const dispWidth = this.sinogramImageElement.width; // Displayed width of sinogram image
const dispHeight = this.sinogramImageElement.height; // Displayed height of sinogram image
const naturalW = this.sinogramImageElement.naturalWidth;
const naturalH = this.sinogramImageElement.naturalHeight;
if (canvas.width !== dispWidth) canvas.width = dispWidth;
if (canvas.height !== dispHeight) canvas.height = dispHeight;
ctx.clearRect(0, 0, dispWidth, dispHeight);
if (this.angleCount <= 0 || naturalW === 0 || naturalH === 0) return;
let anglesAlongImageHeight = true;
// Infer orientation based on natural dimensions of the sinogram image vs angleCount
// This logic assumes that one of the dimensions of the *natural* sinogram image data
// (before it's scaled for display) corresponds to this.angleCount.
if (naturalW === this.angleCount && naturalH !== this.angleCount) {
anglesAlongImageHeight = false; // Angles are columns, highlight is vertical
} else if (naturalH === this.angleCount && naturalW !== this.angleCount) {
anglesAlongImageHeight = true; // Angles are rows, highlight is horizontal
} else if (naturalH === this.angleCount && naturalW === this.angleCount) {
// Square sinogram (angleCount might equal detector_size), assume angles along height by convention
anglesAlongImageHeight = true;
}
// If neither naturalW nor naturalH strictly equals angleCount (and the other doesn't),
// we stick with the default assumption (anglesAlongImageHeight = true).
// This can happen if the sinogram image itself is generated with an aspect ratio
// where neither dimension is exactly `this.angleCount` (e.g. if `generateSinogram` in sinogram.js
// has `size` not equal to `angles` and the output canvas is `size` x `angles` or `angles` x `size`).
// The key is that `this.angleCount` represents the number of projection angles.
ctx.strokeStyle = "rgba(0, 255, 0, 0.7)"; // Green highlight
if (anglesAlongImageHeight) {
// Angles are effectively rows in the displayed sinogram image.
// The number of such rows is this.angleCount.
// We need to highlight the row corresponding to `angleIndex`.
const highlightRowHeight = dispHeight / this.angleCount;
const yPos = (angleIndex + 0.5) * highlightRowHeight;
ctx.lineWidth = Math.max(1, highlightRowHeight * 0.7);
ctx.beginPath();
ctx.moveTo(0, yPos);
ctx.lineTo(dispWidth, yPos);
ctx.stroke();
} else {
// Angles are effectively columns in the displayed sinogram image.
// The number of such columns is this.angleCount.
const highlightColWidth = dispWidth / this.angleCount;
const xPos = (angleIndex + 0.5) * highlightColWidth;
ctx.lineWidth = Math.max(1, highlightColWidth * 0.7);
ctx.beginPath();
ctx.moveTo(xPos, 0);
ctx.lineTo(xPos, dispHeight);
ctx.stroke();
}
},
isOverlayReady() {
return (
this.overlayCanvas &&
@@ -60,6 +144,7 @@ export const UploadImageComponent = {
this.sinogramUrl = "loading";
this.filteredSinogramUrl = null; // Reset filtered sinogram
this.reconstructedUrl = null;
this.manualAngleIndex = 0; // Reset manual angle on new image
m.redraw();
this.imageUrl = url;
@@ -216,29 +301,108 @@ export const UploadImageComponent = {
class: "w-full",
oninput: (e) => {
this.angleCount = parseInt(e.target.value, 10);
this.manualAngleIndex = 0; // Reset manual angle if angleCount changes
// Redraw overlay for new manualAngleIndex=0 with new angleCount
const newTheta = (0 * Math.PI) / this.angleCount; // Theta for index 0
this.drawAngleOverlay(newTheta);
if (this.sinogramHighlightCanvas) {
// If sinogram highlight exists, update it too
this.drawSinogramHighlight(this.manualAngleIndex);
}
this.loadAndProcessDebounced(this.imageUrl); // reprocess with new angle count
},
}),
]),
// Manual Angle Slider for Preview
m("div", { class: "mt-4 w-full max-w-md" }, [
m(
"label",
{ class: "block text-sm font-medium text-gray-700 mb-1" },
`Preview Angle: ${this.manualAngleIndex} (θ ≈ ${(
(this.manualAngleIndex * 180) /
this.angleCount
).toFixed(1)}°)`
),
m("input", {
type: "range",
min: 0,
max: Math.max(0, this.angleCount - 1), // Ensure max is not negative if angleCount is 0 or 1
value: this.manualAngleIndex,
step: 1,
class: "w-full",
disabled: !this.hasLoadedInitialImage, // Disable if no image loaded
oninput: (e) => {
this.manualAngleIndex = parseInt(e.target.value, 10);
const theta = (this.manualAngleIndex * Math.PI) / this.angleCount;
this.drawAngleOverlay(theta);
this.drawSinogramHighlight(this.manualAngleIndex); // Update sinogram highlight
},
}),
]),
m(StepAccordion, { index: 2 }),
// Sinogram
this.sinogramUrl &&
m("div", { class: "mt-10 w-full max-w-md text-center" }, [
m("div", { class: "mt-10 w-full max-w-md text-center relative" }, [
m(
"h2",
{ class: "text-xl font-semibold text-gray-700 mb-4" },
"Generated Sinogram"
),
this.sinogramUrl === "loading"
? m("p", "Processing...")
: m("img", {
src: this.sinogramUrl,
alt: "Sinogram",
class: "rounded shadow max-w-full h-auto mx-auto",
style: "width: 100%; max-height: 300px;",
}),
m("div", { class: "relative" }, [
this.sinogramUrl === "loading"
? m("p", "Processing...")
: m("img", {
src: this.sinogramUrl,
alt: "Sinogram",
class: "rounded shadow max-w-full h-auto mx-auto",
style: "width: 100%; max-height: 300px;",
onload: ({ target }) => {
this.sinogramImageElement = target;
// Set initial dimensions for highlight canvas based on loaded image
if (this.sinogramHighlightCanvas) {
this.sinogramHighlightCanvas.width = target.width;
this.sinogramHighlightCanvas.height = target.height;
}
this.drawSinogramHighlight(this.manualAngleIndex); // Draw initial highlight
},
onerror: () => {
// If sinogram fails to load, clear element
this.sinogramImageElement = null;
this.drawSinogramHighlight(this.manualAngleIndex); // Attempt to clear highlight
},
}),
// Sinogram Highlight Canvas
m("canvas", {
style:
"position:absolute; top:0; left:0; pointer-events:none; max-width: 100%; max-height: 300px;",
oncreate: ({ dom }) => {
this.sinogramHighlightCanvas = dom;
// Ensure dimensions are set if image is already loaded by now (race condition)
if (
this.sinogramImageElement &&
this.sinogramImageElement.complete
) {
dom.width = this.sinogramImageElement.width;
dom.height = this.sinogramImageElement.height;
this.drawSinogramHighlight(this.manualAngleIndex);
}
},
// Set width/height to 0 initially or based on a placeholder if needed
width: this.sinogramImageElement
? this.sinogramImageElement.width
: this.overlayCanvas
? this.overlayCanvas.width
: 300, // Default or match other canvas
height: this.sinogramImageElement
? this.sinogramImageElement.height
: this.overlayCanvas
? this.overlayCanvas.height
: 300,
}),
]),
]),
// Filtered Sinogram Display