def test_get_plane_on_xz_img(self): ref = Image.open("./tests/tiff/xz_c0_t0.tif") obj = LifFile("./tests/testdata_2channel_xz.lif").get_image(0) test = obj.get_plane(c=0, requested_dims={4: 0}) self.assertEqual(test.tobytes(), ref.tobytes()) ref2 = Image.open("./tests/tiff/xz_c1_t8.tif") # 3: z # 4: t test2 = obj.get_plane(c=1, requested_dims={4: 8}) self.assertEqual(test2.tobytes(), ref2.tobytes())
def test_image_loading(self): # order = c, z, t test_array = [[0, 0, 0], [0, 2, 0], [0, 2, 2], [1, 0, 0]] for i in test_array: c = str(i[0]) z = str(i[1]) t = str(i[2]) ref = Image.open("./tests/tiff/c" + c + "z" + z + "t" + t + ".tif") obj = LifFile("./tests/xyzt_test.lif").get_image(0) test = obj.get_frame(z=z, t=t, c=c) self.assertEqual(test.tobytes(), ref.tobytes())
def test_get_plane_on_normal_img(self): # order = c, z, t test_array = [[0, 0, 0], [0, 2, 0], [0, 2, 2], [1, 0, 0]] for i in test_array: c = str(i[0]) z = str(i[1]) t = str(i[2]) ref = Image.open("./tests/tiff/c" + c + "z" + z + "t" + t + ".tif") obj = LifFile("./tests/xyzt_test.lif").get_image(0) # 3: z # 4: t test = obj.get_plane(c=c, requested_dims={3: z, 4: t}) self.assertEqual(test.tobytes(), ref.tobytes())
def test_not_implemented_mosaic(self): import os # Can't test this in CI, don't have permission to publish this if os.path.exists("./tests/private/tile_002.lif"): with self.assertRaises(NotImplementedError): LifFile("./tests/private/tile_002.lif").get_image(0) pass
def _read_immediate(self) -> np.ndarray: # Get image dims indicies lif = LifFile(filename=self._file) image_dim_indices = LifReader._dims_shape(lif=lif) # Catch inconsistent scene dimension sizes if len(image_dim_indices) > 1: # Choose the provided scene log.info( f"File contains variable dimensions per scene, " f"selected scene: {self.specific_s_index} for data retrieval." ) data, _ = LifReader._get_array_from_offset( self._file, self._chunk_offsets, self._chunk_lengths, self.metadata, {Dimensions.Scene: self.specific_s_index}, ) else: # If the list is length one that means that all the scenes in the image # have the same dimensions # Read all data in the image data, _ = LifReader._get_array_from_offset( self._file, self._chunk_offsets, self._chunk_lengths, self.metadata, ) return data
def test_not_that_many_images(self): obj = LifFile("./tests/xyzt_test.lif") with self.assertRaises(ValueError): obj.get_image(10) image = obj.get_image(0) with self.assertRaises(ValueError): image.get_frame(z=10, t=0, c=0) with self.assertRaises(ValueError): image.get_frame(z=0, t=10, c=0) with self.assertRaises(ValueError): image.get_frame(z=0, t=0, c=10) with self.assertRaises(ValueError): image._get_item(100)
def test_private_images_16bit(self): # These tests are for images that are not public. # These images will be pulled from a protected web address # during CI testing. if os.environ.get('READLIF_TEST_DL_PASSWD') is not None \ and os.environ.get('READLIF_TEST_DL_PASSWD') != "": downloadPrivateFile("16bit.lif") downloadPrivateFile("i1c0z2_16b.tif") # Note - readlif produces little endian files, # ImageJ makes big endian files for 16bit by default obj = LifFile("./tests/private/16bit.lif").get_image(1) self.assertEqual(obj.bit_depth[0], 12) ref = Image.open("./tests/private/i1c0z2_16b.tif") test = obj.get_frame(z=2, c=0) self.assertEqual(test.tobytes(), ref.tobytes()) else: print("\nSkipped private test for 16-bit images\n")
def test_private_images_mosaic(self): # These tests are for images that are not public. # These images will be pulled from a protected web address # during CI testing. if os.environ.get('READLIF_TEST_DL_PASSWD') is not None\ and os.environ.get('READLIF_TEST_DL_PASSWD') != "": downloadPrivateFile("tile_002.lif") downloadPrivateFile("i0c1m2z0.tif") obj = LifFile("./tests/private/tile_002.lif").get_image(0) self.assertEqual(obj.dims.m, 165) m_list = [i for i in obj.get_iter_m()] self.assertEqual(len(m_list), 165) ref = Image.open("./tests/private/i0c1m2z0.tif") test = obj.get_frame(c=1, m=2) self.assertEqual(test.tobytes(), ref.tobytes()) else: print("\nSkipped private test for mosaic images\n")
def update_image(): global CENTERS_NO try: session['stack'] = int(request.args.get('stack')) session['zframe'] = int(request.args.get('zframe')) session['channel'] = int(request.args.get('channel')) session['bg_thresh'] = int(request.args.get('bg_thresh')) session['adaptive_thresh'] = int(request.args.get('adaptive_thresh')) session['erosion'] = int(request.args.get('erosion')) session['dilation'] = int(request.args.get('dilation')) session['min_dist'] = int(request.args.get('min_dist')) session['gamma'] = float(request.args.get('gamma')) session['gain'] = float(request.args.get('gain')) # img = cv2.imread('./test.jpg', cv2.IMREAD_GRAYSCALE) lif_file = LifFile(session['file_path']) img_list = [i for i in lif_file.get_iter_image()] img, CENTERS_NO = generate_image(img_list, session['stack'], session['zframe'], session['channel'], session['bg_thresh'], session['adaptive_thresh'], session['erosion'], session['dilation'], session['min_dist'], session['gamma'], session['gain'], connectivity=CONNECTIVITY, circle_radius=CIRCLE_RADIUS) (flag, encodedImage) = cv2.imencode(".jpg", img) response = base64.b64encode(encodedImage) return response except Exception as e: logger.error(e) resp = {'message': 'Failed'} return make_response(jsonify(resp), 400)
def __init__(self, lif_file): """ just specify path to lif-file in constructor """ self.lif_path = Path(lif_file) self.filename = self.lif_path.stem self.filename_full = self.lif_path.name self.outdir = self.lif_path.parent / self.filename self.outdir.mkdir(parents=True, exist_ok=True) self._release_logger() logging.basicConfig(filename=self.outdir / (self.filename + '_extractlog.log'), filemode='w', level=logging.DEBUG, format='%(message)s') print("#########################") print("reading file", self.filename_full) self.lifhandler = LifFile(self.lif_path) # categories under which images "series" will get sorted later self.export_entries = ["xy", "xyc", "xyz", "xyt" ] # currently supported entrytypes for export self.nonexport_entries = [ "xyct", "xycz", "xyzt", "xyczt", "envgraph", "MAF", "other" ] self.grouped_img = {key: [] for key in self.export_entries} self.grouped_img.update({key: [] for key in self.nonexport_entries}) self._get_overview() self._log_overview() self.print_overview() self._write_xml()
def define_lif_pipeline(input_def): # defines a processing pipeline starting with a .lif file fpath_in_lif = glob("{}/*lif".format(input_def["root"]))[0] # use only one lif file prer folder!!! print("Analysing",fpath_in_lif) # load lif file lifobj = LifFile(fpath_in_lif) if input_def["split_z"]: input_def["input_type"] = ".tif" # further processing of the lif file is then based on the tiff folder!!!! new_folder = os.path.join(input_def["root"],"tiff") if not os.path.exists(new_folder): os.makedirs(new_folder) print("Creating folder..." + new_folder) for ex_ch in input_def["export_multiple_ch"]: split_lif_z_to_tiff(lifobj,input_def["root"],input_def["z_step"],ex_ch,input_def["rigth_size"],input_def["mydtype"]) return lifobj,input_def
def _dims_shape(lif: LifFile): """ Get the dimensions for the opened file from the binary data (not the metadata) Parameters ---------- lif: LifFile Returns ------- list[dict] A list of dictionaries containing Dimension / depth. If the shape is consistent across Scenes then the list will have only one Dictionary. If the shape is inconsistent the the list will have a dictionary for each Scene. A consistently shaped file with 3 scenes, 7 time-points and 4 Z slices containing images of (h,w) = (325, 475) would return [ {'S': (0, 3), 'T': (0,7), 'X': (0, 475), 'Y': (0, 325), 'Z': (0, 4)} ]. The result for a similarly shaped file but with different number of time points per scene would yield [ {'S': (0, 1), 'T': (0,8), 'X': (0, 475), 'Y': (0, 325), 'Z': (0, 4)}, {'S': (1, 2), 'T': (0,6), 'X': (0, 475), 'Y': (0, 325), 'Z': (0, 4)}, {'S': (2, 3), 'T': (0,7), 'X': (0, 475), 'Y': (0, 325), 'Z': (0, 4)} ] """ shape_list = [ { Dimensions.Time: (0, img.nt), Dimensions.Channel: (0, img.channels), Dimensions.SpatialZ: (0, img.nz), Dimensions.SpatialY: (0, img.dims[1]), Dimensions.SpatialX: (0, img.dims[0]), } for idx, img in enumerate(lif.get_iter_image()) ] consistent = all(elem == shape_list[0] for elem in shape_list) if consistent: shape_list[0][Dimensions.Scene] = (0, len(shape_list)) shape_list = [shape_list[0]] else: for idx, lst in enumerate(shape_list): lst[Dimensions.Scene] = (idx, idx + 1) return shape_list
def test_iterators(self): images = [i for i in LifFile("./tests/xyzt_test.lif").get_iter_image()] self.assertEqual(len(images), 1) obj = LifFile("./tests/xyzt_test.lif").get_image(0) c_list = [i for i in obj.get_iter_c()] self.assertEqual(len(c_list), 2) t_list = [i for i in obj.get_iter_t()] self.assertEqual(len(t_list), 3) z_list = [i for i in obj.get_iter_z()] self.assertEqual(len(z_list), 3)
def upload_file(): uploaded_file = request.files['imagefile'] if uploaded_file and allowed_file(uploaded_file.filename): original_name = uploaded_file.filename file_name = session['uid'] + "." + original_name.rsplit('.', 1)[1].lower() session['file_path'] = os.path.join(app.config['UPLOAD_FOLDER'], file_name) session['export_dir'] = os.path.join(app.config['EXPORT_FOLDER'], session['uid']) if os.path.exists(session['file_path']): os.remove(session['file_path']) uploaded_file.save(session['file_path']) try: lif_file = LifFile(session['file_path']) img_list = lif_file.image_list stack_dict_list = [] stack_list = [] for img in img_list: img_name = ''.join(e for e in img['name'] if (e.isalnum() or e == ' ')) stack_list.append(img_name) c_list = [i for i in range(img['channels'])] z_list = [i for i in range(img['dims'].z)] stack_dict_list.append({'Z_LIST': z_list, 'C_LIST': c_list}) session['stack_list'] = stack_list session['stack_dict_list'] = stack_dict_list # session['stack_list'] = [1,2] # session['stack_dict_list'] = [{'Z_LIST':[1,2,3,4], 'C_LIST':[1,2,3,4]}, {'Z_LIST':[1,2], 'C_LIST':[1,2]}] return render_template('index.html', stack_list=session['stack_list'], stack_dict_list=session['stack_dict_list']) except Exception as e: logger.error(e) return render_template('index.html', invalid_feedback="Invalid file - Please check if the image file is valid", stack_list=[], stack_dict_list = []) else: return render_template('index.html', invalid_feedback="Invalid file - Please check if file exists and is in correct format", stack_list = [], stack_dict_list = [])
def __init__( self, data: types.FileLike, chunk_by_dims: List[str] = [ Dimensions.SpatialZ, Dimensions.SpatialY, Dimensions.SpatialX, ], S: int = 0, **kwargs, ): # Run super init to check filepath provided super().__init__(data, **kwargs) # Store parameters needed for _daread self.chunk_by_dims = chunk_by_dims self.specific_s_index = S lif = LifFile(filename=self._file) # _chunk_offsets is a list of ndarrays # (only way I could deal with inconsistent scene shape) self._chunk_offsets, self._chunk_lengths = LifReader._compute_offsets(lif=lif)
def test_iterators(self): images = [i for i in LifFile("./tests/xyzt_test.lif").get_iter_image()] self.assertEqual(len(images), 1) obj = LifFile("./tests/xyzt_test.lif").get_image(0) self.assertEqual(repr(obj), "'LifImage object with " "dimensions: " "Dims(x=1024, y=1024, z=3, t=3, m=1)'") c_list = [i for i in obj.get_iter_c()] self.assertEqual(len(c_list), 2) t_list = [i for i in obj.get_iter_t()] self.assertEqual(len(t_list), 3) z_list = [i for i in obj.get_iter_z()] self.assertEqual(len(z_list), 3)
def track_lif(lif_path: str, out_path: str, model: keras.models.Model) -> None: """ Applies ML model (model object) to everything in the lif file. This will write a trackmate xml file via the method tm_xml.write_xml(), and save output tiff image stacks from the lif file. Args: lif_path (str): Path to the lif file out_path (str): Path to output directory model (str): A trained keras.models.Model object Returns: None """ print("loading LIF") lif_data = LifFile(lif_path) print("Iterating over lif") for image in lif_data.get_iter_image(): folder_path = "/".join(str(image.path).strip("/").split('/')[1:]) path = folder_path + "/" + str(image.name) name = image.name if os.path.exists(os.path.join(out_path, path + '.tif.xml')) \ or os.path.exists(os.path.join(out_path, path + '.tif.trackmate.xml')): print(str(path) + '.xml' + ' exists, skipping') continue make_dirs = os.path.join(out_path, folder_path) if not os.path.exists(make_dirs): os.makedirs(make_dirs) print("Processing " + str(path)) start = time.time() # initialize XML creation for this file tm_xml = trackmateXML() i = 1 image_out = image.get_frame() # Initialize the output image images_to_append = [] for frame in image.get_iter_t(): images_to_append.append(frame) np_image = np.asarray(frame.convert('RGB')) image_array = np_image[:, :, ::-1].copy() tm_xml.filename = name + '.tif' tm_xml.imagepath = os.path.join(out_path, folder_path) if tm_xml.nframes < i: # set nframes to the maximum i tm_xml.nframes = i tm_xml.frame = i # preprocess image for network image_array = preprocess_image(image_array) image_array, scale = resize_image(image_array) # process image boxes, scores, labels = model.predict_on_batch( np.expand_dims(image_array, axis=0)) # correct for image scale boxes /= scale # filter the detection boxes pre_passed_boxes = [] pre_passed_scores = [] for box, score, label in zip(boxes[0], scores[0], labels[0]): if score >= 0.2: pre_passed_boxes.append(box.tolist()) pre_passed_scores.append(score.tolist()) passed_boxes, passed_scores = filter_boxes( in_boxes=pre_passed_boxes, in_scores=pre_passed_scores, _passed_boxes=[], _passed_scores=[]) # These are necessary print("found " + str(len(passed_boxes)) + " cells in " + str(path) + " frame " + str(i)) # tell the trackmate writer to add the passed_boxes to the final output xml tm_xml.add_frame_spots(passed_boxes, passed_scores) i += 1 # write the image to trackmate, prepare for next image print("processing time: ", time.time() - start) tm_xml.write_xml() image_out.save(os.path.join(out_path, path + '.tif'), format="tiff", append_images=images_to_append[1:], save_all=True, compression='tiff_lzw')
class lif_summary: """ class which contains all functions to extract images from lif file """ def __init__(self, lif_file): """ just specify path to lif-file in constructor """ self.lif_path = Path(lif_file) self.filename = self.lif_path.stem self.filename_full = self.lif_path.name self.outdir = self.lif_path.parent / self.filename self.outdir.mkdir(parents=True, exist_ok=True) self._release_logger() logging.basicConfig(filename=self.outdir / (self.filename + '_extractlog.log'), filemode='w', level=logging.DEBUG, format='%(message)s') print("#########################") print("reading file", self.filename_full) self.lifhandler = LifFile(self.lif_path) # categories under which images "series" will get sorted later self.export_entries = ["xy", "xyc", "xyz", "xyt" ] # currently supported entrytypes for export self.nonexport_entries = [ "xyct", "xycz", "xyzt", "xyczt", "envgraph", "MAF", "other" ] self.grouped_img = {key: [] for key in self.export_entries} self.grouped_img.update({key: [] for key in self.nonexport_entries}) self._get_overview() self._log_overview() self.print_overview() self._write_xml() def _release_logger(self): """ releases old logger instance, called upon init of new lif might be useful in e.g. jupyter where object not automatically released after export """ logging.shutdown() #clear old logger logging.getLogger().handlers.clear() def _build_query(self, imgentry, query=""): """ constructs cmd to query specified element, takes also care if element is in subfolder imgentry = entry from img_list (=dict) """ # procedure works, however a bit cumbersome... better to directly extract more param in readlif? path = imgentry[ "path"] # subfolders are nested here: projectname/subf1/subf2/ sfolders = path.split("/")[ 1:-1] # split by / and take all except first and last name = imgentry["name"] elquery = 'Element/Children' # main entrypoint, all images are always children of main element for sfolder in sfolders: elquery = elquery + f'/Element[@Name="{sfolder}"]/Children' # build query of all subfolders elquery = elquery + f'/Element[@Name="{name}"]' # attach query for element with specified name query = elquery + query return query def _query_hist(self, imgentry): """ reads out BlackValue and WhiteValue for specific imgentry take care, multichannel not implemented yet, will return values of first found entry """ query = self._build_query(imgentry, "/Data/Image/Attachment/ChannelScalingInfo") rootel = self.lifhandler.xml_root blackval = float(rootel.find(query).attrib["BlackValue"]) whiteval = float(rootel.find(query).attrib["WhiteValue"]) return [blackval, whiteval] def _query_chan(self, imgentry): """ reads out used contrast method + filter cube for specific imgentry returns as list where each item corresponds to channel """ cquery = self._build_query( imgentry, "Data/Image/Attachment/" "ATLCameraSettingDefinition/WideFieldChannelConfigurator/WideFieldChannelInfo" ) rootel = self.lifhandler.xml_root chan_els = rootel.findall(cquery) chaninfo = [ chan_el.attrib["ContrastingMethodName"] + "_" + chan_el.attrib["FluoCubeName"] for chan_el in chan_els ] return chaninfo def _query_timestamp(self, imgentry): """ returns acquision date (= first timestamp) of selected entry """ tsquery = self._build_query(imgentry, "Data/Image/TimeStampList") rootel = self.lifhandler.xml_root # try: ts = rootel.find(tsquery).text ts = ts.split(" ")[0] # except (AttributeError, TypeError): # return None # conversion adapted from bioformats stampLowStart = max(0, len(ts) - 8) stampHighEnd = max(0, stampLowStart) stampHigh = ts[0:stampHighEnd] stampLow = ts[stampLowStart:] low = int(stampLow, 16) high = int(stampHigh, 16) ticks = (high << 32) | low ticks = ticks / 10000 COBOL_EPOCH = 11644473600000 ts_unix = int(ticks - COBOL_EPOCH) # in ms return ts_unix def _log_img(self, imgentry): """ logs currently exported imagentry to logfile """ logging.warning( f"########## exporting entry {imgentry['idx']} ##########") for entry in [ "name", "path", "bit_depth", "dims", "scale", "channels", "chaninfo", "Blackval", "Whiteval", "AcqTS" ]: logging.warning(f'{entry}: %s', imgentry[entry]) def _log_overview(self): """ logs info of entries added by _get_overview to logfile """ logging.warning(f"########## entries found in file ##########") logging.warning(f"- entries which will be exported:") for imgtype in self.export_entries: imglist = self.grouped_img[imgtype] logging.warning(f'{imgtype}: {len(imglist)}') logging.warning(f"- entries whose export is not supported yet:") N_nonexported = 0 for imgtype in self.nonexport_entries: imglist = self.grouped_img[imgtype] if len(imglist) > 0: N_nonexported += len(imglist) logging.warning(f'{imgtype}: {len(imglist)}') if N_nonexported > 0: logging.warning( f"### {N_nonexported} entries won't be exported ###") def print_overview(self): """ prints overview of found entries, shows which ones will be exported """ print("following entries found in file: ") print("- entries which will be exported:") for imgtype in self.export_entries: imglist = self.grouped_img[imgtype] col = '\033[0m' if len(imglist) > 0: col = '\033[92m' print(f'{col} {imgtype}: {len(imglist)}' + '\033[0m') print("- entries whose export is not supported yet:") N_nonexported = 0 for imgtype in self.nonexport_entries: imglist = self.grouped_img[imgtype] col = '\033[0m' if len(imglist) > 0: N_nonexported += len(imglist) col = '\033[93m' print(f'{col} {imgtype}: {len(imglist)}' + '\033[0m') if N_nonexported > 0: print( f"\033[91m {N_nonexported} entries won't be exported \033[0m") def _get_overview(self): """ extracts information of stored images from metadata fills dict self.grouped_img with dict for each imgentry """ for idx, img in enumerate(self.lifhandler.image_list): # print(img) img["idx"] = idx # add index which is used to request frame img["chaninfo"] = self._query_chan(img) img["AcqTS"] = self._query_timestamp(img) img["Blackval"] = None img["Whiteval"] = None # check for special cases first if ('EnvironmentalGraph') in img["name"]: self.grouped_img["envgraph"].append(img) continue if ('Mark_and_Find') in img["path"]: self.grouped_img["MAF"].append(img) continue # check various dimensions to sort entries accordingly dimtuple = img["dims"] Nx, Ny, Nz, NT = dimtuple[0], dimtuple[1], dimtuple[2], dimtuple[3] # dimension tuple must be indexed with int, 0:x, 1:y, 2:z, 3:t, 4:m mosaic tile NC = img["channels"] # xy (simple image) if (Nz == 1 and NT == 1 and NC == 1): # print("entry is xy") self.grouped_img["xy"].append(img) # xyc (multichannel image) elif (Nz == 1 and NT == 1 and NC > 1): # print("entry is xyc") self.grouped_img["xyc"].append(img) # xyz (singlechannel zstack) elif (Nz > 1 and NT == 1 and NC == 1): # print("entry is xyz") self.grouped_img["xyz"].append(img) # xyt singlechannel video/ timelapse elif (Nz == 1 and NT > 1 and NC == 1): # print("entry is xyt") img["fps"] = img["scale"][3] self.grouped_img["xyt"].append(img) # xyct (multichannel video/ timelapse) elif (Nz == 1 and NT > 1 and NC > 1): img["fps"] = img["scale"][3] self.grouped_img["xyct"].append(img) # xycz elif (Nz > 1 and NT == 1 and NC > 1): self.grouped_img["xycz"].append(img) # xyzt elif (Nz > 1 and NT > 1 and NC == 1): img["fps"] = img["scale"][3] self.grouped_img["xyzt"].append(img) # xyczt elif (Nz > 1 and NT > 1 and NC > 1): img["fps"] = img["scale"][3] self.grouped_img["xyczt"].append(img) # add to category other if no previously checked category applies else: self.grouped_img["other"].append(img) # find blackval/whiteval (or even other param if desired) for xy and xyt-images for cat in ["xy", "xyt"]: imglist = self.grouped_img[cat] for img in imglist: black_val, white_val = self._query_hist(img) img["Blackval"] = black_val img["Whiteval"] = white_val def _write_xml(self): """ writes metadata of lif-file in pretty xml """ xmlstr = minidom.parseString(ET.tostring( self.lifhandler.xml_root)).toprettyxml(indent=" ") fname = self.outdir / (self.filename + "_meta.xml") with open(fname, "w") as f: f.write(xmlstr) #https://stackoverflow.com/questions/56682486/xml-etree-elementtree-element-object-has-no-attribute-write def export_xy(self, min_rangespan=0.2, max_clipped=0.2): """ exports all xy image entries: - raw export: tif - compressed export (jpg, scaled to blackval/whiteval which was set during acquisition with burned in title + scale bar) """ # check if entries to export if len(self.grouped_img["xy"]) == 0: return #### raw export folder rawfolder = self.outdir / "Images_xy" / "raw" rawfolder.mkdir(parents=True, exist_ok=True) #### compressed jpg export folder compfolder = self.outdir / "Images_xy" / "compressed" compfolder.mkdir(parents=True, exist_ok=True) # iterate all images for imgentry in self.grouped_img["xy"]: self._log_img(imgentry) img_idx = imgentry["idx"] img_name = imgentry["name"] print(f"exporting image {img_name}") # with meta: \n {imgentry}") """ # option to concatenate subfolders into filename path = imgentry["path"] # subfolders are nested here: projectname/subf1/subf2/ sfolders = path.split("/")[1:-1] # split by / and take all except first and last sfolders.append(img_name) img_name = ("_".join(sfolders)) """ imghandler = self.lifhandler.get_image(img_idx) img = imghandler.get_frame(z=0, t=0, c=0) img_np = np.array(img) resolution_mpp = 1.0 / imgentry["scale_n"][ 1] # unit should be pix per micron of scale_n imgpath = rawfolder / (img_name + ".tif") self.save_single_tif(img_np, imgpath, resolution_mpp) # compressed export: # scale images to 8bit, add scalebar + title, save as jpg in orig resolution bit_resolution = imgentry["bit_depth"][0] img_scale = 2**bit_resolution - 1 # image_adj_contrast = self.adj_contrast(img_np, imgentry["Blackval"]*img_scale, imgentry["Whiteval"]*img_scale) vmin, vmax = self.check_contrast(img_np, imgentry["Blackval"] * img_scale, imgentry["Whiteval"] * img_scale, min_rangespan=min_rangespan, max_clipped=max_clipped) image_adj_contrast = exposure.rescale_intensity( img_np, in_range=(vmin, vmax)) # stretch min/max img_8 = cv2.convertScaleAbs(image_adj_contrast, alpha=(255.0 / img_scale)) labeled_image = self.plot_scalebar(img_8, resolution_mpp, img_name) imgpath_jpg = compfolder / (img_name + ".jpg") skimage.io.imsave(imgpath_jpg, labeled_image) def export_xyz(self): """ exports all xyz image entries (=zstacks) - raw export: tif - compressed export: none """ # check if entries to export if len(self.grouped_img["xyz"]) == 0: return #### raw export folder rawfolder = self.outdir / "Images_xyz" rawfolder.mkdir(parents=True, exist_ok=True) # iterate all images for imgentry in self.grouped_img["xyz"]: self._log_img(imgentry) resolution_mpp = 1.0 / imgentry["scale_n"][ 1] # unit should be pix per micron of scale_n img_idx = imgentry["idx"] img_name = imgentry["name"] print(f"exporting zstack {img_name}") # with meta: \n {imgentry}") imghandler = self.lifhandler.get_image(img_idx) # get correct z-spacing dz # take care! might need to be adjusted if reader.py changes z_spacing = imgentry["scale_n"][3] # planes per micron Nz = imgentry["dims"][2] total_z = Nz / z_spacing dz = total_z / (Nz - 1) dzstring = f"-dz_{dz:.2f}_um".replace( ".", "_" ) # write plane spacing into foldername such that it's easily accessible stackfolder = rawfolder / (img_name + dzstring ) # create overall folder for zstack stackfolder.mkdir(parents=True, exist_ok=True) Nplanes = imgentry["dims"][2] for plane in tqdm.tqdm(np.arange(Nplanes), desc="Plane", file=sys.stdout, position=0, leave=True): img = imghandler.get_frame(z=plane, t=0, c=0) img_np = np.array(img) planepath = stackfolder / f"{img_name}-{plane:04d}.tif" # series_name+"-{:04d}.tif".format(plane)) self.save_single_tif(img_np, planepath, resolution_mpp) def export_xyc(self): """ exports all xyc image entries (=multichannel images) - raw export: tif - compressed export: none """ # check if entries to export if len(self.grouped_img["xyc"]) == 0: return #### raw export folder rawfolder = self.outdir / "Images_xyc" rawfolder.mkdir(parents=True, exist_ok=True) # iterate all images for imgentry in self.grouped_img["xyc"]: self._log_img(imgentry) resolution_mpp = 1.0 / imgentry["scale_n"][ 1] # unit should be pix per micron of scale_n img_idx = imgentry["idx"] img_name = imgentry["name"] imgpath = rawfolder / (img_name + ".tif") print(f"exporting multichannel {img_name}" ) # with meta: \n {imgentry}") imghandler = self.lifhandler.get_image(img_idx) channel_list = [ np.array(img) for img in imghandler.get_iter_c(t=0, z=0) ] img_xyc = np.array(channel_list) self.save_single_tif(img_xyc, imgpath, resolution_mpp, photometric='minisblack') def export_xyt(self, min_rangespan=0.2, max_clipped=0.2): """ exports all xyt image entries (=video/ timelapse entries) directly pipes frames to ffmpeg - large export: .mp4 in full resolution, low compression - small export: .mp4, longest side scaled to 1024, include scalebar """ # check if entries to export if len(self.grouped_img["xyt"]) == 0: return #### hq export folder lgfolder = self.outdir / "Videos" / "lg" lgfolder.mkdir(parents=True, exist_ok=True) #### compressed jpg export folder smfolder = self.outdir / "Videos" / "sm" smfolder.mkdir(parents=True, exist_ok=True) # iterate all entries for imgentry in self.grouped_img["xyt"]: self._log_img(imgentry) resolution_mpp = 1.0 / imgentry["scale_n"][1] img_idx = imgentry["idx"] img_name = imgentry["name"] fps = imgentry['fps'] Nx, Ny, NT = imgentry["dims"][0], imgentry["dims"][1], imgentry[ "dims"][3] Nmax_sm = 1024 # longest side of small video codec_lg, codec_sm = 'libx264', 'libx264' crf_lg, crf_sm = 17, 23 #rescale smaller video such that longest side = 1024 # prevent upscaling Nlong = max(Nx, Ny) scalingfactor = float(Nmax_sm) / Nlong if scalingfactor > 1.0: # don't allow upscaling scalingfactor = 1.0 #print("scaling", scalingfactor) Nx_sm, Ny_sm = int(Nx * scalingfactor), int(Ny * scalingfactor) scalebar = self.create_scalebar( Nx_sm, resolution_mpp / scalingfactor) #create scalebar for small vid scale_width_px = scalebar.shape[0] scale_height_px = scalebar.shape[1] print(f"exporting video {img_name}") # with meta: \n {imgentry}") #print("resolution of small video:", Nx_sm, Ny_sm) imghandler = self.lifhandler.get_image(img_idx) # export of both vids simultaneously, get infos to start ffmpeg subprocess resinfo_lg = f'resolution_xy={resolution_mpp:.6f}_mpp'.replace( ".", "_") resinfo_sm = f'resolution_xy={resolution_mpp/scalingfactor:.6f}_mpp'.replace( ".", "_") # correct by scalingfactor # stores resolution info in metadata (in category comment) for quick access from videofile path_lg = str(lgfolder / (img_name + "_lg.mp4")) # string needed for input to ffmpeg cmd path_sm = str(smfolder / (img_name + "_sm.mp4")) # string needed for input to ffmpeg cmd sizestring_lg = f'{Nx}x{Ny}' # e.g. 1024x1024, xsize x ysize, todo: check if order correct sizestring_sm = f'{Nx_sm}x{Ny_sm}' startt = time.time() #for quick check of exporttimes # write video via pipe to ffmpeg-stream, start process here # solution from https://stackoverflow.com/questions/61260182/how-to-output-x265-compressed-video-with-cv2-videowriter process_lg = sp.Popen(shlex.split( f'"{FFMPEG_BINARY}" -y -s {sizestring_lg} ' f'-pixel_format gray8 -f rawvideo -r {fps} -i pipe: -vcodec {codec_lg} ' f'-pix_fmt yuv420p -crf {crf_lg} -metadata comment="{resinfo_lg}" "{path_lg}"' ), stdin=sp.PIPE, stderr=sp.DEVNULL ) # supress ffmpeg output to console # directly create process for export of smaller vid -> img has to be pulled only once process_sm = sp.Popen(shlex.split( f'"{FFMPEG_BINARY}" -y -s {sizestring_sm} ' f'-pixel_format gray8 -f rawvideo -r {fps} -i pipe: -vcodec {codec_sm} ' f'-pix_fmt yuv420p -crf {crf_sm} -metadata comment="{resinfo_sm}" "{path_sm}"' ), stdin=sp.PIPE, stderr=sp.DEVNULL) # # check correct exposure scaling on one frame (image at half videolength) # save frame also as tif in full res for later access Nmean = int(imgentry["dims"][3] / 2) - 1 # idx of mean frame ~ at half of Nframes mimg = imghandler.get_frame(z=0, t=Nmean, c=0) img_np = np.array(mimg) bit_resolution = imgentry["bit_depth"][0] img_scale = 2**bit_resolution - 1 vmin, vmax = self.check_contrast(img_np, imgentry["Blackval"] * img_scale, imgentry["Whiteval"] * img_scale, min_rangespan=min_rangespan, max_clipped=max_clipped) vmin8, vmax8 = vmin * (255.0 / img_scale), vmax * ( 255.0 / img_scale) # adjust to 8bit # print(imgentry["Blackval"], imgentry["Whiteval"]) # print(vmin8, vmax8) # logging.warning(f'set vmin8, vmax8 to {vmin8}, {vmax8}') # save single tiff in full size stillpath = self.outdir / "Videos" / "tifstills" stillpath.mkdir(parents=True, exist_ok=True) self.save_single_tif(img_np, stillpath / (img_name + ".tif"), resolution_mpp) # kwarg info: # -y: overwrite wo. asking # -s: size # -pixel_format: bgr24 was set... use gray8 for 8bit grayscale # -f: "Force input or output file format. -> here set to raw stream" # -r: framerate, can be used for input and output stream, here only input specified -> output will be same # -i: pipe for frame in tqdm.tqdm(imghandler.get_iter_t(c=0, z=0), desc="Frame", file=sys.stdout, position=0, leave=True, total=NT): img_np = np.array(frame) img8 = cv2.convertScaleAbs( img_np, alpha=(255.0 / img_scale)) # scale to 8bit range img_scaled = exposure.rescale_intensity( img8, in_range=(vmin8, vmax8)) # stretch min/max img_sm = cv2.resize(img_scaled, (Nx_sm, Ny_sm)) # scale down small vid img_sm[-1 - scale_width_px:-1, -1 - scale_height_px:-1] = scalebar # add scalebar process_lg.stdin.write(img_scaled.tobytes()) process_sm.stdin.write(img_sm.tobytes()) for process in [process_lg, process_sm]: process.stdin.close() process.wait() process.terminate() print("video export finished in", time.time() - startt, "s") def export_all(self): """ exports xy, xyc, xyz, xyt entries (all currently supported export options) by calling individual exportfunctions """ self.export_xy(min_rangespan=0.2, max_clipped=0.6) self.export_xyz() self.export_xyc() self.export_xyt(min_rangespan=0.2, max_clipped=0.6) def save_single_tif(self, image, path, resolution_mpp, photometric=None, compress=None): """ saves single imagej-tif into specified folder uses resolution_mpp to indicate resolution in x and y dimensions in microns per pixel """ resolution_ppm = 1 / resolution_mpp # convert micron per pixel to pixel per micron metadata = {'unit': 'um'} if photometric == None: tifffile.imsave( path, image, imagej=True, resolution=(resolution_ppm, resolution_ppm), metadata=metadata, compress=compress) #what if ppm is different in x and y? else: tifffile.imsave(path, image, imagej=True, resolution=(resolution_ppm, resolution_ppm), metadata=metadata, photometric=photometric, compress=compress) def check_contrast(self, image, vmin=None, vmax=None, min_rangespan=0.2, max_clipped=0.2): """ checks if desired scaling range between vmin and vmax yields to a reasonable intensity range (< max_clipped (20 % default) of image over/underexposed, image spans > min_rangespan (20 % default) of range) adjusts vmin and vmax to 0.2 - 99.8 percentile if not """ # check first if contrast is somehow alright # check spanwidth of image vs. spanwidth of defined interval # if rangespan small -> low contrast # rangespan can also be alright but values shifted -> over/ underxposure # -> check both imglimits = np.percentile(image, [5, 95]) rangespan = (imglimits[1] - imglimits[0]) / (vmax - vmin) # check fraction of px outside defined interval (max. 1 = all) # outside px are over/underexposed px_clipped = ((image < vmin) | (image > vmax)).sum() / image.size if ((px_clipped > max_clipped) or (rangespan < min_rangespan)): print( '\033[96m' + "extracted histogram scaling (blackval/ whiteval) would " "correspond to an over/underexposure of > 20 % of the image " "or the image would span < 20 % of chosen range" "-> switching to automatic rescaling to range from 0.2 - 99.8 percentile" + '\033[0m') logging.warning(f'adjusting contrast range to {vmin}, {vmax}') #print(f"vmin {vmin}, vmax {vmax}, immin {imglimits[0]}, immax {imglimits[1]}") vmin, vmax = None, None if None in (vmin, vmax): vmin = np.percentile(image, 0.2) vmax = np.percentile(image, 99.8) return vmin, vmax def create_scalebar(self, dimX_px, microns_per_pixel): """ creates scalebar as np array which then can be transferred to image """ scale_values = [ 1, 2, 5, 10, 15, 20, 30, 40, 50, 70, 100, 150, 200, 300, 400, 500, 700, 1000 ] initial_scale_length = dimX_px * 0.2 * microns_per_pixel text_height_px = int(round(dimX_px * 0.05)) drawfont = ImageFont.truetype("arial.ttf", text_height_px) scale_length_microns = min( scale_values, key=lambda x: abs(x - initial_scale_length)) # pick nearest value scale_caption = str(scale_length_microns) + " µm" scale_length_px = scale_length_microns / microns_per_pixel scale_height_px = dimX_px * 0.01 bg_square_spacer_px = scale_length_px * 0.07 bg_square_length_px = int( round(scale_length_px + 2 * bg_square_spacer_px)) bg_square_height_px = int( round(text_height_px + scale_height_px + 2 * bg_square_spacer_px)) scalebar = Image.new("L", (bg_square_length_px, bg_square_height_px), "white") draw = ImageDraw.Draw(scalebar) w_caption, h_caption = draw.textsize(scale_caption, font=drawfont) draw.rectangle(((0, 0), (bg_square_length_px, bg_square_height_px)), fill="black") draw.rectangle( ((bg_square_spacer_px, bg_square_height_px - bg_square_spacer_px - scale_height_px), (bg_square_length_px - bg_square_spacer_px, bg_square_height_px - bg_square_spacer_px)), fill="white") draw.text( (bg_square_spacer_px + bg_square_length_px / 2 - w_caption / 2, bg_square_spacer_px / 2), scale_caption, font=drawfont, fill="white") output_scalebar = np.array(scalebar) return output_scalebar #todo: reorganize such that cmds are not repeated in create_scalebar... def plot_scalebar(self, input_image, microns_per_pixel, image_name=None): """ plots scalebar + title onto image if desired scalebar is only plotted if image width > 800 px image input: np array """ image_scalebar = Image.fromarray(input_image) # Image is PIL.Image #np.uint8(input_image*255) dimX_px = input_image.shape[1] dimY_px = input_image.shape[0] initial_scale_length = dimX_px * 0.2 * microns_per_pixel text_height_px = int(round(dimY_px * 0.05)) scale_values = [ 1, 2, 5, 10, 15, 20, 30, 40, 50, 70, 100, 150, 200, 300, 400, 500, 700, 1000 ] drawfont = ImageFont.truetype("arial.ttf", text_height_px) scale_length_microns = min( scale_values, key=lambda x: abs(x - initial_scale_length)) # pick nearest value scale_caption = str(scale_length_microns) + " µm" draw = ImageDraw.Draw(image_scalebar) w_caption, h_caption = draw.textsize(scale_caption, font=drawfont) scale_length_px = scale_length_microns / microns_per_pixel scale_height_px = dimY_px * 0.01 bg_square_spacer_px = scale_length_px * 0.07 bg_square_length_px = scale_length_px + 2 * bg_square_spacer_px bg_square_height_px = text_height_px + scale_height_px + 2 * bg_square_spacer_px if dimX_px > 800: #print(dimX_px - bg_square_length_px, dimX_px - bg_square_height_px) draw.rectangle( ((dimX_px - bg_square_length_px, dimY_px - bg_square_height_px), (dimX_px, dimY_px)), fill="black") draw.rectangle( ((dimX_px - bg_square_length_px + bg_square_spacer_px, dimY_px - bg_square_spacer_px - scale_height_px), (dimX_px - bg_square_spacer_px, dimY_px - bg_square_spacer_px)), fill="white") draw.text( (dimX_px - bg_square_length_px + bg_square_spacer_px + bg_square_length_px / 2 - w_caption / 2, dimY_px - bg_square_height_px + bg_square_spacer_px / 2), scale_caption, font=drawfont, fill="white") # scale_caption.decode('utf8') # burn title if provided if image_name != None: title_height_px = int(round(dimY_px * 0.05)) drawfont = ImageFont.truetype("arial.ttf", title_height_px) draw.rectangle(((0, 0), (dimX_px, title_height_px * 1.2)), fill="black") draw.text((0, 0), image_name, font=drawfont, fill="white") output_image = np.array(image_scalebar) return output_image def create_ppt_summary(self): """ creates ppt with all exported images """ print("creating ppt-summary") # take always n elements from list as list, helpfunction def grouper(iterable, n, fillvalue=None): args = [iter(iterable)] * n return zip_longest(*args, fillvalue=fillvalue) # prepare presentation prs = Presentation() title_slide_layout = prs.slide_layouts[0] slide = prs.slides.add_slide(title_slide_layout) title = slide.shapes.title subtitle = slide.placeholders[1] title.text = str(self.filename) subtitle.text = "lif-summary" ############################### # constants for 2 x 3 layout Pt_per_cm = 72.0 / 2.54 slide_width = Pt(25.4 * Pt_per_cm) slide_height = Pt(19.05 * Pt_per_cm) headspace = Pt(30) image_height = Pt(200) d_horizontal = (slide_width - 3 * image_height) / 4 d_vertical = (slide_height - headspace - 2 * image_height) / 3 ############################## # pick 6 images for images_6group in grouper(self.categorized_series['img_simple'], 6, None): #add new slide blank_slide_layout = prs.slide_layouts[6] slide = prs.slides.add_slide(blank_slide_layout) #iterate through rows, columns for rowindex in range(2): for columnindex in range(3): image_index = rowindex * 3 + columnindex if (images_6group[image_index]) != None: image_name = images_6group[image_index]['name'] imagepath = os.path.join(self.filename, "compressed", "images", image_name + ".jpg") pic = slide.shapes.add_picture( imagepath, d_horizontal * (columnindex + 1) + image_height * columnindex, headspace + d_vertical * (rowindex + 1) + image_height * rowindex, height=image_height) """ # pick videos, experimental, works but stillimage is loudspeaker -> create individual stillimage for video in self.categorized_series['img_multiT']: blank_slide_layout = prs.slide_layouts[6] slide = prs.slides.add_slide(blank_slide_layout) videopath = os.path.join(self.filename,"compressed","videos",video['name'] + ".mp4") slide.shapes.add_movie(videopath,d_vertical,d_vertical,slide_height-2*d_vertical,slide_height-2*d_vertical) """ outputpath = os.path.join(str(self.filename), str(self.filename) + '_summary.pptx') prs.save(outputpath)
def test_depth(self): obj = LifFile("./tests/xyzt_test.lif").get_image(0) self.assertEqual(obj.bit_depth[0], 8)
def test_scale(self): obj = LifFile("./tests/xyzt_test.lif").get_image(0) self.assertAlmostEqual(obj.scale[0], 9.8709062997224)
def download_file(): export_option = request.args.get('export_option') if os.path.exists(session['export_dir']): shutil.rmtree(session['export_dir']) os.makedirs(session['export_dir']) lif_file = LifFile(session['file_path']) img_list = [i for i in lif_file.get_iter_image()] data = download_image(export_option, session['export_dir'], img_list, session['stack_list'], session['stack_dict_list'], session['stack'], session['zframe'], session['channel'], session['bg_thresh'], session['adaptive_thresh'], session['erosion'], session['dilation'], session['min_dist'], session['gamma'], session['gain'], CONNECTIVITY, CIRCLE_RADIUS) data_df = pd.DataFrame(data) export_file_path_data = os.path.join(session['export_dir'], "data.csv") data_df.to_csv(export_file_path_data, index=False, sep=";") config = {'background_threshold': session['bg_thresh'], 'adaptive_threshold': session['adaptive_thresh'], 'erosion_iteration': session['erosion'], 'dilation_iteration': session['dilation'], 'minimum_distance': session['min_dist']} export_file_path_config = os.path.join(session['export_dir'], "config.txt") with open(export_file_path_config, 'w') as file: file.write(json.dumps(config)) def retrieve_file_paths(dirName): # setup file paths variable filePaths = [] # Read all directory, subdirectories and file lists for root, directories, files in os.walk(dirName): for filename in files: # Create the full filepath by using os module. filePath = os.path.join(root, filename) filePaths.append(filePath) # return all paths return filePaths # Call the function to retrieve all files and folders of the assigned directory filePaths = retrieve_file_paths(session['export_dir']) # printing the list of all files to be zipped logger.info('The following list of files will be zipped:') for fileName in filePaths: logger.info(fileName) zip_file_path = session['export_dir'] + '.zip' zip_file = zipfile.ZipFile(zip_file_path, 'w') with zip_file: # writing each file one by one for file in filePaths: zip_file.write(file) return_data = io.BytesIO() with open(zip_file_path, 'rb') as fo: return_data.write(fo.read()) # (after writing, cursor will be at last byte, so move it to start) return_data.seek(0) os.remove(zip_file_path) shutil.rmtree(session['export_dir']) return send_file(return_data, mimetype='application/zip', attachment_filename='download.zip')
def test_arbitrary_plane_on_xzt_img(self): obj = LifFile( "./tests/LeicaLASX_wavelength-sweep_example.lif").get_image(0) with self.assertRaises(NotImplementedError): obj.get_plane(display_dims=(1, 5), c=0, requested_dims={2: 31})
def test_new_lasx(self): obj = LifFile("./tests/new_lasx.lif") self.assertEqual(len(obj.image_list), 1)
def _compute_offsets(lif: LifFile) -> Tuple[List[np.ndarray], np.ndarray]: """ Compute the offsets for each of the YX planes so that the LifFile object doesn't need to be created for each YX plane read. Parameters ---------- lif : LifFile The LifFile object with an open file pointer to the file. Returns ------- List[numpy.ndarray] The list of numpy arrays holds the offsets and it should be accessed as [S][T,C,Z]. numpy.ndarray The second numpy array holds the plane read length per Scene. """ scene_list = [] scene_img_length_list = [] for s_index, img in enumerate(lif.get_iter_image()): pixel_type = LifReader.get_pixel_type(lif.xml_root, s_index) ( x_size, y_size, z_size, t_size, ) = img.dims # in comments in this block these correspond to X, Y, Z, T c_size = img.channels # C img_offset, img_block_length = img.offsets offsets = np.zeros(shape=(t_size, c_size, z_size), dtype=np.uint64) t_offset = c_size * z_size z_offset = c_size seek_distance = c_size * z_size * t_size if img_block_length == 0: # In the case of a blank image, we can calculate the length from # the metadata in the LIF. When this is read by the parser, # it is set to zero initially. log.debug( "guessing image length: LifFile assumes 1byte per pixel," " but I think this is wrong!" ) image_len = seek_distance * x_size * y_size * pixel_type.itemsize else: # B = bytes per pixel image_len = int( img_block_length / seek_distance ) # B*X*Y*C*Z*T / C*Z*T = B*X*Y = size of an YX plane for t_index in range(t_size): t_requested = t_offset * t_index # C*Z*t_index for c_index in range(c_size): c_requested = c_index for z_index in range(z_size): z_requested = z_offset * z_index # z_index * C item_requested = ( t_requested + z_requested + c_requested ) # the number of YX frames to jump # self.offsets[0] is the offset to the beginning of the image # block here we index into that block to get the offset for any # YX frame in this image block offsets[t_index, c_index, z_index] = np.uint64( img.offsets[0] + image_len * item_requested ) scene_list.append(offsets) scene_img_length_list.append(image_len) return scene_list, np.asarray(scene_img_length_list, dtype=np.uint64)
def _daread( img: Path, offsets: List[np.ndarray], read_lengths: np.ndarray, chunk_by_dims: List[str] = [ Dimensions.SpatialZ, Dimensions.SpatialY, Dimensions.SpatialX, ], S: int = 0, ) -> Tuple[da.core.Array, str]: """ Read a LIF image file as a delayed dask array where certain dimensions act as the chunk size. Parameters ---------- img: Path The filepath to read. offsets: List[numpy.ndarray] A List of numpy ndarrays offsets, see _compute_offsets for more details. read_lengths: numpy.ndarray A 1D numpy array of read lengths, the index is the scene index chunk_by_dims: List[str] The dimensions to use as the for mapping the chunks / blocks. Default: [Dimensions.SpatialZ, Dimensions.SpatialY, Dimensions.SpatialX] Note: SpatialY and SpatialX will always be added to the list if not present. S: int If the image has different dimensions on any scene from another, the dask array construction will fail. In that case, use this parameter to specify a specific scene to construct a dask array for. Default: 0 (select the first scene) Returns ------- img: dask.array.core.Array The constructed dask array where certain dimensions are chunked. dims: str The dimension order as a string. """ # Get image dims indicies lif = LifFile(filename=img) image_dim_indices = LifReader._dims_shape(lif=lif) # Catch inconsistent scene dimension sizes if len(image_dim_indices) > 1: # Choose the provided scene try: image_dim_indices = image_dim_indices[S] log.info( f"File contains variable dimensions per scene, " f"selected scene: {S} for data retrieval." ) except IndexError: raise exceptions.InconsistentShapeError( f"The LIF image provided has variable dimensions per scene. " f"Please provide a valid index to the 'S' parameter to create a " f"dask array for the index provided. " f"Provided scene index: {S}. Scene index range: " f"0-{len(image_dim_indices)}." ) else: # If the list is length one that means that all the scenes in the image # have the same dimensions # Just select the first dictionary in the list image_dim_indices = image_dim_indices[0] # Uppercase dimensions provided to chunk by dims chunk_by_dims = [d.upper() for d in chunk_by_dims] # Always add Y and X dims to chunk by dims because that is how LIF files work if Dimensions.SpatialY not in chunk_by_dims: log.info( "Adding the Spatial Y dimension to chunk by dimensions as it was not " "found." ) chunk_by_dims.append(Dimensions.SpatialY) if Dimensions.SpatialX not in chunk_by_dims: log.info( "Adding the Spatial X dimension to chunk by dimensions as it was not " "found." ) chunk_by_dims.append(Dimensions.SpatialX) # Setup read dimensions for an example chunk first_chunk_read_dims = {} for dim, (dim_begin_index, dim_end_index) in image_dim_indices.items(): # Only add the dimension if the dimension isn't a part of the chunk if dim not in chunk_by_dims: # Add to read dims first_chunk_read_dims[dim] = dim_begin_index # Read first chunk for information used by dask.array.from_delayed sample, sample_dims = LifReader._get_array_from_offset( im_path=img, offsets=offsets, read_lengths=read_lengths, meta=lif.xml_root, read_dims=first_chunk_read_dims, ) # Get the shape for the chunk and operating shape for the dask array # We also collect the chunk and non chunk dimension ordering so that we can # swap the dimensions after we block the dask array together. sample_chunk_shape = [] operating_shape = [] non_chunk_dimension_ordering = [] chunk_dimension_ordering = [] for i, dim_info in enumerate(sample_dims): # Unpack dim info dim, size = dim_info # If the dim is part of the specified chunk dims then append it to the # sample, and, append the dimension to the chunk dimension ordering if dim in chunk_by_dims: sample_chunk_shape.append(size) chunk_dimension_ordering.append(dim) # Otherwise, append the dimension to the non chunk dimension ordering, and, # append the true size of the image at that dimension else: non_chunk_dimension_ordering.append(dim) operating_shape.append( image_dim_indices[dim][1] - image_dim_indices[dim][0] ) # Convert shapes to tuples and combine the non and chunked dimension orders as # that is the order the data will actually come out of the read data as sample_chunk_shape = tuple(sample_chunk_shape) blocked_dimension_order = ( non_chunk_dimension_ordering + chunk_dimension_ordering ) # Fill out the rest of the operating shape with dimension sizes of 1 to match # the length of the sample chunk. When dask.block happens it fills the # dimensions from inner-most to outer-most with the chunks as long as the # dimension is size 1. Basically, we are adding empty dimensions to the # operating shape that will be filled by the chunks from dask operating_shape = tuple(operating_shape) + (1,) * len(sample_chunk_shape) # Create empty numpy array with the operating shape so that we can iter through # and use the multi_index to create the readers. lazy_arrays = np.ndarray(operating_shape, dtype=object) # We can enumerate over the multi-indexed array and construct read_dims # dictionaries by simply zipping together the ordered dims list and the current # multi-index plus the begin index for that plane. We then set the value of the # array at the same multi-index to the delayed reader using the constructed # read_dims dictionary. dims = [d for d in Dimensions.DefaultOrder] begin_indicies = tuple(image_dim_indices[d][0] for d in dims) for i, _ in np.ndenumerate(lazy_arrays): # Add the czi file begin index for each dimension to the array dimension # index this_chunk_read_indicies = ( current_dim_begin_index + curr_dim_index for current_dim_begin_index, curr_dim_index in zip(begin_indicies, i) ) # Zip the dims with the read indices this_chunk_read_dims = dict( zip(blocked_dimension_order, this_chunk_read_indicies) ) # Remove the dimensions that we want to chunk by from the read dims for d in chunk_by_dims: if d in this_chunk_read_dims: this_chunk_read_dims.pop(d) # Add delayed array to lazy arrays at index lazy_arrays[i] = da.from_delayed( delayed(LifReader._imread)( img, offsets, read_lengths, lif.xml_root, this_chunk_read_dims ), shape=sample_chunk_shape, dtype=sample.dtype, ) # Convert the numpy array of lazy readers into a dask array and fill the inner # most empty dimensions with chunks merged = da.block(lazy_arrays.tolist()) # Because we have set certain dimensions to be chunked and others not # we will need to transpose back to original dimension ordering # Example being, if the original dimension ordering was "SZYX" and we want to # chunk by "S", "Y", and "X" we created an array with dimensions ordering "ZSYX" transpose_indices = [] transpose_required = False for i, d in enumerate(Dimensions.DefaultOrder): new_index = blocked_dimension_order.index(d) if new_index != i: transpose_required = True transpose_indices.append(new_index) else: transpose_indices.append(i) # Only run if the transpose is actually required # The default case is "Z", "Y", "X", which _usually_ doesn't need to be # transposed because that is _usually_ # The normal dimension order of the LIF file anyway if transpose_required: merged = da.transpose(merged, tuple(transpose_indices)) # Because dimensions outside of Y and X can be in any order and present or not # we also return the dimension order string. return merged, "".join(dims)
def test_settings(self): obj = LifFile("./tests/testdata_2channel_xz.lif").get_image(0) self.assertEqual(obj.settings["ObjectiveNumber"], '11506353')
def test_not_lif_file(self): with self.assertRaises(ValueError): LifFile("./tests/tiff/c0z0t0.tif")
def _get_array_from_offset( im_path: Path, offsets: List[np.ndarray], read_lengths: np.ndarray, meta: Element, read_dims: Optional[Dict[str, int]] = None, ) -> Tuple[np.ndarray, List[Tuple[str, int]]]: """ Gets specified bitmap data from the lif file (private). Parameters ---------- im_path: Path Path to the LIF file to read. offsets: List[numpy.ndarray] A List of numpy ndarrays offsets, see _compute_offsets for more details. read_lengths: numpy.ndarray A 1D numpy array of read lengths, the index is the scene index read_dims: Optional[Dict[str, int]] The dimensions to read from the file as a dictionary of string to integer. Default: None (Read all data from the image) Returns ------- numpy.ndarray a stack of images as a numpy.ndarray List[Tuple[str, int]] The shape of the data being returned """ if read_dims is None: read_dims = {} lif = LifFile(im_path) # Data has already been checked for consistency. The dims are either consistent # or S is specified selected_ranges get's the ranges for the Dimension for the # range unless the dim is explicitly specified selected_ranges = LifReader._read_dims_to_ranges(lif, read_dims) s_index = read_dims[Dimensions.Scene] if Dimensions.Scene in read_dims else 0 lif_img = lif.get_image(img_n=s_index) x_size = lif_img.dims[0] y_size = lif_img.dims[1] pixel_type = LifReader.get_pixel_type(meta, s_index) # The ranged dims ranged_dims = [ (dim, len(selected_ranges[dim])) for dim in [ Dimensions.Scene, Dimensions.Time, Dimensions.Channel, Dimensions.SpatialZ, ] ] img_stack = [] # Loop through the dim ranges to return the requested image stack with open(str(im_path), "rb") as image: for s_index in selected_ranges[Dimensions.Scene]: for t_index in selected_ranges[Dimensions.Time]: for c_index in selected_ranges[Dimensions.Channel]: for z_index in selected_ranges[Dimensions.SpatialZ]: # Use the precalculated offset to jump to the begining of # the desired YX plane image.seek(offsets[s_index][t_index, c_index, z_index]) # Read the image data as a bytearray byte_array = image.read(read_lengths[s_index]) # Convert the bytearray to a the type pixel_type typed_array = np.frombuffer( byte_array, dtype=pixel_type ).reshape(x_size, y_size) # LIF stores YX planes so transpose them to get YX typed_array = typed_array.transpose() # Append the YX plane to the image stack. img_stack.append(typed_array) shape = [len(selected_ranges[dim[0]]) for dim in ranged_dims] shape.append(y_size) shape.append(x_size) ranged_dims.append((Dimensions.SpatialY, y_size)) ranged_dims.append((Dimensions.SpatialX, x_size)) return ( np.array(img_stack).reshape(*shape), ranged_dims, ) # in some subset of STCZYX order