From 51cb03a6e3bf46aed2c3e9539af5461757766a2e Mon Sep 17 00:00:00 2001 From: Georgi <georgi.tushev@brain.mpg.de> Date: Fri, 15 Jul 2022 12:55:42 +0200 Subject: [PATCH] use pyvips buffer --- .gitignore | 1 + README.md | 13 ++- czistitcher.py | 181 +++++++++++++++++++++++++++++++++ pystitcher.py | 255 ----------------------------------------------- requirements.txt | 5 + 5 files changed, 199 insertions(+), 256 deletions(-) create mode 100644 czistitcher.py delete mode 100644 pystitcher.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index e43b0f9..ebf9dd9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +venv/ \ No newline at end of file diff --git a/README.md b/README.md index 5be0935..72d666c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# PyStitcher +# CziStitcher Reads a list of CZI files and stitches based on viewer coordinates @@ -8,3 +8,14 @@ Reads a list of CZI files and stitches based on viewer coordinates [aicspylibczi](https://pypi.org/project/aicspylibczi/) [numpy](https://pypi.org/project/numpy/) + +## Installation + +``` +$ git clone https://gitlab.mpcdf.mpg.de/mpibr/scic/pystitcher.git +$ virtualenv -p $(which python3) venv +$ source venv/bin/activate +$ pip install --upgrade pip +$ pip install -r requirements.txt +$ python czistitcher.py /<path_to_czi_files>/image_name.queue +``` diff --git a/czistitcher.py b/czistitcher.py new file mode 100644 index 0000000..50af256 --- /dev/null +++ b/czistitcher.py @@ -0,0 +1,181 @@ +import sys +import os +import numpy +import aicspylibczi +import pyvips +import re + + +def get_files_per_queue(file_queue: str) -> list: + """retrieve all czi files in the stitching queue""" + queue = [] + file_path = os.path.dirname(file_queue) + fh = open(file_queue, "r") + for file_name in fh: + file_name = file_name.strip() + if file_name.endswith(".czi"): + file_czi = os.path.join(file_path, file_name) + queue.append(file_czi) + fh.close() + return queue + + +def get_scene_grid(czi_queue: list) -> numpy.array: + """retrieve bounding box for each czi file in a common grid""" + scene_grid = [] + for file_czi in czi_queue: + czi_obj = aicspylibczi.CziFile(file_czi) + bbox = czi_obj.get_scene_bounding_box() + dims = czi_obj.get_dims_shape() + scene_grid.append([bbox.x, bbox.y, bbox.w, bbox.h, dims[0]['Z'][1], dims[0]['C'][1]]) + scene_grid = numpy.array(scene_grid) + scene_grid[:, 0] = scene_grid[:, 0] - scene_grid[:, 0].min() + scene_grid[:, 1] = scene_grid[:, 1] - scene_grid[:, 1].min() + return scene_grid + + +def get_scene_dims(czi_scene: numpy.array, dim: chr) -> int: + if dim == 'X': + return czi_scene[:, 0].max() + czi_scene[:, 2].max() + + elif dim == 'Y': + return czi_scene[:, 1].max() + czi_scene[:, 3].max() + + elif dim == 'C': + return czi_scene[:, -1].max() + + else: + return -1 + + +# def stitch_scene(czi_queue: list, czi_scene: numpy.array) -> numpy.ndarray: +# """stitch czi images in a single stack, after max-Z-projection""" + +# stack_size_x = get_scene_dims(czi_scene, 'X') +# stack_size_y = get_scene_dims(czi_scene, 'Y') +# stack_size_c = get_scene_dims(czi_scene, 'C') + +# stack = numpy.zeros((stack_size_x, stack_size_y, stack_size_c), dtype=numpy.uint16) +# for l in range(0, len(czi_queue)): + +# czi_obj = aicspylibczi.CziFile(czi_queue[l]) +# img, shp = czi_obj.read_image() +# img = numpy.squeeze(img) +# img = numpy.max(img, axis=1) +# img = numpy.transpose(img) +# scene_x = czi_scene[l, 0] +# scene_y = czi_scene[l, 1] +# scene_w = czi_scene[l, 2] +# scene_h = czi_scene[l, 3] +# print(czi_queue[l], scene_x, scene_y, scene_w, scene_h, img.shape) +# stack[scene_x:(scene_x + scene_w), scene_y:(scene_y + scene_h), :] = img +# #break + +# return stack + +# # print("convert to pyvips") +# # images = [] +# # for c in range(0, stack_size_c): +# # images.append(pyvips.Image.new_from_array(stack[:, :, c])) +# # image = pyvips.Image.arrayjoin(images, across = 1) +# # image.set_type(pyvips.GValue.gint_type, "page-height", stack_size_y) +# # return image + + +def stitch_scene(czi_queue: list, czi_scene: numpy.array) -> pyvips.Image: + """stitch czi images in a single stack, after max-Z-projection""" + + stack_size_x = get_scene_dims(czi_scene, 'X') + stack_size_y = get_scene_dims(czi_scene, 'Y') + stack_size_c = get_scene_dims(czi_scene, 'C') + + stack = [] + for c in range(0, stack_size_c): + stack.append(pyvips.Image.black(stack_size_x, stack_size_y).cast(pyvips.enums.BandFormat.USHORT)) + + for l in range(0, len(czi_queue)): + print(czi_queue[l]) + czi_obj = aicspylibczi.CziFile(czi_queue[l]) + img, shp = czi_obj.read_image() + img = numpy.squeeze(img) #CZYX + img = numpy.max(img, axis=1) #CYX + scene_x = czi_scene[l, 0] + scene_y = czi_scene[l, 1] + for c in range(0, stack_size_c): + # pyvips will flip YX to XY with new_from_array() + frame = pyvips.Image.new_from_array(img[c, :, :]) + stack[c] = stack[c].insert(frame, scene_x, scene_y) + #break + + image = pyvips.Image.arrayjoin(stack, across = 1) + image.set_type(pyvips.GValue.gint_type, "page-height", stack_size_y) + + return image + + +def get_meta_info(file_czi: str) -> str: + czi_obj = aicspylibczi.CziFile(file_czi) + czi_meta = czi_obj.meta + return czi_obj.reader.read_meta() + + +def replace_tag(text, tag, value): + pattern = "<" + tag + ">\d+</" + tag + ">" + replace = "<" + tag + ">" + str(value) + "</" + tag + ">" + return re.sub(pattern, replace, text) + + +if __name__ == "__main__": + + czi_queue = get_files_per_queue(sys.argv[1]) + czi_scene = get_scene_grid(czi_queue) + image = stitch_scene(czi_queue, czi_scene) + + + + # metadata = get_meta_info(czi_queue[0]) + # metadata = replace_tag(metadata, "SizeX", get_scene_dims(czi_scene, 'X')) + # metadata = replace_tag(metadata, "SizeY", get_scene_dims(czi_scene, 'Y')) + # metadata = replace_tag(metadata, "SizeC", get_scene_dims(czi_scene, 'C')) + # metadata = replace_tag(metadata, "SizeZ", 1) + # metadata = re.sub("<\?xml version=\"1.0\"\?>\n","", metadata) + # metadata = re.sub("\ +<SizeM>\d+</SizeM>\n","", metadata) + + print("write tif ...") + outPath = os.path.dirname(czi_queue[0]) + outName = os.path.basename(czi_queue[0]) + outName = os.path.splitext(outName)[0] + "_stitched.tif" + outFile = os.path.join(outPath, outName) + + image_bands = get_scene_dims(czi_scene, 'C') + image_width = get_scene_dims(czi_scene, 'X') + image_height = get_scene_dims(czi_scene, 'Y') + image.set_type(pyvips.GValue.gstr_type, "image-description", + f"""<?xml version="1.0" encoding="UTF-8"?> + <OME xmlns="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"> + <Image ID="Image:0"> + <!-- Minimum required fields about image dimensions --> + <Pixels + BigEndian="false" + DimensionOrder="XYCZT" + ID="Pixels:0" + Interleaved="true" + PhysicalSizeX="0.10378322801247437" + PhysicalSizeXUnit="µm" + PhysicalSizeY="0.10378322801247437" + PhysicalSizeYUnit="µm" + SignificantBits="16" + SizeC="{image_bands}" + SizeT="1" + SizeX="{image_width}" + SizeY="{image_height}" + SizeZ="1" + Type="uint16"> + </Pixels> + </Image> + </OME>""") + + #image.set_type(pyvips.GValue.gstr_type, "image-description", metadata) + image.write_to_file(outFile, pyramid=True, subifd=True, tile=True, compression="none") \ No newline at end of file diff --git a/pystitcher.py b/pystitcher.py deleted file mode 100644 index 2ec3e12..0000000 --- a/pystitcher.py +++ /dev/null @@ -1,255 +0,0 @@ -import sys -import os -import numpy -import aicspylibczi -import pyvips -import re - - -def get_files_per_queue(file_queue: str) -> list: - """retrieve all czi files in the stitching queue""" - queue = [] - file_path = os.path.dirname(file_queue) - fh = open(file_queue, "r") - for file_name in fh: - file_name = file_name.strip() - if file_name.endswith(".czi"): - file_czi = os.path.join(file_path, file_name) - queue.append(file_czi) - fh.close() - return queue - - -def get_scene_grid(czi_queue: list) -> numpy.array: - """retrieve bounding box for each czi file in a common grid""" - scene_grid = [] - for file_czi in czi_queue: - czi_obj = aicspylibczi.CziFile(file_czi) - bbox = czi_obj.get_scene_bounding_box() - dims = czi_obj.get_dims_shape() - scene_grid.append([bbox.x, bbox.y, bbox.w, bbox.h, dims[0]['Z'][1], dims[0]['C'][1]]) - scene_grid = numpy.array(scene_grid) - scene_grid[:, 0] = scene_grid[:, 0] - scene_grid[:, 0].min() - scene_grid[:, 1] = scene_grid[:, 1] - scene_grid[:, 1].min() - return scene_grid - - -# def stitch_scene(czi_queue: list, czi_scene: numpy.array) -> numpy.ndarray: -# """stitch czi images in a single stack, after max-Z-projection""" -# stack_size_x = czi_scene[:, 0].max() -# stack_size_y = czi_scene[:, 1].max() -# stack_size_c = czi_scene[:, -1].max() -# stack = numpy.zeros((stack_size_x, stack_size_y, stack_size_c), dtype=numpy.uint16) -# for l in range(0, len(czi_queue)): -# print(czi_queue[l]) -# czi_obj = aicspylibczi.CziFile(czi_queue[l]) -# img, shp = czi_obj.read_image() -# img = numpy.squeeze(img) -# img = numpy.max(img, axis=1) -# img = numpy.transpose(img) -# scene_x = czi_scene[l, 0] -# scene_y = czi_scene[l, 1] -# scene_w = czi_scene[l, 2] -# scene_h = czi_scene[l, 3] -# stack[scene_x:(scene_x + scene_w), scene_y:(scene_y + scene_h), :] = img -# break -# return stack - - -def get_scene_dims(czi_scene: numpy.array, dim: chr) -> int: - if dim == 'X': - return czi_scene[:, 0].max() - - elif dim == 'Y': - return czi_scene[:, 1].max() - - elif dim == 'C': - return czi_scene[:, -1].max() - - else: - return -1 - - -def stitch_scene(czi_queue: list, czi_scene: numpy.array) -> pyvips.Image: - """stitch czi images in a single stack, after max-Z-projection""" - - stack_size_x = get_scene_dims(czi_scene, 'X') - stack_size_y = get_scene_dims(czi_scene, 'Y') - stack_size_c = get_scene_dims(czi_scene, 'C') - - stack = [] - for c in range(0, stack_size_c): - stack.append(pyvips.Image.black(stack_size_x, stack_size_y).cast(pyvips.enums.BandFormat.USHORT)) - - for l in range(0, len(czi_queue)): - print(czi_queue[l]) - czi_obj = aicspylibczi.CziFile(czi_queue[l]) - img, shp = czi_obj.read_image() - img = numpy.squeeze(img) - img = numpy.max(img, axis=1) - img = numpy.transpose(img) - scene_x = czi_scene[l, 0] - scene_y = czi_scene[l, 1] - for c in range(0, stack_size_c): - frame = pyvips.Image.new_from_array(img[:, :, c]) - stack[0] = stack[0].insert(frame, scene_x, scene_y) - break - - image = pyvips.Image.arrayjoin(stack, across = 1) - image.set_type(pyvips.GValue.gint_type, "page-height", stack_size_y) - - return image - - -def get_meta_info(file_czi: str) -> str: - czi_obj = aicspylibczi.CziFile(file_czi) - czi_meta = czi_obj.meta - return czi_obj.reader.read_meta() - - -def replace_tag(text, tag, value): - pattern = "<" + tag + ">\d+</" + tag + ">" - replace = "<" + tag + ">" + str(value) + "</" + tag + ">" - return re.sub(pattern, replace, text) - - -if __name__ == "__main__": - - czi_queue = get_files_per_queue(sys.argv[1]) - czi_scene = get_scene_grid(czi_queue) - image = stitch_scene(czi_queue, czi_scene) - metadata = get_meta_info(czi_queue[0]) - metadata = replace_tag(metadata, "SizeX", get_scene_dims(czi_scene, 'X')) - metadata = replace_tag(metadata, "SizeY", get_scene_dims(czi_scene, 'Y')) - metadata = replace_tag(metadata, "SizeC", get_scene_dims(czi_scene, 'C')) - metadata = replace_tag(metadata, "SizeZ", 1) - - - metadata = re.sub("<\?xml version=\"1.0\"\?>\n","", metadata) - metadata = re.sub("\ +<SizeM>\d+</SizeM>\n","", metadata) - - image_bands = get_scene_dims(czi_scene, 'C') - image_width = get_scene_dims(czi_scene, 'X') - image_height = get_scene_dims(czi_scene, 'Y') - - - image.set_type(pyvips.GValue.gstr_type, "image-description", - f"""<?xml version="1.0" encoding="UTF-8"?> - <OME xmlns="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"> - <Image ID="Image:0"> - <!-- Minimum required fields about image dimensions --> - <Pixels DimensionOrder="XYCZT" - ID="Pixels:0" - SizeC="{image_bands}" - SizeT="1" - SizeX="{image_width}" - SizeY="{image_height}" - SizeZ="1" - Type="uint16"> - </Pixels> - </Image> - </OME>""") - - image.set_type(pyvips.GValue.gstr_type, "image-description", metadata) - image.write_to_file("/Users/tushevg/Desktop/stitchme/stack.tif", pyramid=True, subifd=True, tile=True, compression="none") - - -# def ExportNDImage(image, fileName, xmlstr): -# image_width = image.shape[0] -# image_height = image.shape[1] -# image_bands = image.shape[2] -# images = [] -# for c in range(0, image_bands): -# images.append(pyvips.Image.new_from_array(image[:,:,c])) - -# out = pyvips.Image.arrayjoin(images, across=1) -# out.set_type(pyvips.GValue.gint_type, "page-height", images[0].height) -# out.set_type(pyvips.GValue.gstr_type, "image-description", -# f"""<?xml version="1.0" encoding="UTF-8"?> -# <OME xmlns="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"> -# <Image ID="Image:0"> -# <!-- Minimum required fields about image dimensions --> -# <Pixels DimensionOrder="XYCZT" -# ID="Pixels:0" -# SizeC="{image_bands}" -# SizeT="1" -# SizeX="{image_width}" -# SizeY="{image_height}" -# SizeZ="1" -# Type="uint16"> -# </Pixels> -# </Image> -# </OME>""") -# #out.set_type(pyvips.GValue.gstr_type, "image-description", xmlstr) -# out.write_to_file(fileName, pyramid=True, subifd=True, tile=True, compression="none") - - -# def ExportPyVipsImage(images, fileName, image_width, image_height, image_bands): - -# out = pyvips.Image.arrayjoin(images, across=1) -# out.set_type(pyvips.GValue.gint_type, "page-height", images[0].height) -# out.set_type(pyvips.GValue.gstr_type, "image-description", -# f"""<?xml version="1.0" encoding="UTF-8"?> -# <OME xmlns="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"> -# <Image ID="Image:0"> -# <!-- Minimum required fields about image dimensions --> -# <Pixels DimensionOrder="XYCZT" -# ID="Pixels:0" -# SizeC="{image_bands}" -# SizeT="1" -# SizeX="{image_width}" -# SizeY="{image_height}" -# SizeZ="1" -# Type="uint16"> -# </Pixels> -# </Image> -# </OME>""") -# #out.set_type(pyvips.GValue.gstr_type, "image-description", xmlstr) -# out.write_to_file(fileName, pyramid=True, subifd=True, tile=True, compression="none") - - -# if __name__ == "__main__": -# fileQueue = "/Users/tushevg/Desktop/projects/InstinctSex/20220502_AFM_15431_I/20220502_AFM_15431_1d_I.queue" -# queue = GetFilesInQueue(fileQueue) -# grid = FindGridPoints(queue) -# ndims = FindNDims(queue) - -# # calculate field of view -# image_width = (grid[:,0] + grid[:,2]).max() + 1 -# image_height = (grid[:,1] + grid[:,3]).max() + 1 -# image_bands = ndims['C'] - -# # allocate stitched image -# images = AllocateImages(image_width, image_height, image_bands) -# #ndimage = numpy.zeros((image_width, image_height, image_bands), dtype="uint16") -# #print(ndimage.shape) - -# # fill -# k = 0 -# for file in queue: -# czi = CziFile(file) -# data = czi.read_image() -# img = numpy.squeeze(data[0]) -# img = numpy.max(img, axis = 1) -# #img = numpy.moveaxis(img, [0,1,2], [2,1,0]) -# #xmlstr = ElementTree.tostring(czi.meta, encoding='unicode', method='xml') -# print(file, img.shape) -# xpos = grid[k,0] -# ypos = grid[k,1] -# #ndimage[xpos:(xpos+img.shape[0]), ypos:(ypos+img.shape[1]), :] = img -# for c in range(0, ndims['C']): -# slide = pyvips.Image.new_from_array(img[c,:,:]) -# images[c] = images[c].insert(slide, xpos, ypos) -# #break -# k = k + 1 - -# # export -# print("export tif") -# #ExportNDImage(ndimage, "stitched.tif", xmlstr) -# ExportPyVipsImage(images, "stack.tif", image_width, image_height, image_bands) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..718666e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +aicspylibczi==3.0.5 +cffi==1.15.1 +numpy==1.23.1 +pycparser==2.21 +pyvips==2.2.1 -- GitLab