Skip to content

Commit dcd42fe

Browse files
committed
Fix Binary Output
- Binary output flag is now read and causes a 2-valued (0/1) uint8 write, as compressed direct boolean tiff writes are incompatible with some software. This should also match old behavior. - Handles filesize calculation correctly for this. - Added np_convert normalization behavior for conversion to float. - Added safe_bool conversion (2-valued uint8) to np_convert - Added error handling for loading straightened volume. - Expanded np_convert tests
1 parent 1f270df commit dcd42fe

File tree

3 files changed

+83
-29
lines changed

3 files changed

+83
-29
lines changed

python/ouroboros/helpers/files.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
from functools import partial
22
from multiprocessing.pool import ThreadPool
33
import os
4-
import shutil
5-
from threading import Thread
64

75
import numpy as np
86
from numpy.typing import ArrayLike
97
from pathlib import Path
10-
from tifffile import imread, TiffWriter, TiffFile
8+
from tifffile import TiffWriter, TiffFile
119
import time
1210

1311
from .shapes import DataShape
@@ -123,24 +121,33 @@ def num_digits_for_n_files(n: int) -> int:
123121
return len(str(n - 1))
124122

125123

126-
def np_convert(dtype: np.dtype, source: ArrayLike, normalize=True):
127-
if not normalize:
128-
return source.astype(dtype)
129-
if np.issubdtype(dtype, np.integer):
130-
dtype_range = np.iinfo(dtype).max - np.iinfo(dtype).min
124+
def np_convert(target_dtype: np.dtype, source: ArrayLike, normalize=True, safe_bool=False):
125+
""" TODO: Fix for Negative Values """
126+
if safe_bool and target_dtype == bool:
127+
return source.astype(target_dtype).astype(np.uint8)
128+
elif np.issubdtype(target_dtype, np.integer) and normalize:
129+
dtype_range = np.iinfo(target_dtype).max - np.iinfo(target_dtype).min
131130
source_range = np.max(source) - np.min(source)
132131

133132
# Avoid divide by 0, esp. as numpy segfaults when you do.
134133
if source_range == 0.0:
135134
source_range = 1.0
136135

137-
return (source * max(int(dtype_range / source_range), 1)).astype(dtype)
138-
elif np.issubdtype(dtype, np.floating):
139-
return source.astype(dtype)
136+
return (source * max(int(dtype_range / source_range), 1)).astype(target_dtype)
137+
elif np.issubdtype(target_dtype, np.floating) and normalize:
138+
source_range = np.max(source) - np.min(source)
139+
140+
# Avoid divide by 0, esp. as numpy segfaults when you do.
141+
if source_range == 0.0:
142+
source_range = 1.0
143+
144+
return (source / source_range).astype(target_dtype)
145+
else:
146+
return source.astype(target_dtype)
140147

141148

142149
def generate_tiff_write(write_func: callable, compression: str | None, micron_resolution: np.ndarray[float],
143-
backprojection_offset: np.ndarray, **kwargs):
150+
backprojection_offset: np.ndarray, **kwargs):
144151
# Volume cache resolution is in voxel size, but .tiff XY resolution is in voxels per unit, so we invert.
145152
resolution = [1.0 / voxel_size for voxel_size in micron_resolution[:2] * 0.0001]
146153
resolutionunit = "CENTIMETER"
@@ -217,6 +224,6 @@ def write_conv_vol(writer: callable, source_path, shape, dtype, *args, **kwargs)
217224
vol = volume_from_intermediates(source_path, shape)
218225
perf["Merge Volume"] = time.perf_counter() - start
219226
start = time.perf_counter()
220-
writer(*args, data=np_convert(dtype, vol.reshape(shape.Y, shape.X), False), **kwargs)
227+
writer(*args, data=np_convert(dtype, vol.reshape(shape.Y, shape.X), normalize=False, safe_bool=True), **kwargs)
221228
perf["Write Merged"] = time.perf_counter() - start
222229
return perf

python/ouroboros/pipeline/backproject_pipeline.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,18 +127,25 @@ def _process(self, input_data: any) -> tuple[any, None] | tuple[None, any]:
127127
print(f"\nFront Projection Shape: {FPShape}")
128128
print(f"\nBack Projection Shape (Z/Y/X):{write_shape}")
129129

130-
pipeline_input.output_file_path = f"{config.output_file_name}_{'_'.join(map(str, full_bounding_box.get_min(np.uint32)))}"
130+
pipeline_input.output_file_path = (f"{config.output_file_name}_"
131+
f"{'_'.join(map(str, full_bounding_box.get_min(np.uint32)))}")
131132
folder_path = Path(config.output_file_folder, pipeline_input.output_file_path)
132133
folder_path.mkdir(exist_ok=True, parents=True)
133134

134135
i_path = Path(config.output_file_folder,
135136
f"{config.output_file_name}_t_{'_'.join(map(str, full_bounding_box.get_min(np.uint32)))}")
136137

137138
if config.make_single_file:
138-
is_big_tiff = calculate_gigabytes_from_dimensions(np.prod(write_shape), np.uint16) > 4 # Check Dtype
139+
is_big_tiff = calculate_gigabytes_from_dimensions(
140+
np.prod(write_shape),
141+
np.uint8 if config.make_backprojection_binary else np.uint16) > 4
139142
else:
140-
is_big_tiff = calculate_gigabytes_from_dimensions(np.prod(write_shape[1:]), np.uint16) > 4 # Check Dtype
143+
is_big_tiff = calculate_gigabytes_from_dimensions(
144+
np.prod(write_shape[1:]),
145+
np.uint8 if config.make_backprojection_binary else np.uint16) > 4
141146

147+
# Generate image writing function
148+
# Combining compression with binary images can cause issues.
142149
bp_offset = pipeline_input.backprojection_offset if config.backproject_min_bounding_box else None
143150
tif_write = partial(generate_tiff_write,
144151
compression=config.backprojection_compression,
@@ -205,7 +212,9 @@ def note_written(write_future):
205212
write_futures.append(write_executor.submit(
206213
write_conv_vol,
207214
tif_write(tifffile.imwrite), i_path.joinpath(f"i_{index:05}"),
208-
ImgSlice(*write_shape[1:]), np.uint16, folder_path.joinpath(f"{index:05}.tif")
215+
ImgSlice(*write_shape[1:]),
216+
bool if config.make_backprojection_binary else np.uint16,
217+
folder_path.joinpath(f"{index:05}.tif")
209218
))
210219
write_futures[-1].add_done_callback(note_written)
211220

@@ -285,8 +294,13 @@ def process_chunk(
285294
start_total = time.perf_counter()
286295

287296
# Load the straightened volume
288-
straightened_volume = tifffile.memmap(straightened_volume_path, mode="r")
289-
durations["memmap"] = [time.perf_counter() - start_total]
297+
try:
298+
straightened_volume = tifffile.memmap(straightened_volume_path, mode="r")
299+
durations["memmap"] = [time.perf_counter() - start_total]
300+
except BaseException as be:
301+
print(f"Error loading Volume: {be} : {straightened_volume_path}")
302+
traceback.print_tb(be.__traceback__, file=sys.stderr)
303+
raise be
290304

291305
# Get the slices from the straightened volume Dumb but maybe bugfix?
292306
start = time.perf_counter()

python/test/helpers/test_files.py

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from pathlib import Path
33

44
import numpy as np
5-
from tifffile import imwrite, TiffFile
65

76
from ouroboros.helpers.files import (
87
format_backproject_output_file,
@@ -169,14 +168,6 @@ def test_num_digits_for_n_files():
169168
assert result == 2
170169

171170

172-
def test_np_convert():
173-
float_data = np.linspace(0, 1, 16)
174-
int_data = np_convert(np.uint16, float_data)
175-
176-
assert np.all(int_data == np.arange(0, np.iinfo(np.uint16).max + 1, np.iinfo(np.uint16).max // 15))
177-
assert np.all(np_convert(np.float32, int_data) == int_data.astype(np.float32))
178-
179-
180171
def test_generate_tiff_write(tmp_path):
181172
micron_resolution = np.array([0.7, 0.7, 0.7])
182173
backprojection_offset = (55, 44, 77)
@@ -284,10 +275,52 @@ def test_increment_volume(tmp_path):
284275
assert np.allclose(volume[1, mapped_source[0]], np.sum(source_weights[[0, 2]]))
285276
assert np.all(np.nonzero(volume)[0] == np.array([0, 0, 1, 1]))
286277
assert np.all(np.nonzero(volume)[1] == np.array([3947, 3952, 3947, 3952]))
287-
278+
288279
assert not sample_path.exists()
289280

290281

282+
def test_np_convert_from_int():
283+
base = np.random.randint(0, 10, 6400).reshape(80, 80)
284+
285+
# Direct Conversion
286+
assert np.all(np_convert(np.float32, base, normalize=False) == base.astype(np.float32))
287+
288+
# Normalized Conversion
289+
assert np.all(np_convert(np.float32, base) == base.astype(np.float32) / (np.max(base) - np.min(base)))
290+
291+
# Safe Bool
292+
safe_bool = np_convert(bool, base, safe_bool=True)
293+
assert safe_bool.dtype == np.uint8
294+
assert np.all(safe_bool == (base > 0))
295+
296+
# Unsafe Bool - Raw bool datatype
297+
safe_bool = np_convert(bool, base, safe_bool=False)
298+
assert safe_bool.dtype == bool
299+
assert np.all(safe_bool == (base > 0))
300+
301+
302+
def test_np_convert_from_float():
303+
base = np.random.randint(0, 16, 6400).reshape(80, 80) * np.random.rand(80, 80)
304+
float_data = np.linspace(0, 1, 16)
305+
306+
# Direct Conversion
307+
assert np.all(np_convert(np.uint16, float_data, normalize=False) == [0] * 15 + [1])
308+
309+
# Normalized Conversion
310+
assert np.all(np_convert(np.uint16, float_data) ==
311+
np.arange(0, np.iinfo(np.uint16).max + 1, np.iinfo(np.uint16).max // 15))
312+
313+
# Safe Bool
314+
safe_bool = np_convert(bool, base, safe_bool=True)
315+
assert safe_bool.dtype == np.uint8
316+
assert np.all(safe_bool == (base > 0))
317+
318+
# Unsafe Bool - Raw bool datatype
319+
safe_bool = np_convert(bool, base, safe_bool=False)
320+
assert safe_bool.dtype == bool
321+
assert np.all(safe_bool == (base > 0))
322+
323+
291324
def test_volume_from_intermediates():
292325
pass
293326

0 commit comments

Comments
 (0)