116 lines
3.7 KiB
JavaScript
116 lines
3.7 KiB
JavaScript
export const steps = [
|
|
{
|
|
title: "1. Upload / Input Image",
|
|
content: `You upload or use a default grayscale image. This is treated like a 2D slice of a physical object.
|
|
|
|
The input image is a 2D function:
|
|
$ f(x, y) $
|
|
This represents how much X-rays are absorbed at each point.`,
|
|
},
|
|
{
|
|
title: "2. Radon Transform (Generating the Sinogram)",
|
|
content: `We rotate a virtual X-ray beam around the image and compute line integrals at each angle — simulating how X-rays pass through.
|
|
|
|
\\[
|
|
R(\\theta, s) = \\int_{-\\infty}^{\\infty} f(s \\cos\\theta - t \\sin\\theta,\\ s \\sin\\theta + t \\cos\\theta)\\ dt
|
|
\\]
|
|
|
|
Where: <br>
|
|
- $\\theta$ = projection angle <br>
|
|
- $s$ = offset from center
|
|
|
|
`,
|
|
},
|
|
{
|
|
title: "3. Sinogram",
|
|
content: `You now have a 2D image where:
|
|
- X-axis = detector position
|
|
- Y-axis = angle
|
|
Each row is a projection.
|
|
|
|
Math:
|
|
$\text{Sinogram} = \{ R(\theta_1, s), R(\theta_2, s), \dots, R(\theta_n, s) \}$
|
|
|
|
`,
|
|
},
|
|
{
|
|
title: "4. Optional: Apply Ramp Filter (FBP)",
|
|
content: `If enabled, we sharpen each projection before back-projecting by amplifying high-frequency content.
|
|
|
|
Math:
|
|
\\[
|
|
P(\\omega) = \\mathcal{F}[R(\\theta, s)] \\\\
|
|
P'(\\omega) = |\\omega| \\cdot P(\\omega) \\\\
|
|
\\text{Filtered Projection} = \\mathcal{F}^{-1}[P'(\\omega)]
|
|
\\]
|
|
|
|
`,
|
|
},
|
|
{
|
|
title: "5. Back Projection",
|
|
content: `Each (filtered) projection is \"smeared\" back into the image space along its angle.
|
|
|
|
\\[
|
|
f'(x, y) = \\int_0^{\\pi} R'(\\theta,\\ x \\cos\\theta + y \\sin\\theta)\\ d\\theta
|
|
\\]`,
|
|
},
|
|
{
|
|
title: "6. Final Reconstruction",
|
|
content: `After all angles are added up, you get a reconstructed image resembling the original.
|
|
|
|
\[
|
|
f'(x, y) \approx f(x, y)
|
|
\]`,
|
|
},
|
|
];
|
|
|
|
export const StepAccordion = {
|
|
view(vnode) {
|
|
const index = vnode.attrs.index;
|
|
const step = steps[index];
|
|
const expanded = vnode.state.expanded ?? false;
|
|
|
|
return m("div", { class: "border-b border-gray-300 py-1 my-2" }, [
|
|
m(
|
|
"button",
|
|
{
|
|
class:
|
|
"w-full text-left font-bold text-lg text-gray-800 focus:outline-none",
|
|
onclick: () => {
|
|
vnode.state.expanded = !vnode.state.expanded;
|
|
},
|
|
},
|
|
step.title
|
|
),
|
|
vnode.state.expanded &&
|
|
m(
|
|
"div",
|
|
{
|
|
class: "mt-2 text-gray-700 whitespace-pre-wrap text-sm",
|
|
onupdate: () => {
|
|
if (window.MathJax) window.MathJax.typesetPromise();
|
|
},
|
|
},
|
|
m.trust(markdownToHTML(step.content))
|
|
),
|
|
]);
|
|
},
|
|
};
|
|
|
|
// Use marked.js for robust Markdown to HTML conversion
|
|
const renderer = new marked.Renderer();
|
|
renderer.image = (href, title, text) => {
|
|
// Add Tailwind classes for styling images
|
|
return `<img src="${href}" alt="${text}" title="${
|
|
title || ""
|
|
}" class="my-2 rounded shadow max-w-full h-auto mx-auto">`;
|
|
};
|
|
|
|
marked.setOptions({ renderer });
|
|
|
|
function markdownToHTML(text) {
|
|
// Replace escaped newlines from template literals with actual newlines for marked
|
|
const processedText = text.replace(/\\n/g, "\n");
|
|
return marked.parse(processedText);
|
|
}
|