Skip to content

Commit

Permalink
fix: update to support RGBA/JPEG and updated structure (#7103)
Browse files Browse the repository at this point in the history
  • Loading branch information
kaloster authored May 24, 2024
1 parent 8a6bd10 commit 3cf258f
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 12 deletions.
36 changes: 30 additions & 6 deletions backend/layers/processing/utils/spatial.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ def _fetch_image(self, content):
np.ndarray: The prepared image array.
"""
resolution = "fullres" if "fullres" in content["images"] else "hires"

image_array = content["images"][resolution]
image_array_uint8 = np.uint8(image_array * 255)
return image_array_uint8
image_array_uint8 = np.uint8(image_array)

return image_array_uint8, resolution

def _process_and_flip_image(self, image_array_uint8):
"""
Expand All @@ -81,10 +83,21 @@ def _process_and_flip_image(self, image_array_uint8):
Image.MAX_IMAGE_PIXELS = None # Disable the image size limit
try:
with Image.fromarray(image_array_uint8) as img:
cropped_img = img.crop(self._calculate_aspect_ratio_crop(img.size)) # Crop the image
cropped_img.save(io.BytesIO(), format="JPEG", quality=100) # Save or manipulate as needed
# #####
# Convert RGBA to RGB if needed for JPEG compatibility:
# bake the alpha channel into the image by pasting it onto
# a white background
# #####
if img.mode == "RGBA":
background = Image.new("RGB", img.size, (255, 255, 255))
background.paste(img, (0, 0), img)
img = background
cropped_img = img.crop(self._calculate_aspect_ratio_crop(img.size))
cropped_img.save(io.BytesIO(), format="JPEG", quality=100)

# Flip the image vertically due to explorer client rendering images upside down
flipped_img = cropped_img.transpose(Image.FLIP_TOP_BOTTOM)

return np.array(flipped_img)
except Exception:
logger.exception("Error processing image")
Expand All @@ -101,7 +114,7 @@ def _generate_deep_zoom_assets(self, image_array, assets_folder):
h, w, bands = image_array.shape
linear = image_array.reshape(w * h * bands)
image = pyvips.Image.new_from_memory(linear.data, w, h, bands, "uchar")
image.dzsave(os.path.join(assets_folder, "spatial"), suffix=".webp")
image.dzsave(os.path.join(assets_folder, "spatial"), suffix=".jpeg")

def _upload_assets(self, assets_folder):
"""
Expand All @@ -126,7 +139,7 @@ def create_deep_zoom_assets(self, container_name, content):
assets_folder = os.path.join(temp_dir, container_name.replace(".cxg", ""))
os.makedirs(assets_folder)

image_array = self._fetch_image(content)
image_array, _ = self._fetch_image(content)
processed_image = self._process_and_flip_image(image_array)
self._generate_deep_zoom_assets(processed_image, assets_folder)
self._upload_assets(assets_folder)
Expand All @@ -145,8 +158,19 @@ def filter_spatial_data(self, content, library_id):
Returns:
dict: The filtered spatial data.
"""
image_array_uint8, resolution = self._fetch_image(content)
with Image.fromarray(image_array_uint8) as img:
width, height = img.size
width = height = min(width, height)
crop_coords = self._calculate_aspect_ratio_crop(img.size)
return {
library_id: {
"image_properties": {
"resolution": resolution,
"crop_coords": crop_coords,
"width": width,
"height": height,
},
"images": {"hires": content["images"]["hires"], "fullres": []},
"scalefactors": {
"spot_diameter_fullres": content["scalefactors"]["spot_diameter_fullres"],
Expand Down
19 changes: 13 additions & 6 deletions tests/unit/processing/test_spatial_assets_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,14 @@ def test__valid_input_metadata_copy(spatial_processor, valid_spatial_data, libra
"""
Test case for verifying the required metadata is present
"""
# Expected output based on the input data
expected_output = {
library_id: {
"image_properties": {
"resolution": "fullres",
"crop_coords": (0, 0, 20, 20),
"width": 20,
"height": 20,
},
"images": {"hires": valid_spatial_data["images"]["hires"], "fullres": []},
"scalefactors": {
"spot_diameter_fullres": valid_spatial_data["scalefactors"]["spot_diameter_fullres"],
Expand All @@ -107,8 +112,9 @@ def test__fetch_image_fullres(spatial_processor, valid_spatial_data):
"""
Test that _fetch_image returns the fullres image when present.
"""
image_array = spatial_processor._fetch_image(valid_spatial_data)
image_array, resolution = spatial_processor._fetch_image(valid_spatial_data)
assert image_array.shape == (20, 20, 3), "Expected fullres image to be returned."
assert resolution == "fullres", "Expected fullres resolution to be returned."


def test__fetch_image_hires(spatial_processor, valid_spatial_data):
Expand All @@ -118,8 +124,9 @@ def test__fetch_image_hires(spatial_processor, valid_spatial_data):
valid_spatial_data_without_fullres = valid_spatial_data.copy()
del valid_spatial_data_without_fullres["images"]["fullres"]

image_array = spatial_processor._fetch_image(valid_spatial_data_without_fullres)
image_array, resolution = spatial_processor._fetch_image(valid_spatial_data_without_fullres)
assert image_array.shape == (10, 10, 3), "Expected hires image to be returned."
assert resolution == "hires", "Expected hires resolution to be returned."


def test__fetch_image_key_error(spatial_processor, valid_spatial_data):
Expand Down Expand Up @@ -206,7 +213,7 @@ def test__generate_deep_zoom_assets(spatial_processor, output_folder, mocker):

# verify dzsave was called correctly on the mock_image object
expected_output_path = os.path.join(assets_folder, "spatial")
mock_image.dzsave.assert_called_once_with(expected_output_path, suffix=".webp")
mock_image.dzsave.assert_called_once_with(expected_output_path, suffix=".jpeg")


def test__upload_assets(spatial_processor, output_folder, mocker):
Expand Down Expand Up @@ -248,7 +255,7 @@ def test__create_deep_zoom_assets(spatial_processor, cxg_container, valid_spatia
mock_temp_dir.return_value.__enter__.return_value = temp_dir_name

# mock return values for the internal methods
mock_fetch_image.return_value = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8)
mock_fetch_image.return_value = (np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8), "fullres")
mock_process_and_flip_image.return_value = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8)

# call the method under test
Expand All @@ -258,7 +265,7 @@ def test__create_deep_zoom_assets(spatial_processor, cxg_container, valid_spatia

# assertions to ensure each step is called
mock_fetch_image.assert_called_once_with(valid_spatial_data)
mock_process_and_flip_image.assert_called_once_with(mock_fetch_image.return_value)
mock_process_and_flip_image.assert_called_once_with(mock_fetch_image.return_value[0])
mock_generate_deep_zoom_assets.assert_called_once_with(mock_process_and_flip_image.return_value, assets_folder)
mock_upload_assets.assert_called_once_with(assets_folder)

Expand Down

0 comments on commit 3cf258f

Please sign in to comment.