moonnifti

    NIfTI-1 voxel access and world-coordinate-preserving transformations in MoonBit

    nifti
    volume
    affine
    scientific-imaging
    voxel
    Download zip
    Version
    0.1.0
    License
    MIT
    Last updated
    8 hours ago
    Downloads
    6

    #MoonNIfTI

    CI

    MoonBit 原生 NIfTI-1 体数据与空间变换库。 读取真实体素,在保留 qform / sform 各自含义的前提下裁剪、置换和翻转数据轴,并写出可被 NiBabel 读取的文件。

    Pure MoonBit scalar NIfTI-1 decoding, voxel access, spatial geometry, ROI and signed-axis transforms. Python/NiBabel is an independent test oracle, not the implementation. Node.js only adapts files, gzip containers and CLI I/O.

    适合科研数据工具、教学合成数据、Wasm 或原生 MoonBit 程序的体数据处理基础层。不是临床诊断产品,不做疾病识别、配准、DICOM 转换或脱敏。

    #已实现

    能力0.1.0 边界
    读取NIfTI-1 n+1 单文件、3D/4D、大小端
    数据类型uint8 / int8 / uint16 / int16 / uint32 / int32 / float32 / float64
    访问x 最快存储序,原始值与缩放值分离,时间帧选择,数据轴切片
    几何独立 qform/sform、qfac、仿射组合/求逆、显式空间选择
    统计有限值最小/最大/均值;NaN、+Inf、-Inf 独立计数
    网格比较尺寸、坐标系代码、已知长度单位换算、空间位置误差
    变换整数 ROI、全部 48 种轴排列与翻转;不插值、不重采样
    写出原始样本字节和缩放保留;两套坐标独立更新;元数据损失报告
    CLIinspect / dump / slice / world / grid / crop / reorient,受限 gzip,禁止覆盖已有输出

    不支持:NIfTI-2、Analyze .hdr/.img 对、RGB/复数/64 位整数体素、向量/张量重定向、任意角度重采样、自动解剖方向标准化。切片是数据轴切片,不能直接称为经过校正的轴位/冠状位/矢状位。

    #构建与快速开始

    经验证的工具链:MoonBit 0.10.14+7d59c7ec9,core 同版本;CLI 使用 Node.js 22 或以上。旧版 MoonBit 不保证兼容。安装方式见 MoonBit 官方说明

    git clone https://github.com/wangjiale6036-dotcom/moonnifti.git cd moonnifti moon update moon build --target js --release node _build/js/release/build/cmd/moonnifti/moonnifti.js --help

    无需 Python 即可对已有文件运行 CLI。以下命令创建合成数据并完成三个独立验证的示例,因此额外需要 Python 3.12、NumPy 和 NiBabel:

    python -m pip install numpy==2.3.5 nibabel==5.3.2 python scripts/scenarios.py work/demo

    work/demo 必须尚不存在。结果包含切片/坐标 JSON、兼容与错位网格报告、门控后的配对切片数据,以及 03-roi.nii.gz03-reoriented.nii。没有真实患者数据,也没有外部数据下载。

    直接运行(下文把完整 JS 路径写作 CLI,请替换为上面的实际路径):

    node CLI inspect input.nii.gz node CLI slice input.nii.gz 2 10 0 node CLI world input.nii.gz 10 20 30 qform node CLI grid image.nii.gz labels.nii.gz 0.001 node CLI crop input.nii.gz roi.nii.gz 10 20 5 32 32 16 node CLI reorient roi.nii.gz arranged.nii 2 0 1 1 0 0

    reorient 中新轴 0/1/2 分别对应旧轴 2/0/1,最后三个 0/1 表示是否翻转新轴。不会把它自动解释为 RAS/LPS。

    退出码:0 成功,1 参数/输入/输出错误,2 网格不兼容。输出文件必须不存在;父目录需预先存在。.gz 输出会压缩,输入由 gzip 魔数识别。JSON 中非有限样本输出为 null,完整计数见 inspect 的 statistics。

    #作为库使用

    模块名 wangjiale6036-dotcom/moonnifti;Mooncakes 发布状态以公开包页面为准,GitHub 源码始终可构建。依赖配置完成后,在 moon.pkg 中导入:

    import { "wangjiale6036-dotcom/moonnifti" @nifti }

    fn process(bytes : Bytes) -> Bytes raise @nifti.NiftiError {
    let image = @nifti.read(bytes)
    let value = image.voxel(1, 2, 3, t=0)
    let xyz = image.world(1, 2, 3, space=QForm)
    ignore(value)
    ignore(xyz)
    let roi = image.crop([0, 0, 0], [2, 3, 4])
    // Inspect roi.warnings before downstream use.
    roi.image.to_bytes()
    }

    示例假设输入足够大且定义了 qform,否则返回错误。ImageAffine 内部存储私有;返回的尺寸、矩阵是副本。库的输入是解压后的 .nii 字节,gzip 不属于核心库接口。参见 公开接口跨包消费测试API 语义

    #空间语义与安全边界

    • qform 和 sform 可以合法不同。PreferSForm 只是在使用时优先选择 sform,没有把两者合并。变换会分别更新每一套已定义坐标,保留各自代码。
    • 若两套空间都未知,不会凭空补一个矩阵;读取允许,世界坐标与空间变换拒绝。未知单位阻止网格兼容判断。
    • 网格兼容只说明空间采样格一致,不保证属于同一受试者、同一解剖结构或已正确配准;默认比较不包含时间轴。
    • 变换只允许 scalar intent 0、label 1002、time-series 2001。不透明扩展默认阻止变换;显式 --drop-extensions 才允许丢弃并报告。
    • 空间操作清除 dim_info 与 slice timing 并报告;头文本原样保留,不是匿名化工具
    • 浮点头字段写回会有舍入误差;空间保持是在明确误差容限下验证,不宣称逐位矩阵相等。
    • .nii 上限 256 MiB、头部 1 MiB、16,000,000 个体素;会产生输入/输出/数组副本,不是零拷贝或流式处理。CLI gzip 解压也有上限。
    • 严格子集拒绝尾随字节、无效 spacing、奇异定义矩阵和不支持类型;不是声称它们全都违反整个 NIfTI 标准。

    #测试与复现

    moon check --target js --deny-warn moon fmt --check moon info node scripts/check-api.mjs moon test --target js --deny-warn moon test --target wasm-gc --deny-warn moon test --target native --deny-warn moon build --target js --release --deny-warn node tests/cli.mjs python scripts/oracle.py --report work/oracle-report.json python scripts/scenarios.py work/scenarios moon package

    本地验收:24 个测试在三个后端通过;其中包含 3,000 次有界头部变异、48 种轴变换属性检查。CLI 37 次进程调用通过,包括损坏 gzip、超大文件、解压上限和拒绝覆盖。NiBabel 交叉验证 276 案例:32 读取、32 裁剪、192 轴变换、12 切片、8 网格比较。三个完整工作流另行执行。

    上述数量不是 3,000 个独立单元测试,也不是完整标准认证。实测独立矩阵最大绝对元素误差约 3.9124e-6;固定合成样例结果不构成所有输入的误差上界。CI 在 Linux / Windows 重跑验收并上传可下载的 CLI、包和场景产物。

    #独立贡献与来源

    这是按公开 NIfTI 规范新写的 MoonBit 实现,不是调用现成 JS/Python 影像引擎的包装。格式、四元数算法、体数据裁剪均非本项目发明;其他语言已有成熟实现。价值在于 MoonBit 可复用数据层和双空间变换写回的可验证实现。

    查重与相邻项目边界 记录公开检索范围、MoonDICOM / MedSeal 差异及未覆盖区域,不承诺全球唯一或评审通过。来源与许可证 区分规范、开发依赖和测试 oracle。

    项目开发和文档使用 AI 辅助,提交历史保留实际迭代;没有空提交或回填时间。许可证:MIT。后续优先增加独立用户样例与兼容性覆盖,再评估 NIfTI-2、类型扩展和更多宿主适配,不把计划功能写成已实现。

    NiftiError

    pub(all) suberror NiftiError {
    NiftiError(String, Int)
    } derive(Eq)

    impl Show for NiftiError

    NiftiError::equal

    fn NiftiError::equal(NiftiError, NiftiError) -> Bool

    NiftiError::not_equal

    fn NiftiError::not_equal(x : NiftiError, y : NiftiError) -> Bool

    NiftiError::output

    fn NiftiError::output(self : NiftiError, logger : &Logger) -> Unit

    NiftiError::to_string

    fn NiftiError::to_string(self : NiftiError) -> String

    Affine

    pub struct Affine {
    // private fields
    }

    An owned, finite, nonsingular row-major 4x4 affine.

    Affine::compose

    fn Affine::compose(self : Affine, right : Affine) -> Affine raise NiftiError

    Composition applies right first, then self.

    Affine::determinant

    fn Affine::determinant(self : Affine) -> Double

    Affine::identity

    fn Affine::identity() -> Affine

    Affine::inverse

    fn Affine::inverse(self : Affine) -> Affine raise NiftiError

    Affine::matrix

    fn Affine::matrix(self : Affine) -> Array[Double]

    Return a copy; callers cannot mutate the matrix.

    Affine::new

    fn Affine::new(values : Array[Double]) -> Affine raise NiftiError

    Affine::transform

    fn Affine::transform(self : Affine, x : Double, y : Double, z : Double) -> Array[Double] raise NiftiError

    Endian

    pub(all) enum Endian {
    Little
    Big
    } derive(Eq)

    impl Show for Endian

    Endian::equal

    fn Endian::equal(Endian, Endian) -> Bool

    Endian::not_equal

    fn Endian::not_equal(x : Endian, y : Endian) -> Bool

    Endian::output

    fn Endian::output(self : Endian, logger : &Logger) -> Unit

    Endian::to_string

    fn Endian::to_string(self : Endian) -> String

    GridComparison

    pub(all) struct GridComparison {
    compatible : Bool
    reason : String
    maximum_corner_error_mm : Double?
    }

    Image

    pub struct Image {
    // private fields
    }

    Storage is private. Images own their input bytes; methods return copies.

    Image::affine

    fn Image::affine(self : Image, space : Space) -> Affine?

    Unknown space is explicit; there is no invented fallback affine.

    Image::byte_order

    fn Image::byte_order(self : Image) -> Endian

    Image::compare_grid

    fn Image::compare_grid(self : Image, other : Image, tolerance_mm? : Double, space? : Space) -> GridComparison raise NiftiError

    Compare spatial grids, NOT anatomical registration or time axes. Coordinate-system codes and known length units are required.

    Image::crop

    fn Image::crop(self : Image, start : Array[Int], size : Array[Int], drop_extensions? : Bool) -> TransformResult raise NiftiError

    A spatial ROI uses half-open bounds [start, start + size), retaining all t. Stored samples/scaling are unchanged; both defined spatial forms move independently.

    Image::datatype

    fn Image::datatype(self : Image) -> Int

    Image::dimensions

    fn Image::dimensions(self : Image) -> Array[Int]

    Image::extensions

    fn Image::extensions(self : Image) -> Int

    Image::form_codes

    fn Image::form_codes(self : Image) -> (Int, Int)

    Image::linear_index

    fn Image::linear_index(self : Image, x : Int, y : Int, z : Int, t? : Int) -> Int raise NiftiError

    Image::raw_at

    fn Image::raw_at(self : Image, index : Int) -> Double raise NiftiError

    Stored samples are decoded without scl_slope/scl_inter.

    Image::reorient

    fn Image::reorient(self : Image, axes : Array[Int], flips : Array[Bool], drop_extensions? : Bool) -> TransformResult raise NiftiError

    New axis j corresponds to old axis axes[j]; flips[j] reverses that axis. This is an exact sample rearrangement, NOT resampling or registration.

    Image::scaled_at

    fn Image::scaled_at(self : Image, index : Int) -> Double raise NiftiError

    Image::scaling

    fn Image::scaling(self : Image) -> (Double, Double)

    Image::slice

    fn Image::slice(self : Image, axis : Int, index : Int, t? : Int, scaled? : Bool) -> Slice raise NiftiError

    Image::statistics

    fn Image::statistics(self : Image, scaled? : Bool) -> Statistics raise NiftiError

    Statistics are over scaled samples unless explicitly disabled.

    Image::to_bytes

    fn Image::to_bytes(self : Image) -> Bytes

    Lossless within the declared file region. Trailing input bytes are rejected.

    Image::units

    fn Image::units(self : Image) -> (Int, Int)

    Image::voxel

    fn Image::voxel(self : Image, x : Int, y : Int, z : Int, t? : Int, scaled? : Bool) -> Double raise NiftiError

    Image::voxel_count

    fn Image::voxel_count(self : Image) -> Int

    Image::voxel_spacing

    fn Image::voxel_spacing(self : Image) -> Array[Double]

    Image::world

    fn Image::world(self : Image, x : Double, y : Double, z : Double, space? : Space) -> Array[Double] raise NiftiError

    Slice

    pub(all) struct Slice {
    width : Int
    height : Int
    values : Array[Double]
    }

    A data-axis slice, with the two remaining axes in ascending order.

    Space

    pub(all) enum Space {
    QForm
    SForm
    PreferSForm
    } derive(Eq)

    Space::equal

    fn Space::equal(Space, Space) -> Bool

    Space::not_equal

    fn Space::not_equal(x : Space, y : Space) -> Bool

    Statistics

    pub(all) struct Statistics {
    finite_count : Int
    nan_count : Int
    positive_infinity_count : Int
    negative_infinity_count : Int
    minimum : Double?
    maximum : Double?
    mean : Double?
    }

    TransformResult

    pub(all) struct TransformResult {
    image : Image
    warnings : Array[String]
    }

    Spatial operations return explicit metadata-loss information.

    read

    fn read(bytes : Bytes) -> Image raise NiftiError

    Parse the bounded NIfTI-1 single-file scalar 3D/4D subset.

    version

    fn version() -> String