from__future__importannotationsimportreimportjsonimportosimportstructfrompathlibimportPathfromtypingimportAnyimportnumpyasnpimporttifffilefromtifffileimportread_scanimage_metadata,matlabstr2pyfromtifffile.tifffileimportbytes2str,read_json,FileHandledef_params_from_metadata_caiman(metadata):""" Generate parameters for CNMF from metadata. Based on the pixel resolution and frame rate, the parameters are set to reasonable values. Parameters ---------- metadata : dict Metadata dictionary resulting from `lcp.get_metadata()`. Returns ------- dict Dictionary of parameters for lbm_mc. """params=_default_params_caiman()ifmetadataisNone:print("No metadata found. Using default parameters.")returnparamsparams["main"]["fr"]=metadata["frame_rate"]params["main"]["dxy"]=metadata["pixel_resolution"]# typical neuron ~16 micronsgSig=round(16/metadata["pixel_resolution"][0])/2params["main"]["gSig"]=(int(gSig),int(gSig))gSiz=(4*gSig+1,4*gSig+1)params["main"]["gSiz"]=gSizmax_shifts=[int(round(10/px))forpxinmetadata["pixel_resolution"]]params["main"]["max_shifts"]=max_shiftsstrides=[int(round(64/px))forpxinmetadata["pixel_resolution"]]params["main"]["strides"]=strides# overlap should be ~neuron diameteroverlaps=[int(round(gSig/px))forpxinmetadata["pixel_resolution"]]ifoverlaps[0]<gSig:print("Overlaps too small. Increasing to neuron diameter.")overlaps=[int(gSig)]*2params["main"]["overlaps"]=overlapsrf_0=(strides[0]+overlaps[0])//2rf_1=(strides[1]+overlaps[1])//2rf=int(np.mean([rf_0,rf_1]))stride=int(np.mean([overlaps[0],overlaps[1]]))params["main"]["rf"]=rfparams["main"]["stride"]=stridereturnparamsdef_default_params_caiman():""" Default parameters for both registration and CNMF. The exception is gSiz being set relative to gSig. Returns ------- dict Dictionary of default parameter values for registration and segmentation. Notes ----- This will likely change as CaImAn is updated. """gSig=6gSiz=(4*gSig+1,4*gSig+1)return{"main":{# Motion correction parameters"pw_rigid":True,"max_shifts":[6,6],"strides":[64,64],"overlaps":[8,8],"min_mov":None,"gSig_filt":[0,0],"max_deviation_rigid":3,"border_nan":"copy","splits_els":14,"upsample_factor_grid":4,"use_cuda":False,"num_frames_split":50,"niter_rig":1,"is3D":False,"splits_rig":14,"num_splits_to_process_rig":None,# CNMF parameters"fr":10,"dxy":(1.0,1.0),"decay_time":0.4,"p":2,"nb":3,"K":20,"rf":64,"stride":[8,8],"gSig":gSig,"gSiz":gSiz,"method_init":"greedy_roi","rolling_sum":True,"use_cnn":False,"ssub":1,"tsub":1,"merge_thr":0.7,"bas_nonneg":True,"min_SNR":1.4,"rval_thr":0.8,},"refit":True,}def_params_from_metadata_suite2p(metadata,ops):""" Tau is 0.7 for GCaMP6f, 1.0 for GCaMP6m, 1.25-1.5 for GCaMP6s """ifmetadataisNone:print("No metadata found. Using default parameters.")returnops# typical neuron ~16 micronsops["fs"]=metadata["frame_rate"]ops["nplanes"]=1ops["nchannels"]=1ops["do_bidiphase"]=0ops["do_regmetrics"]=True# suite2p iterates each plane and takes ops['dxy'][i] where i is the plane indexops["dx"]=[metadata["pixel_resolution"][0]]ops["dy"]=[metadata["pixel_resolution"][1]]returnopsdefreport_missing_metadata(file:os.PathLike|str):tiff_file=tifffile.TiffFile(file)ifnottiff_file.software=="SI":print(f"Missing SI software tag.")ifnottiff_file.description[:6]=="state.":print(f"Missing 'state' software tag.")ifnot"scanimage.SI"intiff_file.description[-256:]:print(f"Missing 'scanimage.SI' in description tag.")defhas_mbo_metadata(file:os.PathLike|str)->bool:""" Check if a TIFF file has metadata from the Miller Brain Observatory. Specifically, this checks for tiff_file.shaped_metadata, which is used to store system and user supplied metadata. Parameters ---------- file: os.PathLike Path to the TIFF file. Returns ------- bool True if the TIFF file has MBO metadata; False otherwise. """ifnotfileornotisinstance(file,(str,os.PathLike)):raiseValueError("Invalid file path provided: must be a string or os.PathLike object."f"Got: {file} of type {type(file)}")# TiffsifPath(file).suffixin[".tif",".tiff"]:try:tiff_file=tifffile.TiffFile(file)if(hasattr(tiff_file,"shaped_metadata")andtiff_file.shaped_metadataisnotNone):returnTrueelse:returnFalseexceptException:returnFalsereturnFalsedefis_raw_scanimage(file:os.PathLike|str)->bool:""" Check if a TIFF file is a raw ScanImage TIFF. Parameters ---------- file: os.PathLike Path to the TIFF file. Returns ------- bool True if the TIFF file is a raw ScanImage TIFF; False otherwise. """ifnotfileornotisinstance(file,(str,os.PathLike)):returnFalseelifPath(file).suffixnotin[".tif",".tiff"]:returnFalsetry:tiff_file=tifffile.TiffFile(file)if(# TiffFile.shaped_metadata is where we store metadata for processed tifs# if this is not empty, we have a processed file# otherwise, we have a raw scanimage tiffhasattr(tiff_file,"shaped_metadata")andtiff_file.shaped_metadataisnotNoneandisinstance(tiff_file.shaped_metadata,(list,tuple))):returnFalseelse:iftiff_file.scanimage_metadataisNone:print(f"No ScanImage metadata found in {file}.")returnFalsereturnTrueexceptException:returnFalse
[docs]defget_metadata(file:os.PathLike|str,z_step=None,verbose=False,strict=False):""" Extract metadata from a TIFF file produced by ScanImage or processed via the save_as function. This function opens the given TIFF file and retrieves critical imaging parameters and acquisition details. It supports both raw ScanImage TIFFs and those modified by downstream processing. If the file contains raw ScanImage metadata, the function extracts key fields such as channel information, number of frames, field-of-view, pixel resolution, and ROI details. When verbose output is enabled, the complete metadata document is returned in addition to the parsed key values. Parameters ---------- file : os.PathLike or str The full path to the TIFF file from which metadata is to be extracted. verbose : bool, optional If True, returns an extended metadata dictionary that includes all available ScanImage attributes. Default is False. z_step : float, optional The z-step size in microns. If provided, it will be included in the returned metadata. Returns ------- dict A dictionary containing the extracted metadata (e.g., number of planes, frame rate, field-of-view, pixel resolution). When verbose is True, the dictionary also includes a key "all" with the full metadata from the TIFF header. Raises ------ ValueError If no recognizable metadata is found in the TIFF file (e.g., the file is not a valid ScanImage TIFF). Notes ----- - num_frames represents the number of frames per z-plane Examples -------- >>> meta = get_metadata("path/to/rawscan_00001.tif") >>> print(meta["num_frames"]) 5345 >>> meta = get_metadata("path/to/assembled_data.tif") >>> print(meta["shape"]) (14, 5345, 477, 477) >>> meta_verbose = get_metadata("path/to/scanimage_file.tif", verbose=True) >>> print(meta_verbose["all"]) {... Includes all ScanImage FrameData ...} """ifisinstance(file,list):returnget_metadata_batch(file)tiff_file=tifffile.TiffFile(file)# previously processed filesifnotis_raw_scanimage(file):returntiff_file.shaped_metadata[0]elifhasattr(tiff_file,"scanimage_metadata"):meta=tiff_file.scanimage_metadataifmetaisNone:returnNonesi=meta.get("FrameData",{})ifnotsi:print(f"No FrameData found in {file}.")returnNoneseries=tiff_file.series[0]pages=tiff_file.pagesprint("Raw tiff fully read.")# Extract ROI and imaging metadataroi_group=meta["RoiGroups"]["imagingRoiGroup"]["rois"]ifisinstance(roi_group,dict):num_rois=1roi_group=[roi_group]else:num_rois=len(roi_group)num_planes=len(si["SI.hChannels.channelSave"])ifnum_rois>1:try:sizes=[roi_group[i]["scanfields"][i]["sizeXY"]foriinrange(num_rois)]num_pixel_xys=[roi_group[i]["scanfields"][i]["pixelResolutionXY"]foriinrange(num_rois)]exceptKeyError:sizes=[roi_group[i]["scanfields"]["sizeXY"]foriinrange(num_rois)]num_pixel_xys=[roi_group[i]["scanfields"]["pixelResolutionXY"]foriinrange(num_rois)]# see if each item in sizes is the sameifstrict:assertall([sizes[0]==sizeforsizeinsizes]),("ROIs have different sizes")assertall([num_pixel_xys[0]==num_pixel_xyfornum_pixel_xyinnum_pixel_xys]),"ROIs have different pixel resolutions"size_xy=sizes[0]num_pixel_xy=num_pixel_xys[0]else:size_xy=[roi_group[0]["scanfields"]["sizeXY"]][0]num_pixel_xy=[roi_group[0]["scanfields"]["pixelResolutionXY"]][0]# TIFF header-derived metadataobjective_resolution=si["SI.objectiveResolution"]frame_rate=si["SI.hRoiManager.scanFrameRate"]# Field-of-view calculations# TODO: We may want an FOV measure that takes into account contiguous ROIs# As of now, this is for a single ROIfov_x_um=round(objective_resolution*size_xy[0])# in micronsfov_y_um=round(objective_resolution*size_xy[1])# in micronsfov_roi_um=(fov_x_um,fov_y_um)# in micronspixel_resolution=(fov_x_um/num_pixel_xy[0],fov_y_um/num_pixel_xy[1])metadata={"num_planes":num_planes,"fov":fov_roi_um,# in microns"fov_px":tuple(num_pixel_xy),"num_rois":num_rois,"frame_rate":frame_rate,"pixel_resolution":np.round(pixel_resolution,2),"ndim":series.ndim,"dtype":"int16","size":series.size,"tiff_pages":len(pages),"roi_width_px":num_pixel_xy[0],"roi_height_px":num_pixel_xy[1],"objective_resolution":objective_resolution,}ifverbose:metadata["all"]=metareturnmetadataelse:returnmetadataelse:raiseValueError(f"No metadata found in {file}.")
defget_metadata_batch(files:list[os.PathLike|str],z_step=None,verbose=False):""" Extract and aggregate metadata from a list of TIFF files produced by ScanImage. Parameters ---------- files : list of str or PathLike List of paths to TIFF files. z_step : float, optional Z-step in microns to include in the returned metadata. verbose : bool, optional If True, include full metadata from the first TIFF in 'all' key. Returns ------- dict Aggregated metadata dictionary with total frame count and per-file page counts. """total_frames=0frame_indices=[]first_meta=Nonefori,finenumerate(files):tf=tifffile.TiffFile(f)num_pages=len(tf.pages)frame_indices.append(num_pages)total_frames+=num_pagesifi==0:ifnotis_raw_scanimage(f):base=tf.shaped_metadata[0]["image"]elif(hasattr(tf,"scanimage_metadata")andtf.scanimage_metadataisnotNone):base=get_metadata(f,z_step=z_step,verbose=verbose)else:raiseValueError(f"No metadata found in {f}.")first_meta=base.copy()first_meta["num_frames"]=total_framesfirst_meta["frame_indices"]=frame_indicesreturnfirst_metadefparams_from_metadata(metadata,base_ops,pipeline="suite2p"):""" Use metadata to get sensible default pipeline parameters. If ops are not provided, uses suite2p.default_ops(). Sets framerate, pixel resolution, and do_metrics=True. Parameters ---------- metadata : dict Result of mbo.get_metadata() base_ops : dict Ops dict to use as a base. pipeline : str, optional The pipeline to use. Default is "suite2p". """ifpipeline.lower()=="caiman":print("Warning: CaImAn is not stable, proceed at your own risk.")return_params_from_metadata_caiman(metadata)elifpipeline.lower()=="suite2p":print("Setting pipeline to suite2p")return_params_from_metadata_suite2p(metadata,base_ops)else:raiseValueError(f"Pipeline {pipeline} not recognized. Use 'caiman' or 'suite2'")defread_scanimage_metadata_tifffile(fh:FileHandle,/)->tuple[dict[str,Any],dict[str,Any],int]:"""FROM TIFFFILE for DEVELOPMENT Read ScanImage BigTIFF v3 or v4 static and ROI metadata from file. The settings can be used to read image and metadata without parsing the TIFF file. Frame data and ROI groups can alternatively be obtained from the Software and Artist tags of any TIFF page. Parameters: fh: Binary file handle to read from. Returns: - Non-varying frame data, parsed with :py:func:`matlabstr2py`. - ROI group data, parsed from JSON. - Version of metadata (3 or 4). Raises: ValueError: File does not contain valid ScanImage metadata. """fh.seek(0)try:byteorder,version=struct.unpack("<2sH",fh.read(4))ifbyteorder!=b"II"orversion!=43:raiseValueError("not a BigTIFF file")fh.seek(16)magic,version,size0,size1=struct.unpack("<IIII",fh.read(16))ifmagic!=117637889orversionnotin{3,4}:raiseValueError(f"invalid magic {magic} or version {version} number")exceptUnicodeDecodeErrorasexc:raiseValueError("file must be opened in binary mode")fromexcexceptExceptionasexc:raiseValueError("not a ScanImage BigTIFF v3 or v4 file")fromexcframe_data=matlabstr2py(bytes2str(fh.read(size0)[:-1]))roi_data=read_json(fh,"<",0,size1,0)ifsize1>1else{}returnframe_data,roi_data,versiondefmatlabstr(obj):"""Convert Python dict to ScanImage-style MATLAB string."""def_format(v):ifisinstance(v,list):ifall(isinstance(i,str)foriinv):return"{"+" ".join(f"'{i}'"foriinv)+"}"return"["+" ".join(str(i)foriinv)+"]"ifisinstance(v,str):returnf"'{v}'"ifisinstance(v,bool):return"true"ifvelse"false"returnstr(v)return"\n".join(f"{k} = {_format(v)}"fork,vinobj.items())def_parse_value(value_str):ifvalue_str.startswith("'")andvalue_str.endswith("'"):returnvalue_str[1:-1]ifvalue_str=="true":returnTrueifvalue_str=="false":returnFalseifvalue_str=="NaN":returnfloat("nan")ifvalue_str=="Inf":returnfloat("inf")ifre.match(r"^\d+(\.\d+)?$",value_str):returnfloat(value_str)if"."invalue_strelseint(value_str)ifre.match(r"^\[(.*)]$",value_str):return[_parse_value(v.strip())forvinvalue_str[1:-1].split()]returnvalue_strdef_parse_key_value(parse_line):key_str,value_str=parse_line.split(" = ",1)returnkey_str,_parse_value(value_str)defparse(metadata_str):""" Parses the metadata string from a ScanImage Tiff file. :param metadata_str: :return metadata_kv, metadata_json: """lines=metadata_str.split("\n")metadata_kv={}json_portion=[]parsing_json=Falseforlineinlines:line=line.strip()ifnotline:continueifline.startswith("SI."):key,value=_parse_key_value(line)metadata_kv[key]=valueelifline.startswith("{"):parsing_json=Trueifparsing_json:json_portion.append(line)metadata_json=json.loads("\n".join(json_portion))returnmetadata_kv,metadata_jsondeffind_scanimage_metadata(path):withtifffile.TiffFile(path)astif:ifhasattr(tif,"scanimage_metadata"):returntif.scanimage_metadatap=tif.pages[0]cand=[]fortagin("ImageDescription","Software"):iftaginp.tags:cand.append(p.tags[tag].value)ifgetattr(p,"description",None):cand.append(p.description)cand.extend(str(tif.__dict__.get(k,""))forkintif.__dict__)forsincand:ifisinstance(s,bytes):s=s.decode(errors="ignore")m=re.search(r"{.*ScanImage.*}",s,re.S)ifm:try:returnjson.loads(m.group(0))exceptException:returnm.group(0)returnNone