Draw the angle overlay on sinogram
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user