├── nifti-zarr-schema-1.0.rc1.json └── README.md /nifti-zarr-schema-1.0.rc1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema", 3 | "$id": "https://raw.githubusercontent.com/neuroscales/nifti-zarr/main/nifti-zarr-schema-1.0.rc1.json", 4 | "title": "NIfTI-Zarr v1.0.rc1 - NIfTI header in JSON form", 5 | "type": "object", 6 | "properties": { 7 | "NIIHeaderSize": { 8 | "type": "integer" 9 | }, 10 | "A75DataTypeName": { 11 | "type": "string" 12 | }, 13 | "A75DBName": { 14 | "type": "string" 15 | }, 16 | "A75Extends": { 17 | "type": "integer" 18 | }, 19 | "A75SessionError": { 20 | "type": "integer" 21 | }, 22 | "A75Regular": { 23 | "type": "integer" 24 | }, 25 | "DimInfo": { 26 | "type": "object", 27 | "properties": { 28 | "Freq": { 29 | "enum": [ 30 | 0, 31 | 1, 32 | 2, 33 | 3 34 | ] 35 | }, 36 | "Phase": { 37 | "enum": [ 38 | 0, 39 | 1, 40 | 2, 41 | 3 42 | ] 43 | }, 44 | "Slice": { 45 | "enum": [ 46 | 0, 47 | 1, 48 | 2, 49 | 3 50 | ] 51 | } 52 | } 53 | }, 54 | "Dim": { 55 | "description": "Number of voxels for each dimension", 56 | "type": "array", 57 | "items": { 58 | "type": "integer", 59 | "minimum": 0 60 | }, 61 | "minItems": 3, 62 | "maxItems": 5 63 | }, 64 | "Param1": { 65 | "description": "First intent parameter", 66 | "type": [ 67 | "number", 68 | "null" 69 | ] 70 | }, 71 | "Param2": { 72 | "description": "Second intent parameter", 73 | "type": [ 74 | "number", 75 | "null" 76 | ] 77 | }, 78 | "Param3": { 79 | "description": "Third intent parameter", 80 | "type": [ 81 | "number", 82 | "null" 83 | ] 84 | }, 85 | "Intent": { 86 | "description": "Data intent or meaning", 87 | "enum": [ 88 | "", 89 | "corr", 90 | "ttest", 91 | "ftest", 92 | "zscore", 93 | "chi2", 94 | "beta", 95 | "binomial", 96 | "gamma", 97 | "poisson", 98 | "normal", 99 | "ncftest", 100 | "ncchi2", 101 | "logistic", 102 | "laplace", 103 | "uniform", 104 | "ncttest", 105 | "weibull", 106 | "chi", 107 | "invgauss", 108 | "extval", 109 | "pvalue", 110 | "logpvalue", 111 | "log10pvalue", 112 | "estimate", 113 | "label", 114 | "neuronames", 115 | "matrix", 116 | "symmatrix", 117 | "dispvec", 118 | "vector", 119 | "point", 120 | "triangle", 121 | "quaternion", 122 | "unitless", 123 | "tseries", 124 | "elem", 125 | "rgb", 126 | "rgba", 127 | "shape", 128 | "fsl_fnirt_displacement_field", 129 | "fsl_cubic_spline_coefficients", 130 | "fsl_dct_coefficients", 131 | "fsl_quadratic_spline_coefficients", 132 | "fsl_topup_cubic_spline_coefficients", 133 | "fsl_topup_quadratic_spline_coefficients", 134 | "fsl_topup_field" 135 | ] 136 | }, 137 | "DataType": { 138 | "description": "Data type, in numpy format", 139 | "type": "string" 140 | }, 141 | "BitDepth": { 142 | "type": "integer" 143 | }, 144 | "FirstSliceID": { 145 | "description": "First slice index", 146 | "type": "integer" 147 | }, 148 | "VoxelSize": { 149 | "description": "Spacing between elements for each dimension", 150 | "type": "array", 151 | "items": { 152 | "type": "number", 153 | "minimum": 0 154 | }, 155 | "minItems": 3, 156 | "maxItems": 5 157 | }, 158 | "Orientation": { 159 | "description": "Handedness of the coordinate system to denote the positive direction along an axis. For example, if the positive x-axis is in the direction from left to right, then 'x': 'r' ", 160 | "type": "object", 161 | "properties": { 162 | "x": { 163 | "enum": [ 164 | "r", 165 | "l", 166 | "a", 167 | "p", 168 | "i", 169 | "s" 170 | ] 171 | }, 172 | "y": { 173 | "enum": [ 174 | "r", 175 | "l", 176 | "a", 177 | "p", 178 | "i", 179 | "s" 180 | ] 181 | }, 182 | "z": { 183 | "enum": [ 184 | "r", 185 | "l", 186 | "a", 187 | "p", 188 | "i", 189 | "s" 190 | ] 191 | } 192 | } 193 | }, 194 | "NIIByteOffset": { 195 | "type": "integer" 196 | }, 197 | "ScaleSlope": { 198 | "description": "Intensity transform Slope", 199 | "type": "number" 200 | }, 201 | "ScaleOffset": { 202 | "description": "Intensity transform offset", 203 | "type": "number" 204 | }, 205 | "LastSliceID": { 206 | "description": "Last slice index", 207 | "type": "integer" 208 | }, 209 | "SliceType": { 210 | "description": "A valid slice timing code (see table 4.6)", 211 | "enum": [ 212 | "", 213 | "seq+", 214 | "seq-", 215 | "alt+", 216 | "alt-", 217 | "alt2+", 218 | "alt2-" 219 | ] 220 | }, 221 | "Unit": { 222 | "description": "Space and time units", 223 | "type": "object", 224 | "properties": { 225 | "L": { 226 | "enum": [ 227 | "", 228 | "m", 229 | "mm", 230 | "um" 231 | ] 232 | }, 233 | "T": { 234 | "enum": [ 235 | "", 236 | "s", 237 | "ms", 238 | "us" 239 | ] 240 | } 241 | } 242 | }, 243 | "MaxIntensity": { 244 | "description": "Max display intensity", 245 | "type": "number" 246 | }, 247 | "MinIntensity": { 248 | "description": "Min display intensity", 249 | "type": "number" 250 | }, 251 | "SliceTime": { 252 | "description": "Time for 1 slice", 253 | "type": "number" 254 | }, 255 | "TimeOffset": { 256 | "description": "Time axis shift", 257 | "type": "number" 258 | }, 259 | "A75GlobalMax": { 260 | "type": "integer" 261 | }, 262 | "A75GlobalMin": { 263 | "type": "integer" 264 | }, 265 | "Description": { 266 | "description": "Any text you like", 267 | "type": "string", 268 | "maxLength": 80 269 | }, 270 | "AuxFile": { 271 | "description": "Auxiliary filename", 272 | "type": "string", 273 | "maxLength": 24 274 | }, 275 | "QForm": { 276 | "description": "A valid xform name (see table 4.5)", 277 | "enum": [ 278 | "", 279 | "scanner_anat", 280 | "aligned_anat", 281 | "talairach", 282 | "mni_152", 283 | "template_other" 284 | ] 285 | }, 286 | "SForm": { 287 | "description": "A valid xform name (see table 4.5)", 288 | "enum": [ 289 | "", 290 | "scanner_anat", 291 | "aligned_anat", 292 | "talairach", 293 | "mni_152", 294 | "template_other" 295 | ] 296 | }, 297 | "Quatern": { 298 | "properties": { 299 | "b": { 300 | "type": "number" 301 | }, 302 | "c": { 303 | "type": "number" 304 | }, 305 | "d": { 306 | "type": "number" 307 | } 308 | } 309 | }, 310 | "QuaternOffset": { 311 | "properties": { 312 | "x": { 313 | "type": "number" 314 | }, 315 | "y": { 316 | "type": "number" 317 | }, 318 | "z": { 319 | "type": "number" 320 | } 321 | } 322 | }, 323 | "Affine": { 324 | "description": "3x4 Matrix", 325 | "type": "array", 326 | "items": { 327 | "type": "array", 328 | "items": { 329 | "type": "number" 330 | }, 331 | "minItems": 4, 332 | "maxItems": 4 333 | }, 334 | "minItems": 3, 335 | "maxItems": 3 336 | }, 337 | "Name": { 338 | "description": "'name' or meaning of data", 339 | "type": "string" 340 | }, 341 | "NIIFormat": { 342 | "description": "NIfTI magic string", 343 | "enum": [ 344 | "ni1", 345 | "n+1", 346 | "ni2", 347 | "n+2" 348 | ] 349 | }, 350 | "NIFTIExtension": { 351 | "description": "Four bytes following header indicating the exist of extension", 352 | "items": { 353 | "type": "number" 354 | }, 355 | "maxItems": 4, 356 | "minItems": 4, 357 | "type": "array" 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NIfTI-Zarr draft specification 2 | 3 | * Status of this document: release candidate 4 | * Editor: Yael Balbastre 5 | * Version: 1.0.rc1 6 | 7 | ## Abstract 8 | 9 | This document specifies the nifti-zarr format for storing neuroimaging 10 | data in the cloud. 11 | 12 | ## Table of Content 13 | 14 | 0. [References](#0-references) 15 | 1. [Introduction](#1-introduction) 16 | 2. [Format Specification](#2-format-specification) 17 | 3. [Main differences with NIfTI and/or OME-NGFF](#3-main-differences-with-nifti-andor-ome-ngff) 18 | 4. [Conversion tables](#4-conversion-tables) 19 | 5. [Reference implementations](#5-reference-implementations) 20 | 21 | ## 0. References 22 | 23 | * [__Zarr__](https://zarr.readthedocs.io) is a format for the storage of 24 | chunked, compressed, N-dimensional arrays inspired by HDF5, h5py and bcolz. 25 | * [__OME-NGFF__](https://ngff.openmicroscopy.org) (Next Generation File 26 | Format) is a format based on zarr for the storage of biomedical imaging data. 27 | * [__NIfTI__](https://nifti.nimh.nih.gov) (Neuroimaging Informatics 28 | Technology Initiative) is a single-file/single-resolution storage format 29 | for 3D+ neuroimaging data. 30 | * [__JNIfTI__](https://github.com/NeuroJSON/jnifti/tree/master) is a pure JSON 31 | implementation of the NIfTI format. 32 | * [__BIDS__](https://bids-specification.readthedocs.io) (Brain Imaging 33 | Data Structure) is a simple and intuitive way to organize and describe data. 34 | 35 | ## 1. Introduction 36 | 37 | As biomedical imaging scales up, it is increasingly making use of remote 38 | storage, remote computing and remote visualization. Classical file 39 | formats—which store array data contiguously in a single file—are limited 40 | at large scales as 41 | 42 | 1. They often do not store data at multiple resolutions; 43 | 2. They do not offer efficient parallel access to data chunks; 44 | 3. They do not efficiently compress 3D raster data. 45 | 46 | These limits are very clear when it comes to visualizing very large data 47 | volumes, which cannot be loaded in memory in full. In this context, it is 48 | preferable to only load the data required to display a given scene (either 49 | a large field-of-view at low-resolution, or a small field-of-view at high 50 | resolution). 51 | 52 | The [Zarr](https://zarr.readthedocs.io) format was developed to bypass 53 | the limitations of single-file formats such as [HDF5](https://www.hdfgroup.org/). 54 | The microscopy community is currently developping its own standard for 55 | cloud-friendly biomedical imaging data 56 | ([OME-NGFF](https://ngff.openmicroscopy.org))—with a Zarr-based implementation— 57 | and adds rules for storing multi-resolutions images and medical-specific 58 | metadata such as axis names and voxel sizes. However, this community 59 | has needs in terms of metadata and coordinate-space description that are 60 | relatively complex, as they need to conform to different organs, a wide range 61 | of acquisition systems, and different tissue processing pipelines. This has 62 | drastically slowed down the adoption of a coordinate transform standard, 63 | which hampers the use of OME-NGFF with neuroimaging data in two ways: 64 | 65 | 1. at the time of this writing, the current version of the format 66 | ([0.5](https://ngff.openmicroscopy.org/0.5/index.html)) only handles 67 | canonical scales and offsets; 68 | 2. the coordinate transform standard 69 | [being drafted](https://github.com/ome/ngff/pull/138) is more flexible 70 | than required for pure neuroimaging applications, which may prevent its 71 | widespread adoption by the neuroimaging community. 72 | 73 | In contrast, the neuroimaging community has adopted and used a standard 74 | "world" coordinate frame for decades, where 75 | 76 | ```text 77 | +x = left -> right 78 | +y = posterior -> anterior 79 | +z = inferior -> superior 80 | ``` 81 | 82 | An affine transform is used to map from the F-ordered voxel space $(i, j, k)$ 83 | to world space $(x, y, z)$. The neuroimaging community has also created a 84 | simple data exchange format—[NIfTI](https://nifti.nimh.nih.gov)—that is 85 | widely embraced and is the mandatory file format in standardization 86 | efforts such as [BIDS](https://bids-specification.readthedocs.io). 87 | However, the lack of multiresolution and/or chunk support in NIfTI has 88 | led BIDS to adopt OME-TIFF and OME-ZARR as mandatory formats for its 89 | microscopy component. 90 | 91 | Let us further add that OME-NGFF is mostly oriented towards microscopy, 92 | whereas neuroimaging formats focus on magnetic resonance imaging (MRI), 93 | computed tomography (CT), and/or positon emission tomography (PET). 94 | 95 | The NIfTI-Zarr (`nii.zarr`) specification attempts to merge the best of 96 | both worlds, in the simplest possible way. Like NIfTI, it aims to make the 97 | implementation of I/O libraries as simple as possible, to maximize chances 98 | that it gets adopted by the community. Its guiding principles are 99 | 100 | * __OME-Zarr compliant:__ any `nii.zarr` file should be a valid `ome.zarr` file; 101 | * __OME-Zarr minimal:__ only implements the minimum set of metadata necessary 102 | to describe [multi-resolution] neuroimaging data; 103 | * __NIfTI-compliant:__ the binary nifti header should be stored in its raw 104 | form in an additional `nifti` zarr array; 105 | * __NIfTI-priority:__ if metadata conflict across the nifti header and OME 106 | attributes, the nifti metadata should take precedence. 107 | 108 | > [!NOTE] 109 | > 110 | > * Being OME-NGFF compliant does not mean (for now) that the OME-NGFF 111 | > transform and the NIfTI transform match. Currently, OME-NGFF only handles 112 | > scales (for voxel sizes) and translations (for origin shifts caused by 113 | > pyramid methods). It is therefore impossible to encode an affine tranform - 114 | > or even swap axes - using the current OME-NGFF specification. What we 115 | > mean by OME-NGFF compliant is that any OME-NGFF viewer will correctly 116 | > display the content of the file _in scaled voxel space_. 117 | > * OME-NGFF does not currently offer the possibility to store an intensity 118 | > transform. This means that OME-NGFF viewers will not use the intensity 119 | > affine transform encoded by `scl_slope` and `scl_inter` in the nifti header. 120 | > * That said, the nifti layer added on top of OME-NGFF is light enough that 121 | > viewer developers may easily extend their software to handle 122 | > 1. an affine geometric tranform, and 123 | > 2. an affine intensity transform. 124 | > * In modern languages such as Python and Julia, a virtual array 125 | > that points to the raw data can easily be encapsulated in a high-level 126 | > class that applies the intensity transform on the fly. This is 127 | > examplified in our Python and Julia reference implementations, which 128 | > respectively leverage `nibabel`'s `Nifti1Image` and `NIfTI.jl`'s `NIVolume`. 129 | 130 | The simplicity of these guiding principles should make the adoption of 131 | `nii.zarr` in cloud environments (almost) as straightforward as the adoption 132 | of compressed-NIfTI (`.nii.gz`). 133 | 134 | ## 2. Format specification 135 | 136 | A NIfTI-Zarr file __MUST__ be a valid 137 | [OME-Zarr multi-resolution image](https://ngff.openmicroscopy.org/latest/#image-layout) 138 | (and therefore also a valid 139 | [Zarr dataset](https://zarr-specs.readthedocs.io/en/latest/specs.html)), 140 | whose directory structure and metadata are described in sections 141 | [2.1](#21-directory-structure), [2.2](#22-multiresolution-metadata) and 142 | [2.3](#23-ome-ngff-metadata). 143 | 144 | In addition, it __MUST__ store the nifti header corresponding to the finest 145 | level of the pyramid as a Zarr array with the `"nifti"` key, as described 146 | in section [2.4](#24-nifti-header). 147 | 148 | > [!NOTE] 149 | > At the time of this writing, Zarr is at version 3 and OME-NGFF is at 150 | > version 0.5. NIfTI-Zarr being a simple layer within an OME-Zarr object, 151 | > it does not impose a specific Zarr or OME-NGFF version. However, note that 152 | > OME-NGFF v0.4 specifies that it should be used with Zarr v2, while 153 | > OME-NGFF v0.5 specifies that it should be used with Zarr v3. 154 | > 155 | > Similarly, the NIfTI header may follow the 156 | > [NIfTI v1](https://nifti.nimh.nih.gov/pub/dist/src/niftilib/nifti1.h) or 157 | > [NIfTI v2](https://nifti.nimh.nih.gov/pub/dist/doc/nifti2.h) 158 | > specifications. 159 | > 160 | > All examples below use the OME-NGFF v0.4 + Zarr v2 specification, but 161 | > they could equivalently have used OME-NGFF v0.5 + Zarr v3. 162 | 163 | ### 2.1. Directory structure 164 | 165 | __REF:__ 166 | - OME-NGFF latest: [https://ngff.openmicroscopy.org/latest/#image-layout](https://ngff.openmicroscopy.org/latest/#image-layout) 167 | - OME-NGFF v0.5: [https://ngff.openmicroscopy.org/0.5/#image-layout](https://ngff.openmicroscopy.org/0.5/#image-layout) 168 | - OME-NGFF v0.4: [https://ngff.openmicroscopy.org/0.4/#image-layout](https://ngff.openmicroscopy.org/0.4/#image-layout) 169 | 170 | ```text 171 | └── mri.nii.zarr # A NIfTI volume converted to Zarr. 172 | │ 173 | ├── .zgroup # Each volume is a Zarr group, of arrays. 174 | ├── .zattrs # Group level attributes are stored in the .zattrs 175 | | # file include the OME "multiscales" key. 176 | | # In addition, the group level attributes 177 | │ # may also contain "_ARRAY_DIMENSIONS" for 178 | | # compatibility with xarray if this group directly 179 | | # contains multi-scale arrays. 180 | | 181 | ├── nifti # The NIfTI header is stored as a raw array 182 | │ ├── .zarray # of bytes, with an optional JSON form stored in 183 | │ ├── .zattrs # the associated .zattrs file. 184 | │ └── 0 185 | │ 186 | ├── 0 # Each multiscale level is stored as a separate 187 | | # Zarr array, which is a folder containing chunk 188 | │ ... # files which compose the array. 189 | | 190 | └── n # The name of the array is arbitrary with the 191 | | # ordering defined by the "multiscales" metadata, 192 | │ # but is often a sequence starting at 0. 193 | │ 194 | ├── .zarray # All image arrays must be up to 5-dimensional 195 | │ # with the axis of type time before type channel, 196 | | # before spatial axes. 197 | │ 198 | └─ t # Chunks are stored with the nested directory 199 | └─ c # layout. All but the last chunk element are stored 200 | └─ z # as directories. The terminal chunk is a file. 201 | └─ y # Together the directory and file names provide the 202 | └─ x # "chunk coordinate" (t, c, z, y, x), where the 203 | # maximum coordinate will be dimension_size / chunk_size. 204 | ``` 205 | 206 | ### 2.2. Multiresolution metadata 207 | 208 | __REF:__ 209 | - Zarr v3: [https://zarr-specs.readthedocs.io/en/latest/v3/core/v3.0.html#array-metadata](https://zarr-specs.readthedocs.io/en/latest/v3/core/v3.0.html#array-metadata) 210 | - Zarr v2: [https://zarr-specs.readthedocs.io/en/latest/v2/v2.0.html#arrays](https://zarr-specs.readthedocs.io/en/latest/v2/v2.0.html#arrays) 211 | 212 | ```yaml 213 | # {filename}.nii.zarr/{0..n}/.zarray 214 | { 215 | "chunks": [ 216 | 1, # Number of time chunks 217 | 3, # Number of channel chunks 218 | 1000, # Number of z chunks 219 | 1000, # Number of y chunks 220 | 1000, # Number of x chunks 221 | ], 222 | "compressor": { # Codec used to compress chunks 223 | "id": "blosc", # MUST be "blosc" or "zlib" 224 | "cname": "lz4", 225 | "clevel": 5, 226 | "shuffle": 1 227 | }, 228 | "dtype": " 1 millimeter) 276 | "type": "scale", 277 | "scale": [1.0, 1.0, 1.0, 1.0, 1.0] 278 | }] 279 | }, 280 | { 281 | "path": "2", 282 | "coordinateTransformations": [{ 283 | # the voxel size for the third scale level 284 | # (downscaled by a factor of 4 -> 2 millimeter) 285 | "type": "scale", 286 | "scale": [1.0, 1.0, 2.0, 2.0, 2.0] 287 | }] 288 | } 289 | ], 290 | "coordinateTransformations": [{ 291 | # the time unit (0.1 seconds), 292 | # which is the same for each scale level 293 | "type": "scale", 294 | "scale": [0.1, 1.0, 1.0, 1.0, 1.0] 295 | }], 296 | } 297 | ] 298 | } 299 | ``` 300 | 301 | __FUTURE CHANGES:__ as soon as affine coordinate transforms are integrated 302 | into the OME-NGFF standard, it will be used to encode both the NIfTI 303 | qform and sform as valid OME-NGFF metadata. However, only these two 304 | transforms will be accepted in a valid NIfTI-Zarr file. None of the 305 | more advanced combinations of affine and nonlinear transforms will be 306 | accepted. Similarly, only a very specific set of coordinate spaces will 307 | be accepted. 308 | 309 | ### 2.4. NIfTI header 310 | 311 | __REF__: 312 | 313 | * NIfTI v1: [https://nifti.nimh.nih.gov/pub/dist/src/niftilib/nifti1.h](https://nifti.nimh.nih.gov/pub/dist/src/niftilib/nifti1.h) 314 | * NIfTI v2: [https://nifti.nimh.nih.gov/pub/dist/doc/nifti2.h](https://nifti.nimh.nih.gov/pub/dist/doc/nifti2.h) 315 | * JNIfTI v1: [https://github.com/NeuroJSON/jnifti/blob/master/JNIfTI_specification.md#niftiheader](https://github.com/NeuroJSON/jnifti/blob/master/JNIfTI_specification.md#niftiheader) 316 | * Zarr v3: [https://zarr-specs.readthedocs.io/en/latest/v3/core/v3.0.html#array-metadata](https://zarr-specs.readthedocs.io/en/latest/v3/core/v3.0.html#array-metadata) 317 | * Zarr v2: [https://zarr-specs.readthedocs.io/en/latest/v2/v2.0.html#arrays](https://zarr-specs.readthedocs.io/en/latest/v2/v2.0.html#arrays) 318 | * NIfTI-Zarr v1.0.rc1: [nifti-zarr-schema-1.0.rc1.json](./nifti-zarr-schema-1.0.rc1.json) 319 | 320 | The nifti header ([v1](https://nifti.nimh.nih.gov/pub/dist/src/niftilib/nifti1.h) 321 | or [v2](https://nifti.nimh.nih.gov/pub/dist/doc/nifti2.h)) __MUST__ be encoded 322 | as a single-chunk zarr array of bytes under the `nifti` key: 323 | 324 | ```text 325 | └── mri.nii.zarr 326 | │ 327 | └── nifti # The NIfTI header is stored as a raw array 328 | ├── .zarray # of bytes, with an optional JSON form stored in 329 | ├── .zattrs # the associated .zattrs file. 330 | └── 0 331 | ``` 332 | 333 | ```yaml 334 | # {filename}.nii.zarr/nifti/.zarray 335 | { 336 | "zarr_format": 2, # MUST be 2 337 | "shape": [348], # MUST be header length if `dtype="u1"` else 1 338 | "compressor": { 339 | "id": "zlib", # MUST be `{"id": "zlib", "level": 0..9}` or `null` 340 | "level": 9 # MUST be in 0..9, if "zlib" 341 | }, 342 | "dtype": "u1", # MUST be "u1" or "S{length:d}" 343 | "order": "C", # UNUSED ("F" or "C") 344 | "chunks": [1], # MUST be 1 345 | "fill_value": null, # UNUSED 346 | } 347 | ``` 348 | 349 | A JSON variant of the nifti header __MAY__ be encoded in the attributes file 350 | (`.zattrs` or `attrs.json`). The JSON variant is only provided for 351 | human-readability. Its values __SHOULD__ be compatible with those of the 352 | binary header. If values conflict between the binary and JSON headers, 353 | the binary form __MUST__ take precedence. The JSON variant __MUST__ be 354 | compatible with the [`JNIfTI/NIFTIHeader`](https://github.com/NeuroJSON/jnifti/blob/master/JNIfTI_specification.md#niftiheader) 355 | specification. The JSON variant __MUST__ be compatible with the 356 | [`NIfTI-Zarr schema`](./nifti-zarr-schema-1.0.rc1.json) specification. 357 | 358 | ```yaml 359 | # filename.nii.zarr/nifti/.zattrs 360 | { 361 | # ------------------------------------------------------------------ 362 | # All other tags **MAY** contain JSON representations of the nifti 363 | # header. Not all fields are included. This JSON representation is 364 | # OPTIONAL and has a lower priority than the binary header. 365 | # ------------------------------------------------------------------ 366 | 367 | "NIIFormat": b"ni1\0", # MUST be one of "ni1\0", "n+1\0", "ni2\0", "n+2\0" 368 | "Dim": [128, 128, 128, 1, 3], # SHOULD match filename.nii.zarr/0/.zarray["shape"][[4, 3, 2, 0, 1]] 369 | "VoxelSize": [ # xtztc unit size, SHOULD be compatible with: 370 | 1.5, 1.5, 1.5, # filename.nii.zarr/.zattrs["multiscales"][0]["datasets"][0]["coordinateTransformations"][0]["scale"][2:5][::-1] 371 | 0.1, # filename.nii.zarr/.zattrs["multiscales"][0]["coordinateTransformations"][0]["scale"][0] 372 | 1.0, # filename.nii.zarr/.zattrs["multiscales"][0]["coordinateTransformations"][0]["scale"][1] 373 | ], 374 | "Unit": { # xyzt unit, SHOULD be compatible with: 375 | "L": "mm", # filename.nii.zarr/.zattrs["multiscales"][0]["axes"][2:]["unit"] 376 | "T": "s", # filename.nii.zarr/.zattrs["multiscales"][0]["axes"][0]["unit"] 377 | }, 378 | "DataType": "single", # SHOULD be compatible with filename.nii.zarr/0/.zarray["dtype"] 379 | "DimInfo": { 380 | "Freq": 1, # MUST be one of {0, 1, 2, 3} 381 | "Phase": 2, # MUST be one of {0, 1, 2, 3} 382 | "Slice": 3 # MUST be one of {0, 1, 2, 3} 383 | }, 384 | "Intent": "dispvec", # MUST be a valid intent code (see table 4.2) 385 | "Param1": None, 386 | "Param2": None, 387 | "Param3": None, 388 | "Name": "", 389 | "ScaleSlope": 1.0, # Data scaling: slope 390 | "ScaleOffset": 0.0, # Data scaling: intercept 391 | "SliceType": "seq+", # MUST be a valid slice timing code (see table 4.6) 392 | "FirstSliceID": 0 , # First slice index 393 | "LastSliceID": 127, # Last slice index 394 | "SliceTime": 1.0, # Time for 1 slice. 395 | "MinIntensity": 0.0, # Min display intensity 396 | "MaxIntensity": 1.0, # Max display intensity 397 | "TimeOffset": 0.0, # Time axis shift 398 | "Description": "An MRI", # Any text you like 399 | "AuxFile": "/path/to/aux", # Auxiliary filename 400 | "QForm": 0, # MUST be a valid xform code (see table 4.5) 401 | "Quatern": { # Quaternion 402 | "b": b, 403 | "c": c, 404 | "d": d 405 | }, 406 | "QuaternOffset": { # Translation 407 | "x": tx, 408 | "y": ty, 409 | "z": tz 410 | }, 411 | "SForm": 0, # MUST be a valid xform code (see table 4.5) 412 | "Affine": [ 413 | [axx, axy, axz, tx], # 1st row affine transform 414 | [ayx, ayy, ayz, ty], # 2nd row affine transform 415 | [azx, azy, azz, tz], # 3rd row affine transform 416 | ] 417 | } 418 | ``` 419 | 420 | Some fields __SHOULD__ be equivalent to their OME-Zarr counterparts: 421 | 422 | ```python 423 | nifti.zattrs["Dim"] == 0.zarray["shape"][[4, 3, 2, 0, 1]] # Level 0 zarray 424 | nifti.zattrs["DataType"] == *.zarray["dtype"] # All zarrays 425 | nifti.zattrs["VoxelSize"][:3] == zattrs["multiscales"][0]["datasets"][0]["coordinateTransformations"][0]["scale"][2:5][::-1] 426 | nifti.zattrs["VoxelSize"][3] == zattrs["multiscales"][0]["coordinateTransformations"][0]["scale"][0] 427 | nifti.zattrs["VoxelSize"][4] == zattrs["multiscales"][0]["coordinateTransformations"][0]["scale"][1] 428 | ``` 429 | 430 | ## 3. Main differences with NIfTI and/or OME-NGFF 431 | 432 | * Following the OME-NGFF specifcation, dimensions are ordered as 433 | [T, C, Z, Y, X] (in C order) as opposed to [C, T, Z, Y, X]. 434 | * To conform with the NIfTI expectation, on load data should be returned 435 | as a [C, T, Z, Y, X] array (in C order; [X, Y, Z, T, C] in F order). 436 | 437 | ### 3.1. NIfTI features that are not supported by NIfTI-Zarr 438 | 439 | * Any file with more than 5 dimensions 440 | 441 | ### 3.2. OME-NGFF features that are not supported by NIfTI-Zarr 442 | 443 | * Image collections 444 | * Image with labels 445 | * High-content screening data 446 | * OMERO metadata 447 | 448 | ## 4. Conversion tables 449 | 450 | ### Table 4.1. NIfTI header 451 | 452 | As a reminder, the nifti1 header has the following structure: 453 | 454 | | Type | Name | NIfTI-1 usage | JNIfTI | JSON type | 455 | | ---------- | ---------------- | ----------------------------------- | --------------------- | -------------------------------------- | 456 | | `int` | `sizeof_hdr` | __MUST__ be 348 | `"NIIHeaderSize"` | `integer` | 457 | | `char` | `data_type` | ~~UNUSED~~ | `"A75DataTypeName"` | `string` | 458 | | `char` | `db_name` | ~~UNUSED~~ | `"A75DBName"` | `string` | 459 | | `int` | `extents` | ~~UNUSED~~ | `"A75Extends"` | `integer` | 460 | | `short` | `session_error` | ~~UNUSED~~ | `"A75SessionError"` | `integer` | 461 | | `char` | `regular` | ~~UNUSED~~ | `"A75Regular"` | `integer` | 462 | | `char` | `dim_info` | MRI slice ordering. | `"DimInfo"` | `property` | 463 | | | | | `"DimInfo"/"Freq"` | `integer` | 464 | | | | | `"DimInfo"/"Phase"` | `integer` | 465 | | | | | `"DimInfo"/"Slice"` | `integer` | 466 | | `short[8]` | `dim` | Data array dimensions. | `"Dim"` | `array[integer]` | 467 | | `float` | `intent_p1` | 1st intent parameter. | `"Param1"` | `number` | 468 | | `float` | `intent_p2` | 2nd intent parameter. | `"Param2"` | `number` | 469 | | `float` | `intent_p3` | 3rd intent parameter. | `"Param3"` | `number` | 470 | | `short` | `intent_code` | `NIFTI_INTENT_*` code. | `"Intent"` | `[integer, string]` | 471 | | `short` | `datatype` | Defines data type! | `"DataType"` | `[integer, string]` | 472 | | `short` | `bitpix` | Number bits/voxel. | `"BitDepth"` | `integer` | 473 | | `short` | `slice_start` | First slice index. | `"FirstSliceID"` | `integer` | 474 | | `float[8]` | `pixdim` | Grid spacings. | `"VoxelSize"` | `array[number]` | 475 | | | | | `"Orientation"` | `property` | 476 | | | | | `"Orientation"/"x"` | `enum: ["l", "r", "a", "p", "i", "s"]` | 477 | | | | | `"Orientation"/"y"` | `enum: ["l", "r", "a", "p", "i", "s"]` | 478 | | | | | `"Orientation"/"z"` | `enum: ["l", "r", "a", "p", "i", "s"]` | 479 | | `float` | `vox_offset` | Offset into .nii file | `"NIIByteOffset"` | `number` | 480 | | `float` | `scl_slope` | Data scaling: slope. | `"ScaleSlope"` | `number` | 481 | | `float` | `scl_inter` | Data scaling: offset. | `"ScaleOffset"` | `number` | 482 | | `short` | `slice_end` | Last slice index. | `"LastSliceID"` | `integer` | 483 | | `char` | `slice_code` | Slice timing order. | `"SliceType"` | `[integer, string]` | 484 | | `char` | `xyzt_units` | Units of `pixdim[1..4]` | `"Unit"` | `property` | 485 | | | | | `"Unit"/"L"` | `[integer, string]` | 486 | | | | | `"Unit"/"T"` | `[integer, string]` | 487 | | `float` | `cal_max` | Max display intensity | `"MaxIntensity"` | `number` | 488 | | `float` | `cal_min` | Min display intensity | `"MinIntensity"` | `number` | 489 | | `float` | `slice_duration` | Time for 1 slice. | `"SliceTime"` | `number` | 490 | | `float` | `toffset` | Time axis shift. | `"TimeOffset"` | `number` | 491 | | `int` | `glmax` | ~~UNUSED~~ | `"A75GlobalMax"` | `integer` | 492 | | `int` | `glmin` | ~~UNUSED~~ | `"A75GlobalMin"` | `integer` | 493 | | `char[80]` | `descrip` | any text you like. | `"Description"` | `string` | 494 | | `char[24]` | `aux_file` | auxiliary filename. | `"AuxFile"` | `string` | 495 | | `short` | `qform_code` | `NIFTI_XFORM_*` code. | `"QForm"` | `integer` | 496 | | `short` | `sform_code` | `NIFTI_XFORM_*` code. | `"SForm"` | `integer` | 497 | | `float` | `quatern_b` | Quaternion b param. | `"Quatern"/"b"` | `number` | 498 | | `float` | `quatern_c` | Quaternion c param. | `"Quatern"/"c"` | `number` | 499 | | `float` | `quatern_d` | Quaternion d param. | `"Quatern"/"d"` | `number` | 500 | | `float` | `qoffset_x` | Quaternion x shift. | `"QuaternOffset"/"x"` | `number` | 501 | | `float` | `qoffset_y` | Quaternion y shift. | `"QuaternOffset"/"y"` | `number` | 502 | | `float` | `qoffset_z` | Quaternion z shift. | `"QuaternOffset"/"z"` | `number` | 503 | | `float[4]` | `srow_x` | 1st row affine transform. | `"Affine"/[0]` | `array[number]` | 504 | | `float[4]` | `srow_y` | 2nd row affine transform. | `"Affine"/[1]` | `array[number]` | 505 | | `float[4]` | `srow_z` | 3rd row affine transform. | `"Affine"/[2]` | `array[number]` | 506 | | `char[16]` | `intent_name` | 'name' or meaning of data. | `"Name"` | `string` | 507 | | `char[4]` | `magic` | __MUST__ be `"ni1\0"` or `"n+1\0"`. | `"NIIFormat"` | `string` | 508 | 509 | ### Table 4.2. Data types 510 | 511 | In Zarr, the byte order __MUST__ be specified by prepending one of 512 | `{"|", "<", ">"}` to the data type string. 513 | 514 | | Data type | NIfTI | Zarr | JNIfTI | 515 | | ------------ | ---------------- | ----------------------------------------------------------------------------------------- | -------------- | 516 | | `uint8` | `2` | "|u1" | `"uint8"` | 517 | | `int16` | `4` | `"i2"` | `"int16"` | 518 | | `int32` | `8` | `"i4"` | `"int32"` | 519 | | `float32` | `16` | `"f4"` | `"single"` | 520 | | `complex64` | `32` | `"c8"` | `"complex64"` | 521 | | `float64` | `64` | `"f8"` | `"double"` | 522 | | `rgb24` | `128` | [["r", "|u1"], ["g", "|u1"], ["b", "|u1"]] | `"rgb24"` | 523 | | `int8` | `256` | "|i1" | `"int8"` | 524 | | `uint16` | `512` | `"u2"` | `"uint16"` | 525 | | `uint32` | `768` | `"u4"` | `"uint32"` | 526 | | `int64` | `1024` | `"i8"` | `"int64"` | 527 | | `uint64` | `1280` | `"u8"` | `"uint64"` | 528 | | `float128` | `1536` | `"f16"` | `"double128"` | 529 | | `complex128` | `1792` | `"c16"` | `"complex128"` | 530 | | `complex256` | `2048` | `"c32"` | `"complex256"` | 531 | | `rgba32` | `2304` | [["r", "|u1"], ["g", "|u1"], ["b", "|u1"], ["a", "|u1"]] | `"rgba32"` | 532 | | `bool` | 🛑 unsupported! | "|b1" | | 533 | | `timedelta` | 🛑 unsupported! | `"m8[{unit}]"` | | 534 | | `time` | 🛑 unsupported! | `"M8[{unit}]"` | | 535 | 536 | ### Table 4.3. Units 537 | 538 | In OME-NGFF, units must be names from the UDUNITS-2 database. 539 | 540 | | Unit | NIfTI | UDUNITS-2 | OME-NGFF axis | JNIfTI | 541 | | ----------- | ----- | --------------- | ------------- | --------- | 542 | | unknown | `0` | `""` | | `""` | 543 | | meter | `1` | `"meter"` | `"space"` | `"m"` | 544 | | millimeter | `2` | `"millimeter"` | `"space"` | `"mm"` | 545 | | micron | `3` | `"micrometer"` | `"space"` | `"um"` | 546 | | second | `8` | `"second"` | `"time"` | `"s"` | 547 | | millisecond | `16` | `"millisecond"` | `"time"` | `"ms"` | 548 | | microsecond | `24` | `"microsecond"` | `"time"` | `"us"` | 549 | | hertz | `32` | `"hertz"` | `"channel"` | `"hz"` | 550 | | ppm | `40` | `"micro"` | `"channel"` | `"ppm"` | 551 | | rad | `48` | `"radian"` | `"channel"` | `"rad/s"` | 552 | 553 | ### Table 4.4. Intents 554 | 555 | | Intent | NIfTI | JNIfTI | `len(intent_p)` | Intent parameters | 556 | | -------------------------------- | ------ | --------------- | --------------- | ------------------------------------- | 557 | | None | `0` | `"none"` | `0` | [] | 558 | | Correlation coefficient R | `2` | `"corr"` | `1` | [dof] | 559 | | Student t statistic | `3` | `"ttest"` | `1` | [dof] | 560 | | Fisher F statistic | `4` | `"ftest"` | `2` | [num dof, den dof] | 561 | | Standard normal | `5` | `"zscore"` | `0` | [] | 562 | | Chi-squared | `6` | `"chi2"` | `1` | [dof] | 563 | | Beta distribution | `7` | `"beta"` | `2` | [a, b] | 564 | | Binomial distribution | `8` | `"binomial"` | `2` | [nb trials, prob per trial] | 565 | | Gamma distribution | `9` | `"gamma"` | `2` | [shape, scale] | 566 | | Poisson distribution | `10` | `"poisson"` | `1` | [mean] | 567 | | Normal distribution | `11` | `"normal"` | `2` | [mean, standard deviation] | 568 | | Noncentral F statistic | `12` | `"ncftest"` | `3` | [num dof, den dof, num noncentrality] | 569 | | Noncentral chi-squared statistic | `13` | `"ncchi2"` | `2` | [dof, noncentrality] | 570 | | Logistic distribution | `14` | `"logistic"` | `2` | [location, scale] | 571 | | Laplace distribution | `15` | `"laplace"` | `2` | [location, scale] | 572 | | Uniform distribution | `16` | `"uniform"` | `2` | [lower end, upper end] | 573 | | Noncentral t statistic | `17` | `"ncttest"` | `2` | [dof, noncentrality] | 574 | | Weibull distribution | `18` | `"weibull"` | `3` | [location, scale, power] | 575 | | Chi distribution | `19` | `"chi"` | `1` | [dof] | 576 | | Inverse Gaussian | `20` | `"invgauss"` | `2` | [mu, lambda] | 577 | | Extreme value type I | `21` | `"extval"` | `2` | [location, scale] | 578 | | Data is a 'p-value' | `22` | `"pvalue"` | `0` | [] | 579 | | Data is ln(p-value) | `23` | `"logpvalue"` | `0` | [] | 580 | | Data is log10(p-value) | `24` | `"log10pvalue"` | `0` | [] | 581 | | Parameter estimate | `1001` | `"estimate"` | `0` | [] | 582 | | Index into set of labels | `1002` | `"label"` | `0` | [] | 583 | | Index into NeuroNames set | `1003` | `"neuronames"` | `0` | [] | 584 | | MxN matrix at each voxel | `1004` | `"matrix"` | `2` | [M, N] | 585 | | NxN matrix at each voxel | `1005` | `"symmatrix"` | `1` | [N] | 586 | | Displacement field | `1006` | `"dispvec"` | `0` | [] | 587 | | Vector field | `1007` | `"vector"` | `0` | [] | 588 | | Spatial coordinate | `1008` | `"point"` | `0` | [] | 589 | | Triangle (3 indices) | `1009` | `"triangle"` | `0` | [] | 590 | | Quaternion (4 values) | `1010` | `"quaternion"` | `0` | [] | 591 | | Dimensionless value | `1011` | `"unitless"` | `0` | [] | 592 | | Gifti time series | `2001` | `"tseries"` | `0` | [] | 593 | | Gifti node index | `2002` | `"elem"` | `0` | [] | 594 | | Gifti RGB (3 values) | `2003` | `"rgb"` | `0` | [] | 595 | | Gifti RGBA (4 values) | `2004` | `"rgba"` | `0` | [] | 596 | | Gifti shape | `2005` | `"shape"` | `0` | [] | 597 | 598 | Additional FSL codes: 599 | 600 | | Intent | NIfTI | JNIfTI | 601 | | ---------------------- | ------ | ------------------------------------------- | 602 | | FSL displacement field | `2006` | `"FSL_FNIRT_DISPLACEMENT_FIELD"` | 603 | | FSL cubic spline | `2007` | `"FSL_CUBIC_SPLINE_COEFFICIENTS"` | 604 | | FSL DCT coefficients | `2008` | `"FSL_DCT_COEFFICIENTS"` | 605 | | FSL quad spline | `2009` | `"FSL_QUADRATIC_SPLINE_COEFFICIENTS"` | 606 | | FSL-TOPUP cubic spline | `2016` | `"FSL_TOPUP_CUBIC_SPLINE_COEFFICIENTS"` | 607 | | FSL-TOPUP quad spline | `2017` | `"FSL_TOPUP_QUADRATIC_SPLINE_COEFFICIENTS"` | 608 | | FSL-TOPUP field | `2018` | `"FSL_TOPUP_FIELD"` | 609 | 610 | ### Table 4.5. Xforms 611 | 612 | | Transform | NIfTI | JNIfTI | 613 | | --------- | ----- | ------------- | 614 | | Unknown | `0` | `"unknown"` | 615 | | Scanner | `1` | `"scanner"` | 616 | | Aligned | `2` | `"aligned"` | 617 | | Talairach | `3` | `"talairach"` | 618 | | MNI | `4` | `"mni"` | 619 | | Template | `5` | `"template"` | 620 | 621 | ### Table 4.6. Slice order 622 | 623 | | Order | NIfTI | JNIfTI | 624 | | ------------------------- | ----- | --------- | 625 | | Unknown | `0` | `""` | 626 | | Sequential increasing | `1` | `"seq+"` | 627 | | Sequential decreasing | `2` | `"seq-"` | 628 | | alternating increasing | `3` | `"alt+"` | 629 | | alternating decreasing | `4` | `"alt-"` | 630 | | alternating increasing #2 | `5` | `"alt2+"` | 631 | | alternating decreasing #2 | `6` | `"alt2-"` | 632 | 633 | ## 5. Reference implementations 634 | 635 | Reference software to convert data between `.nii[.gz]` and `.nii.zarr` 636 | is provided. 637 | 638 | ### 5.1. [Python](https://github.com/neuroscales/nifti-zarr-py) 639 | 640 | ```python 641 | from niizarr import nii2zarr, zarr2nii 642 | 643 | # convert from nii.gz to nii.zarr 644 | nii2zarr('/path/to/mri.nii.gz', '/path/to/mri.nii.zarr') 645 | 646 | # convert from nii.zarr to nii.gz 647 | # The pyramid level can be selected with `level=L`, where 0 is the 648 | # base/finest level. 649 | zarr2nii('/path/to/mri.nii.zarr', '/path/to/mri.nii.gz') 650 | zarr2nii('/path/to/mri.nii.zarr', '/path/to/mri_2x.nii.gz', level=1) 651 | 652 | # Encapsulate a nifti-zarr into a nibabel.Nifti1Image object, whose 653 | # dataobj is a dask array 654 | img = zarr2nii('/path/to/mri.nii.zarr') 655 | ``` 656 | 657 | ### 5.2. [Julia](https://github.com/neuroscales/NIfTIZarr.jl) 658 | 659 | ```julia 660 | import NIfTIZarr: nii2zarr, zarr2nii 661 | 662 | # convert from nii.gz to nii.zarr 663 | nii2zarr("path/to/nifti.nii.gz", "s3://path/to/bucket") 664 | 665 | # convert from nii.zarr to nii.gz 666 | # The pyramid level can be selected with `level=L`, where 0 is the 667 | # base/finest level. 668 | zarr2nii("/path/to/mri.nii.zarr", "/path/to/mri.nii.gz") 669 | zarr2nii("/path/to/mri.nii.zarr", "/path/to/mri_2x.nii.gz"; level=1) 670 | 671 | # Encapsulate a nifti-zarr into a NIVolume object, whose 672 | # raw data is a DiskArray 673 | img = zarr2nii("s3://path/to/bucket") 674 | ``` 675 | 676 | ### 5.3. [NGtools](https://github.com/neuroscales/ngtools) 677 | 678 | The [`ngtools`](https://github.com/neuroscales/ngtools) package allows 679 | NIfTI-Zarr files to be properly oriented and displayed in 680 | [`neuroglancer`](https://github.com/google/neuroglancer). 681 | --------------------------------------------------------------------------------