Skip to content

Commit 6ee2e7f

Browse files
authored
Remove panimg from main environment (#4541)
This PR removes panimg as a direct dependency and puts everything into the virtual environment. As vips is no longer in the main environment this should solve the issue with pycurl installation on all platforms. Tests are removed that were only testing panimg functionality, I checked that they are present in the panimg repo - these were left over from the extraction of panimg from this codebase a few years ago. If this all works then we should create another package for the shared models between panimg and grand challenge, for now they are copied in. See DIAGNijmegen/rse-cloud-infrastructure#212
1 parent 8bbec1d commit 6ee2e7f

16 files changed

Lines changed: 205 additions & 997 deletions

File tree

app/grandchallenge/cases/models.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,16 @@
2727
from django_deprecate_fields import deprecate_field
2828
from grand_challenge_dicom_de_identifier.deidentifier import DicomDeidentifier
2929
from guardian.shortcuts import assign_perm, get_groups_with_perms, remove_perm
30-
from panimg.models import MAXIMUM_SEGMENTS_LENGTH, ColorSpace, ImageType
3130
from pydantic import ConfigDict, Field, field_validator
3231
from pydantic.alias_generators import to_camel
3332
from pydantic.dataclasses import dataclass
3433
from storages.utils import clean_name
3534

35+
from grandchallenge.cases.panimg_models import (
36+
MAXIMUM_SEGMENTS_LENGTH,
37+
ColorSpace,
38+
ImageType,
39+
)
3640
from grandchallenge.core.error_handlers import (
3741
DICOMImageSetUploadErrorHandler,
3842
RawImageUploadSessionErrorHandler,

app/grandchallenge/cases/panimg.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,51 @@
33
from pathlib import Path
44

55
from django.utils._os import safe_join
6-
from panimg.models import PanImgFile, PostProcessorResult
76
from pydantic import TypeAdapter
87

8+
from grandchallenge.cases.panimg_models import (
9+
PanImgFile,
10+
PanImgResult,
11+
PostProcessorOptions,
12+
PostProcessorResult,
13+
)
14+
15+
16+
def convert(*, input_directory, output_directory, builders):
17+
builders_command = []
18+
for builder in builders:
19+
builders_command.extend(["--image-builder", builder])
20+
21+
panimg_command = shlex.join(
22+
[
23+
"panimg",
24+
"convert",
25+
"--input-dir",
26+
str(input_directory.resolve()),
27+
"--output-dir",
28+
str(output_directory.resolve()),
29+
*builders_command,
30+
"--no-post-processing",
31+
]
32+
)
33+
34+
cli_result = subprocess.run(
35+
[
36+
"bash",
37+
"-c",
38+
f"source /opt/virtualenvs/panimg/bin/activate && {panimg_command}",
39+
],
40+
text=True,
41+
check=True,
42+
capture_output=True,
43+
)
44+
45+
panimg_result: PanImgResult = TypeAdapter(PanImgResult).validate_json(
46+
cli_result.stdout.splitlines()[-1]
47+
)
48+
49+
return panimg_result
50+
951

1052
def post_process(*, image_file, output_directory):
1153
panimg_file = _download_image_file(
@@ -23,7 +65,7 @@ def post_process(*, image_file, output_directory):
2365
"--input-file",
2466
str(panimg_file.file),
2567
"--post-processor",
26-
"DZI",
68+
PostProcessorOptions.DZI,
2769
]
2870
)
2971

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# These models are extracted from panimg itself and need to be used
2+
# in both places. It will be best to create a new python package
3+
# for this so that it can be used by both grand challenge and panimg
4+
5+
from enum import Enum
6+
from pathlib import Path
7+
from uuid import UUID
8+
9+
from pydantic.dataclasses import dataclass
10+
11+
12+
class ImageBuilderOptions(str, Enum):
13+
MHD = "MHD"
14+
NIFTI = "NIFTI"
15+
NRRD = "NRRD"
16+
DICOM = "DICOM"
17+
TIFF = "TIFF"
18+
OCT = "OCT"
19+
FALLBACK = "FALLBACK"
20+
21+
22+
class PostProcessorOptions(str, Enum):
23+
DZI = "DZI"
24+
25+
26+
# NOTE: Only int8 or uint8 data types are checked for segments
27+
# so the true maximum is 256
28+
MAXIMUM_SEGMENTS_LENGTH = 64
29+
30+
31+
class ColorSpace(str, Enum):
32+
GRAY = "GRAY"
33+
RGB = "RGB"
34+
RGBA = "RGBA"
35+
YCBCR = "YCBCR"
36+
37+
38+
ITK_COLOR_SPACE_MAP = {
39+
1: ColorSpace.GRAY,
40+
3: ColorSpace.RGB,
41+
4: ColorSpace.RGBA,
42+
}
43+
44+
45+
class ImageType(str, Enum):
46+
MHD = "MHD"
47+
TIFF = "TIFF"
48+
DZI = "DZI"
49+
50+
51+
class EyeChoice(str, Enum):
52+
OCULUS_DEXTER = "OD"
53+
OCULUS_SINISTER = "OS"
54+
UNKNOWN = "U"
55+
NOT_APPLICABLE = "NA"
56+
57+
58+
@dataclass(frozen=True)
59+
class PanImg:
60+
pk: UUID
61+
name: str
62+
width: int
63+
height: int
64+
depth: int | None
65+
voxel_width_mm: float | None
66+
voxel_height_mm: float | None
67+
voxel_depth_mm: float | None
68+
timepoints: int | None
69+
resolution_levels: int | None
70+
window_center: float | None
71+
window_width: float | None
72+
color_space: ColorSpace
73+
eye_choice: EyeChoice
74+
segments: frozenset[int] | None = None
75+
76+
77+
@dataclass(frozen=True)
78+
class PanImgFile:
79+
image_id: UUID
80+
image_type: ImageType
81+
file: Path
82+
directory: Path | None = None
83+
84+
85+
@dataclass
86+
class PanImgResult:
87+
new_images: set[PanImg]
88+
new_image_files: set[PanImgFile]
89+
consumed_files: set[Path]
90+
file_errors: dict[Path, list[str]]
91+
92+
93+
@dataclass
94+
class PostProcessorResult:
95+
new_image_files: set[PanImgFile]

app/grandchallenge/cases/tasks.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import re
22
import zipfile
3-
from collections.abc import Callable, Sequence
3+
from collections.abc import Sequence
44
from dataclasses import asdict, dataclass
55
from pathlib import Path
66
from shutil import rmtree
7+
from subprocess import CalledProcessError
78
from tempfile import TemporaryDirectory
89

910
import boto3
@@ -22,8 +23,6 @@
2223
from grand_challenge_dicom_de_identifier.exceptions import (
2324
RejectedDICOMFileError,
2425
)
25-
from panimg import convert
26-
from panimg.models import PanImgResult
2726

2827
from grandchallenge.cases.models import (
2928
DICOMImageSetUpload,
@@ -34,7 +33,11 @@
3433
PostProcessImageTaskStatusChoices,
3534
RawImageUploadSession,
3635
)
37-
from grandchallenge.cases.panimg import post_process
36+
from grandchallenge.cases.panimg import convert, post_process
37+
from grandchallenge.cases.panimg_models import (
38+
ImageBuilderOptions,
39+
PanImgResult,
40+
)
3841
from grandchallenge.components.backends.exceptions import RetryStep
3942
from grandchallenge.components.backends.utils import UUID4_REGEX, safe_extract
4043
from grandchallenge.components.models import ComponentInterface
@@ -189,8 +192,8 @@ def _handle_error(*, error_message):
189192
base_directory=tmp_dir,
190193
upload_session=upload_session,
191194
)
192-
except RuntimeError as error:
193-
if "std::bad_alloc" in str(error):
195+
except CalledProcessError as error:
196+
if error.returncode == 137:
194197
_handle_error(
195198
error_message=(
196199
"The uploaded images were too large to process, "
@@ -297,15 +300,14 @@ def import_images(
297300
*,
298301
input_directory: Path,
299302
origin: RawImageUploadSession | None = None,
300-
builders: Sequence[Callable] | None = None,
301-
recurse_subdirectories: bool = True,
303+
builders: Sequence[str] | None = None,
302304
) -> ImporterResult:
303305
"""
304306
Creates Image objects from a set of files.
305307
306308
Parameters
307309
----------
308-
files
310+
input_directory
309311
A Set of files that can form one or many Images
310312
origin
311313
The RawImageUploadSession (if any) that was the source of these files
@@ -318,13 +320,24 @@ def import_images(
318320
any file errors
319321
320322
"""
323+
if builders is None:
324+
builders = [
325+
ImageBuilderOptions.MHD,
326+
ImageBuilderOptions.NIFTI,
327+
ImageBuilderOptions.NRRD,
328+
ImageBuilderOptions.DICOM,
329+
ImageBuilderOptions.TIFF,
330+
ImageBuilderOptions.OCT,
331+
ImageBuilderOptions.FALLBACK,
332+
]
333+
321334
with TemporaryDirectory() as output_directory:
335+
panimg_output_dir = Path(output_directory) / "output"
336+
322337
panimg_result = convert(
323338
input_directory=input_directory,
324-
output_directory=output_directory,
339+
output_directory=panimg_output_dir,
325340
builders=builders,
326-
post_processors=[], # Do the post-processing later
327-
recurse_subdirectories=recurse_subdirectories,
328341
)
329342

330343
_check_all_ids(panimg_result=panimg_result)

app/grandchallenge/components/backends/base.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from json import JSONDecodeError
1414
from math import ceil
1515
from pathlib import Path
16+
from subprocess import CalledProcessError
1617
from tempfile import SpooledTemporaryFile, TemporaryDirectory
1718
from typing import NamedTuple
1819
from uuid import UUID
@@ -30,10 +31,10 @@
3031
from django.db import transaction
3132
from django.utils._os import safe_join
3233
from django.utils.functional import cached_property
33-
from panimg.image_builders import image_builder_mhd, image_builder_tiff
3434
from pydantic import BaseModel, ConfigDict
3535
from pydantic_core import to_json
3636

37+
from grandchallenge.cases.panimg_models import ImageBuilderOptions
3738
from grandchallenge.cases.tasks import import_images
3839
from grandchallenge.components.backends.exceptions import (
3940
ComponentException,
@@ -892,17 +893,24 @@ def _create_images_result(self, *, interface):
892893
)
893894

894895
with TemporaryDirectory() as tmpdir:
896+
input_directory = Path(tmpdir)
897+
895898
self._download_output_files(
896-
output_files=output_files, tmpdir=tmpdir, prefix=prefix
899+
output_files=output_files,
900+
target_directory=input_directory,
901+
prefix=prefix,
897902
)
898903

899904
try:
900905
importer_result = import_images(
901-
input_directory=tmpdir,
902-
builders=[image_builder_mhd, image_builder_tiff],
906+
input_directory=input_directory,
907+
builders=[
908+
ImageBuilderOptions.MHD,
909+
ImageBuilderOptions.TIFF,
910+
],
903911
)
904-
except RuntimeError as error:
905-
if "std::bad_alloc" in str(error):
912+
except CalledProcessError as error:
913+
if error.returncode == 137:
906914
raise ComponentException(
907915
"The output image was too large to process, "
908916
"please try again with smaller images"
@@ -932,11 +940,15 @@ def _create_images_result(self, *, interface):
932940

933941
return civ
934942

935-
def _download_output_files(self, *, output_files, tmpdir, prefix):
943+
def _download_output_files(
944+
self, *, output_files, target_directory, prefix
945+
):
936946
for file in output_files:
937947
try:
938948
root_key = safe_join("/", file["Key"])
939-
dest = safe_join(tmpdir, Path(root_key).relative_to(prefix))
949+
dest = safe_join(
950+
target_directory, Path(root_key).relative_to(prefix)
951+
)
940952
except (SuspiciousFileOperation, ValueError):
941953
logger.warning(f"Skipping {file=}")
942954
continue

app/grandchallenge/components/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
from django.utils.translation import gettext_lazy as _
3535
from django_deprecate_fields import deprecate_field
3636
from django_extensions.db.fields import AutoSlugField
37-
from panimg.models import MAXIMUM_SEGMENTS_LENGTH
3837
from pydantic_core import MISSING
3938

4039
from grandchallenge.cases.models import (
@@ -43,6 +42,7 @@
4342
ImageFile,
4443
RawImageUploadSession,
4544
)
45+
from grandchallenge.cases.panimg_models import MAXIMUM_SEGMENTS_LENGTH
4646
from grandchallenge.charts.specs import components_line
4747
from grandchallenge.components.backends.exceptions import (
4848
CIVNotEditableException,

app/grandchallenge/workstation_configs/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
from django.db.models import PositiveSmallIntegerField
1010
from django_extensions.db.models import TitleSlugDescriptionModel
1111
from guardian.shortcuts import assign_perm
12-
from panimg.models import MAXIMUM_SEGMENTS_LENGTH
1312

13+
from grandchallenge.cases.panimg_models import MAXIMUM_SEGMENTS_LENGTH
1414
from grandchallenge.core.fields import HexColorField
1515
from grandchallenge.core.guardian import (
1616
GroupObjectPermissionBase,

0 commit comments

Comments
 (0)