|
| 1 | +# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file. |
| 2 | + |
| 3 | +"""Image ROI manipulation application test (copy/paste, import/export)""" |
| 4 | + |
| 5 | +# pylint: disable=invalid-name # Allows short reference names like x, y, ... |
| 6 | +# guitest: show |
| 7 | + |
| 8 | +from __future__ import annotations |
| 9 | + |
| 10 | +import os |
| 11 | +import tempfile |
| 12 | +from typing import TYPE_CHECKING |
| 13 | + |
| 14 | +from sigima.io import read_roi |
| 15 | +from sigima.objects import NewImageParam, create_image_roi |
| 16 | +from sigima.tests.data import create_multigaussian_image |
| 17 | + |
| 18 | +from datalab.env import execenv |
| 19 | +from datalab.objectmodel import get_uuid |
| 20 | +from datalab.tests import datalab_test_app_context |
| 21 | + |
| 22 | +if TYPE_CHECKING: |
| 23 | + from datalab.gui.panel.image import ImagePanel |
| 24 | + |
| 25 | +SIZE = 200 |
| 26 | + |
| 27 | +# Image ROIs: |
| 28 | +IROI1 = [100, 100, 75, 100] # Rectangle |
| 29 | +IROI2 = [66, 100, 50] # Circle |
| 30 | +IROI3 = [100, 100, 100, 150, 150, 133] # Polygon |
| 31 | + |
| 32 | + |
| 33 | +def test_image_roi_copy_paste(): |
| 34 | + """Test image ROI copy and paste functionality""" |
| 35 | + with datalab_test_app_context(console=False) as win: |
| 36 | + execenv.print("Image ROI Copy/Paste test:") |
| 37 | + panel: ImagePanel = win.imagepanel |
| 38 | + param = NewImageParam.create(height=SIZE, width=SIZE) |
| 39 | + |
| 40 | + # Create first image with ROI |
| 41 | + ima1 = create_multigaussian_image(param) |
| 42 | + ima1.title = "Image with ROI" |
| 43 | + roi1 = create_image_roi("rectangle", IROI1) |
| 44 | + roi1.add_roi(create_image_roi("circle", IROI2)) |
| 45 | + ima1.roi = roi1 |
| 46 | + panel.add_object(ima1) |
| 47 | + |
| 48 | + # Create second image without ROI |
| 49 | + ima2 = create_multigaussian_image(param) |
| 50 | + ima2.title = "Image without ROI" |
| 51 | + panel.add_object(ima2) |
| 52 | + |
| 53 | + # Create third image without ROI |
| 54 | + ima3 = create_multigaussian_image(param) |
| 55 | + ima3.title = "Image without ROI 2" |
| 56 | + panel.add_object(ima3) |
| 57 | + |
| 58 | + execenv.print(" Initial state:") |
| 59 | + execenv.print(f" Image 1 ROI: {ima1.roi is not None}") |
| 60 | + execenv.print(f" Image 2 ROI: {ima2.roi is not None}") |
| 61 | + execenv.print(f" Image 3 ROI: {ima3.roi is not None}") |
| 62 | + |
| 63 | + # Select first image and copy its ROI |
| 64 | + panel.objview.set_current_item_id(get_uuid(ima1)) |
| 65 | + panel.copy_roi() |
| 66 | + execenv.print(" Copied ROI from Image 1") |
| 67 | + |
| 68 | + # Select second image and paste ROI |
| 69 | + panel.objview.set_current_item_id(get_uuid(ima2)) |
| 70 | + panel.paste_roi() |
| 71 | + execenv.print(" Pasted ROI to Image 2") |
| 72 | + |
| 73 | + # Verify that ima2 now has the same ROI as ima1 |
| 74 | + assert ima2.roi is not None, "Image 2 should have ROI after paste" |
| 75 | + assert len(ima2.roi) == len(ima1.roi), "ROI should have same number of regions" |
| 76 | + execenv.print(f" Image 2 now has {len(ima2.roi)} ROI regions") |
| 77 | + |
| 78 | + # Select third image and paste ROI (should create new ROI) |
| 79 | + panel.objview.set_current_item_id(get_uuid(ima3)) |
| 80 | + panel.paste_roi() |
| 81 | + execenv.print(" Pasted ROI to Image 3") |
| 82 | + |
| 83 | + assert ima3.roi is not None, "Image 3 should have ROI after paste" |
| 84 | + assert len(ima3.roi) == len(ima1.roi), "ROI should have same number of regions" |
| 85 | + execenv.print(f" Image 3 now has {len(ima3.roi)} ROI regions") |
| 86 | + |
| 87 | + # Test pasting to image that already has ROI (should combine) |
| 88 | + panel.objview.set_current_item_id(get_uuid(ima2)) |
| 89 | + panel.copy_roi() |
| 90 | + execenv.print(" Copied ROI from Image 2") |
| 91 | + |
| 92 | + # Add a different ROI to ima1 |
| 93 | + roi_new = create_image_roi("polygon", IROI3) |
| 94 | + ima1.roi.add_roi(roi_new) |
| 95 | + original_roi_count = len(ima1.roi) |
| 96 | + execenv.print(f" Image 1 now has {original_roi_count} ROI regions") |
| 97 | + |
| 98 | + # Paste the ROI from ima2 into ima1 (should combine) |
| 99 | + panel.objview.set_current_item_id(get_uuid(ima1)) |
| 100 | + panel.paste_roi() |
| 101 | + execenv.print(" Pasted ROI to Image 1 (should combine)") |
| 102 | + |
| 103 | + # Get fresh reference to ima1 from panel |
| 104 | + ima1_updated = panel.objmodel[get_uuid(ima1)] |
| 105 | + assert ima1_updated.roi is not None, "Image 1 should still have ROI" |
| 106 | + # After combining, ima1 should have more regions than before |
| 107 | + assert len(ima1_updated.roi) >= original_roi_count, ( |
| 108 | + f"Expected at least {original_roi_count} ROI regions, " |
| 109 | + f"got {len(ima1_updated.roi)}" |
| 110 | + ) |
| 111 | + execenv.print( |
| 112 | + f" Image 1 now has {len(ima1_updated.roi)} ROI regions (combined)" |
| 113 | + ) |
| 114 | + |
| 115 | + execenv.print(" ✓ Image ROI copy/paste test passed") |
| 116 | + |
| 117 | + |
| 118 | +def test_image_roi_copy_paste_multiple_selection(): |
| 119 | + """Test image ROI paste to multiple selected images""" |
| 120 | + with datalab_test_app_context(console=False) as win: |
| 121 | + execenv.print("Image ROI Copy/Paste with multiple selection test:") |
| 122 | + panel: ImagePanel = win.imagepanel |
| 123 | + param = NewImageParam.create(height=SIZE, width=SIZE) |
| 124 | + |
| 125 | + # Create source image with ROI |
| 126 | + ima_src = create_multigaussian_image(param) |
| 127 | + ima_src.title = "Source with ROI" |
| 128 | + roi = create_image_roi("rectangle", IROI1) |
| 129 | + roi.add_roi(create_image_roi("circle", IROI2)) |
| 130 | + ima_src.roi = roi |
| 131 | + panel.add_object(ima_src) |
| 132 | + |
| 133 | + # Create multiple target images without ROI |
| 134 | + target_images = [] |
| 135 | + for i in range(3): |
| 136 | + ima = create_multigaussian_image(param) |
| 137 | + ima.title = f"Target image {i + 1}" |
| 138 | + panel.add_object(ima) |
| 139 | + target_images.append(ima) |
| 140 | + |
| 141 | + execenv.print(f" Created {len(target_images)} target images") |
| 142 | + |
| 143 | + # Copy ROI from source |
| 144 | + panel.objview.set_current_item_id(get_uuid(ima_src)) |
| 145 | + panel.copy_roi() |
| 146 | + execenv.print(" Copied ROI from source image") |
| 147 | + |
| 148 | + # Select all target images |
| 149 | + target_uuids = [get_uuid(img) for img in target_images] |
| 150 | + panel.objview.set_current_item_id(target_uuids[0]) |
| 151 | + for uuid in target_uuids[1:]: |
| 152 | + panel.objview.set_current_item_id(uuid, extend=True) |
| 153 | + |
| 154 | + execenv.print(f" Selected {len(target_uuids)} target images") |
| 155 | + |
| 156 | + # Paste ROI to all selected images |
| 157 | + panel.paste_roi() |
| 158 | + execenv.print(" Pasted ROI to all selected images") |
| 159 | + |
| 160 | + # Verify all target images have ROI |
| 161 | + for i, img in enumerate(target_images): |
| 162 | + assert img.roi is not None, f"Target image {i + 1} should have ROI" |
| 163 | + assert len(img.roi) == len(ima_src.roi), ( |
| 164 | + f"Target image {i + 1} should have {len(ima_src.roi)} ROI regions" |
| 165 | + ) |
| 166 | + execenv.print(f" Target image {i + 1}: {len(img.roi)} ROI regions ✓") |
| 167 | + |
| 168 | + execenv.print(" ✓ Multiple selection paste test passed") |
| 169 | + |
| 170 | + |
| 171 | +def test_image_roi_import_export(): |
| 172 | + """Test image ROI import and export to/from file functionality""" |
| 173 | + with datalab_test_app_context(console=False) as win: |
| 174 | + execenv.print("Image ROI Import/Export test:") |
| 175 | + panel: ImagePanel = win.imagepanel |
| 176 | + param = NewImageParam.create(height=SIZE, width=SIZE) |
| 177 | + |
| 178 | + # Create first image with ROI |
| 179 | + ima1 = create_multigaussian_image(param) |
| 180 | + ima1.title = "Image with ROI" |
| 181 | + roi1 = create_image_roi("rectangle", IROI1) |
| 182 | + roi1.add_roi(create_image_roi("circle", IROI2)) |
| 183 | + roi1.add_roi(create_image_roi("polygon", IROI3)) |
| 184 | + ima1.roi = roi1 |
| 185 | + panel.add_object(ima1) |
| 186 | + |
| 187 | + original_roi_count = len(ima1.roi) |
| 188 | + execenv.print(f" Image 1 has {original_roi_count} ROI regions") |
| 189 | + |
| 190 | + # Export ROI to file |
| 191 | + roi_file = tempfile.mktemp(suffix=".dlabroi") |
| 192 | + try: |
| 193 | + execenv.print(" Exporting ROI to temporary file") |
| 194 | + |
| 195 | + # Select first image and export its ROI |
| 196 | + panel.objview.set_current_item_id(get_uuid(ima1)) |
| 197 | + panel.export_roi_to_file(roi_file) |
| 198 | + execenv.print(" ✓ ROI exported") |
| 199 | + |
| 200 | + # Verify file was created |
| 201 | + assert os.path.exists(roi_file), "ROI file should have been created" |
| 202 | + |
| 203 | + # Read the exported ROI directly to verify content |
| 204 | + exported_roi = read_roi(roi_file) |
| 205 | + assert len(exported_roi) == original_roi_count, ( |
| 206 | + f"Exported ROI should have {original_roi_count} regions" |
| 207 | + ) |
| 208 | + execenv.print(f" ✓ Exported ROI has {len(exported_roi)} regions") |
| 209 | + |
| 210 | + # Create second image without ROI |
| 211 | + ima2 = create_multigaussian_image(param) |
| 212 | + ima2.title = "Image without ROI" |
| 213 | + panel.add_object(ima2) |
| 214 | + assert ima2.roi is None, "Image 2 should not have ROI initially" |
| 215 | + |
| 216 | + # Import ROI from file to second image |
| 217 | + panel.objview.set_current_item_id(get_uuid(ima2)) |
| 218 | + panel.import_roi_from_file(roi_file) |
| 219 | + execenv.print(" Imported ROI to Image 2") |
| 220 | + |
| 221 | + # Get fresh reference to ima2 from panel |
| 222 | + ima2_updated = panel.objmodel[get_uuid(ima2)] |
| 223 | + assert ima2_updated.roi is not None, "Image 2 should have ROI after import" |
| 224 | + assert len(ima2_updated.roi) == original_roi_count, ( |
| 225 | + f"Imported ROI should have {original_roi_count} regions" |
| 226 | + ) |
| 227 | + execenv.print(f" ✓ Image 2 now has {len(ima2_updated.roi)} ROI regions") |
| 228 | + |
| 229 | + # Test importing ROI to image that already has ROI (should combine) |
| 230 | + ima3 = create_multigaussian_image(param) |
| 231 | + ima3.title = "Image with existing ROI" |
| 232 | + roi3 = create_image_roi("circle", [150, 150, 40]) |
| 233 | + ima3.roi = roi3 |
| 234 | + panel.add_object(ima3) |
| 235 | + initial_roi_count = len(ima3.roi) |
| 236 | + execenv.print(f" Image 3 has {initial_roi_count} ROI region initially") |
| 237 | + |
| 238 | + # Import ROI (should combine with existing) |
| 239 | + panel.objview.set_current_item_id(get_uuid(ima3)) |
| 240 | + panel.import_roi_from_file(roi_file) |
| 241 | + execenv.print(" Imported ROI to Image 3 (should combine)") |
| 242 | + |
| 243 | + # Get fresh reference to ima3 from panel |
| 244 | + ima3_updated = panel.objmodel[get_uuid(ima3)] |
| 245 | + assert ima3_updated.roi is not None, "Image 3 should still have ROI" |
| 246 | + # After combining, should have more regions |
| 247 | + assert len(ima3_updated.roi) >= initial_roi_count, ( |
| 248 | + f"Expected at least {initial_roi_count} ROI regions, " |
| 249 | + f"got {len(ima3_updated.roi)}" |
| 250 | + ) |
| 251 | + execenv.print( |
| 252 | + f" ✓ Image 3 now has {len(ima3_updated.roi)} ROI regions (combined)" |
| 253 | + ) |
| 254 | + finally: |
| 255 | + # Clean up temporary file |
| 256 | + if os.path.exists(roi_file): |
| 257 | + try: |
| 258 | + os.unlink(roi_file) |
| 259 | + except (PermissionError, OSError): |
| 260 | + pass # Ignore cleanup errors on Windows |
| 261 | + |
| 262 | + execenv.print(" ✓ Image ROI import/export test passed") |
| 263 | + |
| 264 | + |
| 265 | +if __name__ == "__main__": |
| 266 | + test_image_roi_copy_paste() |
| 267 | + test_image_roi_copy_paste_multiple_selection() |
| 268 | + test_image_roi_import_export() |
0 commit comments