diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index 8a63bef..6b0342b 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -35,11 +35,11 @@ jobs:
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- - name: Install cosign
+ - name: Install Cosign
if: github.event_name != 'pull_request'
- uses: sigstore/cosign-installer@f3c664df7af409cb4873aa5068053ba9d61a57b6 #v2.6.0
+ uses: sigstore/cosign-installer@v3.1.1
with:
- cosign-release: 'v1.11.0'
+ cosign-release: 'v2.2.1'
# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..5b25f54
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,23 @@
+# schapirolabor/background_subtraction: Changelog
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## v0.4.1 - [2023.11.21]
+
+Complete rework of Backsub to include Palom's pyramid writer (https://github.com/labsyspharm/palom).
+Added dask array chunking and delayed execution for subtraction that happenes while the output pyramidal `ome.tif` is being created.
+Added `CHANGELOG.md`.
+
+### `Added`
+- `--chunk-size` parameter
+- Palom's pyramid writer
+
+### `Fixed`
+- Fixed issue with RAM inefficiency - reworked Backsub.
+
+### `Removed`
+- `--pyramid` tag introduced in v0.3.4, for smaller images, a smaller tile size should be specified now.
+
+
+I did not keep a changelog before version v0.4.1.
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 5cb0955..91574fc 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,13 @@
-FROM mambaorg/micromamba:0.26.0
-COPY --chown=$MAMBA_USER:$MAMBA_USER environment.yml /tmp/env.yaml
-RUN micromamba install -y -n base -f /tmp/env.yaml && \
- micromamba clean --all --yes
-ENV PATH="${PATH}:/opt/conda/bin"
+FROM continuumio/miniconda3
+
+COPY environment.yml .
+RUN apt-get update -qq && apt-get install -y \
+ build-essential \
+ ffmpeg \
+ libsm6 \
+ libxext6
+
+RUN conda env create -f environment.yml
+ENV PATH="/opt/conda/envs/backsub/bin:$PATH"
WORKDIR /background_subtraction
-COPY . .
+COPY . .
\ No newline at end of file
diff --git a/README.md b/README.md
index 2428a95..03b7f18 100644
--- a/README.md
+++ b/README.md
@@ -13,13 +13,13 @@ Marker*corrected* = Marker*raw* - Background / Exposure>> [
+ 'x_1',
+ 'x_2',
+ 'x_3',
+ 'x_4',
+ 'x_5',
+ 'x_6',
+ 'Mosaic 4_1',
+ 'Mosaic 4_2',
+ 'Mosaic 4_3',
+ 'Mosaic 4_4'
+ ]
+ '''
+ matched_channel_names = []
+ for idx, (n, c) in enumerate( zip(channel_names, num_channels_each_mosaic*num_channels_each_mosaic[0]) ):
+ c=1
+ nl = n
+ if type(n) == str:
+ nl = [n]
+ if len(nl) == 1:
+ nl = nl
+ matched_channel_names.append(nl)
+ return make_unique_str(
+ [n for l in matched_channel_names for n in l]
+ )
+
+def make_unique_str(str_list):
+ if len(set(str_list)) == len(str_list):
+ return str_list
+ else:
+ max_length = max([len(s) for s in str_list])
+ str_np = np.array(str_list, dtype=np.dtype(('U', max_length+10)))
+ unique, counts = np.unique(str_np, return_counts=True)
+ has_duplicate = unique[counts > 1]
+ for n in has_duplicate:
+ suffixes = [
+ f"_{i}"
+ for i in range(1, (str_np == n).sum()+1)
+ ]
+ str_np[str_np == n] = np.char.add(n, suffixes)
+ return make_unique_str(list(str_np))
+
+def normalize_mosaics(mosaics):
+ dtypes = set(m.dtype for m in mosaics)
+ if any([np.issubdtype(d, np.floating) for d in dtypes]):
+ max_dtype = np.dtype(np.float32)
+ else:
+ max_dtype = max(dtypes)
+ normalized = []
+ for m in mosaics:
+ assert m.ndim == 2 or m.ndim == 3
+ if m.ndim == 2:
+ m = m[np.newaxis, :]
+ normalized.append(m.astype(max_dtype, copy=False))
+ return normalized
+
+
+def write_pyramid(
+ mosaics,
+ output_path,
+ pixel_size=1,
+ channel_names=None,
+ verbose=True,
+ downscale_factor=4,
+ compression=None,
+ is_mask=False,
+ tile_size=None,
+ save_RAM=False,
+ kwargs_tifffile=None
+):
+ mosaics = normalize_mosaics(mosaics)
+ ref_m = mosaics[0]
+ path = output_path
+ num_channels = count_num_channels(mosaics)
+ base_shape = ref_m.shape[1:3]
+ assert int(downscale_factor) == downscale_factor
+ assert downscale_factor < min(base_shape)
+ pyramid_setting = PyramidSetting(
+ downscale_factor=int(downscale_factor),
+ tile_size=max(ref_m.chunksize)
+ )
+ num_levels = pyramid_setting.num_levels(base_shape)
+ tile_shapes = pyramid_setting.tile_shapes(base_shape)
+ shapes = pyramid_setting.pyramid_shapes(base_shape)
+
+ if tile_size is not None:
+ assert tile_size % 16 == 0, (
+ f"tile_size must be None or multiples of 16, not {tile_size}"
+ )
+ tile_shapes = [(tile_size, tile_size)] * num_levels
+ print(tile_shapes)
+ dtype = ref_m.dtype
+
+ software = f'backsub {_version}'
+ metadata = {
+ 'Creator': software,
+ 'Pixels': {
+ 'PhysicalSizeX': pixel_size, 'PhysicalSizeXUnit': '\u00b5m',
+ 'PhysicalSizeY': pixel_size, 'PhysicalSizeYUnit': '\u00b5m'
+ },
+ }
+
+ if channel_names is not None:
+ num_channels_each_mosaic = [
+ count_num_channels([m])
+ for m in mosaics
+ ]
+ names = format_channel_names(num_channels_each_mosaic, channel_names)
+ if len(names) == num_channels:
+ metadata.update({
+ 'Channel': {'Name': names},
+ })
+
+ logger.info(f"Writing to {path}")
+ with tifffile.TiffWriter(path, bigtiff=True) as tif:
+ kwargs = dict(
+ metadata=metadata,
+ software=software,
+ compression=compression
+ )
+ if kwargs_tifffile is None:
+ kwargs_tifffile = {}
+ tif.write(
+ data=tile_from_combined_mosaics(
+ mosaics, tile_shape=tile_shapes[0], save_RAM=save_RAM
+ ),
+ shape=(num_channels, *shapes[0]),
+ subifds=int(num_levels - 1),
+ dtype=dtype,
+ tile=tile_shapes[0],
+ **{**kwargs, **kwargs_tifffile}
+ )
+ logger.info('Generating pyramid')
+ for level, (shape, tile_shape) in enumerate(
+ zip(shapes[1:], tile_shapes[1:])
+ ):
+ if verbose:
+ logger.info(f" Level {level+1} ({shape[0]} x {shape[1]})")
+ tif.write(
+ data=tile_from_pyramid(
+ path,
+ num_channels,
+ tile_shape=tile_shape,
+ downscale_factor=downscale_factor,
+ level=level,
+ is_mask=is_mask,
+ save_RAM=save_RAM
+ ),
+ shape=(num_channels, *shape),
+ subfiletype=1,
+ dtype=dtype,
+ tile=tile_shape,
+ **{
+ **dict(compression=compression),
+ **kwargs_tifffile
+ }
+ )
+
+def count_num_channels(imgs):
+ for img in imgs:
+ assert img.ndim == 2 or img.ndim == 3
+ return sum([
+ 1 if img.ndim == 2 else img.shape[0]
+ for img in imgs
+ ])
+
+
+def tile_from_combined_mosaics(mosaics, tile_shape, save_RAM=False):
+ num_rows, num_cols = mosaics[0].shape[1:3]
+ h, w = tile_shape
+ n = len(mosaics)
+ for idx, m in enumerate(mosaics):
+ for cidx, c in enumerate(m):
+ # the performance is heavily degraded without pre-computing the
+ # mosaic channel
+ with tqdm.dask.TqdmCallback(
+ ascii=True,
+ desc=(
+ f"Assembling mosaic {idx+1:2}/{n:2} (channel"
+ f" {cidx+1:2}/{m.shape[0]:2})"
+ ),
+ ):
+ c = da_to_zarr(c) if save_RAM else c.compute()
+ for y in range(0, num_rows, h):
+ for x in range(0, num_cols, w):
+ yield np.array(c[y:y+h, x:x+w])
+ # yield m[y:y+h, x:x+w].copy().compute()
+ c = None
+
+def tile_from_pyramid(
+ path,
+ num_channels,
+ tile_shape,
+ downscale_factor=2,
+ level=0,
+ is_mask=False,
+ save_RAM=False
+):
+ # workaround progress bar
+ # https://forum.image.sc/t/tifffile-ome-tiff-generation-is-taking-too-much-ram/41865/26
+ pbar = tqdm.tqdm(total=num_channels, ascii=True, desc=f'Processing channel')
+ for c in range(num_channels):
+ img = da.from_zarr(zarr.open(tifffile.imread(
+ path, series=0, level=level, aszarr=True
+ )))
+ if img.ndim == 2:
+ img = img.reshape(1, *img.shape)
+ img = img[c]
+ # read using key seems to generate a RAM spike
+ # img = tifffile.imread(path, series=0, level=level, key=c)
+ if not is_mask:
+ img = img.map_blocks(
+ cv2.blur,
+ ksize=(downscale_factor, downscale_factor), anchor=(0, 0)
+ )
+ img = da_to_zarr(img) if save_RAM else img.compute()
+ num_rows, num_columns = img.shape
+ h, w = tile_shape
+ h *= downscale_factor
+ w *= downscale_factor
+ last_c = range(num_channels)[-1]
+ last_y = range(0, num_rows, h)[-1]
+ last_x = range(0, num_columns, w)[-1]
+ for y in range(0, num_rows, h):
+ for x in range(0, num_columns, w):
+ if (y == last_y) & (x == last_x):
+ pbar.update(1)
+ if c == last_c:
+ pbar.close()
+ yield np.array(img[y:y+h:downscale_factor, x:x+w:downscale_factor])
+ # setting img to None seems necessary to prevent RAM spike
+ img = None
+
+
+def da_to_zarr(da_img, zarr_store=None, num_workers=None, out_shape=None, chunks=None):
+ if zarr_store is None:
+ if out_shape is None:
+ out_shape = da_img.shape
+ if chunks is None:
+ chunks = da_img.chunksize
+ zarr_store = zarr.create(
+ out_shape,
+ chunks=chunks,
+ dtype=da_img.dtype,
+ overwrite=True
+ )
+ da_img.to_zarr(zarr_store, compute=False).compute(
+ num_workers=num_workers
+ )
+ return zarr_store
+
+
+def process_markers(markers):
+ markers['ind'] = range(0, len(markers))
+ if 'remove' not in markers:
+ markers['remove'] = ["False" for i in range(len(markers))]
+ else:
+ markers['remove'] = markers['remove'] == True
+
+ markers['keep'] = markers['remove'] == False
+
+ markers = markers.drop(columns=['remove'])
+
+ return markers
+
+def detect_pixel_size(img_path,pixel_size=None):
+ if pixel_size is None:
+ print('Pixel size overwrite not specified')
+ try:
+ metadata = ome_types.from_tiff(img_path)
+ pixel_size = metadata.images[0].pixels.physical_size_x
+ except Exception as err:
+ print(err)
+ print('Pixel size detection using ome-types failed')
+ pixel_size = None
+ return pixel_size
+
+def scale_background(background_channel, scalar):
+ background_channel = np.rint(ne.evaluate("background_channel * scalar"))
+ return np.where(background_channel>65535,65535,background_channel.astype(np.uint16))
+
+def subtract(channel_to_process, background):
+ return np.where(channel_to_process65535,65535,back.astype(np.uint16))
- # set the pixel value to 0 if the image channel value is lower than the scaled background channel value
- # otherwise, subtract.
- output[channel] = np.where(image[channel]= 1
- num_channels, h, w = level_full_shapes[level]
- tshape = tile_shapes[level] or (h, w)
- tiff = tifffile.TiffFile(outpath)
- zimg = zarr.open(tiff.aszarr(series=0, level=level-1, squeeze=False))
- for c in range(num_channels):
- sys.stdout.write(
- f"\r processing channel {c + 1}/{num_channels}"
- )
- sys.stdout.flush()
- th = tshape[0] * scale
- tw = tshape[1] * scale
- for y in range(0, zimg.shape[1], th):
- for x in range(0, zimg.shape[2], tw):
- a = zimg[c, y:y+th, x:x+tw, 0]
- a = skimage.transform.downscale_local_mean(
- a, (scale, scale)
- )
- if np.issubdtype(zimg.dtype, np.integer):
- a = np.around(a)
- a = a.astype('uint16')
- yield a
-
-def main(args):
- img_raw = AI.AICSImage(args.root)
- img = img_raw.get_image_dask_data("CYX")
-
- markers_raw = pd.read_csv(args.markers)
- markers = process_markers(copy.copy(markers_raw))
-
- output = dask.array.empty_like(img)
-
- output = subtract(img, markers, output)
- output = remove_back(output, markers)
-
- markers_raw = markers_raw[markers_raw.remove != True]
- markers_raw = markers_raw.drop("remove", axis = 1)
- markers_raw.to_csv(args.markerout, index=False)
+ background_marker = markers.iloc[np.array(markers.marker_name == markers.background[channel])]
+ scalar = markers[markers.ind == channel].exposure.values / background_marker.exposure.values
+ mosaic_out[channel] = subtract_channels(mosaic[channel], mosaic[background_marker.ind.values[0]], scalar, chunk_size)
+ print(f"Channel {markers.marker_name[channel]} ({channel}) processed, background subtraction")
+
+ # removes channels from the image as specified in the markers file
+ mosaic_out = mosaic_out[np.where(markers.keep)[0]]
+ channel_names = list(markers.marker_name[markers.keep])
+
+ write_pyramid(
+ [mosaic_out], out_path, channel_names=channel_names, downscale_factor=2, pixel_size=pixel_size, save_RAM=False, tile_size=args.tile_size
+ )
+ markers = markers[markers.keep]
+ markers = markers.drop(columns=['keep','ind'])
+ markers.to_csv(args.markerout, index=False)
- # Processing metadata - highly adapted to Lunaphore outputs
- # check if metadata is present
- try:
- print(img_raw.metadata.images[0])
- metadata = img_raw.metadata
- metadata = process_metadata(img_raw.metadata, markers)
- except:
- metadata = None
-
- if args.pixel_size != None:
- # If specified, the input pixel size is used
- pixel_size = args.pixel_size
-
- else:
- try:
- if img_raw.metadata.images[0].pixels.physical_size_x != None:
- pixel_size = img_raw.metadata.images[0].pixels.physical_size_x
- else:
- pixel_size = 1.0
- except:
- # If no pixel size specified anywhere, use default 1.0
- pixel_size = 1.0
- if (args.pyramid == True) and (int(args.tile_size)<=max(output[0].shape)):
-
- # construct levels
- tile_size = int(args.tile_size)
- scale = 2
-
- print()
- dtype = output.dtype
- base_shape = output[0].shape
- num_channels = output.shape[0]
- num_levels = (np.ceil(np.log2(max(base_shape) / tile_size)) + 1).astype(int)
- factors = 2 ** np.arange(num_levels)
- shapes = (np.ceil(np.array(base_shape) / factors[:,None])).astype(int)
- print(base_shape)
- print(np.ceil(np.log2(max(base_shape)/tile_size))+1)
-
- print("Pyramid level sizes: ")
- for i, shape in enumerate(shapes):
- print(f" level {i+1}: {format_shape(shape)}", end="")
- if i == 0:
- print("(original size)", end="")
- print()
- print()
- print(shapes)
-
- level_full_shapes = []
- for shape in shapes:
- level_full_shapes.append((num_channels, shape[0], shape[1]))
- level_shapes = shapes
- tip_level = np.argmax(np.all(level_shapes < tile_size, axis=1))
- tile_shapes = [
- (tile_size, tile_size) if i <= tip_level else None
- for i in range(len(level_shapes))
- ]
-
- # write pyramid
- with tifffile.TiffWriter(args.output, ome=True, bigtiff=True) as tiff:
- tiff.write(
- data = output,
- shape = level_full_shapes[0],
- subifds=int(num_levels-1),
- dtype=dtype,
- resolution=(10000 / pixel_size, 10000 / pixel_size, "centimeter"),
- tile=tile_shapes[0]
- )
- for level, (shape, tile_shape) in enumerate(
- zip(level_full_shapes[1:], tile_shapes[1:]), 1
- ):
- tiff.write(
- data = subres_tiles(level, level_full_shapes, tile_shapes, args.output, scale),
- shape=shape,
- subfiletype=1,
- dtype=dtype,
- tile=tile_shape
- )
- else:
- # write image
- with tifffile.TiffWriter(args.output, ome=True, bigtiff=True) as tiff:
- tiff.write(
- data = output,
- shape = output.shape,
- dtype=output.dtype,
- resolution=(10000 / pixel_size, 10000 / pixel_size, "centimeter"),
- )
- try:
- tifffile.tiffcomment(args.output, to_xml(metadata))
- except:
- pass
- # note about metadata: the channels, planes etc were adjusted not to include the removed channels, however
- # the channel ids have stayed the same as before removal. E.g if channels 1 and 2 are removed,
- # the channel ids in the metadata will skip indices 1 and 2 (channel_id:0, channel_id:3, channel_id:4 ...)
- print()
-
-
-
if __name__ == '__main__':
- # Read in arguments
+ _version = 'v0.4.1'
args = get_args()
# Run script
diff --git a/environment.yml b/environment.yml
index e17117e..957a760 100644
--- a/environment.yml
+++ b/environment.yml
@@ -1,14 +1,18 @@
-name: bsub
+name: backsub
channels:
- - anaconda
- - conda-forge
- - defaults
+ - conda-forge
+ - defaults
+ - anaconda
dependencies:
- - libgcc-ng
- - python=3.9.13
- - aicsimageio=4.9.2
- - scikit-image=0.19.2
- - numexpr=2.8.3
- - tifffile=2022.8.12
- - scipy=1.9.3
- - procps-ng
+ - "python=3.9"
+ - "openslide=3.4.1"
+ - "scikit-image=0.19.2"
+ - "numexpr=2.8.3"
+ - "tifffile=2022.8.12"
+ - "scipy=1.9.3"
+ - "pandas=2.1.1"
+ - "zarr=2.3.2"
+ - procps-ng
+ - pip
+ - pip:
+ - palom
\ No newline at end of file
diff --git a/scripts/old/background_sub_v034.py b/scripts/old/background_sub_v034.py
new file mode 100644
index 0000000..69bdd6d
--- /dev/null
+++ b/scripts/old/background_sub_v034.py
@@ -0,0 +1,313 @@
+from __future__ import print_function, division
+from multiprocessing.spawn import import_main_path
+import sys
+import copy
+import argparse
+import numpy as np
+import tifffile
+import zarr
+import skimage.transform
+from aicsimageio import aics_image as AI
+import pandas as pd
+import numexpr as ne
+from ome_types import from_tiff, to_xml
+from os.path import abspath
+from argparse import ArgumentParser as AP
+import time
+import dask
+from memory_profiler import profile
+# This API is apparently changing in skimage 1.0 but it's not clear to
+# me what the replacement will be, if any. We'll explicitly import
+# this so it will break loudly if someone tries this with skimage 1.0.
+try:
+ from skimage.util.dtype import _convert as dtype_convert
+except ImportError:
+ from skimage.util.dtype import convert as dtype_convert
+
+
+# arg parser
+def get_args():
+ # Script description
+ description="""Subtracts background - Lunaphore platform"""
+
+ # Add parser
+ parser = AP(description=description, formatter_class=argparse.RawDescriptionHelpFormatter)
+
+ # Sections
+ inputs = parser.add_argument_group(title="Required Input", description="Path to required input file")
+ inputs.add_argument("-r", "--root", dest="root", action="store", required=True, help="File path to root image file.")
+ inputs.add_argument("-m", "--markers", dest="markers", action="store", required=True, help="File path to required markers.csv file")
+ inputs.add_argument("--pixel-size", metavar="SIZE", dest = "pixel_size", type=float, default = None, action = "store",help="pixel size in microns; default is 1.0")
+ inputs.add_argument("--pyramid", dest="pyramid", required=False, default=True, help="Should output be pyramidal?")
+ inputs.add_argument("--tile-size", dest="tile_size", required=False, default=1024, help="Tile size for pyramid generation")
+ inputs.add_argument("--version", action="version", version="v0.3.4")
+
+ outputs = parser.add_argument_group(title="Output", description="Path to output file")
+ outputs.add_argument("-o", "--output", dest="output", action="store", required=True, help="Path to output file")
+ outputs.add_argument("-mo", "--marker-output", dest="markerout", action="store", required=True, help="Path to output marker file")
+
+ arg = parser.parse_args()
+
+ # Standardize paths
+ arg.root = abspath(arg.root)
+ arg.markers = abspath(arg.markers)
+ arg.output = abspath(arg.output)
+ arg.markerout = abspath(arg.markerout)
+
+ return arg
+
+def preduce(coords, img_in, img_out):
+ print(img_in.dtype)
+ (iy1, ix1), (iy2, ix2) = coords
+ (oy1, ox1), (oy2, ox2) = np.array(coords) // 2
+ tile = skimage.img_as_float32(img_in[iy1:iy2, ix1:ix2])
+ tile = skimage.transform.downscale_local_mean(tile, (2, 2))
+ tile = dtype_convert(tile, 'uint16')
+ #tile = dtype_convert(tile, img_in.dtype)
+ img_out[oy1:oy2, ox1:ox2] = tile
+
+def format_shape(shape):
+ return "%dx%d" % (shape[1], shape[0])
+
+def process_markers(markers):
+ # add column with starting indices (which match the image channel indices)
+ # this should be removed soon
+ markers['ind'] = range(0, len(markers))
+
+ # if the 'remove' column is not specified, all channels are kept. If it is
+ # present, it is converted to a boolean indicating which channels should be removed
+ if 'remove' not in markers:
+ markers['remove'] = ["False" for i in range(len(markers))]
+ else:
+ markers['remove'] = markers['remove'] == True
+ # invert the markers.remove column to indicate which columns to keep
+ markers['remove'] = markers['remove'] == False
+
+ return markers
+
+def process_metadata(metadata, markers):
+ try:
+ metadata.screens[0].reagents = [metadata.screens[0].reagents[i] for i in markers[markers.remove == True].ind]
+ except:
+ pass
+ try:
+ metadata.structured_annotations = [metadata.structured_annotations[i] for i in markers[markers.remove == True].ind]
+ except:
+ pass
+ # these two are required
+ metadata.images[0].pixels.size_c = len(markers[markers.remove == True])
+ metadata.images[0].pixels.channels = [metadata.images[0].pixels.channels[i] for i in markers[markers.remove == True].ind]
+ try:
+ metadata.images[0].pixels.planes = [metadata.images[0].pixels.planes[i] for i in markers[markers.remove == True].ind]
+ except:
+ pass
+ try:
+ metadata.images[0].pixels.tiff_data_blocks[0].plane_count = sum(markers.remove == True)
+ except:
+ pass
+ return metadata
+
+
+# NaN values return True for the statement below in this version of Python. Did not use math.isnan() since the values
+# are strings if present
+def isNaN(x):
+ return x != x
+
+def subtract_channel(image, markers, channel, background_marker, output):
+ scalar = markers[markers.ind == channel].exposure.values / background_marker.exposure.values
+
+ # create temporary dataframe which will store the multiplied background rounded up to nearest integer
+ # [0] at the end needed to get [x, y] shape, and not have [1, x, y]
+ back = copy.copy(image[background_marker.ind])[0]
+ # subtract background from processed channel and if the background intensity for a certain pixel was larger than
+ # the processed channel, set intensity to 0 (no negative values)
+ back = np.rint(ne.evaluate("back * scalar"))
+ back = np.where(back>65535,65535,back.astype(np.uint16))
+ # set the pixel value to 0 if the image channel value is lower than the scaled background channel value
+ # otherwise, subtract.
+ output[channel] = np.where(image[channel]= 1
+ num_channels, h, w = level_full_shapes[level]
+ tshape = tile_shapes[level] or (h, w)
+ tiff = tifffile.TiffFile(outpath)
+ zimg = zarr.open(tiff.aszarr(series=0, level=level-1, squeeze=False))
+ for c in range(num_channels):
+ sys.stdout.write(
+ f"\r processing channel {c + 1}/{num_channels}"
+ )
+ sys.stdout.flush()
+ th = tshape[0] * scale
+ tw = tshape[1] * scale
+ for y in range(0, zimg.shape[1], th):
+ for x in range(0, zimg.shape[2], tw):
+ a = zimg[c, y:y+th, x:x+tw, 0]
+ a = skimage.transform.downscale_local_mean(
+ a, (scale, scale)
+ )
+ if np.issubdtype(zimg.dtype, np.integer):
+ a = np.around(a)
+ a = a.astype('uint16')
+ yield a
+
+@profile
+def main(args):
+ img_raw = AI.AICSImage(args.root)
+ img = img_raw.get_image_dask_data("CYX")
+
+ markers_raw = pd.read_csv(args.markers)
+ markers = process_markers(copy.copy(markers_raw))
+
+ output = dask.array.empty_like(img)
+
+ output = subtract(img, markers, output)
+ output = remove_back(output, markers)
+
+ markers_raw = markers_raw[markers_raw.remove != True]
+ markers_raw = markers_raw.drop("remove", axis = 1)
+ markers_raw.to_csv(args.markerout, index=False)
+
+ # Processing metadata - highly adapted to Lunaphore outputs
+ # check if metadata is present
+ try:
+ print(img_raw.metadata.images[0])
+ metadata = img_raw.metadata
+ metadata = process_metadata(img_raw.metadata, markers)
+ except:
+ metadata = None
+
+ if args.pixel_size != None:
+ # If specified, the input pixel size is used
+ pixel_size = args.pixel_size
+
+ else:
+ try:
+ if img_raw.metadata.images[0].pixels.physical_size_x != None:
+ pixel_size = img_raw.metadata.images[0].pixels.physical_size_x
+ else:
+ pixel_size = 1.0
+ except:
+ # If no pixel size specified anywhere, use default 1.0
+ pixel_size = 1.0
+ if (args.pyramid == True) and (int(args.tile_size)<=max(output[0].shape)):
+
+ # construct levels
+ tile_size = int(args.tile_size)
+ scale = 2
+
+ print()
+ dtype = output.dtype
+ base_shape = output[0].shape
+ num_channels = output.shape[0]
+ num_levels = (np.ceil(np.log2(max(base_shape) / tile_size)) + 1).astype(int)
+ factors = 2 ** np.arange(num_levels)
+ shapes = (np.ceil(np.array(base_shape) / factors[:,None])).astype(int)
+ print(base_shape)
+ print(np.ceil(np.log2(max(base_shape)/tile_size))+1)
+
+ print("Pyramid level sizes: ")
+ for i, shape in enumerate(shapes):
+ print(f" level {i+1}: {format_shape(shape)}", end="")
+ if i == 0:
+ print("(original size)", end="")
+ print()
+ print()
+ print(shapes)
+
+ level_full_shapes = []
+ for shape in shapes:
+ level_full_shapes.append((num_channels, shape[0], shape[1]))
+ level_shapes = shapes
+ tip_level = np.argmax(np.all(level_shapes < tile_size, axis=1))
+ tile_shapes = [
+ (tile_size, tile_size) if i <= tip_level else None
+ for i in range(len(level_shapes))
+ ]
+
+ # write pyramid
+ with tifffile.TiffWriter(args.output, ome=True, bigtiff=True) as tiff:
+ tiff.write(
+ data = output,
+ shape = level_full_shapes[0],
+ subifds=int(num_levels-1),
+ dtype=dtype,
+ resolution=(10000 / pixel_size, 10000 / pixel_size, "centimeter"),
+ tile=tile_shapes[0]
+ )
+ for level, (shape, tile_shape) in enumerate(
+ zip(level_full_shapes[1:], tile_shapes[1:]), 1
+ ):
+ tiff.write(
+ data = subres_tiles(level, level_full_shapes, tile_shapes, args.output, scale),
+ shape=shape,
+ subfiletype=1,
+ dtype=dtype,
+ tile=tile_shape
+ )
+ else:
+ # write image
+ with tifffile.TiffWriter(args.output, ome=True, bigtiff=True) as tiff:
+ tiff.write(
+ data = output,
+ shape = output.shape,
+ dtype=output.dtype,
+ resolution=(10000 / pixel_size, 10000 / pixel_size, "centimeter"),
+ )
+ try:
+ tifffile.tiffcomment(args.output, to_xml(metadata))
+ except:
+ pass
+ # note about metadata: the channels, planes etc were adjusted not to include the removed channels, however
+ # the channel ids have stayed the same as before removal. E.g if channels 1 and 2 are removed,
+ # the channel ids in the metadata will skip indices 1 and 2 (channel_id:0, channel_id:3, channel_id:4 ...)
+ print()
+
+
+
+if __name__ == '__main__':
+ # Read in arguments
+ args = get_args()
+
+ # Run script
+ st = time.time()
+ main(args)
+ rt = time.time() - st
+ print(f"Script finished in {rt // 60:.0f}m {rt % 60:.0f}s")