Skip to content

Commit

Permalink
Add support for supersampling backprojection to match mip levels.
Browse files Browse the repository at this point in the history
  • Loading branch information
We-Gold committed Aug 7, 2024
1 parent bd86039 commit a6240db
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 16 deletions.
11 changes: 2 additions & 9 deletions python/ouroboros/common/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,5 @@

server_logger = logging.getLogger("uvicorn")

DEFAULT_LOGGER = server_logger


def get_logger():
return DEFAULT_LOGGER


def get_server_logger():
return server_logger
# Primary export, import this from other modules
logger = server_logger
10 changes: 5 additions & 5 deletions python/ouroboros/common/server_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
save_output_for_backproject_docker,
save_output_for_slice_docker,
)
from ouroboros.common.logging import get_logger
from ouroboros.common.logging import logger
from ouroboros.common.pipelines import backproject_pipeline, slice_pipeline
from ouroboros.common.server_types import BackProjectTask, SliceTask, Task

Expand All @@ -28,8 +28,8 @@ def handle_slice_core(task: SliceTask, slice_options):
return error

# Log the pipeline statistics
get_logger().info("Slice Pipeline Statistics:")
get_logger().info(pipeline.get_step_statistics())
logger.info("Slice Pipeline Statistics:")
logger.info(pipeline.get_step_statistics())


def handle_slice(task: SliceTask):
Expand Down Expand Up @@ -88,8 +88,8 @@ def handle_backproject_core(task: BackProjectTask, options):
return error

# Log the pipeline statistics
get_logger().info("Backproject Pipeline Statistics:")
get_logger().info(pipeline.get_step_statistics())
logger.info("Backproject Pipeline Statistics:")
logger.info(pipeline.get_step_statistics())


def handle_backproject(task: BackProjectTask):
Expand Down
2 changes: 1 addition & 1 deletion python/ouroboros/helpers/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class CommonOptions(BaseModel):
flush_cache: bool = False # Whether to flush the cache after processing
make_single_file: bool = True # Whether to save the output to a single file
max_ram_gb: int = 0 # Maximum amount of RAM to use in GB (0 means no limit)
output_mip_level: int = 0 # MIP level for the output image layer


@model_with_json
Expand All @@ -33,7 +34,6 @@ class SliceOptions(CommonOptions):
False # Whether to connect the start and end of the given annotation points
)
annotation_mip_level: int = 0 # MIP level for the annotation layer
output_mip_level: int = 0 # MIP level for the output image layer

@field_serializer("bounding_box_params")
def serialize_bounding_box_params(self, value: BoundingBoxParams):
Expand Down
211 changes: 210 additions & 1 deletion python/ouroboros/pipeline/backproject_pipeline.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import scipy
import scipy.ndimage
from ouroboros.helpers.memory_usage import GIGABYTE, calculate_gigabytes_from_dimensions
from ouroboros.helpers.slice import (
detect_color_channels,
generate_coordinate_grid_for_rect,
make_volume_binary,
write_slices_to_volume,
)
from ouroboros.helpers.volume_cache import VolumeCache
from ouroboros.helpers.volume_cache import VolumeCache, get_mip_volume_sizes
from ouroboros.helpers.bounding_boxes import BoundingBox
from .pipeline import PipelineStep
from ouroboros.helpers.options import BackprojectOptions
Expand Down Expand Up @@ -346,6 +348,37 @@ def _process(self, input_data: any) -> tuple[any, None] | tuple[None, any]:
except BaseException as e:
return f"Error creating single tif file: {e}"

# Rescale the backprojected volume to the output mip level
if pipeline_input.slice_options.output_mip_level != config.output_mip_level:
output_name = f"{folder_path}-temp"

error = rescale_mip_volume(
pipeline_input.source_url,
pipeline_input.slice_options.output_mip_level,
config.output_mip_level,
single_path=(
None if config.make_single_file is False else folder_path + ".tif"
),
folder_path=(folder_path if config.make_single_file is False else None),
output_name=output_name,
compression=config.backprojection_compression,
)

if error is not None:
return error

# Remove the original backprojected volume
if config.make_single_file:
os.remove(folder_path + ".tif")
else:
shutil.rmtree(folder_path)

# Rename the rescaled volume
if config.make_single_file:
os.rename(output_name + ".tif", folder_path + ".tif")
else:
os.rename(output_name, folder_path)

# Update the pipeline input with the output file path
pipeline_input.backprojected_folder_path = folder_path

Expand Down Expand Up @@ -507,3 +540,179 @@ def create_volume_chunks(
chunks_and_boxes.append((bounding_box, chunk_boxes))

return chunks_and_boxes


def rescale_mip_volume(
source_url,
current_mip,
target_mip,
single_path=None,
folder_path=None,
output_name="out",
compression=None,
) -> str | None:
"""
Rescale the volume to the mip level.
Parameters
----------
source_url : str
The URL of the volume.
current_mip : int
The current mip level of the volume.
target_mip : int
The target mip level of the volume.
single_path : str
The path to the single tif file.
folder_path : str
The path to the folder containing the tif files.
compression : str, optional
The compression to use for the resulting tif file.
The default is None.
Returns
-------
str | None
Error message if an error occurred.
"""

if single_path is None and folder_path is None:
return "Either single_path or folder_path must be provided."

if target_mip == current_mip:
return None

if single_path is not None:
return rescale_single_tif(
source_url,
current_mip,
target_mip,
single_path,
compression=compression,
file_name=output_name + ".tif",
)

return rescale_folder_tif(
source_url,
current_mip,
target_mip,
folder_path,
compression=compression,
folder_name=output_name,
)


def rescale_single_tif(
source_url,
current_mip,
target_mip,
single_path,
file_name="out.tif",
compression=None,
):
with tifffile.TiffFile(single_path) as tif:
tif_shape = (len(tif.pages),) + tif.pages[0].shape

scaling_factors, _ = calculate_scaling_factors(
source_url, current_mip, target_mip, tif_shape
)

with tifffile.TiffWriter(file_name) as output_volume:
for i in range(tif_shape[0]):
tif_layer = tif.pages[i].asarray()

# Give the tif a new 0th dimension so that it can be resized along all axes
tif_layer = np.expand_dims(tif_layer, axis=0)

layers = scipy.ndimage.zoom(tif_layer, scaling_factors, order=3)

size = layers.shape[0]

# Save the layers to the tif file
for j in range(size):
output_volume.write(
layers[j],
contiguous=compression is None or compression == "none",
compression=compression,
software="ouroboros",
)

return None


def rescale_folder_tif(
source_url,
current_mip,
target_mip,
folder_path,
folder_name="out",
compression=None,
):
# Create output folder if it doesn't exist
output_folder = folder_name
os.makedirs(output_folder, exist_ok=True)

tifs = get_sorted_tif_files(folder_path)

if len(tifs) == 0:
return "No tif files found in the folder."

# Determine the shape of the tif stack
new_shape = (len(tifs), *tifffile.imread(join_path(folder_path, tifs[0])).shape)

scaling_factors, resolution_factors = calculate_scaling_factors(
source_url, current_mip, target_mip, new_shape
)

num_digits = len(str(len(tifs)))

first_index = int(tifs[0].split(".")[0])

output_index = int(first_index * resolution_factors[0])

# Resize the volume
for i in range(len(tifs)):
tif = tifffile.imread(join_path(folder_path, tifs[i]))

# Give the tif a new 0th dimension so that it can be resized along all axes
tif = np.expand_dims(tif, axis=0)

layers = scipy.ndimage.zoom(tif, scaling_factors, order=3)

size = layers.shape[0]

# Write the layers to new tif files
for j in range(size):
tifffile.imwrite(
join_path(output_folder, f"{str(output_index).zfill(num_digits)}.tif"),
layers[j],
contiguous=True if compression is None else False,
compression=compression,
)
output_index += 1

return None


def calculate_scaling_factors(source_url, current_mip, target_mip, tif_shape):
# Determine the current and target resolutions
mip_sizes = get_mip_volume_sizes(source_url)

current_resolution = mip_sizes[current_mip]
target_resolution = mip_sizes[target_mip]

# Determine the scaling factor for each axis as a tuple
resolution_factors = tuple(
max(target_resolution[i] / current_resolution[i], 1)
for i in range(len(target_resolution))
)

has_color_channels = len(tif_shape) == 4
num_channels = tif_shape[-1] if has_color_channels else 1

# Determine the scaling factor for each axis as a tuple
scaling_factors = resolution_factors + (
(num_channels,) if has_color_channels else ()
)

return scaling_factors, resolution_factors
1 change: 1 addition & 0 deletions src/renderer/src/interfaces/options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ export class BackprojectOptionsFile extends CompoundEntry {
new Entry('config_path', 'Slice Configuration File', '', 'filePath'),
new Entry('output_file_folder', 'Output File Folder', './', 'filePath'),
new Entry('output_file_name', 'Output File Name', 'sample', 'string'),
new Entry('output_mip_level', 'Output MIP Level', 0, 'number'),
new Entry('backprojection_compression', 'Backprojection Compression', 'zlib', 'string'),
new Entry('make_single_file', 'Output Single File', false, 'boolean'),
new Entry('backproject_min_bounding_box', 'Output Min Bounding Box', true, 'boolean'),
Expand Down

0 comments on commit a6240db

Please sign in to comment.