@fireants/fireants

中文文档
0.1.20260913发布于 2026/09/14SEE LICENSE IN LICENSETypeScriptES Module零依赖近 30 天下载 7023

Browser medical image registration -- rigid, affine, SyN and greedy deformable -- as a wasm module, threaded where the page is cross-origin isolated. A C port of FireANTs.

原始使用文档
文档目录11 节
  1. Installing
  2. CPU threads and WebGPU
  3. API
  4. Stages
  5. gzip
  6. Threads
  7. Bundlers
  8. Image size and memory
  9. Errors
  10. Building
  11. Publishing

@fireants/fireants

Medical image registration in the browser: rigid, affine, SyN and greedy deformable alignment on a threaded CPU or WebGPU backend. Two NIfTI ArrayBuffers go in, the warped moving image comes out.

This is a modified derivative work. The module is compiled from cfireants, a pure C reimplementation of FireANTs — the files have been changed, none of the original Python is carried over, and this package is not endorsed by the FireANTs authors. It is distributed under the FireANTs License v1.0 (LICENSE), whose section 4 requires the copyright notices, license text and bibliography to travel with it; see THIRD_PARTY_NOTICES.md, which carries the citations. If you publish work using this, cite the FireANTs paper.

Public package publication is gated pending redistribution review. Section 4(a) enumerates the permitted forms of redistribution, and an npm package is neither the original repository nor a wheel on a package manager named in the project's documentation. 4(a)(iii) (a modified fork retaining notices and bibliography) is the clause this package relies on. The release workflow will build and test the tarball, but will not publish it to npm or attach it publicly until the repository variable CFIREANTS_NPM_REDISTRIBUTION_APPROVED is explicitly set to true after written clearance or a reviewed interpretation has been recorded.

Installing

Once redistribution approval has been recorded, install the package from npm:

npm install @fireants/fireants

The package is named fireants; cfireants remains the name of the native C executable and the internal WebAssembly assets. Each GitHub release also carries the immutable npm tarball and SHA-256 file produced by .github/workflows/release-npm.yml.

import { register } from '@fireants/fireants'

const { image, elapsedMs, variant } = await register(
  await fixedFile.arrayBuffer(),   // .nii or .nii.gz
  await movingFile.arrayBuffer(),
)
// image is gzipped if the moving input was

CPU threads and WebGPU

Three wasm builds ship. The two CPU variants are chosen automatically; WebGPU is selected explicitly with backend: 'webgpu':

variantneedssmall 2 mm pairquality
mta cross-origin isolated page13.6 s0.9555
stnothing84.9 s0.9555
gpunavigator.gpu — no isolation15.7 s0.9557

Chrome, 14 cores; quality is global NCC against the template. WebGPU is not currently the fast one. At this size Asyncify suspension and the parts still on the CPU (notably SyN warp inversion) cost more than the GPU saves — the same run is 6.4 s natively. What it buys is that it needs no cross-origin isolation: on a host that cannot send COOP/COEP the comparison is not 15.7 s against 13.6 s but 15.7 s against 84.9 s.

A 1 mm brain single-threaded takes several minutes, which is why the st timeout default is 30 minutes against mt's 15.

The threaded build is -pthread, so it imports shared memory, and a page that is not cross-origin isolated cannot instantiate it — and HANGS rather than failing. Emscripten's pool spawn throws DataCloneError: SharedArrayBuffer transfer requires self.crossOriginIsolated inside a promise nobody holds, so the factory neither resolves nor rejects. This package therefore asks the question before it fetches anything, and quietly takes st when the answer is no — a page on GitHub Pages, which cannot set these headers, still works. result.variant says which one ran.

To get mt, serve the top-level document with:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Putting them on the .wasm response instead does nothing: isolation is a property of the document. Note the cost, which is the usual reason a deployment turns isolation back off: under require-corp every cross-origin subresource the page loads — images, fonts, analytics, embeds — must itself send Cross-Origin-Resource-Policy or proper CORS, or be blocked.

Ask before you promise the user a fast run:

import { checkSupport } from '@fireants/fireants'

const { threaded, variant, threads, reasons } = checkSupport()
if (!threaded) console.warn(`falling back to ${variant}: ${reasons.join('; ')}`)

WebGPU is asked about separately, because the only honest answer needs requestAdapter() and is therefore async — navigator.gpu exists in browsers that then hand back no adapter:

import { checkWebgpuSupport, register } from '@fireants/fireants'

if (await checkWebgpuSupport()) await register(fixed, moving, { backend: 'webgpu' })

backend: 'webgpu' never falls back. Where there is no device it throws CfireantsError with code: 'no-webgpu', so a caller who asked for the GPU finds out rather than being handed a CPU result labelled as one. The gpu build is single-threaded for the work it does not put on the device, so threads is meaningless there and passing it is refused; result.variant is 'gpu'.

API

register(
  fixed: ArrayBuffer | ArrayBufferView,
  moving: ArrayBuffer | ArrayBufferView,
  options?: RegisterOptions,
): Promise<RegisterResult>

checkSupport(): SupportReport
checkWebgpuSupport(): Promise<boolean>

RegisterResult is { image: ArrayBuffer, elapsedMs: number, variant: 'mt' | 'st' | 'gpu', threads: number, log: string } — elapsedMs is time inside the module, log is everything it wrote to stdout and stderr.

SupportReport is { supported, threaded, variant, threads, reasons }: supported is false only where there is no WebAssembly at all, threaded says whether mt is usable, and reasons says why it is not.

optiondefaultmeaning
backend'cpu''cpu' or 'webgpu'; WebGPU is explicit and never silently falls back
transform'syn'preset stage sequence: 'rigid', 'affine', 'syn', 'greedy'
stages—explicit stages, in order; overrides transform
verbose00 silent, 1 summary, 2 per-iteration. onLog only sees what this emits
gzipmatches the moving inputgzip the returned buffer
workertrue where Worker existsrun in a Web Worker. The only real cancellation. Node has none, so it defaults to false there
assetPaththis module's directorywhere cfireants.js/cfireants.wasm are served from
threadstrueCPU only: false forces st; a positive integer caps the pool via --threads
timeoutMs900000 (mt/gpu) / 1800000 (st)give up waiting. SyN is minutes, not seconds
onLog—receives the module's output lines as they arrive
signal—AbortSignal; under worker: true it terminates the module
args—appended to argv verbatim, after everything else

Stages

Stages are the CLI's own strings, passed through unparsed. There is no shell, so write them plain — SyN[0.1,0.5,1.0], never 'SyN[0.1,0.5,1.0]' with the quotes inside the string.

await register(fixed, moving, {
  verbose: 2,
  onLog: (line) => console.log(line),
  stages: [
    { transform: 'Rigid[0.003]',      metric: 'MI[32]', convergence: '[200x100x50,1e-6,10]', shrinkFactors: '4x2x1' },
    { transform: 'Affine[0.001]',     metric: 'MI[32]', convergence: '[200x100x50,1e-6,10]', shrinkFactors: '4x2x1' },
    { transform: 'SyN[0.1,0.5,1.0]',  metric: 'CC[5]',  convergence: '[200x100x50,1e-6,10]', shrinkFactors: '4x2x1' },
  ],
})

metric defaults to MI for the linear stages and CC for the deformable ones, matching the executable. If both images are brain-extracted, pass CC[5] at every stage instead: on such a pair it is both more accurate and faster. At verbose: 2 the module writes progress to stderr, which onLog receives line by line:

  SyN scale 2: 96x114x96
    iter 50/100 loss=-0.789890

Parse it if you want a progress bar; the raw lines are also joined into result.log.

gzip

The wasm module links no zlib — the C reads and writes plain .nii only — so all compression happens in TypeScript, via DecompressionStream and CompressionStream. Inputs are sniffed by magic bytes, not by filename, so a .nii that is really gzipped still works. An environment without those streams gets a no-compression-streams refusal rather than a corrupt volume.

Threads

The threaded pool sizes itself to navigator.hardwareConcurrency. A positive integer caps it through the CLI's --threads flag — 14 cores is 13.6 s on the small 2 mm pair, threads: 4 is 29.8 s — and still selects the threaded build, since the single-threaded one cannot honour any value but 1. threads: false forces the single-threaded build even on an isolated page, which is worth having when you would rather leave the cores to the page.

Node is exempt from the isolation rule: it has SharedArrayBuffer, needs no headers to use it, and runs the threaded build at full width.

Bundlers

The emscripten glue is loaded by URL at run time and locates its own .wasm through its own import.meta.url, so the two files must stay adjacent and unhashed. Vite and Rollup will neither rewrite that URL nor emit the assets. Copy cfireants-mt.js, cfireants-mt.wasm, cfireants.js, cfireants.wasm, cfireants-gpu.js, cfireants-gpu.wasm, and worker.js out of this package's dist/ into somewhere served, and point assetPath at it:

await register(fixed, moving, { assetPath: '/cfireants/' })

assetPath must be same-origin: it feeds a dynamic import() and a new Worker(), either of which would otherwise run a third party's code with access to the patient's volumes.

Image size and memory

Peak memory tracks the fixed image, not the moving one. SyN allocates its displacement fields, optimiser moments and correlation intermediates in the fixed image's grid, so an oversized moving volume costs only its own storage.

FixedMovingPeakWall, 14 threads
182x218x182, 1mm template182x218x1821.80 GB46.7s
182x218x182, 1mm template256x256x2561.92 GB56.3s
256x256x256182x218x1823.88 GB185.1s

wasm32 caps the heap at 4 GB and a browser tab has less to give than node, so the last row has no headroom and a larger fixed image will fail. Register to a template at 1mm and pass the large volume as moving.

The gpu build is tighter still: its heap is a fixed 1 GiB, because emscripten's WebGPU port cannot be combined with ALLOW_MEMORY_GROWTH. It also needs an adapter whose maxStorageBufferBindingSize fits the largest single binding: SyN's five-channel correlation workspace (144 MB for a 1mm template), or three channels for Greedy and the linear stages (87 MB, inside the 128 MiB baseline). The wrapper requests the adapter's own reported size limits, so a capable GPU gets them.

Before starting, the wrapper reads dim[1..3] out of both NIfTI headers, works out which stages will run, and checks the workspace against maxStorageBufferBindingSize and the largest upload against maxBufferSize. If either fails it throws, naming the bytes needed and the limit that refused. That is the whole of it: a baseline adapter fails fast with a message you can act on, it does not gain the ability to run 1 mm SyN. Tiling the workspace is the fix that would, and it is not written. Use Greedy, the CPU backend, or a smaller fixed image on such a device.

Errors

Every refusal throws a CfireantsError with a code you can branch on:

codewhen
bad-inputnot an ArrayBuffer/typed array, or empty
no-compression-streamsgzip needed but DecompressionStream/CompressionStream is absent
unsupported-environmentno WebAssembly. A page without cross-origin isolation is not an error — CPU silently gets the single-threaded build
no-webgpubackend: 'webgpu' was requested and there is no navigator.gpu, no adapter, or no device. Never a silent downgrade to CPU
unsupported-optiona bad option, a cross-origin assetPath, or threads on a build that cannot honour it
timeoutthe module did not initialise, or did not finish, in timeoutMs
registration-failedthe module exited non-zero, or wrote no output
abortedthe AbortSignal fired

error.log carries the module's own output when the failure came from inside it. There is no fallback behind any of these: a registration that silently downgraded would return a plausible volume that is wrong.

Building

npm install
npm run checkVersion
npm run build      # build the wasm, stage it into dist/, emit types, bundle
npm run typecheck
npm run smokePackage -- . ../validate/small
npm run smokeWebgpu -- .  # Chrome: packaged CPU/WebGPU Rigid→Affine→SyN parity

npm run build runs build-wasm.sh first, which needs emscripten on PATH (6.0.2 is what CI pins) and builds from the C sources in the parent repository. scripts/copy-wasm.mjs then stages all six wasm artifacts out of js/wasm/ into dist/; set CFIREANTS_WASM_DIR if they are written somewhere else.

src/main.c, package.json, both root version fields in package-lock.json, and the release tag must carry the same version. CI checks this instead of rewriting release metadata. Pull requests and pushes run native CTest, build, and smoke-test both CPU variants from the packed tarball. The browser CPU/WebGPU parity check is available locally with npm run smokeWebgpu.

Publishing

The first publication needs a one-time bootstrap because npm trusted publishers are configured from an existing package's settings:

  1. Record redistribution approval and set the GitHub repository variable CFIREANTS_NPM_REDISTRIBUTION_APPROVED=true.
  2. Add a granular npm publish token as the GitHub Actions secret NPM_TOKEN.
  3. Commit the version in src/main.c, js/package.json, and js/package-lock.json, tag that commit with the same v-prefixed version, and publish the GitHub release.
  4. In the new package's npm settings, add the GitHub Actions trusted publisher for neurolabusc/cfireants, workflow release-npm.yml, with direct npm publish allowed. Then delete the NPM_TOKEN secret.

That release builds from source, verifies the version, runs native and packaged WASM smoke tests, publishes @fireants/fireants with provenance, and attaches the exact tarball plus its SHA-256 checksum to the GitHub release. Future releases authenticate to npm with short-lived OIDC credentials and need no npm token.

版本与发布记录

@fireants/fireants 共发布 1 个版本,首次发布于 2026/09/14;过去 12 个月发布了 1 个版本。

版本发布日期距今
0.1.20260913latest2026/09/142 个月前

关于 @fireants/fireants 的常见问题

@fireants/fireants 是什么?

Browser medical image registration -- rigid, affine, SyN and greedy deformable -- as a wasm module, threaded where the page is cross-origin isolated. A C port of FireANTs.

如何安装 @fireants/fireants?

在项目目录运行 npm install @fireants/fireants,或使用 pnpm add / yarn add / bun add。

@fireants/fireants 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@fireants/fireants 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@fireants/fireants 可以商用吗?

@fireants/fireants 使用 SEE LICENSE IN LICENSE 许可。许可条款写在包内的独立文件中,请阅读该文件确认可否商用。以上为概括,不构成法律意见,请以许可原文为准。

@fireants/fireants 的下载量有多少?

根据 npm 官方下载统计,@fireants/fireants 在 2026/10/15 – 2026/11/13 这 30 天内被下载 7,023 次,日均约 234 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@fireants/fireants 还在维护吗?

最新版本 0.1.20260913 发布于 2026/09/14(2 个月前);至今共发布 1 个版本,其中过去 12 个月发布了 1 个;首次发布于 2026/09/14。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。