Package: nd2
nd2.nd2(Nikon NIS Elements) file reader.This reader provides a pure python implementation of the Nikon ND2 SDK.Itusedto wrap the official SDK with Cython, but has since been completely
rewritten to be pure python (for performance, ease of distribution, and
maintenance) while retaining complete API parity with the official SDK.Note:This library is not affiliated with Nikon in any way, but we are
grateful for assistance from the SDK developers at Laboratory Imaging.Features good metadata retrieval, and directto_daskandto_xarrayoptions
for lazy and/or annotated arrays.This library is tested against many nd2 files with the goal of maximizing
compatibility and data extraction. (If you find an nd2 file that fails in some
way, pleaseopen an issuewith
the file!):book:Documentationinstallpipinstallnd2or from conda:condainstall-cconda-forgend2Legacy nd2 file supportLegacy nd2 (JPEG2000) files are also supported, but requireimagecodecs. To
install with support for these files use thelegacyextra:pipinstallnd2[legacy]Faster XML parsingMuch of the metadata in the file stored as XML. If found in the environment,nd2will uselxmlwhich is much faster
than the built-inxmlmodule. To install with support forlxmluse:pipinstallnd2lxmlUsage and APIFull API documentation is available athttps://tlambert03.github.io/nd2Quick summary below:importnd2importnumpyasnpmy_array=nd2.imread('some_file.nd2')# read to numpy arraymy_array=nd2.imread('some_file.nd2',dask=True)# read to dask arraymy_array=nd2.imread('some_file.nd2',xarray=True)# read to xarraymy_array=nd2.imread('some_file.nd2',xarray=True,dask=True)# read to dask-xarray# or open a file with nd2.ND2Filef=nd2.ND2File('some_file.nd2')# (you can also use nd2.ND2File() as a context manager)withnd2.ND2File('some_file.nd2')asndfile:print(ndfile.metadata)...# ATTRIBUTES:   # example outputf.path# 'some_file.nd2'f.shape# (10, 2, 256, 256)f.ndim# 4f.dtype# np.dtype('uint16')f.size# 1310720  (total voxel elements)f.sizes# {'T': 10, 'C': 2, 'Y': 256, 'X': 256}f.is_rgb# False (whether the file is rgb)# if the file is RGB, `f.sizes` will have# an additional {'S': 3} component# ARRAY OUTPUTSf.asarray()# in-memory np.ndarray - or use np.asarray(f)f.to_dask()# delayed dask.array.Arrayf.to_xarray()# in-memory xarray.DataArray, with labeled axes/coordsf.to_xarray(delayed=True)# delayed xarray.DataArray# see below for examples of these structures# METADATA          # returns instance of ...f.attributes# nd2.structures.Attributesf.metadata# nd2.structures.Metadataf.frame_metadata(0)# nd2.structures.FrameMetadata (frame-specific meta)f.experiment# List[nd2.structures.ExpLoop]f.text_info# dict of misc infof.voxel_size()# VoxelSize(x=0.65, y=0.65, z=1.0)f.rois# Dict[int, nd2.structures.ROI]f.binary_data# any binary masks stored in the file.  See below.f.events()# returns tabular "Recorded Data" view from in NIS Elements/Viewer# with info for each frame in the experiment.# output is passabled to pandas.DataFramef.ome_metadata()# returns metadata as an ome_types.OME object# (requires ome-types package)# allll the metadata we can find...# no attempt made to standardize or parse it# look in here if you're searching for metadata that isn't exposed in the above# but try not to rely on it, as it's not guaranteed to be stablef.unstructured_metadata()f.close()# don't forget to close when not using a context manager!f.closed# boolean, whether the file is closedMetadata structuresThese follow the structure of the nikon SDK outputs (where relevant).
Here are some example outputsattributesAttributes(bitsPerComponentInMemory=16,bitsPerComponentSignificant=16,componentCount=2,heightPx=32,pixelDataType='unsigned',sequenceCount=60,widthBytes=128,widthPx=32,compressionLevel=None,compressionType=None,tileHeightPx=None,tileWidthPx=None,channelCount=2)metadataNote: themetadatafor legacy (JPEG2000) files will be a plain unstructured dict.Metadata(contents=Contents(channelCount=2,frameCount=60),channels=[Channel(channel=ChannelMeta(name='Widefield Green',index=0,colorRGB=65371,emissionLambdaNm=535.0,excitationLambdaNm=None),loops=LoopIndices(NETimeLoop=None,TimeLoop=0,XYPosLoop=1,ZStackLoop=2),microscope=Microscope(objectiveMagnification=10.0,objectiveName='Plan Fluor 10x Ph1 DLL',objectiveNumericalAperture=0.3,zoomMagnification=1.0,immersionRefractiveIndex=1.0,projectiveMagnification=None,pinholeDiameterUm=None,modalityFlags=['fluorescence']),volume=Volume(axesCalibrated=[True,True,True],axesCalibration=[0.652452890023035,0.652452890023035,1.0],axesInterpretation=(<AxisInterpretation.distance:'distance'>,<AxisInterpretation.distance:'distance'>,<AxisInterpretation.distance:'distance'>),bitsPerComponentInMemory=16,bitsPerComponentSignificant=16,cameraTransformationMatrix=[-0.9998932296054086,-0.014612644841559427,0.014612644841559427,-0.9998932296054086],componentCount=1,componentDataType='unsigned',voxelCount=[32,32,5],componentMaxima=[0.0],componentMinima=[0.0],pixelToStageTransformationMatrix=None)),Channel(channel=ChannelMeta(name='Widefield Red',index=1,colorRGB=22015,emissionLambdaNm=620.0,excitationLambdaNm=None),loops=LoopIndices(NETimeLoop=None,TimeLoop=0,XYPosLoop=1,ZStackLoop=2),microscope=Microscope(objectiveMagnification=10.0,objectiveName='Plan Fluor 10x Ph1 DLL',objectiveNumericalAperture=0.3,zoomMagnification=1.0,immersionRefractiveIndex=1.0,projectiveMagnification=None,pinholeDiameterUm=None,modalityFlags=['fluorescence']),volume=Volume(axesCalibrated=[True,True,True],axesCalibration=[0.652452890023035,0.652452890023035,1.0],axesInterpretation=(<AxisInterpretation.distance:'distance'>,<AxisInterpretation.distance:'distance'>,<AxisInterpretation.distance:'distance'>),bitsPerComponentInMemory=16,bitsPerComponentSignificant=16,cameraTransformationMatrix=[-0.9998932296054086,-0.014612644841559427,0.014612644841559427,-0.9998932296054086],componentCount=1,componentDataType='unsigned',voxelCount=[32,32,5],componentMaxima=[0.0],componentMinima=[0.0],pixelToStageTransformationMatrix=None))])experiment[TimeLoop(count=3,nestingLevel=0,parameters=TimeLoopParams(startMs=0.0,periodMs=1.0,durationMs=0.0,periodDiff=PeriodDiff(avg=16278.339965820312,max=16411.849853515625,min=16144.830078125)),type='TimeLoop'),XYPosLoop(count=4,nestingLevel=1,parameters=XYPosLoopParams(isSettingZ=True,points=[Position(stagePositionUm=[26950.2,-1801.6000000000001,498.46000000000004],pfsOffset=None,name=None),Position(stagePositionUm=[31452.2,-1801.6000000000001,670.7],pfsOffset=None,name=None),Position(stagePositionUm=[35234.3,2116.4,664.08],pfsOffset=None,name=None),Position(stagePositionUm=[40642.9,-3585.1000000000004,555.12],pfsOffset=None,name=None)]),type='XYPosLoop'),ZStackLoop(count=5,nestingLevel=2,parameters=ZStackLoopParams(homeIndex=2,stepUm=1.0,bottomToTop=True,deviceName='Ti2 ZDrive'),type='ZStackLoop')]roisROIs found in the metadata are available atND2File.rois, which is adictofnd2.structures.ROIobjects, keyed by the ROI ID:{1:ROI(id=1,info=RoiInfo(shapeType=<RoiShapeType.Rectangle:3>,interpType=<InterpType.StimulationROI:4>,cookie=1,color=255,label='',stimulationGroup=0,scope=1,appData=0,multiFrame=False,locked=False,compCount=2,bpc=16,autodetected=False,gradientStimulation=False,gradientStimulationBitDepth=0,gradientStimulationLo=0.0,gradientStimulationHi=0.0),guid='{87190352-9B32-46E4-8297-C46621C1E1EF}',animParams=[AnimParam(timeMs=0.0,enabled=1,centerX=-0.4228425369685782,centerY=-0.5194951478743071,centerZ=0.0,rotationZ=0.0,boxShape=BoxShape(sizeX=0.21256931608133062,sizeY=0.21441774491682075,sizeZ=0.0),extrudedShape=ExtrudedShape(sizeZ=0,basePoints=[]))]),...}text_info{'capturing':'Flash4.0, SN:101412\r\nSample 1:\r\nExposure: 100 ms\r\nBinning: 1x1\r\nScan Mode: Fast\r\nSample 2:\r\nExposure: 100 ms\r\nBinning: 1x1\r\nScan Mode: Fast','date':'9/28/2021  9:41:27 AM','description':'Metadata:\r\nDimensions: T(3) x XY(4) x λ(2) x Z(5)\r\nCamera Name: Flash4.0, SN:101412\r\nNumerical Aperture: 0.3\r\nRefractive Index: 1\r\nNumber of Picture Planes: 2\r\nPlane #1:\r\nName: Widefield Green\r\nComponent Count: 1\r\nModality: Widefield Fluorescence\r\nCamera Settings:   Exposure: 100 ms\r\nBinning: 1x1\r\nScan Mode: Fast\r\nMicroscope Settings:   Nikon Ti2, FilterChanger(Turret-Lo): 3 (FITC)\r\nNikon Ti2, Shutter(FL-Lo): Open\r\nNikon Ti2, Shutter(DIA LED): Closed\r\nNikon Ti2, Illuminator(DIA): Off\r\nNikon Ti2, Illuminator(DIA) Iris intensity: 3.0\r\nAnalyzer Slider: Extracted\r\nAnalyzer Cube: Extracted\r\nCondenser: 1 (Shutter)\r\nPFS, state: On\r\nPFS, offset: 7959\r\nPFS, mirror: Inserted\r\nPFS, Dish Type: Glass\r\nZoom: 1.00x\r\nSola, Shutter(Sola): Active\r\nSola, Illuminator(Sola) Voltage: 100.0\r\nPlane #2:\r\nName: Widefield Red\r\nComponent Count: 1\r\nModality: Widefield Fluorescence\r\nCamera Settings:   Exposure: 100 ms\r\nBinning: 1x1\r\nScan Mode: Fast\r\nMicroscope Settings:   Nikon Ti2, FilterChanger(Turret-Lo): 4 (TRITC)\r\nNikon Ti2, Shutter(FL-Lo): Open\r\nNikon Ti2, Shutter(DIA LED): Closed\r\nNikon Ti2, Illuminator(DIA): Off\r\nNikon Ti2, Illuminator(DIA) Iris intensity: 1.5\r\nAnalyzer Slider: Extracted\r\nAnalyzer Cube: Extracted\r\nCondenser: 1 (Shutter)\r\nPFS, state: On\r\nPFS, offset: 7959\r\nPFS, mirror: Inserted\r\nPFS, Dish Type: Glass\r\nZoom: 1.00x\r\nSola, Shutter(Sola): Active\r\nSola, Illuminator(Sola) Voltage: 100.0\r\nTime Loop: 3\r\n- Equidistant (Period 1 ms)\r\nZ Stack Loop: 5\r\n- Step: 1 µm\r\n- Device: Ti2 ZDrive','optics':'Plan Fluor 10x Ph1 DLL'}binary_dataThis property returns annd2.BinaryLayersobject representing all of the
binary masks in the nd2 file.And2.BinaryLayersobject is a sequence of individualnd2.BinaryLayerobjects (one for each binary layer found in the file). EachBinaryLayerin
the sequence is a named tuple that has, among other things, anameattribute,
and adataattribute that is list of numpy arrays (one for each frame in the
experiment) orNoneif the binary layer had no data in that frame.The most common use case will be to cast either the entireBinaryLayersobject
or an individualBinaryLayerto anumpy.ndarray:>>>importnd2>>>nd2file=nd2.ND2File('path/to/file.nd2')>>>binary_layers=nd2file.binary_data# The output array will have shape# (n_binary_layers, *coord_shape, *frame_shape).>>>np.asarray(binary_layers)For example, if the data in the nd2 file has shape(nT, nZ, nC, nY, nX), and
there are 4 binary layers, then the output ofnp.asarray(nd2file.binary_data)will
have shape(4, nT, nZ, nY, nX). (Note that thenCdimension is not present
in the output array, and the binary layers are always in the first axis).You can also cast an individualBinaryLayerto a numpy array:>>>binary_layer=binary_layers[0]>>>np.asarray(binary_layer)events()This property returns the tabular data reported in theImage Properties > Recorded Datatab of the NIS Viewer.(There will be a column for each tag in theCustomDataV2_0section ofcustom_dataabove, as well as any additional events found in the metadata)The format of the return type data is controlled by theorientargument:'records': list of dicts -[{column -> value}, ...](default)'dict': dict of dicts -{column -> {index -> value}, ...}'list': dict of lists -{column -> [value, ...]}Not every column header appears in every event, so whenorientis either'dict'or'list',float('nan')will be inserted to maintain a consistent
length for each column.# with `orient='records'` (DEFAULT)[{'Time [s]':1.32686654,'Z-Series':-2.0,'Exposure Time [ms]':100.0,'PFS Offset':0,'PFS Status':0,'X Coord [µm]':31452.2,'Y Coord [µm]':-1801.6,'Z Coord [µm]':552.74,'Ti2 ZDrive [µm]':552.74},{'Time [s]':1.69089657,'Z-Series':-1.0,'Exposure Time [ms]':100.0,'PFS Offset':0,'PFS Status':0,'X Coord [µm]':31452.2,'Y Coord [µm]':-1801.6,'Z Coord [µm]':553.74,'Ti2 ZDrive [µm]':553.74},{'Time [s]':2.04194662,'Z-Series':0.0,'Exposure Time [ms]':100.0,'PFS Offset':0,'PFS Status':0,'X Coord [µm]':31452.2,'Y Coord [µm]':-1801.6,'Z Coord [µm]':554.74,'Ti2 ZDrive [µm]':554.74},{'Time [s]':2.38194662,'Z-Series':1.0,'Exposure Time [ms]':100.0,'PFS Offset':0,'PFS Status':0,'X Coord [µm]':31452.2,'Y Coord [µm]':-1801.6,'Z Coord [µm]':555.74,'Ti2 ZDrive [µm]':555.74},{'Time [s]':2.63795663,'Z-Series':2.0,'Exposure Time [ms]':100.0,'PFS Offset':0,'PFS Status':0,'X Coord [µm]':31452.2,'Y Coord [µm]':-1801.6,'Z Coord [µm]':556.74,'Ti2 ZDrive [µm]':556.74}]# with `orient='list'`{'Time [s]':array([1.32686654,1.69089657,2.04194662,2.38194662,2.63795663]),'Z-Series':array([-2.,-1.,0.,1.,2.]),'Exposure Time [ms]':array([100.,100.,100.,100.,100.]),'PFS Offset':array([0,0,0,0,0],dtype=int32),'PFS Status':array([0,0,0,0,0],dtype=int32),'X Coord [µm]':array([31452.2,31452.2,31452.2,31452.2,31452.2]),'Y Coord [µm]':array([-1801.6,-1801.6,-1801.6,-1801.6,-1801.6]),'Z Coord [µm]':array([552.74,553.74,554.74,555.74,556.74]),'Ti2 ZDrive [µm]':array([552.74,553.74,554.74,555.74,556.74])}# with `orient='dict'`{'Time [s]':{0:1.32686654,1:1.69089657,2:2.04194662,3:2.38194662,4:2.63795663},'Z-Series':{0:-2.0,1:-1.0,2:0.0,3:1.0,4:2.0},'Exposure Time [ms]':{0:100.0,1:100.0,2:100.0,3:100.0,4:100.0},'PFS Offset []':{0:0,1:0,2:0,3:0,4:0},'PFS Status []':{0:0,1:0,2:0,3:0,4:0},'X Coord [µm]':{0:31452.2,1:31452.2,2:31452.2,3:31452.2,4:31452.2},'Y Coord [µm]':{0:-1801.6,1:-1801.6,2:-1801.6,3:-1801.6,4:-1801.6},'Z Coord [µm]':{0:552.74,1:553.74,2:554.74,3:555.74,4:556.74},'Ti2 ZDrive [µm]':{0:552.74,1:553.74,2:554.74,3:555.74,4:556.74}}You can pass the output ofevents()topandas.DataFrame:In[1]:pd.DataFrame(nd2file.events())Out[1]:Time[s]Z-SeriesExposureTime[ms]PFSOffsetPFSStatus[]XCoord[µm]YCoord[µm]ZCoord[µm]Ti2ZDrive[µm]01.326867-2.0100.00031452.2-1801.6552.74552.7411.690897-1.0100.00031452.2-1801.6553.74553.7422.0419470.0100.00031452.2-1801.6554.74554.7432.3819471.0100.00031452.2-1801.6555.74555.7442.6379572.0100.00031452.2-1801.6556.74556.7458.702229-2.0100.00031452.2-1801.6552.70552.7069.036269-1.0100.00031452.2-1801.6553.70553.7079.3303190.0100.00031452.2-1801.6554.68554.6889.6393491.0100.00031452.2-1801.6555.70555.7099.9063692.0100.00031452.2-1801.6556.64556.641011.481439-2.0100.00031452.2-1801.6552.68552.681111.796479-1.0100.00031452.2-1801.6553.68553.681212.0894790.0100.00031452.2-1801.6554.68554.681312.3715391.0100.00031452.2-1801.6555.68555.681412.6654692.0100.00031452.2-1801.6556.68556.68ome_metadata()See theome-types documentationfor details on
theOMEtype returned by this method.In[1]:ome=nd2file.ome_metadata()In[2]:print(ome)OME(instruments=[<1Instrument>],images=[<1Image>],creator='nd2 v0.7.1')In[3]:print(ome.to_xml())<OMExmlns="http://www.openmicroscopy.org/Schemas/OME/2016-06"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.openmicroscopy.org/Schemas/OME/2016-06 http://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd"Creator="nd2 v0.7.1.dev2+g4ea166e.d20230709"><InstrumentID="Instrument:0"><DetectorModel="Hamamatsu Dual C14440-20UP"SerialNumber="Hamamatsu Dual C14440-20UP"ID="Detector:0"/></Instrument><ImageID="Image:0"Name="test39"><AcquisitionDate>2023-07-08T09:30:55</AcquisitionDate>...Contributing / DevelopmentTo test locally and contribute. Clone this repo, then:pip install -e .[dev]To download sample data:pip install requests
python scripts/download_samples.pythen run tests:pytest(and feel free to open an issue if that doesn't work!)alternativesHere are some other nd2 readers that I know of, though many
of them are unmaintained:pims_nd2-pims-based reader.
ctypes wrapper around the v9.00 (2015) SDKnd2reader-pims-based reader, using
reverse-engineered file headers. mostly tested on files from NIS Elements
4.30.02nd2file-another pure-python, chunk map
reader, unmaintained?pyND2SDK-windows-only cython wrapper
around the v9.00 (2015) SDK. not on PyPIThe motivating factors for this library were:support for as many nd2 files as possible, with a large test suite
an and emphasis on correctnesspims-independent delayed reader based on daskaxis-associated metadata via xarray