Nikon ND2
Nikon NIS-Elements saves microscope acquisitions as .nd2 files. OpenReadout returns the images with their channels, Z planes and time points, physical sizes, per-frame records, events and ROIs. Derived from public files by hex dump and a Python chunk walker, cross-checked against the public BSD-3 source of the nd2 Python package (a pure-Python reader). Provenance: docs/provenance/nd2.md.
ND2 (“Nikon NIS-Elements image”) comes in two generations. The modern one (NIS-Elements ≥ 3, Ver2.x/Ver3.x) is a flat sequence of named chunks with a chunk map at the end of the file. The legacy one (NIS-Elements 2.x) is a JPEG 2000 box sequence whose frames are JPEG 2000 codestreams and whose metadata are XML boxes (§ Legacy container). Both are read.
Modern container (all integers little-endian)
Chunk
| offset | size | field | value / meaning |
|---|---|---|---|
| 0 | u32 | chunk_magic |
0x0ABECEDA (bytes DA CE BE 0A) |
| 4 | u32 | name_len |
bytes of the name field (padded; observed 32 and 4128) |
| 8 | u64 | data_len |
payload length |
| 16 | name_len |
name |
ASCII, ends with !, zero padded |
16 + name_len |
data_len |
payload |
Chunks are 4096-byte aligned in the corpus (chunk_alignment), but the reader never assumes alignment. A frame chunk whose declared data_len runs past the end of the file is reported as corrupt by read_plane, even when the pixel bytes it needs happen to be present.
File signature chunk (offset 0)
Name ND2 FILE SIGNATURE CHUNK NAME01!, payload 64 bytes starting with the ASCII version, e.g. Ver3.0.
Chunk map
The last 8 bytes of the file hold chunk_map_offset, the offset of a chunk named ND2 FILEMAP SIGNATURE NAME 0001!. Its payload is a sequence of entries: name (ASCII up to and including !), u64 chunk_offset (offset of the chunk header), u64 chunk_data_len. The list ends with the entry ND2 CHUNK MAP SIGNATURE 0000001! (the 32 bytes before the final offset repeat that name).
When the map is missing or damaged (interrupted acquisition), rescue_scan walks the file for chunk_magic followed by a plausible name and rebuilds the map; check reports that this happened. When uiSequenceCount is 0 (acquisition never finalized) the frame count is the number of ImageDataSeq|N! chunks found.
Chunk names we use
| name | payload | role |
|---|---|---|
ImageAttributesLV! |
LV tree SLxImageAttributes |
geometry: uiWidth, uiHeight, uiWidthBytes (row stride), uiComp (interleaved samples per pixel = channels), uiBpcInMemory, uiBpcSignificant, uiSequenceCount (frames), eCompression, ePixelType, uiVirtualComponents |
ImageMetadataLV! |
LV tree SLxExperiment |
the acquisition loop tree (below); absent for single-frame files |
ImageMetadataSeqLV|0! |
LV tree SLxPictureMetadata |
first frame: dTimeMSec, dTimeAbsolute (Julian day of the experiment start), dXPos, dYPos, dZPos, dCalibration (µm/px), wsObjectiveName, dObjectiveMag, dObjectiveNA, dRefractIndex1 (else dRefractIndex; immersion refractive index → extra.refractive_index, which OME-TIFF export writes as ObjectiveSettings/@RefractiveIndex), sPicturePlanes (channels, § Channels). Only frame 0 carries one in every corpus file |
ImageCalibrationLV|0! |
LV tree SLxCalibration |
dCalibration µm/px, dAspect, sObjective |
ImageTextInfoLV! |
LV tree SLxImageTextInfo |
free text; TextInfoItem_5 holds a Dimensions: T(3) x XY(4) x λ(2) x Z(5) line, TextInfoItem_9 a local-time date string, TextInfoItem_13 the optics, TextInfoItem_14 the application version. When the frame metadata and the calibration chunk name no objective, TextInfoItem_13 is the objective name and the description’s Numerical Aperture: 1.4 line its NA (ome-jonas-control002); the magnification is the number before x in the name (20xC → 20) |
ImageDataSeq|N! |
f64 frame_timestamp_ms then uiHeight rows of uiWidthBytes bytes |
frame N’s pixels, channels interleaved per pixel |
CustomDataVar|CustomDataV2_0! |
LV tree | the custom-data tag table (§ Per-frame records) |
CustomData|<tag>! |
raw array, one value per frame | a custom-data column named by the tag table (X, Y, Z, Z1, PFS_STATUS, Camera_ExposureTime1, …) |
CustomData|AcqTimesCache! |
f64 per frame | acquisition time of each frame, ms since the experiment start |
ImageEventsLV!, ImageEvents!, CustomData|ExperimentEventsV1_0! |
LV / XML RLxExperimentRecord |
experiment events (§ Events) |
CustomData|RoiMetadata_v1! |
LV tree | ROIs (§ ROIs) |
CustomDataVar|AppInfo_V1_0! and other CustomDataVar|* |
LV | decoded into info --view full’s vendor tree |
LV (“lite variant”) encoding
A sequence of items: u8 item_type, u8 name_units (UTF-16 code units including the terminator), UTF-16LE name, then a value:
| type | value |
|---|---|
| 1 | u8 boolean |
| 2 | i32 |
| 3 | u32 |
| 4 | i64 |
| 5 | u64 |
| 6 | f64 |
| 7 | u64 (opaque) |
| 8 | UTF-16LE string, 00 00 terminated |
| 9 | u64 length + bytes. Either opaque bytes (validity masks such as pItemValid, matrices; exposed as numbers up to 4096 bytes) or a nested LV structure, recognized by a plausible item header (type 1–11 and a name of ≥ 1 character, or type 76) and decoded when that succeeds |
| 11 | nested level: u32 child_count, u64 level_len, then child_count items, then child_count × u64 offsets (skipped) |
76 (L) |
compressed block: after a 10-byte header the rest of the buffer is a zlib stream of more items, spliced into the enclosing level |
Repeated names inside a level (e.g. "" under ppNextLevelEx, aN planes) become arrays. Nesting is bounded (64 levels) so malformed input errors out instead of recursing without end.
Experiment loop tree → frame index
SLxExperiment is a loop with eType, uLoopPars (the parameters; uiCount is the declared count), pItemValid (a per-item validity mask), uiRepeatCount and ppNextLevelEx children (one per nested loop). Loop types (numbering from the nd2 package’s public enumeration):
eType |
our name | frames it contributes | notes |
|---|---|---|---|
| 1 | time_loop |
uiCount |
dPeriod ms between frames |
| 2 | xy_position_loop |
number of Points whose pItemValid entry is set |
Points with dPosX/Y/Z, dPosName, dPFSOffset; bRelativeXY adds dReferenceX/Y; older trees store parallel lists dPosX, dPosY, dPosZ. ome-karl-sample-image: uiCount 15, one valid point, 21 frames written |
| 4 | z_stack_loop |
uiCount |
dZStep µm (when 0: |dZHigh − dZLow| / (count − 1)) |
| 6 | spectral_loop |
none | the λ loop: carries pPlanes (the channels, count pPlanes/uiCount). Channels are interleaved inside each frame, so it never indexes frames and does not add a nesting level |
| 7 | custom_loop |
uiCount |
no corpus file; folded into T |
| 8 | ne_time_loop |
sum of uiCount over periods in pPeriod whose pPeriodValid flag is set |
uLoopPars/uiCount counts all periods including invalid ones. Periods keep their own dPeriod; time_periods in extra lists them |
| other | loop_type_N |
uiCount |
folded into T, with a note |
Rules for walking the tree (mirroring the nd2 package, validated on every corpus file): a node without loop parameters, whose type is 0, or whose count is 0 ends its branch; at a nesting level that already holds a loop, a sibling of the same kind replaces it only when it counts more frames, and a sibling of another kind is ignored. uiRepeatCount > 1 appears only on a nested loop and equals the number of times its parent runs it (ome-jonas-control002: a Z loop with repeat 65 under a 65-step NE-time loop); it adds no axis and is reported in extra.loops. Legacy trees may list loops as siblings LoopNo00, LoopNo01, … outermost first.
Frame index = row-major flattening of the counted loops from outermost to innermost. For T(3) × XY(4) × Z(5): frame = t*20 + p*5 + z. Several T-axis loops (NE time plus a custom loop) combine mixed-radix into one T index.
When the loops describe fewer frames than the file holds (ome-aryeh-b16-14-12: 50 described, 51 written; Bio-Formats also reports 50) the extra frames are not addressed. When they describe more (interrupted acquisition) the outermost loop is cut to the frames written (rounded up) and planes past the end report corrupt; both cases add layout_note to notes and a loop_mismatch warning to check.
Pixel types
ePixelType 1 with uiBpcInMemory 8/16/32 → uint8/uint16/uint32; ePixelType 2 with 32 → float (inferred from prior art, no float corpus file yet). Planes whose uiCompCount is 3 are RGB (samples_per_pixel 3); uiComp is the sum of the planes’ component counts (zenodo8161776-VPA002: four RGB planes, uiComp 12).
RGB sample order. A modern file stores the three samples of a colour-camera plane as B, G, R (validated: in zenodo8161776-VPA002 the DAPI plane lights sample 0 and the Texas Red plane sample 2; the nd2 package’s pseudo-wavelengths 420/515/590 nm per component agree). Planes are returned R, G, B, so export --to ome-tiff writes true photometric RGB; info says so per RGB image with extra.sample_order (RGB) and extra.stored_sample_order (BGR). The nd2 package returns stored order, so the corpus oracle reverses its S axis for modern RGB files. Legacy (JPEG 2000) planes keep the codestream order, which each jp2h colr box declares as sRGB (stored_sample_order RGB; inferred, no legacy file with a reference). Four-component planes are not handled as RGB.
eCompression: 2 = uncompressed; 0 = lossless, where the block after the timestamp is a zlib stream inflating to uiHeight × uiWidthBytes (confirmed on ome-jonas-nd2Test-Exception-2); 1 = lossy. No public lossy sample exists (checked: every modern corpus file, the four ND2 files of Zenodo records 21162526 and 8161776, and the ND2 of Zenodo 5277605 read over HTTP ranges, all eCompression 2; the nd2 package’s published sample metadata lists no lossy file and its reader has no lossy decoder). The codec is therefore unknown and lossy frames report Unsupported (exit 6) with metadata still readable.
Version 2 metadata (XML variants)
Version 2.x files carry the same metadata as XML documents in chunks named without LV (ImageAttributes!, ImageMetadata!, ImageMetadataSeq|N!, ImageCalibration|N!, ImageTextInfo!): <variant version="1.0"><no_name runtype="CLxListVariant"><uiWidth runtype="lx_uint32" value="696"/>…. Scalar runtypes: lx_uint32, lx_int32, lx_uint64, lx_int64, double, bool, CLxStringW, CLxByteArray (base64). Lists are elements with children (_00, _01, … or named; repeated element names become arrays). variant_decode produces the same JSON shape as lv_decode; the wrapper (SLx… or no_name) is unwrapped by unwrap_root. Version 2 picture planes live under sPicturePlanes/sPlane and name channels by sDescription or sOpticalConfigName.
Channels (sPicturePlanes)
sPicturePlanes/sPlaneNew/aN (or sPlane/aN), in index order, one per channel:
| field | our field | notes |
|---|---|---|
sDescription (else sOpticalConfigName) |
channels[].name |
|
uiCompCount |
samples per pixel | 3 = RGB |
uiColor (else pFluorescentProbe/m_uiColor) |
channels[].color |
red in the lowest byte: 65280 → #00FF00, 255 → #FF0000 (byte order checked against the nd2 package’s colours on every corpus file) |
uiModalityMask (else eModality mapped to a mask) |
extra.channel_settings[].modality, channels[].acquisition_mode |
bits below |
pFluorescentProbe/m_sName |
channels[].fluorophore |
when not empty |
pFilterPath/m_pFilter/*/m_sName |
extra.channel_settings[].filters |
|
probe then first filter m_ExcitationSpectrum / m_EmissionSpectrum |
channels[].excitation_nm / emission_nm |
a probe spectrum gives the wavelength of its point with the largest dTValue (first on ties); a filter spectrum given by a rising and a falling edge (eType 2 and 3) gives the centre of the band (2026-09-26: the first-on-ties rule picked the rising edge, e.g. 500 nm for a 500–550 nm filter), other filter spectra their peak; for excitation, a filter spectrum of several eType 4 points gives the point at the plane’s index. Points carry dWavelength or, in older files, integral uiWavelength (the nd2 package ignores the latter). RGB planes get none |
emission filter points of eType 2 and 3 (rising and falling edge) |
channels[].emission_range_nm |
|
sSampleSetting/aK (K = uiSampleIndex, or the plane index when 0; the only entry when there is one, shared by every plane, as on the Nikon A1plus files of Zenodo 21162526) |
extra.channel_settings[] |
pCameraSetting/{CameraUserName, CameraUniqueName} → detector, dExposureTime (else PropertiesQuality/Exposure) → exposure_ms, PropertiesQuality/GainMultiplier → em_gain, FormatQuality/fmtDesc/dBinningX × dBinningY → binning, sSpecSettings key: value lines → camera_settings; pObjectiveSetting fills the objective when frame 0 has none |
older files: per-plane sCameraSetting |
same | sCameraName, dExposure, dGain → gain, dCamBinningX/Y, sSpecSettings |
Modality mask bits (values from the nd2 package’s public enumeration; our names): 0x1 fluorescence, 0x2 brightfield, 0x10 phase_contrast, 0x20 dic, 0x40 rcm, 0x80 vcs, 0x100 camera, 0x200 laser_scanning_confocal, 0x400 spinning_disk_confocal, 0x800 swept_field_confocal_slit, 0x1000 swept_field_confocal_pinhole, 0x2000 dsd_confocal, 0x4000 sim, 0x8000 isim, 0x10000 multiphoton, 0x20000 tirf, 0x40000 live_sr, 0x100000 pmt, 0x200000 spectral, 0x400000 vaas_if, 0x800000 vaas_nf, 0x1000000 transmitted_light_detector, 0x2000000 non_descanned_detector, 0x4000000 virtual_filter, 0x8000000 gaasp, 0x10000000 remainder, 0x20000000 aux, 0x40000000 sora. A mask with neither of the two light bits reads as brightfield for RGB planes and fluorescence otherwise. eModality 0–12 map to fluorescence+camera, brightfield+camera, fluorescence+laser_scanning_confocal, fluorescence+spinning_disk_confocal, fluorescence+swept_field_confocal_slit, fluorescence+multiphoton+laser_scanning_confocal, brightfield+phase_contrast, brightfield+dic, fluorescence+spectral+laser_scanning_confocal, fluorescence+vaas_nf+laser_scanning_confocal, fluorescence+vaas_if+laser_scanning_confocal, fluorescence+vaas_nf+laser_scanning_confocal, dsd_confocal.
acquisition_mode is a readable summary: Brightfield / Phase Contrast / DIC, or Widefield, Spinning Disk Confocal, Laser Scanning Confocal, Swept Field Confocal, Multiphoton or TIRF Fluorescence.
Time
dTimeAbsolute of frame 0 is the Julian day number (UTC) of the experiment start: acquired_at = ISO-8601 of (jdn − 2440587.5) × 86 400 000 ms since the Unix epoch, rounded to the millisecond. Values outside 1900–2100 (uninitialized clocks store small numbers, e.g. ome-jonas-control002 397.7) give no acquired_at. extra.acquired_at_source is then julian_day_utc. Only when the Julian day is missing or implausible is the local-time text TextInfoItem_9 normalized instead (text_datetime_to_iso8601, source text_local_time): it is ISO-8601 without an offset, because the text is the acquisition PC’s wall-clock time in its own locale and the file records neither the zone nor the locale. Its date order varies with that locale (9/28/2021 9:34:47 AM is month-first, 06/03/2009 10:58:30 AM in aics-ND2-jonas-header-test2 is day-first — 6 March per its Julian day, 3-7-2026 13:32:25 is day-first), so a date whose day and month are both ≤ 12 and differ yields no acquired_at rather than a guess (ome-jonas-control002: 11/3/2009). The text itself is always kept as extra.acquired_at_text (ome-karl-sample-image: text 06/06/2017 11:15:06, JDN → 2017-06-06T09:15:06.980Z, the site being UTC+2). A frame’s absolute time is jdn + time_ms / 86 400 000.
When acquired_at comes from the text (extra.acquired_at_source = text_local_time), info adds a note that the value is local clock time with no recorded time zone (book/src/guides/metadata.md § Timestamps).
Per-frame records
info --view full embeds one record per frame under images[i].extra.frames (the first 100 per image; --all-frames for all), with frame_records_total and frames_truncated. A record covers all channels of a frame (they are interleaved in one frame and share one timestamp).
| field | source |
|---|---|
frame |
sequence index (ImageDataSeq|N!) |
t, z |
the frame’s T and Z index within the image |
period |
index of the NE-time period the frame falls in (only with more than one period) |
time_ms |
CustomData|AcqTimesCache! (legacy: VIMD dTimeMSec) |
acquired_at |
time_ms added to the start Julian day |
stage_x_um, stage_y_um |
custom-data columns X, Y; else the XY-loop point (legacy: dXPos, dYPos) |
stage_z_um |
column Z, else the lowest-numbered Z<n> column (a Z drive; ome-karl-sample-image has only Z2, “Ti ZDrive”); legacy dZPos |
pfs_status, pfs_offset |
columns PFS_STATUS, PFS_OFFSET |
exposure_ms, camera_temperature_c |
columns Camera_ExposureTime1, CameraTemp1 |
exposure_ms_per_channel |
legacy: each plane’s sCameraSetting/dExposure |
tags |
every other custom-data column by tag id (Z1, Camera_ExposureTime2, 1-AO0-Op, …) |
The tag table CustomTagDescription_v1.0/Tag<n> has ID (the chunk suffix), Type (value_kind: 3 = f64, 2 = i32, 1 = UTF-16 strings in equal slots), Size (values), Desc and Unit.
Events
RLxExperimentRecord: compact records pEvents/*/{T time ms, T2 second clock, M meaning, D description, A data, I id, S stimulation}, or (older and legacy) pFirstEvent/no_name[]/{dTime, eMeaning, wsDescription, wsData}. Normalized to extra.events[] = {time_ms, meaning, meaning_code, description, data} (at most 256 in info; event_count gives the total). Meaning codes 0–54 (from the nd2 package’s public enumeration) get our names: unspecified, autofocus, user_1_old … user_4_old, jobs, command, macro, pause, resume, cancel, ram_grab_zero_time, time_loop_next_phase, refocus, stimulation, external_stimulation, experiment_start, experiment_end, phase_start, phase_end, before_xy_move, after_xy_move, before_z_series, after_z_series, before_lambda_loop, after_lambda_loop, before_large_image, after_large_image, before_stimulation, after_stimulation, user_events, stream_data, user_1 … user_8, before_capture, after_capture, real_time_ttl_data, no_acquisition_start, no_acquisition_end, hardware_error, storm_event, incubation_info, incubation_error, interactive_experiment_end, experiment_pause, wid_replenishment_start, wid_replenishment_end, nstorm. Legacy IEVE boxes hold the older form (aics-ND2-aryeh-but3-cont200-1: command events such as Wait(2);).
ROIs
CustomData|RoiMetadata_v1! → RoiMetadata_v1 with keys carrying a lowercase type prefix that is dropped (m_vect2PerMPoint_Size → 2PerMPoint_Size): global ROIs Global_Size, Global_<i>; per-position ROIs 2PerMPoint_Size, 2PerMPoint_<p>/{Size, "<i>"}. Each ROI: Id, Info/{ShapeType, InterpType, Scope, Label, Color, …}, AnimParams_Size, AnimParams_<k>/{TimeMs, CenterX, CenterY, CenterZ, RotationZ, BoxShape/{SizeX, SizeY, SizeZ}, ExtrudedShape/{SizeZ, BasePoints_Size, BasePoints_<j>}}. Normalized to extra.rois[] = {id, label, shape, role, scope, position_index, color, keyframes[{time_ms, center, rotation_z, box_size, extrusion_z, base_points}]} with shape names any, raster, point, rectangle, ellipse, polygon, bezier, line, polyline, circle, square, ring, spiral and roles any, standard, background, reference, stimulation. The layout was taken from the nd2 package; zenodo14231228-Sla2-WT-18-roi (2026-09-26, found by a remote survey of 452 public Zenodo ND2 files, most of which hold empty ROI trees) has one global background rectangle whose id, shape, role, scope, colour, centre (0.2326, −0.6022) and box (0.1316 × 0.1123) equal the nd2 package’s. The geometry is reported as stored: those values are not pixels or micrometres of the 1000 × 1000 image (0.065 µm pixels), and their unit and origin are not established, so no conversion is made.
Legacy container (NIS-Elements 2.x)
JPEG 2000 file format boxes (u32 big-endian length, four-character type, payload; length 1 = u64 extended length, 0 = to end of file). It starts with the JPEG 2000 signature box (00 00 00 0C 6A 50 20 20 0D 0A 87 0A), then ftyp, jp2h (whose ihdr gives height, width, components and bits per component − 1), then one jp2c box per picture plane per frame, frame-major, then xml metadata boxes and a uuid box.
The last 40 bytes are the ASCII signature LABORATORY IMAGING ND BOX MAP 00 (BOX_MAP_SIGNATURE) and a u64 little-endian distance from the end of the file to the box map: u32 big-endian entry count, then 16-byte entries box_type (4 bytes), tag (4 bytes), u64 little-endian box_offset. Frame codestreams are jp2c/LUNK (the signature, ftyp and jp2h boxes are also tagged LUNK). XML boxes by tag and root element:
| tag | root element | content |
|---|---|---|
ARTT |
AdvancedImageAttributes |
SignificantBits, VirtualComponents |
ACAL |
Calibration |
attributes Calibration_11 … (µm/px) |
VCAL |
CalibrationSeq (one per frame) |
dCalibration, dAspect, sObjective |
VIMD |
MetadataSeq (one per frame) |
the per-frame picture metadata of modern files (dTimeMSec, dTimeAbsolute, dXPos, sPicturePlanes/sPlane/aN, …) |
AIM1 |
Metadata_V1.2 |
the experiment tree under vMetadata |
AIMD |
Metadata |
the experiment tree directly, or as LoopNo00, LoopNo01, … |
TINF |
TextInfo |
TextInfoItem elements with Text and Index attributes |
IEVE |
Events |
events (older form) |
SROB |
ReportObjects |
not used |
Variant-wrapped boxes (<X><variant><no_name>…) decode with the version-2 variant rules (legacy_xml_decode); attribute-style boxes decode to their attributes as strings. The frame count is the number of VCAL boxes; frame f, channel c is codestream f × planes + c, where a 3-component codestream holds an RGB plane. Codestreams are raw J2K (FF 4F FF 51); the SIZ marker’s size is checked against ihdr before decoding, and decoding uses the pure-Rust dicom-toolkit-jpeg2000 crate at native bit depth (bit-exact against imagecodecs/OpenJPEG on all five corpus files). When the box map is missing the reader walks the boxes from the start and tags XML boxes by root element; check reports the missing map as truncated.
Vocabulary (every public identifier in openreadout-nd2 must appear here)
| identifier | meaning |
|---|---|
Nd2Reader, Nd2File, FORMAT_ID, CHUNK_MAGIC, CHUNK_MAGIC_BYTES, FILE_SIGNATURE, CHUNK_MAP_SIGNATURE, FILE_MAP_NAME, LEGACY_SIGNATURE |
entry points and constants |
ChunkHeader, chunk_magic, name_len, data_len, name, payload_offset |
chunk header |
ChunkMapEntry, chunk_offset, chunk_data_len, chunk_map_offset, rescue_scan, rescued |
chunk map |
LvValue, lv_decode, item_type, name_units, child_count, level_len |
LV decoder |
Attributes, width, height, row_bytes, components, bits_in_memory, bits_significant, frame_count, compression, pixel_kind, virtual_components |
SLxImageAttributes |
LoopKind { TimeLoop, XyPositionLoop, ZStackLoop, SpectralLoop, CustomLoop, NeTimeLoop, Other }, Loop, kind, count, declared_count, level, invalid_items, repeat_count, z_step_um, period_ms, periods, positions, axis, name |
experiment tree |
Position, pos_x, pos_y, pos_z, pos_name, pfs_offset |
XY-loop points |
Period, start_ms, duration_ms |
valid NE-time periods |
FrameLayout, t_count, p_count, z_count, frame_index, frame_timestamp_ms, loop_indices, coords, frames, layout_note |
frame addressing |
PlaneDesc, description, component_count, sample_index, color_abgr, modality_mask, modality, fluorophore, filters, emission_nm, excitation_nm, emission_band_nm, camera |
channel planes |
CameraSetting, detector, exposure_ms, binning, gain, em_gain, settings_text |
per-channel detector settings |
MODALITY_BITS, modality_flags, acquisition_mode, color_hex |
channel normalization |
FrameMeta, time_ms, start_jdn, stage_x, stage_y, stage_z, calibration_um, objective_name, objective_mag, objective_na, refractive_index |
first-frame metadata |
jdn_to_iso8601, text_datetime_to_iso8601 |
Julian day → ISO-8601 UTC; unambiguous local-time text → ISO-8601 without offset |
CustomTag, tag_id, value_kind, size, unit, custom_tags_from_lv, tag_values, f64_values, record_field, stage_z_tag, base_record, legacy_record_fields |
per-frame records |
events_from_lv, rois_from_lv |
events and ROIs |
frame_row, deinterleave, frame_payload |
plane assembly |
variant_decode, legacy_xml_decode, unwrap_root, base64_decode, runtype |
XML variants |
LegacyFile, LegacyBox, BOX_MAP_SIGNATURE, box_type, tag, box_offset, boxes, box_map_offset, tagged, codestreams, read_box, xml, image_header, codestream_size |
legacy container |
Nd2Dataset, open, path, file_len, problems, read_chunk |
opened-file state and chunk access |
from_lv, from_raw, from_loops, loops_from_lv, planes_from_lv, order |
constructors that normalize decoded LV/variant trees; order is the counted-loop nesting (axis, count) list |
emission_nm, excitation_nm |
plane wavelengths when the picture-plane record carries them |
fuzz_chunk_map, fuzz_normalize |
byte-slice entry points for the cargo-fuzz targets (fuzzing feature only; not a stable API) |
Performance / robustness merge (2026-09-23)
Frame and output geometry remain file-relative and size-checked; contiguous full-channel planes reuse the frame buffer and other layouts copy by row/chunk. The common plane guard is capped at 4 GiB.
How this reader was derived, file by file: provenance log.