/ concept-collection / mri-scanner
Sign in
concept-collection / mri-scanner
mri-scanner / scripts / gen-phantoms.mjs
112 lines · 3.7 KBBlameHistoryRaw
1// Generate the two built-in digital phantoms as genuine KomaMRI `.phantom`
2// (HDF5) files: a small cube and a small sphere, ~10 mm, densely and uniformly
3// sampled with spins. Structure mirrors ../KomaMRI.jl write_phantom:
4// root attrs: Version, Name, Ns, Dims
5// group "position": x, y, z (metres)
6// group "contrast": ρ, T1, T2, T2s, Δw (s, s, s, rad/s)
7//
8// Run: npm run gen-phantoms (writes src/phantom/data/{cube,sphere}.phantom)
9import * as h5 from 'h5wasm/node'
10import fs from 'node:fs'
11import path from 'node:path'
12import { fileURLToPath } from 'node:url'
14const __dirname = path.dirname(fileURLToPath(import.meta.url))
15const OUT_DIR = path.resolve(__dirname, '../src/phantom/data')
17// Geometry (metres) and uniform spin spacing.
18const SIZE_MM = 10 // cube side / sphere diameter
19const SPACING_MM = 0.8 // uniform grid spacing
20const MM = 1e-3
22// Uniform tissue properties (a single "water-like" material for the whole object).
23const RHO = 1.0
24const T1 = 1.0 // s
25const T2 = 0.1 // s
26const T2S = 0.05 // s
27const DW = 0.0 // rad/s
29/** Build a centred uniform grid of points, keeping those for which keep(x,y,z) (metres). */
30function buildGrid(sizeMm, spacingMm, keep) {
31 const n = Math.floor(sizeMm / spacingMm) + 1 // points per axis
32 const span = (n - 1) * spacingMm // actual extent covered
33 const start = -span / 2 // centre on origin
34 const xs = [],
35 ys = [],
36 zs = []
37 for (let i = 0; i < n; i++) {
38 const x = (start + i * spacingMm) * MM
39 for (let j = 0; j < n; j++) {
40 const y = (start + j * spacingMm) * MM
41 for (let k = 0; k < n; k++) {
42 const z = (start + k * spacingMm) * MM
43 if (keep(x, y, z)) {
44 xs.push(x)
45 ys.push(y)
46 zs.push(z)
47 }
48 }
49 }
50 }
51 return {
52 x: Float32Array.from(xs),
53 y: Float32Array.from(ys),
54 z: Float32Array.from(zs),
55 }
58function writePhantom(name, grid) {
59 const ns = grid.x.length
60 const fill = (v) => {
61 const a = new Float32Array(ns)
62 a.fill(v)
63 return a
64 }
65 // h5wasm/node writes to the real working directory, so use a temp name and
66 // unlink it after reading the bytes back out.
67 const filename = `.gen-${name}.phantom.tmp`
68 const f = new h5.File(filename, 'w')
69 f.create_attribute('Version', '1.0.0')
70 f.create_attribute('Name', name)
71 f.create_attribute('Ns', ns, [], '<i4')
72 f.create_attribute('Dims', 3, [], '<i4')
74 const pos = f.create_group('position')
75 pos.create_dataset({ name: 'x', data: grid.x, dtype: '<f4' })
76 pos.create_dataset({ name: 'y', data: grid.y, dtype: '<f4' })
77 pos.create_dataset({ name: 'z', data: grid.z, dtype: '<f4' })
79 const con = f.create_group('contrast')
80 con.create_dataset({ name: 'ρ', data: fill(RHO), dtype: '<f4' })
81 con.create_dataset({ name: 'T1', data: fill(T1), dtype: '<f4' })
82 con.create_dataset({ name: 'T2', data: fill(T2), dtype: '<f4' })
83 con.create_dataset({ name: 'T2s', data: fill(T2S), dtype: '<f4' })
84 con.create_dataset({ name: 'Δw', data: fill(DW), dtype: '<f4' })
86 f.flush()
87 f.close()
89 const bytes = h5.FS.readFile(filename)
90 h5.FS.unlink(filename)
91 const outPath = path.join(OUT_DIR, `${name}.phantom`)
92 fs.writeFileSync(outPath, Buffer.from(bytes))
93 console.log(` ${name}.phantom: ${ns} spins, ${bytes.length} bytes -> ${path.relative(process.cwd(), outPath)}`)
96async function main() {
97 await h5.ready
98 fs.mkdirSync(OUT_DIR, { recursive: true })
99 const r = (SIZE_MM / 2) * MM
100 console.log('Generating phantoms (size %d mm, spacing %d mm):', SIZE_MM, SPACING_MM)
101 writePhantom(
102 'cube',
103 buildGrid(SIZE_MM, SPACING_MM, () => true),
104 )
105 writePhantom(
106 'sphere',
107 buildGrid(SIZE_MM, SPACING_MM, (x, y, z) => x * x + y * y + z * z <= r * r + 1e-18),
108 )
109 console.log('Done.')
112main()
moveopenescclose