From 39643821204032d801947d5e741102d833e604a9 Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Wed, 4 Jun 2025 12:14:03 -0400 Subject: [PATCH 01/24] getting started nb reflects todos --- getting_started.ipynb | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/getting_started.ipynb b/getting_started.ipynb index 7100fee..e12ed8f 100644 --- a/getting_started.ipynb +++ b/getting_started.ipynb @@ -16,7 +16,7 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -50,7 +50,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -77,7 +77,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -97,12 +97,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Open an instance in pydicom from COD" + "## Open a specific instance in pydicom from COD" ] }, { "cell_type": "code", - "execution_count": 39, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -114,6 +114,16 @@ } ], "source": [ + "# Do not explicitly pull the tar\n", + "\n", + "# cod_obj.get_metadata() -> filter for instances user wants -> \n", + "\n", + "# cod_obj.get_instances() -> cod_obj.get_metadata().instances. OrderedDict by same logic as thumbnails (will allow index access). Raise an error if sorting fails.\n", + "\n", + "# cod_obj.get_instance(instance_uid) -> get_instances()[instance_uid]\n", + "\n", + "# instance.open() should fetch tar if necessary\n", + "\n", "cod_obj = CODObject(datastore_path=datastore_path, \n", " client=client, \n", " study_uid=instance_a.study_uid(), \n", @@ -287,6 +297,8 @@ "metadata": {}, "outputs": [], "source": [ + "# series_metadata = CODObject.dicomweb(GET {datastore_path}/studies/{study_uid}/series/{series_uid}/metadata, client)\n", + "\n", "# get study-level metadata (returns a dict of study level tags)\n", "study_metadata = handle_request(f\"GET {datastore_path}/studies/{instance_a.study_uid()}/metadata\", client)\n", "assert study_metadata[\"00100020\"][\"Value\"][0] == \"GRDNB4C659BSD9NZ\"\n", From 85b5b3e580efdfbd0346f464033d4d22332be634 Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Wed, 4 Jun 2025 15:17:05 -0400 Subject: [PATCH 02/24] getting started notebook works through instance opening --- getting_started.ipynb | 76 ++++++++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/getting_started.ipynb b/getting_started.ipynb index e12ed8f..932ec5e 100644 --- a/getting_started.ipynb +++ b/getting_started.ipynb @@ -16,7 +16,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -29,6 +29,8 @@ "import tempfile\n", "import os\n", "\n", + "assert hasattr(CODObject, \"get_instances\")\n", + "\n", "client = storage.Client()\n", "\n", "datastore_path = \"gs://cod-test-bucket/test-datastore/v1.0/dicomweb\"\n", @@ -50,7 +52,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -77,7 +79,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -102,38 +104,44 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "GRDNB4C659BSD9NZ\n" + "All instances UIDs in the series: dict_keys(['1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612', '1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455'])\n", + "Instance with UID 1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612 has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612\n", + "Instance with index 1 has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n", + "Instance object Instance(uri=/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp3d4tqvsj_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm, hashed_uids=False, instance_uid=1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455, series_uid=1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506, study_uid=1.2.826.0.1.3680043.8.498.77805869330689203045629680212005263354, dependencies=[]) has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n" ] } ], "source": [ - "# Do not explicitly pull the tar\n", - "\n", - "# cod_obj.get_metadata() -> filter for instances user wants -> \n", - "\n", - "# cod_obj.get_instances() -> cod_obj.get_metadata().instances. OrderedDict by same logic as thumbnails (will allow index access). Raise an error if sorting fails.\n", - "\n", - "# cod_obj.get_instance(instance_uid) -> get_instances()[instance_uid]\n", - "\n", - "# instance.open() should fetch tar if necessary\n", - "\n", "cod_obj = CODObject(datastore_path=datastore_path, \n", " client=client, \n", " study_uid=instance_a.study_uid(), \n", " series_uid=instance_a.series_uid(), \n", " lock=False)\n", - "cod_obj.pull_tar(dirty=True)\n", - "fetched_instance = cod_obj.get_metadata(dirty=True).instances[instance_a.instance_uid()]\n", - "with fetched_instance.open() as f:\n", + "# get a dict of all instances in the series\n", + "print(\"All instances UIDs in the series: \", cod_obj.get_instances(dirty=True).keys())\n", + "# get a specific instance by uid\n", + "instance_from_uid = cod_obj.get_instance(instance_a.instance_uid(), dirty=True)\n", + "# get a specific instance by index (instances as an ordered list by InstanceNumber, SliceLocation, etc.)\n", + "second_instance = cod_obj.get_instance_by_index(1, dirty=True)\n", + "# open an instance by uid\n", + "with cod_obj.open_instance(instance_b.instance_uid(), dirty=True) as f:\n", + " ds = pydicom.dcmread(f)\n", + " print(f\"Instance with UID {instance_b.instance_uid()} has SOPInstanceUID: {ds.SOPInstanceUID}\")\n", + "# open an instance by index\n", + "with cod_obj.open_instance(1, dirty=True) as f:\n", " ds = pydicom.dcmread(f)\n", - " print(ds.PatientName)" + " print(f\"Instance with index {1} has SOPInstanceUID: {ds.SOPInstanceUID}\")\n", + "# open an instance by object\n", + "with cod_obj.open_instance(second_instance, dirty=True) as f:\n", + " ds = pydicom.dcmread(f)\n", + " print(f\"Instance object {second_instance} has SOPInstanceUID: {ds.SOPInstanceUID}\")" ] }, { @@ -149,6 +157,8 @@ "metadata": {}, "outputs": [], "source": [ + "# remove is really just syntactic sugar for truncate_everything_except(stuff_you_didnt_remove)\n", + "\n", "with CODObject(datastore_path=datastore_path, \n", " client=client, \n", " study_uid=instance_a.study_uid(), \n", @@ -211,6 +221,12 @@ } ], "source": [ + "# change to cod_obj.get_thumbnail(generate_if_missing=True) -> uint numpy array... fetch and return thumbnail if it exists, generate it if it doesn't (or fail if generate_if_missing=False)\n", + "\n", + "# instance.get_thumbnail() -> cod_obj.get_thumbnail()[slice_for_instance]\n", + "\n", + "# cod_obj.get_instance_from_thumbnail_index(thumbnail_index) -> Instance\n", + "\n", "with CODObject(datastore_path=datastore_path, \n", " client=client, \n", " study_uid=instance_a.study_uid(), \n", @@ -233,6 +249,12 @@ "metadata": {}, "outputs": [], "source": [ + "# cod_obj.remove_custom_tag(tag_name)\n", + "\n", + "# rename custom tag to metadata field: cod_obj.add_metadata_field(field_name, field_value)\n", + "\n", + "# clearer nomenclature\n", + "\n", "with CODObject(datastore_path=datastore_path, \n", " client=client, \n", " study_uid=instance_a.study_uid(), \n", @@ -256,6 +278,20 @@ "metadata": {}, "outputs": [], "source": [ + "# right now when you open a cod object you are opening it in read mode\n", + "# but if we did want to support write mode, then the backend changes a lot\n", + "# ex: rather than pulling a tar, you would pull the tar and actually extract it so you have the files\n", + "# then you would be able to modify the files directly\n", + "# you would basically not have metadata (the metadata.json would be invalid because you're changing stuff)\n", + "# would be way easier to just have an truncate() call under the hood when the write is over\n", + "\n", + "# simplified: metadata/get_metadata()/get_thumbnail() inherently slow due to having to generate them on the fly\n", + "# you can't write a cod file, but you can write on instances\n", + "\n", + "# cod_obj.get_instance(instance_uid).open(\"w\") -> extract the tar, modify the instance, then truncate()\n", + "\n", + "# more advanced and efficient would be write mode on the cod object itself, so we're not extracting/truncating for every instance\n", + "\n", "with CODObject(datastore_path=datastore_path, \n", " client=client, \n", " study_uid=instance_a.study_uid(), \n", @@ -299,6 +335,8 @@ "source": [ "# series_metadata = CODObject.dicomweb(GET {datastore_path}/studies/{study_uid}/series/{series_uid}/metadata, client)\n", "\n", + "# lose the GET... later, if supported, can add options for POST, PUT, etc.\n", + "\n", "# get study-level metadata (returns a dict of study level tags)\n", "study_metadata = handle_request(f\"GET {datastore_path}/studies/{instance_a.study_uid()}/metadata\", client)\n", "assert study_metadata[\"00100020\"][\"Value\"][0] == \"GRDNB4C659BSD9NZ\"\n", From 11582125df3457757d5f35fd36d3cbb80656474c Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Wed, 4 Jun 2025 15:17:36 -0400 Subject: [PATCH 03/24] new public methods for getting/opening instances --- cloud_optimized_dicom/cod_object.py | 78 ++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/cloud_optimized_dicom/cod_object.py b/cloud_optimized_dicom/cod_object.py index a020899..36b1758 100644 --- a/cloud_optimized_dicom/cod_object.py +++ b/cloud_optimized_dicom/cod_object.py @@ -2,7 +2,7 @@ import os import tarfile from tempfile import TemporaryDirectory -from typing import Callable, Optional +from typing import Callable, Optional, Union from google.cloud import storage from google.cloud.storage.constants import STANDARD_STORAGE_CLASS @@ -207,6 +207,82 @@ def get_metadata( ) return self._metadata + @public_method + def get_instances(self, strict_sorting: bool = True, dirty: bool = False): + """Get a dictionary mapping instance UIDs to instances. These instance UIDs are hashed if `hashed_uids=True`, otherwise they are the original UIDs. + COD will attempt to sort this dictionary so that instances appear in the proper order. + + Args: + strict_sorting: bool - If `True`, raise an error if sorting fails (log a warning if `False`). + dirty: bool - Must be `True` if the CODObject is "dirty" (i.e. `lock=False`). + """ + metadata = self.get_metadata(dirty=dirty) + metadata._sort_instances() + if not metadata.is_sorted: + if strict_sorting: + raise ValueError(f"Sorting was unsuccessful, and strict_sorting=True") + else: + logger.warning(f"Instance dict is unsorted") + return metadata.instances + + @public_method + def get_instance(self, instance_uid: str, dirty: bool = False) -> Instance: + """Get an instance by uid. `instance_uid` should be hashed if `hashed_uids=True`, otherwise it should be the original UID.""" + return self.get_instances(strict_sorting=False, dirty=dirty)[instance_uid] + + @public_method + def get_instance_by_index(self, index: int, dirty: bool = False) -> Instance: + """Get an instance by index. + + Args: + index: int - The index of the instance to get. + dirty: bool - Must be `True` if the CODObject is "dirty" (i.e. `lock=False`). + """ + # for access by index, we require strict sorting + return list(self.get_instances(strict_sorting=True, dirty=dirty).values())[ + index + ] + + @public_method + def open_instance(self, instance: Union[Instance, str, int], dirty: bool = False): + """Open an instance (first fetches the series tar if necessary). For convenience, the instance parameter can be one of: + - `Instance`: An actual instance object to open. + - `str`: An instance UID to open (hashed if `hashed_uids=True`). + - `int`: The index of an instance to open. + + Args: + instance: Instance | str | int - The instance to open + dirty: bool - Must be `True` if the CODObject is "dirty" (i.e. `lock=False`). + + Returns: + A file pointer to the instance. + + Raises: + ValueError: If the instance parameter is invalid. + FileNotFoundError: If the instance is not found in the CODObject. + """ + # validate instance parameter + if isinstance(instance, Instance): + if ( + instance.get_instance_uid( + hashed=self.hashed_uids, trust_hints_if_available=True + ) + not in self.get_metadata(dirty=dirty).instances + ): + raise FileNotFoundError(f"Instance not found in CODObject: {instance}") + elif isinstance(instance, str): + instance = self.get_instance(instance, dirty=dirty) + elif isinstance(instance, int): + instance = self.get_instance_by_index(instance, dirty=dirty) + else: + raise ValueError( + f"Invalid instance parameter: {instance} (must be Instance, str, or int)" + ) + # pull the tar file if necessary + if not self._tar_synced: + self.pull_tar(dirty=dirty) + return instance.open() + @public_method def append( self, From b3c425238498b38c8d2361bc263b5b1fbbf821dd Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Wed, 4 Jun 2025 15:18:10 -0400 Subject: [PATCH 04/24] instance.fetch raises valueerror if remote nested within tar --- cloud_optimized_dicom/instance.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cloud_optimized_dicom/instance.py b/cloud_optimized_dicom/instance.py index 07a34be..2c06c09 100644 --- a/cloud_optimized_dicom/instance.py +++ b/cloud_optimized_dicom/instance.py @@ -86,6 +86,12 @@ def fetch(self): if not is_remote(self.dicom_uri): return + # raise an error if the instance is nested in a tar + if self.is_nested_in_tar: + raise ValueError( + f"Direct fetching of instances in remote tars is not supported... use CODObject.open_instance() instead" + ) + # we store the path, not the file object, so that instances can be pickled (allows them to be passed between beam.DoFns) with tempfile.NamedTemporaryFile(suffix=".dcm", delete=False) as temp_file: self._temp_file_path = temp_file.name From 31c1a3891f8fb7fe7b66838f4eb4a81f635920ec Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Wed, 4 Jun 2025 15:18:37 -0400 Subject: [PATCH 05/24] series metadata supports instance sorting --- cloud_optimized_dicom/series_metadata.py | 31 ++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/cloud_optimized_dicom/series_metadata.py b/cloud_optimized_dicom/series_metadata.py index 916342b..02a6c42 100644 --- a/cloud_optimized_dicom/series_metadata.py +++ b/cloud_optimized_dicom/series_metadata.py @@ -7,6 +7,7 @@ from google.cloud import storage from cloud_optimized_dicom.instance import Instance +from cloud_optimized_dicom.thumbnail import _sort_instances @dataclass @@ -19,8 +20,10 @@ class SeriesMetadata: hashed_uids (bool): Flag indicating whether the series uses de-identified UIDs. instances (dict[str, Instance]): Mapping of instance UID (hashed if `hashed_uids=True`) to Instance object custom_tags (dict): Any additional user defined data - If loading existing metadata, this is inferred by the presence of the key `deid_study_uid` as opposed to `study_uid`. - If creating new metadata, this is inferred by the presence/absence of `instance.uid_hash_func` for any instances that have been added. + is_sorted (bool): Flag indicating whether the instances dict is sorted + + If loading existing metadata, `hashed_uids` is inferred by the presence of the key `deid_study_uid` as opposed to `study_uid`. + If creating new metadata, `hashed_uids` is inferred by the presence/absence of `instance.uid_hash_func` for any instances that have been added. """ study_uid: str @@ -28,6 +31,7 @@ class SeriesMetadata: hashed_uids: bool instances: dict[str, Instance] = field(default_factory=dict) custom_tags: dict = field(default_factory=dict) + is_sorted: bool = False def _add_custom_tag(self, tag_name: str, tag_value, overwrite_existing=False): """Add a custom tag to the series metadata""" @@ -38,6 +42,29 @@ def _add_custom_tag(self, tag_name: str, tag_value, overwrite_existing=False): ) self.custom_tags[tag_name] = tag_value + def _sort_instances(self): + """Sort the instances dict, the same way instances are sorted for the thumbnail. + + If sorting is successful, set `is_sorted=True`. + If sorting is unsuccessful, set `is_sorted=False`. + """ + # early exit if already sorted + if self.is_sorted: + return + # map instances to their uids + instance_to_uid = {instance: uid for uid, instance in self.instances.items()} + # get a list of all instances (unsorted) + unsorted_instances = list(instance_to_uid.keys()) + # attempt sorting + try: + sorted_instances = _sort_instances(unsorted_instances, strict=True) + self.instances = { + instance_to_uid[instance]: instance for instance in sorted_instances + } + self.is_sorted = True + except ValueError: + self.is_sorted = False + def to_dict(self) -> dict: # TODO version handling once we have a new version study_uid_key = "deid_study_uid" if self.hashed_uids else "study_uid" From 3810b1dfbf1540778388b832c6579f2e216f373a Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Wed, 4 Jun 2025 15:19:06 -0400 Subject: [PATCH 06/24] sample metadata.json files contain UID tags (necessary to avoid a fetch) --- .../tests/test_data/valid_deid_metadata.json | 24 +++++++++++++++++ .../tests/test_data/valid_metadata.json | 26 ++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/cloud_optimized_dicom/tests/test_data/valid_deid_metadata.json b/cloud_optimized_dicom/tests/test_data/valid_deid_metadata.json index 4ff5ade..6c2742f 100644 --- a/cloud_optimized_dicom/tests/test_data/valid_deid_metadata.json +++ b/cloud_optimized_dicom/tests/test_data/valid_deid_metadata.json @@ -5,6 +5,18 @@ "instances": { "instance_uid_1": { "metadata": { + "0020000E": { + "vr": "UI", + "Value": [ + "some_series_uid" + ] + }, + "0020000D": { + "vr": "UI", + "Value": [ + "some_study_uid" + ] + }, "00080000": { "vr": "UL", "Value": [612] @@ -61,6 +73,18 @@ }, "instance_uid_2": { "metadata": { + "0020000E": { + "vr": "UI", + "Value": [ + "some_series_uid" + ] + }, + "0020000D": { + "vr": "UI", + "Value": [ + "some_study_uid" + ] + }, "00080000": { "vr": "UL", "Value": [612] diff --git a/cloud_optimized_dicom/tests/test_data/valid_metadata.json b/cloud_optimized_dicom/tests/test_data/valid_metadata.json index 87609a5..6eba195 100644 --- a/cloud_optimized_dicom/tests/test_data/valid_metadata.json +++ b/cloud_optimized_dicom/tests/test_data/valid_metadata.json @@ -5,6 +5,18 @@ "instances": { "instance_uid_1": { "metadata": { + "0020000E": { + "vr": "UI", + "Value": [ + "some_series_uid" + ] + }, + "0020000D": { + "vr": "UI", + "Value": [ + "some_study_uid" + ] + }, "00080000": { "vr": "UL", "Value": [612] @@ -26,7 +38,7 @@ "00080018": { "vr": "UI", "Value": [ - "1.2.392.200036.9116.2.6.1.44063.1805437483.1707395280.529740" + "some_instance_uid" ] }, "7FE00010": { @@ -61,6 +73,18 @@ }, "instance_uid_2": { "metadata": { + "0020000E": { + "vr": "UI", + "Value": [ + "some_series_uid" + ] + }, + "0020000D": { + "vr": "UI", + "Value": [ + "some_study_uid" + ] + }, "00080000": { "vr": "UL", "Value": [612] From 661ed837a9d6b8f4944d2d144a4774837c5825eb Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Wed, 4 Jun 2025 15:19:28 -0400 Subject: [PATCH 07/24] test that opening a remote tar nested instance raises error --- cloud_optimized_dicom/tests/test_instance.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cloud_optimized_dicom/tests/test_instance.py b/cloud_optimized_dicom/tests/test_instance.py index d6de0b0..c10b56c 100644 --- a/cloud_optimized_dicom/tests/test_instance.py +++ b/cloud_optimized_dicom/tests/test_instance.py @@ -35,6 +35,13 @@ def test_remote_open(self): ds = pydicom3.dcmread(f) self.assertEqual(ds.SOPInstanceUID, self.test_instance_uid) + def test_remote_tar_open_raises_error(self): + instance = Instance( + dicom_uri="gs://some_series.tar://instances/some_instance.dcm" + ) + with self.assertRaises(ValueError): + instance.open() + def test_validate(self): instance = Instance(self.local_instance_path) self.assertIsNone(instance._instance_uid) From 5f98023a7bb96014af75ca2198cd9c9872e047bd Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Wed, 4 Jun 2025 15:20:20 -0400 Subject: [PATCH 08/24] added strict mode to instance sort method (default false) --- cloud_optimized_dicom/thumbnail.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/cloud_optimized_dicom/thumbnail.py b/cloud_optimized_dicom/thumbnail.py index 576742f..5d6b09a 100644 --- a/cloud_optimized_dicom/thumbnail.py +++ b/cloud_optimized_dicom/thumbnail.py @@ -170,9 +170,10 @@ def _generate_thumbnail_frame_and_anchors( return thumbnail, anchors -def _sort_instances(instances: list[Instance]) -> list[Instance]: +def _sort_instances(instances: list[Instance], strict=False) -> list[Instance]: """Attempt to sort instances by instance_number tag. Try slice_location if that fails. - If both fail, return the instances in the order they were fetched, and log a warning. + If both fail, and `strict=False`, return the instances in the order they were fetched and log a warning. + If both fail, and `strict=True`, raise a ValueError. """ # if there's only one instance, return it as is if len(instances) <= 1: @@ -184,10 +185,11 @@ def _sort_instances(instances: list[Instance]) -> list[Instance]: continue # sortable attributes are expected to be stored in metadata as "tag": {"vr":"VR","Value":[some_value]} return sorted(instances, key=lambda x: x.metadata[tag]["Value"][0]) - # if no sorting was successful, return the instances in the order they were fetched - logger.warning( - f"Unable to sort instances by any known sorting attributes ({', '.join(SORTING_ATTRIBUTES.keys())})" - ) + # if we get here, sorting failed + msg = f"Unable to sort instances by any known sorting attributes ({', '.join(SORTING_ATTRIBUTES.keys())})" + if strict: + raise ValueError(msg) + logger.warning(msg) return instances From cf9fdec1ecd8d0b8973af396d56f8976719c6604 Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Thu, 5 Jun 2025 17:20:08 -0400 Subject: [PATCH 09/24] read_thumbnail_into_array util method --- cloud_optimized_dicom/utils.py | 40 +++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/cloud_optimized_dicom/utils.py b/cloud_optimized_dicom/utils.py index e75ebf4..2e1b0a7 100644 --- a/cloud_optimized_dicom/utils.py +++ b/cloud_optimized_dicom/utils.py @@ -6,15 +6,16 @@ "study_uid": "0020000D", } - import collections import io import logging from base64 import b64encode from typing import Optional +import cv2 import filetype import google_crc32c +import numpy as np from google.cloud import storage from google.cloud.storage.retry import DEFAULT_RETRY @@ -147,6 +148,43 @@ def parse_uids_from_metadata( return instance_uid, series_uid, study_uid +def read_thumbnail_into_array(thumbnail_path: str) -> np.ndarray: + """Read a thumbnail from disk into a numpy array. + + Args: + thumbnail_path: str - The path to the thumbnail on disk. + + Returns: + np.ndarray - The thumbnail as a numpy array. The shape of the array is `(N, H, W, 3)` for a video, or `(H, W, 3)` for a single frame image. + + Raises: + ValueError: If reading the thumbnail fails for any reason (e.g. file not found, invalid format, etc.) + """ + if thumbnail_path.endswith(".mp4"): + cap = cv2.VideoCapture(thumbnail_path) + if not cap.isOpened(): + raise ValueError(f"Failed to open video at {thumbnail_path}") + + frames = [] + while True: + ret, frame = cap.read() + if not ret: + break + frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + cap.release() + + if not frames: + raise ValueError(f"No frames extracted from video at {thumbnail_path}") + return np.stack(frames, axis=0) # Shape: (N, H, W, 3) + elif thumbnail_path.endswith(".jpg"): + img = cv2.imread(thumbnail_path, cv2.IMREAD_COLOR) + if img is None: + raise ValueError(f"Failed to read image at {thumbnail_path}") + return cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + else: + raise ValueError(f"Unsupported thumbnail format: {thumbnail_path}") + + def public_method(func): """Decorator for public CODObject methods. Enforces that clean operations require a lock, and warns about dirty operations on locked objects. From 91a652fa43ac5d36abf797f628cdbb2a39f47fd4 Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Thu, 5 Jun 2025 17:20:27 -0400 Subject: [PATCH 10/24] fetch_thumbnail utility method --- cloud_optimized_dicom/thumbnail.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/cloud_optimized_dicom/thumbnail.py b/cloud_optimized_dicom/thumbnail.py index 5d6b09a..cbbce17 100644 --- a/cloud_optimized_dicom/thumbnail.py +++ b/cloud_optimized_dicom/thumbnail.py @@ -330,6 +330,31 @@ def generate_thumbnail( return thumbnail_path +def fetch_thumbnail(cod_obj: "CODObject", dirty: bool = False) -> str: + """Download thumbnail from GCS for given cod object. + + Returns: + thumbnail_path: the path to the thumbnail on disk + + Raises: + ValueError: if the cod object has no thumbnail metadata + NotFound: if the thumbnail blob does not exist in GCS + """ + thumbnail_metadata = cod_obj.get_custom_tag("thumbnail", dirty=dirty) + if thumbnail_metadata is None: + raise ValueError(f"Thumbnail metadata not found for {cod_obj}") + thumbnail_uri = thumbnail_metadata["uri"] + logger.info(f"Fetching thumbnail from {thumbnail_uri}") + thumbnail_blob = storage.Blob.from_string(thumbnail_uri, client=cod_obj.client) + thumbnail_local_path = os.path.join( + cod_obj.get_temp_dir().name, thumbnail_uri.split("/")[-1] + ) + thumbnail_blob.download_to_filename(thumbnail_local_path) + # we just fetched the thumbnail, so it is guaranteed to be in the same state as the datastore + cod_obj._thumbnail_synced = True + return thumbnail_local_path + + @dataclasses.dataclass class ThumbnailCoordConverter: orig_w: int From 07e4367cfc10288a7bf83cd729dc6bc9fad6f11b Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Thu, 5 Jun 2025 17:21:16 -0400 Subject: [PATCH 11/24] thumbnail refactor: now get_thumbnail() -> np array --- cloud_optimized_dicom/cod_object.py | 52 +++++++++++++++++------------ 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/cloud_optimized_dicom/cod_object.py b/cloud_optimized_dicom/cod_object.py index 36b1758..4a636c9 100644 --- a/cloud_optimized_dicom/cod_object.py +++ b/cloud_optimized_dicom/cod_object.py @@ -4,6 +4,7 @@ from tempfile import TemporaryDirectory from typing import Callable, Optional, Union +import numpy as np from google.cloud import storage from google.cloud.storage.constants import STANDARD_STORAGE_CLASS from google.cloud.storage.retry import DEFAULT_RETRY @@ -21,12 +22,13 @@ from cloud_optimized_dicom.instance import Instance from cloud_optimized_dicom.locker import CODLocker from cloud_optimized_dicom.series_metadata import SeriesMetadata -from cloud_optimized_dicom.thumbnail import generate_thumbnail +from cloud_optimized_dicom.thumbnail import fetch_thumbnail, generate_thumbnail from cloud_optimized_dicom.truncate import remove, truncate from cloud_optimized_dicom.utils import ( generate_ptr_crc32c, is_remote, public_method, + read_thumbnail_into_array, upload_and_count_file, ) @@ -429,35 +431,43 @@ def get_custom_tag(self, tag_name: str, dirty: bool = False) -> Optional[dict]: return self.get_metadata(dirty=dirty).custom_tags.get(tag_name, None) @public_method - def generate_thumbnail(self, overwrite_existing: bool = False, dirty: bool = False): - """Generate a thumbnail for a COD object. + def get_thumbnail( + self, generate_if_missing: bool = True, dirty: bool = False + ) -> np.ndarray: + """Get the thumbnail for a COD object. Args: - overwrite_existing: Whether to overwrite the existing thumbnail, if it exists. + generate_if_missing: Whether to generate a thumbnail if it does not exist, or is stale. dirty: Whether the operation is dirty. Returns: - The local path to the thumbnail after saving it to disk (or `None` if no thumbnail was generated) - """ - return generate_thumbnail( - cod_obj=self, overwrite_existing=overwrite_existing, dirty=dirty - ) + The thumbnail as a numpy array. - @public_method - def fetch_thumbnail(self, dirty: bool = False) -> str: - """Fetch the thumbnail for a COD object. Returns the local path to the thumbnail after downloading it. Raises an error if the thumbnail does not exist.""" + Raises: + ValueError: If the thumbnail does not exist and `generate_if_missing=False`, or if opening the thumbnail fails for any reason. + """ thumbnail_metadata = self.get_custom_tag("thumbnail", dirty=dirty) - if thumbnail_metadata is None: - raise ValueError(f"Thumbnail not found for {self}") - thumbnail_uri = thumbnail_metadata["uri"] - thumbnail_blob = storage.Blob.from_string(thumbnail_uri, client=self.client) + # Cases where we need to generate a new thumbnail: + # 1. The thumbnail metadata does not exist (i.e. the thumbnail has never been generated) + # 2. The thumbnail metadata exists but the number of instances it contains does not match the cod object (i.e. the thumbnail is stale) + if thumbnail_metadata is None or len(thumbnail_metadata["instances"]) != len( + self.get_instances(strict_sorting=False, dirty=dirty) + ): + if not generate_if_missing: + raise ValueError( + f"Thumbnail either stale or not found for {self} (and generate_if_missing=False)" + ) + generate_thumbnail(cod_obj=self, overwrite_existing=True, dirty=dirty) + thumbnail_metadata = self.get_custom_tag("thumbnail", dirty=dirty) + elif not os.path.exists(thumbnail_metadata["uri"]): + # Fetch case: we have thumbnail metadata but the thumbnail does not exist on disk, so we just have to fetch it + fetch_thumbnail(cod_obj=self, dirty=dirty) + # once we get here, we have the thumbnail on disk -> read and return it + thumbnail_file_name = os.path.basename(thumbnail_metadata["uri"]) thumbnail_local_path = os.path.join( - self.get_temp_dir().name, thumbnail_uri.split("/")[-1] + self.get_temp_dir().name, thumbnail_file_name ) - thumbnail_blob.download_to_filename(thumbnail_local_path) - # we just fetched the thumbnail, so it is guaranteed to be in the same state as the datastore - self._thumbnail_synced = True - return thumbnail_local_path + return read_thumbnail_into_array(thumbnail_local_path) @public_method def upload_error_log(self, message: str): From a7ce6ae12b6128ac1de2de52f4f61f1c5d140dfa Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Thu, 5 Jun 2025 17:21:32 -0400 Subject: [PATCH 12/24] thumbnail tests pass after refactor --- cloud_optimized_dicom/tests/test_thumbnail.py | 102 ++++++++---------- 1 file changed, 46 insertions(+), 56 deletions(-) diff --git a/cloud_optimized_dicom/tests/test_thumbnail.py b/cloud_optimized_dicom/tests/test_thumbnail.py index 0a4863b..1c9b0f4 100644 --- a/cloud_optimized_dicom/tests/test_thumbnail.py +++ b/cloud_optimized_dicom/tests/test_thumbnail.py @@ -18,7 +18,7 @@ def ingest_and_generate_thumbnail( instance_paths: list[str], datastore_path: str, client: storage.Client -): +) -> tuple[CODObject, np.ndarray]: instances = [Instance(dicom_uri=path) for path in instance_paths] with CODObject( datastore_path=datastore_path, @@ -28,55 +28,44 @@ def ingest_and_generate_thumbnail( lock=False, ) as cod_obj: cod_obj.append(instances, dirty=True) - cod_obj.generate_thumbnail(dirty=True) - return cod_obj + return cod_obj, cod_obj.get_thumbnail(dirty=True) def validate_thumbnail( testcls: unittest.TestCase, + thumbnail: np.ndarray, cod_obj: CODObject, expected_frame_count: int, expected_frame_size: tuple[int, int] = (DEFAULT_SIZE, DEFAULT_SIZE), - save_loc: str = None, + dirty: bool = True, ): - thumbnail_name = "thumbnail.mp4" if expected_frame_count > 1 else "thumbnail.jpg" - thumbnail_path = os.path.join(cod_obj.temp_dir.name, thumbnail_name) - cap = cv2.VideoCapture(thumbnail_path) - if not cap.isOpened(): - raise ValueError("Failed to open video stream.") - - # Get the total number of frames - frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - # get frame size (width, height) + testcls.assertTrue( + len(thumbnail.shape) == 3 or len(thumbnail.shape) == 4, + "Thumbnail must be a 3D or 4D array", + ) + # 3D array -> jpg -> (H, W, 3); 4D array -> mp4 -> (N, H, W, 3) + num_frames = thumbnail.shape[0] if len(thumbnail.shape) > 3 else 1 frame_size = ( - int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), - int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)), + thumbnail.shape[1:3] if len(thumbnail.shape) > 3 else thumbnail.shape[0:2] + ) + testcls.assertEqual( + num_frames, + expected_frame_count, + f"Expected {expected_frame_count} frames, got {num_frames}", + ) + testcls.assertEqual( + frame_size, + expected_frame_size, + f"Expected frame size {expected_frame_size}, got {frame_size}", ) - - # Check content variation for each frame - for _ in range(frame_count): - ret, frame = cap.read() - if not ret: - break - # Calculate standard deviation of pixel values - std_dev = frame.std() - # Assert that there is meaningful variation (not a blank/black image) - testcls.assertGreater(std_dev, 10.0, "Thumbnail appears to be blank or uniform") - - cap.release() - testcls.assertEqual(frame_count, expected_frame_count) - testcls.assertEqual(frame_size, expected_frame_size) - if save_loc: - with open(save_loc, "wb") as f, open(thumbnail_path, "rb") as f2: - f.write(f2.read()) # test the thumbnail coord converter - instance_uid = list(cod_obj._metadata.custom_tags["thumbnail"]["instances"].keys())[ - 0 - ] - thumbnail_frame_metadata = cod_obj._metadata.custom_tags["thumbnail"]["instances"][ - instance_uid - ]["frames"][0] + instance_uid = list( + cod_obj.get_custom_tag("thumbnail", dirty=dirty)["instances"].keys() + )[0] + thumbnail_frame_metadata = cod_obj.get_custom_tag("thumbnail", dirty=dirty)[ + "instances" + ][instance_uid]["frames"][0] converter = ThumbnailCoordConverter.from_anchors( thumbnail_frame_metadata["anchors"] ) @@ -114,10 +103,10 @@ def setUp(self): def test_gen_monochrome1(self): """Test thumbnail generation for a single frame DICOM file (MONOCHROME1)""" dicom_path = os.path.join(self.test_data_dir, "monochrome1.dcm") - cod_obj = ingest_and_generate_thumbnail( + cod_obj, thumbnail = ingest_and_generate_thumbnail( [dicom_path], self.datastore_path, self.client ) - validate_thumbnail(self, cod_obj, expected_frame_count=1) + validate_thumbnail(self, thumbnail, cod_obj, expected_frame_count=1) reloaded_metadata = SeriesMetadata.from_bytes(cod_obj._metadata.to_bytes()) self.assertIsNotNone(reloaded_metadata.custom_tags["thumbnail"]) self.assertDictEqual( @@ -128,10 +117,10 @@ def test_gen_monochrome1(self): def test_gen_monochrome2(self): """Test thumbnail generation for a single frame DICOM file (MONOCHROME2)""" dicom_path = os.path.join(self.test_data_dir, "monochrome2.dcm") - cod_obj = ingest_and_generate_thumbnail( + cod_obj, thumbnail = ingest_and_generate_thumbnail( [dicom_path], self.datastore_path, self.client ) - validate_thumbnail(self, cod_obj, expected_frame_count=1) + validate_thumbnail(self, thumbnail, cod_obj, expected_frame_count=1) def test_gen_mp4_mixed_phot_interp(self): """Test thumbnail generation for a series of DICOM files with different photometric interpretations (YBR_RCT and MONOCHROME2)""" @@ -141,18 +130,18 @@ def test_gen_mp4_mixed_phot_interp(self): for f in os.listdir(series_folder) if f.endswith(".dcm") ] - cod_obj = ingest_and_generate_thumbnail( + cod_obj, thumbnail = ingest_and_generate_thumbnail( dicom_paths, self.datastore_path, self.client ) - validate_thumbnail(self, cod_obj, expected_frame_count=10) + validate_thumbnail(self, thumbnail, cod_obj, expected_frame_count=10) def test_gen_mp4_ybr_rct_multiframe(self): """Test thumbnail generation for a multiframe DICOM file (YBR_RCT)""" multiframe_path = os.path.join(self.test_data_dir, "ybr_rct_multiframe.dcm") - cod_obj = ingest_and_generate_thumbnail( + cod_obj, thumbnail = ingest_and_generate_thumbnail( [multiframe_path], self.datastore_path, self.client ) - validate_thumbnail(self, cod_obj, expected_frame_count=78) + validate_thumbnail(self, thumbnail, cod_obj, expected_frame_count=78) reloaded_metadata = SeriesMetadata.from_bytes(cod_obj._metadata.to_bytes()) self.assertIsNotNone(reloaded_metadata.custom_tags["thumbnail"]) self.assertDictEqual( @@ -174,7 +163,7 @@ def test_sync_and_fetch(self): lock=True, ) as cod_obj: cod_obj.append([instance]) - cod_obj.generate_thumbnail() + cod_obj.get_thumbnail() cod_obj.sync() # with a new cod object, fetch and validate thumbnail with CODObject( @@ -186,11 +175,10 @@ def test_sync_and_fetch(self): ) as cod_obj: # thumbnail is not synced - it exists in datastore, but we haven't pulled it self.assertFalse(cod_obj._thumbnail_synced) - thumbnail_path = cod_obj.fetch_thumbnail(dirty=True) + thumbnail = cod_obj.get_thumbnail(dirty=True) # thumbnail is now synced self.assertTrue(cod_obj._thumbnail_synced) - self.assertTrue(os.path.exists(thumbnail_path)) - validate_thumbnail(self, cod_obj, expected_frame_count=1) + validate_thumbnail(self, thumbnail, cod_obj, expected_frame_count=1) def test_update_existing_thumbnail(self): """Test that updating an existing thumbnail works""" @@ -211,9 +199,10 @@ def test_update_existing_thumbnail(self): lock=True, ) as cod_obj: cod_obj.append([instance_a]) - thumbnail_path = cod_obj.generate_thumbnail() - self.assertTrue(os.path.exists(thumbnail_path)) - self.assertTrue(thumbnail_path.endswith(".jpg")) + thumbnail = cod_obj.get_thumbnail() + validate_thumbnail( + self, thumbnail, cod_obj, expected_frame_count=1, dirty=False + ) cod_obj.sync() instance_b = Instance( dicom_uri=os.path.join( @@ -232,6 +221,7 @@ def test_update_existing_thumbnail(self): lock=True, ) as cod_obj: cod_obj.append([instance_b]) - thumbnail_path = cod_obj.generate_thumbnail(overwrite_existing=True) - self.assertTrue(os.path.exists(thumbnail_path)) - self.assertTrue(thumbnail_path.endswith(".mp4")) + thumbnail = cod_obj.get_thumbnail() + validate_thumbnail( + self, thumbnail, cod_obj, expected_frame_count=2, dirty=False + ) From 845ea7a8bc09ac68f8ca16f26ddde1045629ad41 Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Fri, 6 Jun 2025 11:30:19 -0400 Subject: [PATCH 13/24] matplotlib as a testing extra --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 3e7877a..9e9b6db 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,7 @@ extras_require={ "test": [ "pydicom==2.3.0", + "matplotlib", ], }, ) From 963628aa1d9390f55ac9101596c362483affab26 Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Fri, 6 Jun 2025 11:31:57 -0400 Subject: [PATCH 14/24] can get instance slice of thumbnail --- cloud_optimized_dicom/cod_object.py | 35 ++++++++++++++---- cloud_optimized_dicom/tests/test_thumbnail.py | 37 +++++++++++++++++++ cloud_optimized_dicom/thumbnail.py | 35 ++++++++++++++++++ 3 files changed, 99 insertions(+), 8 deletions(-) diff --git a/cloud_optimized_dicom/cod_object.py b/cloud_optimized_dicom/cod_object.py index 4a636c9..2175a94 100644 --- a/cloud_optimized_dicom/cod_object.py +++ b/cloud_optimized_dicom/cod_object.py @@ -22,7 +22,11 @@ from cloud_optimized_dicom.instance import Instance from cloud_optimized_dicom.locker import CODLocker from cloud_optimized_dicom.series_metadata import SeriesMetadata -from cloud_optimized_dicom.thumbnail import fetch_thumbnail, generate_thumbnail +from cloud_optimized_dicom.thumbnail import ( + fetch_thumbnail, + generate_thumbnail, + get_instance_thumbnail_slice, +) from cloud_optimized_dicom.truncate import remove, truncate from cloud_optimized_dicom.utils import ( generate_ptr_crc32c, @@ -432,12 +436,16 @@ def get_custom_tag(self, tag_name: str, dirty: bool = False) -> Optional[dict]: @public_method def get_thumbnail( - self, generate_if_missing: bool = True, dirty: bool = False + self, + generate_if_missing: bool = True, + instance_uid: Optional[str] = None, + dirty: bool = False, ) -> np.ndarray: - """Get the thumbnail for a COD object. + """Get the thumbnail for a COD object, in the form of a numpy array. Args: generate_if_missing: Whether to generate a thumbnail if it does not exist, or is stale. + instance_uid: If provided, only return the slice of the thumbnail corresponding to the given instance UID. dirty: Whether the operation is dirty. Returns: @@ -459,15 +467,26 @@ def get_thumbnail( ) generate_thumbnail(cod_obj=self, overwrite_existing=True, dirty=dirty) thumbnail_metadata = self.get_custom_tag("thumbnail", dirty=dirty) - elif not os.path.exists(thumbnail_metadata["uri"]): - # Fetch case: we have thumbnail metadata but the thumbnail does not exist on disk, so we just have to fetch it - fetch_thumbnail(cod_obj=self, dirty=dirty) - # once we get here, we have the thumbnail on disk -> read and return it + # thumbnail metadata guaranteed to be populated at this point thumbnail_file_name = os.path.basename(thumbnail_metadata["uri"]) thumbnail_local_path = os.path.join( self.get_temp_dir().name, thumbnail_file_name ) - return read_thumbnail_into_array(thumbnail_local_path) + # Fetch case: we have thumbnail metadata but the thumbnail does not exist on disk, so we just have to fetch it + if not os.path.exists(thumbnail_local_path): + fetch_thumbnail(cod_obj=self, dirty=dirty) + # thumbnail guaranteed to be on disk at this point -> read and return it (or slice it if instance UID is provided) + thumbnail_array = read_thumbnail_into_array(thumbnail_local_path) + # return the raw array if no instance UIDs are provided + if instance_uid is None: + return thumbnail_array + # otherwise, return the slice(s) of the thumbnail corresponding to the given instance UIDs + return get_instance_thumbnail_slice( + cod_obj=self, + thumbnail_array=thumbnail_array, + instance_uid=instance_uid, + dirty=dirty, + ) @public_method def upload_error_log(self, message: str): diff --git a/cloud_optimized_dicom/tests/test_thumbnail.py b/cloud_optimized_dicom/tests/test_thumbnail.py index 1c9b0f4..f4a39ea 100644 --- a/cloud_optimized_dicom/tests/test_thumbnail.py +++ b/cloud_optimized_dicom/tests/test_thumbnail.py @@ -59,6 +59,12 @@ def validate_thumbnail( f"Expected frame size {expected_frame_size}, got {frame_size}", ) + # make sure the thumbnail is not blank (all pixels are same value) + some_pixel_value = ( + thumbnail[50, 50] if len(thumbnail.shape) == 3 else thumbnail[0, 50, 50] + ) + testcls.assertFalse(np.all(thumbnail == some_pixel_value), "Thumbnail is blank") + # test the thumbnail coord converter instance_uid = list( cod_obj.get_custom_tag("thumbnail", dirty=dirty)["instances"].keys() @@ -225,3 +231,34 @@ def test_update_existing_thumbnail(self): validate_thumbnail( self, thumbnail, cod_obj, expected_frame_count=2, dirty=False ) + + def test_get_instance_slice(self): + """Test that we can get a slice of the thumbnail for a given instance""" + # make sure we can get a single img slice from a series thumbnail + series_folder = os.path.join(self.test_data_dir, "series") + dicom_paths = [ + os.path.join(series_folder, f) + for f in os.listdir(series_folder) + if f.endswith(".dcm") + ] + cod_obj, thumbnail = ingest_and_generate_thumbnail( + dicom_paths, self.datastore_path, self.client + ) + validate_thumbnail(self, thumbnail, cod_obj, expected_frame_count=10) + some_instance_uid = cod_obj.get_instance_by_index(5, dirty=True).instance_uid() + thumbnail_slice = cod_obj.get_thumbnail( + instance_uid=some_instance_uid, dirty=True + ) + validate_thumbnail(self, thumbnail_slice, cod_obj, expected_frame_count=1) + + # make sure that getting a slice of a series with a single multiframe returns the same as the overall thumbnail + multiframe_path = os.path.join(self.test_data_dir, "ybr_rct_multiframe.dcm") + cod_obj, thumbnail = ingest_and_generate_thumbnail( + [multiframe_path], self.datastore_path, self.client + ) + validate_thumbnail(self, thumbnail, cod_obj, expected_frame_count=78) + some_instance_uid = cod_obj.get_instance_by_index(0, dirty=True).instance_uid() + thumbnail_slice = cod_obj.get_thumbnail( + instance_uid=some_instance_uid, dirty=True + ) + self.assertTrue(np.all(thumbnail_slice == thumbnail)) diff --git a/cloud_optimized_dicom/thumbnail.py b/cloud_optimized_dicom/thumbnail.py index cbbce17..a15791e 100644 --- a/cloud_optimized_dicom/thumbnail.py +++ b/cloud_optimized_dicom/thumbnail.py @@ -355,6 +355,41 @@ def fetch_thumbnail(cod_obj: "CODObject", dirty: bool = False) -> str: return thumbnail_local_path +def get_instance_thumbnail_slice( + cod_obj: "CODObject", + thumbnail_array: np.ndarray, + instance_uid: str, + dirty: bool = False, +) -> np.ndarray: + """Get a slice of the thumbnail for a given instance. + + Args: + cod_obj: The COD object to get the thumbnail slice for. + instance_uid: The UID of the instance to get the thumbnail slice for. + generate_if_missing: Whether to generate a thumbnail if it does not exist. If False, will raise an error if the thumbnail does not exist. + dirty: Whether the operation is dirty. + + Returns: + thumbnail_slice: a numpy array of the thumbnail slice + """ + thumbnail_metadata = cod_obj.get_custom_tag("thumbnail", dirty=dirty) + # if thumbnail only contains one instance, assert that is the instance requested and return the full array + if len(thumbnail_metadata["instances"]) == 1: + assert ( + instance_uid in thumbnail_metadata["instances"] + ), f"Instance UID {instance_uid} not found in thumbnail metadata" + return thumbnail_array + instance_frame_metadata = thumbnail_metadata["instances"][instance_uid]["frames"] + thumbnail_indices = [frame["thumbnail_index"] for frame in instance_frame_metadata] + # if we get here, we have a video thumbnail + instance_slice = thumbnail_array[thumbnail_indices] + # if the instance slice is a single frame, return the frame (i.e. squeeze the first dimension) + if instance_slice.shape[0] == 1: + return instance_slice[0] + # otherwise, return the instance slice video + return instance_slice + + @dataclasses.dataclass class ThumbnailCoordConverter: orig_w: int From 84f29645fd32d62b70a66f4fce5760ccaa435194 Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Fri, 6 Jun 2025 11:32:15 -0400 Subject: [PATCH 15/24] gs notebook reflects thumbnail functionality --- getting_started.ipynb | 50 ++++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/getting_started.ipynb b/getting_started.ipynb index 932ec5e..6e2e03c 100644 --- a/getting_started.ipynb +++ b/getting_started.ipynb @@ -16,9 +16,17 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 10, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Matplotlib is building the font cache; this may take a moment.\n" + ] + } + ], "source": [ "from cloud_optimized_dicom.cod_object import CODObject\n", "from cloud_optimized_dicom.instance import Instance\n", @@ -28,6 +36,7 @@ "import pydicom\n", "import tempfile\n", "import os\n", + "import matplotlib.pyplot as plt\n", "\n", "assert hasattr(CODObject, \"get_instances\")\n", "\n", @@ -52,7 +61,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -79,7 +88,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -104,7 +113,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -114,7 +123,7 @@ "All instances UIDs in the series: dict_keys(['1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612', '1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455'])\n", "Instance with UID 1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612 has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612\n", "Instance with index 1 has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n", - "Instance object Instance(uri=/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp3d4tqvsj_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm, hashed_uids=False, instance_uid=1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455, series_uid=1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506, study_uid=1.2.826.0.1.3680043.8.498.77805869330689203045629680212005263354, dependencies=[]) has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n" + "Instance object Instance(uri=/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp1zk6ofcq_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm, hashed_uids=False, instance_uid=1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455, series_uid=1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506, study_uid=1.2.826.0.1.3680043.8.498.77805869330689203045629680212005263354, dependencies=[]) has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n" ] } ], @@ -153,7 +162,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -182,7 +191,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ @@ -209,15 +218,25 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Thumbnail generated at: /var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp2i4t1gce_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/thumbnail.mp4\n" + "Generated series thumbnail with shape: (2, 128, 128, 3) ([n_frames, height, width, 3])\n" ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ @@ -232,8 +251,15 @@ " study_uid=instance_a.study_uid(), \n", " series_uid=instance_a.series_uid(), \n", " lock=False) as cod_obj:\n", - " thumbnail_local_path = cod_obj.generate_thumbnail(dirty=True)\n", - " print(\"Thumbnail generated at: \", thumbnail_local_path)\n" + " thumbnail = cod_obj.get_thumbnail(dirty=True)\n", + " print(f\"Generated series thumbnail with shape: {thumbnail.shape} ([n_frames, height, width, 3])\")\n", + " # get thumbnail slice for a specific instance\n", + " instance_b_thumbnail = cod_obj.get_thumbnail(instance_uid=instance_b.instance_uid(), dirty=True)\n", + " # display the thumbnail\n", + " plt.imshow(instance_b_thumbnail)\n", + " plt.title(\"Instance B Thumbnail\")\n", + " plt.axis(\"off\")\n", + " plt.show()\n" ] }, { From 9115fab34a1f1df3ebd1a36c46476f25d1008856 Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Fri, 6 Jun 2025 11:33:48 -0400 Subject: [PATCH 16/24] docstring fix --- cloud_optimized_dicom/thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud_optimized_dicom/thumbnail.py b/cloud_optimized_dicom/thumbnail.py index a15791e..7e8b876 100644 --- a/cloud_optimized_dicom/thumbnail.py +++ b/cloud_optimized_dicom/thumbnail.py @@ -365,8 +365,8 @@ def get_instance_thumbnail_slice( Args: cod_obj: The COD object to get the thumbnail slice for. + thumbnail_array: The numpy array of the full series thumbnail. instance_uid: The UID of the instance to get the thumbnail slice for. - generate_if_missing: Whether to generate a thumbnail if it does not exist. If False, will raise an error if the thumbnail does not exist. dirty: Whether the operation is dirty. Returns: From 52730b8ecc1d223a20877281b715b8f64714f029 Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Fri, 6 Jun 2025 11:36:48 -0400 Subject: [PATCH 17/24] thumbnail util methods infer cod op dirtiness from lock bool --- cloud_optimized_dicom/cod_object.py | 5 ++--- cloud_optimized_dicom/thumbnail.py | 12 +++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/cloud_optimized_dicom/cod_object.py b/cloud_optimized_dicom/cod_object.py index 2175a94..952f21a 100644 --- a/cloud_optimized_dicom/cod_object.py +++ b/cloud_optimized_dicom/cod_object.py @@ -465,7 +465,7 @@ def get_thumbnail( raise ValueError( f"Thumbnail either stale or not found for {self} (and generate_if_missing=False)" ) - generate_thumbnail(cod_obj=self, overwrite_existing=True, dirty=dirty) + generate_thumbnail(cod_obj=self, overwrite_existing=True) thumbnail_metadata = self.get_custom_tag("thumbnail", dirty=dirty) # thumbnail metadata guaranteed to be populated at this point thumbnail_file_name = os.path.basename(thumbnail_metadata["uri"]) @@ -474,7 +474,7 @@ def get_thumbnail( ) # Fetch case: we have thumbnail metadata but the thumbnail does not exist on disk, so we just have to fetch it if not os.path.exists(thumbnail_local_path): - fetch_thumbnail(cod_obj=self, dirty=dirty) + fetch_thumbnail(cod_obj=self) # thumbnail guaranteed to be on disk at this point -> read and return it (or slice it if instance UID is provided) thumbnail_array = read_thumbnail_into_array(thumbnail_local_path) # return the raw array if no instance UIDs are provided @@ -485,7 +485,6 @@ def get_thumbnail( cod_obj=self, thumbnail_array=thumbnail_array, instance_uid=instance_uid, - dirty=dirty, ) @public_method diff --git a/cloud_optimized_dicom/thumbnail.py b/cloud_optimized_dicom/thumbnail.py index 7e8b876..798c83e 100644 --- a/cloud_optimized_dicom/thumbnail.py +++ b/cloud_optimized_dicom/thumbnail.py @@ -289,15 +289,15 @@ def _generate_instance_lookup_dict( def generate_thumbnail( cod_obj: "CODObject", overwrite_existing: bool = False, - dirty: bool = False, ): """Generate a thumbnail for a COD object. Args: cod_obj: The COD object to generate a thumbnail for. overwrite_existing: Whether to overwrite the existing thumbnail, if it exists. - dirty: Whether the operation is dirty. """ + # can infer whether the operation is dirty by checking if the cod object is locked + dirty = not cod_obj.lock if ( cod_obj.get_custom_tag("thumbnail", dirty=dirty) is not None and not overwrite_existing @@ -330,7 +330,7 @@ def generate_thumbnail( return thumbnail_path -def fetch_thumbnail(cod_obj: "CODObject", dirty: bool = False) -> str: +def fetch_thumbnail(cod_obj: "CODObject") -> str: """Download thumbnail from GCS for given cod object. Returns: @@ -340,7 +340,7 @@ def fetch_thumbnail(cod_obj: "CODObject", dirty: bool = False) -> str: ValueError: if the cod object has no thumbnail metadata NotFound: if the thumbnail blob does not exist in GCS """ - thumbnail_metadata = cod_obj.get_custom_tag("thumbnail", dirty=dirty) + thumbnail_metadata = cod_obj.get_custom_tag("thumbnail", dirty=not cod_obj.lock) if thumbnail_metadata is None: raise ValueError(f"Thumbnail metadata not found for {cod_obj}") thumbnail_uri = thumbnail_metadata["uri"] @@ -359,7 +359,6 @@ def get_instance_thumbnail_slice( cod_obj: "CODObject", thumbnail_array: np.ndarray, instance_uid: str, - dirty: bool = False, ) -> np.ndarray: """Get a slice of the thumbnail for a given instance. @@ -367,12 +366,11 @@ def get_instance_thumbnail_slice( cod_obj: The COD object to get the thumbnail slice for. thumbnail_array: The numpy array of the full series thumbnail. instance_uid: The UID of the instance to get the thumbnail slice for. - dirty: Whether the operation is dirty. Returns: thumbnail_slice: a numpy array of the thumbnail slice """ - thumbnail_metadata = cod_obj.get_custom_tag("thumbnail", dirty=dirty) + thumbnail_metadata = cod_obj.get_custom_tag("thumbnail", dirty=not cod_obj.lock) # if thumbnail only contains one instance, assert that is the instance requested and return the full array if len(thumbnail_metadata["instances"]) == 1: assert ( From a1cbbcbc490c5d7537a2111e8870a7315e35ffb5 Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Fri, 6 Jun 2025 11:59:16 -0400 Subject: [PATCH 18/24] get_instance_by_thumbnail_index --- cloud_optimized_dicom/cod_object.py | 19 ++++++ cloud_optimized_dicom/tests/test_thumbnail.py | 22 ++++++ cloud_optimized_dicom/thumbnail.py | 28 ++++++++ getting_started.ipynb | 68 ++++++++----------- 4 files changed, 97 insertions(+), 40 deletions(-) diff --git a/cloud_optimized_dicom/cod_object.py b/cloud_optimized_dicom/cod_object.py index 952f21a..e628e5e 100644 --- a/cloud_optimized_dicom/cod_object.py +++ b/cloud_optimized_dicom/cod_object.py @@ -25,6 +25,7 @@ from cloud_optimized_dicom.thumbnail import ( fetch_thumbnail, generate_thumbnail, + get_instance_by_thumbnail_index, get_instance_thumbnail_slice, ) from cloud_optimized_dicom.truncate import remove, truncate @@ -249,6 +250,24 @@ def get_instance_by_index(self, index: int, dirty: bool = False) -> Instance: index ] + @public_method + def get_instance_by_thumbnail_index( + self, thumbnail_index: int, dirty: bool = False + ) -> Instance: + """Get an instance by thumbnail index. + + Args: + thumbnail_index: int - The index of the thumbnail from you want the instance for. + dirty: bool - Must be `True` if the CODObject is "dirty" (i.e. `lock=False`). + + Returns: + instance: The instance corresponding to the thumbnail index. + + Raises: + ValueError: if the cod object has no thumbnail metadata, or `thumbnail_index` is out of bounds + """ + return get_instance_by_thumbnail_index(self, thumbnail_index) + @public_method def open_instance(self, instance: Union[Instance, str, int], dirty: bool = False): """Open an instance (first fetches the series tar if necessary). For convenience, the instance parameter can be one of: diff --git a/cloud_optimized_dicom/tests/test_thumbnail.py b/cloud_optimized_dicom/tests/test_thumbnail.py index f4a39ea..4adc7eb 100644 --- a/cloud_optimized_dicom/tests/test_thumbnail.py +++ b/cloud_optimized_dicom/tests/test_thumbnail.py @@ -262,3 +262,25 @@ def test_get_instance_slice(self): instance_uid=some_instance_uid, dirty=True ) self.assertTrue(np.all(thumbnail_slice == thumbnail)) + + def test_get_instance_by_thumbnail_index(self): + """Test that we can get an instance by thumbnail index""" + # make sure we can get a single img slice from a series thumbnail + series_folder = os.path.join(self.test_data_dir, "series") + dicom_paths = [ + os.path.join(series_folder, f) + for f in os.listdir(series_folder) + if f.endswith(".dcm") + ] + cod_obj, thumbnail = ingest_and_generate_thumbnail( + dicom_paths, self.datastore_path, self.client + ) + validate_thumbnail(self, thumbnail, cod_obj, expected_frame_count=10) + # because this series is 10 single frame instances, we expect the check below to pass + instance_retrieved_by_index = cod_obj.get_instance_by_index(5, dirty=True) + instance_retrieved_by_thumbnail_index = cod_obj.get_instance_by_thumbnail_index( + 5, dirty=True + ) + self.assertEqual( + instance_retrieved_by_index, instance_retrieved_by_thumbnail_index + ) diff --git a/cloud_optimized_dicom/thumbnail.py b/cloud_optimized_dicom/thumbnail.py index 798c83e..5d7aa34 100644 --- a/cloud_optimized_dicom/thumbnail.py +++ b/cloud_optimized_dicom/thumbnail.py @@ -388,6 +388,34 @@ def get_instance_thumbnail_slice( return instance_slice +def get_instance_by_thumbnail_index( + cod_obj: "CODObject", thumbnail_index: int +) -> Instance: + """Get an instance by thumbnail index. + + Args: + thumbnail_index: int - The index of the thumbnail from you want the instance for. + + Returns: + instance: The instance corresponding to the thumbnail index. + + Raises: + ValueError: if the cod object has no thumbnail metadata, or `thumbnail_index` is out of bounds + """ + thumbnail_metadata = cod_obj.get_custom_tag("thumbnail", dirty=not cod_obj.lock) + if not thumbnail_metadata: + raise ValueError(f"Thumbnail metadata not found for {cod_obj}") + thumbnail_index_to_instance_frame = thumbnail_metadata[ + "thumbnail_index_to_instance_frame" + ] + if (num_frames := len(thumbnail_index_to_instance_frame)) <= thumbnail_index: + raise ValueError( + f"Thumbnail index {thumbnail_index} is out of bounds for {cod_obj} (has {num_frames} frames)" + ) + instance_uid, _ = thumbnail_index_to_instance_frame[thumbnail_index] + return cod_obj.get_instance(instance_uid=instance_uid, dirty=not cod_obj.lock) + + @dataclasses.dataclass class ThumbnailCoordConverter: orig_w: int diff --git a/getting_started.ipynb b/getting_started.ipynb index 6e2e03c..8abddd6 100644 --- a/getting_started.ipynb +++ b/getting_started.ipynb @@ -16,17 +16,9 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 1, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Matplotlib is building the font cache; this may take a moment.\n" - ] - } - ], + "outputs": [], "source": [ "from cloud_optimized_dicom.cod_object import CODObject\n", "from cloud_optimized_dicom.instance import Instance\n", @@ -61,7 +53,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -88,7 +80,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -113,7 +105,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -123,7 +115,7 @@ "All instances UIDs in the series: dict_keys(['1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612', '1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455'])\n", "Instance with UID 1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612 has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612\n", "Instance with index 1 has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n", - "Instance object Instance(uri=/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp1zk6ofcq_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm, hashed_uids=False, instance_uid=1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455, series_uid=1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506, study_uid=1.2.826.0.1.3680043.8.498.77805869330689203045629680212005263354, dependencies=[]) has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n" + "Instance object Instance(uri=/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmps99_wvqt_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm, hashed_uids=False, instance_uid=1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455, series_uid=1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506, study_uid=1.2.826.0.1.3680043.8.498.77805869330689203045629680212005263354, dependencies=[]) has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n" ] } ], @@ -162,7 +154,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -191,7 +183,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -213,12 +205,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Generate a thumbnail" + "## Working with thumbnails" ] }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -240,22 +232,18 @@ } ], "source": [ - "# change to cod_obj.get_thumbnail(generate_if_missing=True) -> uint numpy array... fetch and return thumbnail if it exists, generate it if it doesn't (or fail if generate_if_missing=False)\n", - "\n", - "# instance.get_thumbnail() -> cod_obj.get_thumbnail()[slice_for_instance]\n", - "\n", - "# cod_obj.get_instance_from_thumbnail_index(thumbnail_index) -> Instance\n", - "\n", "with CODObject(datastore_path=datastore_path, \n", " client=client, \n", " study_uid=instance_a.study_uid(), \n", " series_uid=instance_a.series_uid(), \n", " lock=False) as cod_obj:\n", + " # generate a thumbnail\n", " thumbnail = cod_obj.get_thumbnail(dirty=True)\n", " print(f\"Generated series thumbnail with shape: {thumbnail.shape} ([n_frames, height, width, 3])\")\n", + " # retrieve an instance from its index in the thumbnail (note that after sorting based on InstanceNumber, SliceLocation, etc., instance_a is the second instance in the series)\n", + " assert cod_obj.get_instance_by_thumbnail_index(1, dirty=True) == instance_a\n", " # get thumbnail slice for a specific instance\n", " instance_b_thumbnail = cod_obj.get_thumbnail(instance_uid=instance_b.instance_uid(), dirty=True)\n", - " # display the thumbnail\n", " plt.imshow(instance_b_thumbnail)\n", " plt.title(\"Instance B Thumbnail\")\n", " plt.axis(\"off\")\n", @@ -271,7 +259,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -300,7 +288,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -355,7 +343,7 @@ }, { "cell_type": "code", - "execution_count": 83, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -391,18 +379,18 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "[Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpu4ust4fw_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm'\n", + "[Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpiie59xs7_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm'\n", "Traceback (most recent call last):\n", " File \"/Users/cal/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py\", line 439, in _create_or_append_tar\n", " instance.append_to_series_tar(tar)\n", - " File \"/Users/cal/work/cloud_optimized_dicom/cloud_optimized_dicom/instance.py\", line 311, in append_to_series_tar\n", + " File \"/Users/cal/work/cloud_optimized_dicom/cloud_optimized_dicom/instance.py\", line 317, in append_to_series_tar\n", " tar.add(self.dicom_uri, arcname=f\"/instances/{uid_for_uri}.dcm\")\n", " File \"/opt/homebrew/Cellar/python@3.11/3.11.11/Frameworks/Python.framework/Versions/3.11/lib/python3.11/tarfile.py\", line 2194, in add\n", " tarinfo = self.gettarinfo(name, arcname)\n", @@ -410,12 +398,12 @@ " File \"/opt/homebrew/Cellar/python@3.11/3.11.11/Frameworks/Python.framework/Versions/3.11/lib/python3.11/tarfile.py\", line 2067, in gettarinfo\n", " statres = os.lstat(name)\n", " ^^^^^^^^^^^^^^\n", - "FileNotFoundError: [Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpu4ust4fw_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm'\n", - "[Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpu4ust4fw_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm'\n", + "FileNotFoundError: [Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpiie59xs7_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm'\n", + "[Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpiie59xs7_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm'\n", "Traceback (most recent call last):\n", " File \"/Users/cal/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py\", line 439, in _create_or_append_tar\n", " instance.append_to_series_tar(tar)\n", - " File \"/Users/cal/work/cloud_optimized_dicom/cloud_optimized_dicom/instance.py\", line 311, in append_to_series_tar\n", + " File \"/Users/cal/work/cloud_optimized_dicom/cloud_optimized_dicom/instance.py\", line 317, in append_to_series_tar\n", " tar.add(self.dicom_uri, arcname=f\"/instances/{uid_for_uri}.dcm\")\n", " File \"/opt/homebrew/Cellar/python@3.11/3.11.11/Frameworks/Python.framework/Versions/3.11/lib/python3.11/tarfile.py\", line 2194, in add\n", " tarinfo = self.gettarinfo(name, arcname)\n", @@ -423,24 +411,24 @@ " File \"/opt/homebrew/Cellar/python@3.11/3.11.11/Frameworks/Python.framework/Versions/3.11/lib/python3.11/tarfile.py\", line 2067, in gettarinfo\n", " statres = os.lstat(name)\n", " ^^^^^^^^^^^^^^\n", - "FileNotFoundError: [Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpu4ust4fw_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm'\n" + "FileNotFoundError: [Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpiie59xs7_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm'\n" ] }, { "ename": "ValueError", - "evalue": "GRADIENT_STATE_LOGS:FAILED_TO_TAR_ALL_INSTANCES:/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpu4ust4fw_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm\n/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpu4ust4fw_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm", + "evalue": "GRADIENT_STATE_LOGS:FAILED_TO_TAR_ALL_INSTANCES:/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpiie59xs7_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm\n/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpiie59xs7_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm", "output_type": "error", "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[64]\u001b[39m\u001b[32m, line 30\u001b[39m\n\u001b[32m 23\u001b[39m i.uid_hash_func = example_hash_function\n\u001b[32m 24\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m CODObject(datastore_path=deid_datastore_path, \n\u001b[32m 25\u001b[39m client=client, \n\u001b[32m 26\u001b[39m study_uid=hashed_study_uid, \n\u001b[32m 27\u001b[39m series_uid=hashed_series_uid,\n\u001b[32m 28\u001b[39m hashed_uids=\u001b[38;5;28;01mTrue\u001b[39;00m,\n\u001b[32m 29\u001b[39m lock=\u001b[38;5;28;01mFalse\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m deid_cod:\n\u001b[32m---> \u001b[39m\u001b[32m30\u001b[39m \u001b[43mdeid_cod\u001b[49m\u001b[43m.\u001b[49m\u001b[43mappend\u001b[49m\u001b[43m(\u001b[49m\u001b[43minstances\u001b[49m\u001b[43m=\u001b[49m\u001b[43minstances\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdirty\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/utils.py:166\u001b[39m, in \u001b[36mpublic_method..wrapper\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 162\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.lock:\n\u001b[32m 163\u001b[39m logger.warning(\n\u001b[32m 164\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mPerforming dirty operation \u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfunc.\u001b[34m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33m on locked CODObject: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 165\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m166\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/cod_object.py:230\u001b[39m, in \u001b[36mCODObject.append\u001b[39m\u001b[34m(self, instances, treat_metadata_diffs_as_same, max_instance_size, max_series_size, delete_local_origin, dirty)\u001b[39m\n\u001b[32m 210\u001b[39m \u001b[38;5;129m@public_method\u001b[39m\n\u001b[32m 211\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mappend\u001b[39m(\n\u001b[32m 212\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 218\u001b[39m dirty: \u001b[38;5;28mbool\u001b[39m = \u001b[38;5;28;01mFalse\u001b[39;00m,\n\u001b[32m 219\u001b[39m ):\n\u001b[32m 220\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Append a list of instances to the COD object.\u001b[39;00m\n\u001b[32m 221\u001b[39m \n\u001b[32m 222\u001b[39m \u001b[33;03m Args:\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 228\u001b[39m \u001b[33;03m dirty: bool - Must be `True` if the CODObject is \"dirty\" (i.e. `lock=False`).\u001b[39;00m\n\u001b[32m 229\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m230\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mappend\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 231\u001b[39m \u001b[43m \u001b[49m\u001b[43mcod_object\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 232\u001b[39m \u001b[43m \u001b[49m\u001b[43minstances\u001b[49m\u001b[43m=\u001b[49m\u001b[43minstances\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 233\u001b[39m \u001b[43m \u001b[49m\u001b[43mdelete_local_origin\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdelete_local_origin\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 234\u001b[39m \u001b[43m \u001b[49m\u001b[43mtreat_metadata_diffs_as_same\u001b[49m\u001b[43m=\u001b[49m\u001b[43mtreat_metadata_diffs_as_same\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 235\u001b[39m \u001b[43m \u001b[49m\u001b[43mmax_instance_size\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmax_instance_size\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 236\u001b[39m \u001b[43m \u001b[49m\u001b[43mmax_series_size\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmax_series_size\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 237\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[11]\u001b[39m\u001b[32m, line 31\u001b[39m\n\u001b[32m 23\u001b[39m i.uid_hash_func = example_hash_function\n\u001b[32m 24\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m CODObject(datastore_path=deid_datastore_path, \n\u001b[32m 25\u001b[39m client=client, \n\u001b[32m 26\u001b[39m study_uid=hashed_study_uid, \n\u001b[32m (...)\u001b[39m\u001b[32m 29\u001b[39m lock=\u001b[38;5;28;01mFalse\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m deid_cod:\n\u001b[32m 30\u001b[39m \u001b[38;5;66;03m# TODO: this doesnt work yet - need to extract the instances from the original tar\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m31\u001b[39m \u001b[43mdeid_cod\u001b[49m\u001b[43m.\u001b[49m\u001b[43mappend\u001b[49m\u001b[43m(\u001b[49m\u001b[43minstances\u001b[49m\u001b[43m=\u001b[49m\u001b[43minstances\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdirty\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/utils.py:204\u001b[39m, in \u001b[36mpublic_method..wrapper\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 200\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.lock:\n\u001b[32m 201\u001b[39m logger.warning(\n\u001b[32m 202\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mPerforming dirty operation \u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfunc.\u001b[34m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33m on locked CODObject: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 203\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m204\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/cod_object.py:329\u001b[39m, in \u001b[36mCODObject.append\u001b[39m\u001b[34m(self, instances, treat_metadata_diffs_as_same, max_instance_size, max_series_size, delete_local_origin, dirty)\u001b[39m\n\u001b[32m 309\u001b[39m \u001b[38;5;129m@public_method\u001b[39m\n\u001b[32m 310\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mappend\u001b[39m(\n\u001b[32m 311\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 317\u001b[39m dirty: \u001b[38;5;28mbool\u001b[39m = \u001b[38;5;28;01mFalse\u001b[39;00m,\n\u001b[32m 318\u001b[39m ):\n\u001b[32m 319\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Append a list of instances to the COD object.\u001b[39;00m\n\u001b[32m 320\u001b[39m \n\u001b[32m 321\u001b[39m \u001b[33;03m Args:\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 327\u001b[39m \u001b[33;03m dirty: bool - Must be `True` if the CODObject is \"dirty\" (i.e. `lock=False`).\u001b[39;00m\n\u001b[32m 328\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m329\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mappend\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 330\u001b[39m \u001b[43m \u001b[49m\u001b[43mcod_object\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 331\u001b[39m \u001b[43m \u001b[49m\u001b[43minstances\u001b[49m\u001b[43m=\u001b[49m\u001b[43minstances\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 332\u001b[39m \u001b[43m \u001b[49m\u001b[43mdelete_local_origin\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdelete_local_origin\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 333\u001b[39m \u001b[43m \u001b[49m\u001b[43mtreat_metadata_diffs_as_same\u001b[49m\u001b[43m=\u001b[49m\u001b[43mtreat_metadata_diffs_as_same\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 334\u001b[39m \u001b[43m \u001b[49m\u001b[43mmax_instance_size\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmax_instance_size\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 335\u001b[39m \u001b[43m \u001b[49m\u001b[43mmax_series_size\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmax_series_size\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 336\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py:84\u001b[39m, in \u001b[36mappend\u001b[39m\u001b[34m(cod_object, instances, delete_local_origin, treat_metadata_diffs_as_same, max_instance_size, max_series_size)\u001b[39m\n\u001b[32m 82\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m append_result\n\u001b[32m 83\u001b[39m \u001b[38;5;66;03m# handle new\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m84\u001b[39m append_result = \u001b[43m_handle_new\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcod_object\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstate_change\u001b[49m\u001b[43m.\u001b[49m\u001b[43mnew\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mappend_result\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 85\u001b[39m metrics.TAR_SUCCESS_COUNTER.inc()\n\u001b[32m 86\u001b[39m metrics.TAR_BYTES_PROCESSED.inc(os.path.getsize(cod_object.tar_file_path))\n", "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py:391\u001b[39m, in \u001b[36m_handle_new\u001b[39m\u001b[34m(cod_object, new_state_changes, append_result)\u001b[39m\n\u001b[32m 381\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_handle_new\u001b[39m(\n\u001b[32m 382\u001b[39m cod_object: \u001b[33m\"\u001b[39m\u001b[33mCODObject\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 383\u001b[39m new_state_changes: \u001b[38;5;28mlist\u001b[39m[\u001b[38;5;28mtuple\u001b[39m[Instance, Optional[SeriesMetadata], Optional[\u001b[38;5;28mstr\u001b[39m]]],\n\u001b[32m 384\u001b[39m append_result: AppendResult,\n\u001b[32m 385\u001b[39m ) -> AppendResult:\n\u001b[32m 386\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 387\u001b[39m \u001b[33;03m Create/append to tar & upload; add to series metadata & upload.\u001b[39;00m\n\u001b[32m 388\u001b[39m \u001b[33;03m Returns:\u001b[39;00m\n\u001b[32m 389\u001b[39m \u001b[33;03m updated_append_result\u001b[39;00m\n\u001b[32m 390\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m391\u001b[39m instances_added_to_tar = \u001b[43m_handle_create_tar\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcod_object\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnew_state_changes\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 392\u001b[39m _handle_create_metadata(cod_object, instances_added_to_tar)\n\u001b[32m 393\u001b[39m \u001b[38;5;66;03m# update append result\u001b[39;00m\n", "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py:415\u001b[39m, in \u001b[36m_handle_create_tar\u001b[39m\u001b[34m(cod_object, new_state_changes)\u001b[39m\n\u001b[32m 412\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(cod_object._metadata.instances) > \u001b[32m0\u001b[39m:\n\u001b[32m 413\u001b[39m cod_object.pull_tar(dirty=\u001b[38;5;129;01mnot\u001b[39;00m cod_object.lock)\n\u001b[32m--> \u001b[39m\u001b[32m415\u001b[39m instances_added_to_tar = \u001b[43m_create_or_append_tar\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 416\u001b[39m \u001b[43m \u001b[49m\u001b[43mcod_object\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[43mnew\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mnew\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m_\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m_\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mnew_state_changes\u001b[49m\u001b[43m]\u001b[49m\n\u001b[32m 417\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 418\u001b[39m _create_sqlite_index(cod_object)\n\u001b[32m 419\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m instances_added_to_tar\n", "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py:447\u001b[39m, in \u001b[36m_create_or_append_tar\u001b[39m\u001b[34m(cod_object, instances_to_add)\u001b[39m\n\u001b[32m 445\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(instances_added_to_tar) == \u001b[32m0\u001b[39m:\n\u001b[32m 446\u001b[39m uri_str = \u001b[33m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33m\"\u001b[39m.join([instance.dicom_uri \u001b[38;5;28;01mfor\u001b[39;00m instance \u001b[38;5;129;01min\u001b[39;00m instances_to_add])\n\u001b[32m--> \u001b[39m\u001b[32m447\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mGRADIENT_STATE_LOGS:FAILED_TO_TAR_ALL_INSTANCES:\u001b[39m\u001b[38;5;132;01m{\u001b[39;00muri_str\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m 448\u001b[39m logger.info(\n\u001b[32m 449\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mGRADIENT_STATE_LOGS:POPULATED_TAR:\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mcod_object.tar_file_path\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m (\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mos.path.getsize(cod_object.tar_file_path)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m bytes)\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 450\u001b[39m )\n\u001b[32m 451\u001b[39m \u001b[38;5;66;03m# tar has been altered, so it is no longer in sync with the datastore\u001b[39;00m\n", - "\u001b[31mValueError\u001b[39m: GRADIENT_STATE_LOGS:FAILED_TO_TAR_ALL_INSTANCES:/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpu4ust4fw_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm\n/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpu4ust4fw_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm" + "\u001b[31mValueError\u001b[39m: GRADIENT_STATE_LOGS:FAILED_TO_TAR_ALL_INSTANCES:/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpiie59xs7_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm\n/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpiie59xs7_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm" ] } ], From 84a189eb23274f0cc61d7e217d253d0849f80ccf Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Fri, 6 Jun 2025 12:10:48 -0400 Subject: [PATCH 19/24] BREAKING CHANGE: rename custom_tag -> metadata_field --- cloud_optimized_dicom/cod_object.py | 28 +++++++------ cloud_optimized_dicom/series_metadata.py | 26 ++++++------ .../tests/test_metadata_serialization.py | 4 +- cloud_optimized_dicom/tests/test_thumbnail.py | 16 ++++---- cloud_optimized_dicom/thumbnail.py | 14 +++---- getting_started.ipynb | 40 +++++++++---------- 6 files changed, 65 insertions(+), 63 deletions(-) diff --git a/cloud_optimized_dicom/cod_object.py b/cloud_optimized_dicom/cod_object.py index e628e5e..9b9851d 100644 --- a/cloud_optimized_dicom/cod_object.py +++ b/cloud_optimized_dicom/cod_object.py @@ -113,7 +113,7 @@ def __init__( self._metadata_synced = _metadata_synced # if the thumbnail exists, it is not synced (we did not fetch it) self._thumbnail_synced = ( - self.get_custom_tag("thumbnail", dirty=not lock) is None + self.get_metadata_field("thumbnail", dirty=not lock) is None ) def _validate_uids(self): @@ -413,7 +413,7 @@ def _sync_thumbnail(self): if self._thumbnail_synced: logger.info(f"Skipping thumbnail sync - thumbnail already synced: {self}") return - thumbnail_metadata = self.get_custom_tag("thumbnail") + thumbnail_metadata = self.get_metadata_field("thumbnail") if thumbnail_metadata is None: logger.info(f"Skipping thumbnail sync - thumbnail does not exist: {self}") return @@ -434,24 +434,26 @@ def _sync_thumbnail(self): self._thumbnail_synced = True @public_method - def add_custom_tag( + def add_metadata_field( self, - tag_name: str, - tag_value: dict, + field_name: str, + field_value: dict, overwrite_existing: bool = True, dirty: bool = False, ): - """Add a custom tag to the metadata""" - self.get_metadata(dirty=dirty)._add_custom_tag( - tag_name, tag_value, overwrite_existing + """Add a custom field to the metadata""" + self.get_metadata(dirty=dirty)._add_metadata_field( + field_name, field_value, overwrite_existing ) # modifying metadata means it is not synced to the datastore self._metadata_synced = False @public_method - def get_custom_tag(self, tag_name: str, dirty: bool = False) -> Optional[dict]: - """Get a custom tag from the metadata. Returns `None` if the tag does not exist.""" - return self.get_metadata(dirty=dirty).custom_tags.get(tag_name, None) + def get_metadata_field( + self, field_name: str, dirty: bool = False + ) -> Optional[dict]: + """Get a custom field from the metadata. Returns `None` if the field does not exist.""" + return self.get_metadata(dirty=dirty).metadata_fields.get(field_name, None) @public_method def get_thumbnail( @@ -473,7 +475,7 @@ def get_thumbnail( Raises: ValueError: If the thumbnail does not exist and `generate_if_missing=False`, or if opening the thumbnail fails for any reason. """ - thumbnail_metadata = self.get_custom_tag("thumbnail", dirty=dirty) + thumbnail_metadata = self.get_metadata_field("thumbnail", dirty=dirty) # Cases where we need to generate a new thumbnail: # 1. The thumbnail metadata does not exist (i.e. the thumbnail has never been generated) # 2. The thumbnail metadata exists but the number of instances it contains does not match the cod object (i.e. the thumbnail is stale) @@ -485,7 +487,7 @@ def get_thumbnail( f"Thumbnail either stale or not found for {self} (and generate_if_missing=False)" ) generate_thumbnail(cod_obj=self, overwrite_existing=True) - thumbnail_metadata = self.get_custom_tag("thumbnail", dirty=dirty) + thumbnail_metadata = self.get_metadata_field("thumbnail", dirty=dirty) # thumbnail metadata guaranteed to be populated at this point thumbnail_file_name = os.path.basename(thumbnail_metadata["uri"]) thumbnail_local_path = os.path.join( diff --git a/cloud_optimized_dicom/series_metadata.py b/cloud_optimized_dicom/series_metadata.py index 02a6c42..1c30249 100644 --- a/cloud_optimized_dicom/series_metadata.py +++ b/cloud_optimized_dicom/series_metadata.py @@ -19,7 +19,7 @@ class SeriesMetadata: series_uid (str): The series UID of this series (should match `CODObject.series_uid`) hashed_uids (bool): Flag indicating whether the series uses de-identified UIDs. instances (dict[str, Instance]): Mapping of instance UID (hashed if `hashed_uids=True`) to Instance object - custom_tags (dict): Any additional user defined data + metadata_fields (dict): Any additional user defined data is_sorted (bool): Flag indicating whether the instances dict is sorted If loading existing metadata, `hashed_uids` is inferred by the presence of the key `deid_study_uid` as opposed to `study_uid`. @@ -30,17 +30,19 @@ class SeriesMetadata: series_uid: str hashed_uids: bool instances: dict[str, Instance] = field(default_factory=dict) - custom_tags: dict = field(default_factory=dict) + metadata_fields: dict = field(default_factory=dict) is_sorted: bool = False - def _add_custom_tag(self, tag_name: str, tag_value, overwrite_existing=False): - """Add a custom tag to the series metadata""" - # Raise error if tag exists and we're not overwriting existing tags - if hasattr(self.custom_tags, tag_name) and not overwrite_existing: + def _add_metadata_field( + self, field_name: str, field_value, overwrite_existing=False + ): + """Add a custom field to the series metadata""" + # Raise error if field exists and we're not overwriting existing fields + if field_name in self.metadata_fields and not overwrite_existing: raise ValueError( - f"Metadata tag {tag_name} already exists (and overwrite_existing=False)" + f"Metadata field {field_name} already exists (and overwrite_existing=False)" ) - self.custom_tags[tag_name] = tag_value + self.metadata_fields[field_name] = field_value def _sort_instances(self): """Sort the instances dict, the same way instances are sorted for the thumbnail. @@ -79,7 +81,7 @@ def to_dict(self) -> dict: }, }, } - return {**base_dict, **self.custom_tags} + return {**base_dict, **self.metadata_fields} def to_bytes(self) -> bytes: """Convert from SeriesMetadata -> dict -> JSON -> bytes""" @@ -122,15 +124,15 @@ def from_dict( for instance_uid, instance_dict in cod_dict.get("instances", {}).items() } - # Treat any remaining keys as custom tags - custom_tags = series_metadata_dict + # Treat any remaining keys as metadata fields + metadata_fields = series_metadata_dict return cls( study_uid=study_uid, series_uid=series_uid, hashed_uids=is_hashed, instances=instances, - custom_tags=custom_tags, + metadata_fields=metadata_fields, ) @classmethod diff --git a/cloud_optimized_dicom/tests/test_metadata_serialization.py b/cloud_optimized_dicom/tests/test_metadata_serialization.py index a1e55df..e05b7d5 100644 --- a/cloud_optimized_dicom/tests/test_metadata_serialization.py +++ b/cloud_optimized_dicom/tests/test_metadata_serialization.py @@ -39,9 +39,9 @@ def _assert_load_success(self, metadata: SeriesMetadata): ) # make sure thumbnail custom tags are present - self.assertListEqual(list(metadata.custom_tags.keys()), ["thumbnail"]) + self.assertListEqual(list(metadata.metadata_fields.keys()), ["thumbnail"]) self.assertListEqual( - list(metadata.custom_tags["thumbnail"].keys()), + list(metadata.metadata_fields["thumbnail"].keys()), ["uri", "thumbnail_index_to_instance_frame", "instances", "version"], ) diff --git a/cloud_optimized_dicom/tests/test_thumbnail.py b/cloud_optimized_dicom/tests/test_thumbnail.py index 4adc7eb..053d750 100644 --- a/cloud_optimized_dicom/tests/test_thumbnail.py +++ b/cloud_optimized_dicom/tests/test_thumbnail.py @@ -67,9 +67,9 @@ def validate_thumbnail( # test the thumbnail coord converter instance_uid = list( - cod_obj.get_custom_tag("thumbnail", dirty=dirty)["instances"].keys() + cod_obj.get_metadata_field("thumbnail", dirty=dirty)["instances"].keys() )[0] - thumbnail_frame_metadata = cod_obj.get_custom_tag("thumbnail", dirty=dirty)[ + thumbnail_frame_metadata = cod_obj.get_metadata_field("thumbnail", dirty=dirty)[ "instances" ][instance_uid]["frames"][0] converter = ThumbnailCoordConverter.from_anchors( @@ -114,10 +114,10 @@ def test_gen_monochrome1(self): ) validate_thumbnail(self, thumbnail, cod_obj, expected_frame_count=1) reloaded_metadata = SeriesMetadata.from_bytes(cod_obj._metadata.to_bytes()) - self.assertIsNotNone(reloaded_metadata.custom_tags["thumbnail"]) + self.assertIsNotNone(reloaded_metadata.metadata_fields["thumbnail"]) self.assertDictEqual( - reloaded_metadata.custom_tags["thumbnail"], - cod_obj._metadata.custom_tags["thumbnail"], + reloaded_metadata.metadata_fields["thumbnail"], + cod_obj._metadata.metadata_fields["thumbnail"], ) def test_gen_monochrome2(self): @@ -149,10 +149,10 @@ def test_gen_mp4_ybr_rct_multiframe(self): ) validate_thumbnail(self, thumbnail, cod_obj, expected_frame_count=78) reloaded_metadata = SeriesMetadata.from_bytes(cod_obj._metadata.to_bytes()) - self.assertIsNotNone(reloaded_metadata.custom_tags["thumbnail"]) + self.assertIsNotNone(reloaded_metadata.metadata_fields["thumbnail"]) self.assertDictEqual( - reloaded_metadata.custom_tags["thumbnail"], - cod_obj._metadata.custom_tags["thumbnail"], + reloaded_metadata.metadata_fields["thumbnail"], + cod_obj._metadata.metadata_fields["thumbnail"], ) def test_sync_and_fetch(self): diff --git a/cloud_optimized_dicom/thumbnail.py b/cloud_optimized_dicom/thumbnail.py index 5d7aa34..f11aee4 100644 --- a/cloud_optimized_dicom/thumbnail.py +++ b/cloud_optimized_dicom/thumbnail.py @@ -299,7 +299,7 @@ def generate_thumbnail( # can infer whether the operation is dirty by checking if the cod object is locked dirty = not cod_obj.lock if ( - cod_obj.get_custom_tag("thumbnail", dirty=dirty) is not None + cod_obj.get_metadata_field("thumbnail", dirty=dirty) is not None and not overwrite_existing ): logger.info(f"Skipping thumbnail generation for {cod_obj} (already exists)") @@ -317,9 +317,9 @@ def generate_thumbnail( cod_obj, instances, instance_to_instance_uid ) thumbnail_path = _save_thumbnail_to_disk(cod_obj, all_frames) - cod_obj.add_custom_tag( - tag_name="thumbnail", - tag_value=thumbnail_metadata, + cod_obj.add_metadata_field( + field_name="thumbnail", + field_value=thumbnail_metadata, overwrite_existing=True, dirty=dirty, ) @@ -340,7 +340,7 @@ def fetch_thumbnail(cod_obj: "CODObject") -> str: ValueError: if the cod object has no thumbnail metadata NotFound: if the thumbnail blob does not exist in GCS """ - thumbnail_metadata = cod_obj.get_custom_tag("thumbnail", dirty=not cod_obj.lock) + thumbnail_metadata = cod_obj.get_metadata_field("thumbnail", dirty=not cod_obj.lock) if thumbnail_metadata is None: raise ValueError(f"Thumbnail metadata not found for {cod_obj}") thumbnail_uri = thumbnail_metadata["uri"] @@ -370,7 +370,7 @@ def get_instance_thumbnail_slice( Returns: thumbnail_slice: a numpy array of the thumbnail slice """ - thumbnail_metadata = cod_obj.get_custom_tag("thumbnail", dirty=not cod_obj.lock) + thumbnail_metadata = cod_obj.get_metadata_field("thumbnail", dirty=not cod_obj.lock) # if thumbnail only contains one instance, assert that is the instance requested and return the full array if len(thumbnail_metadata["instances"]) == 1: assert ( @@ -402,7 +402,7 @@ def get_instance_by_thumbnail_index( Raises: ValueError: if the cod object has no thumbnail metadata, or `thumbnail_index` is out of bounds """ - thumbnail_metadata = cod_obj.get_custom_tag("thumbnail", dirty=not cod_obj.lock) + thumbnail_metadata = cod_obj.get_metadata_field("thumbnail", dirty=not cod_obj.lock) if not thumbnail_metadata: raise ValueError(f"Thumbnail metadata not found for {cod_obj}") thumbnail_index_to_instance_frame = thumbnail_metadata[ diff --git a/getting_started.ipynb b/getting_started.ipynb index 8abddd6..63a5071 100644 --- a/getting_started.ipynb +++ b/getting_started.ipynb @@ -115,7 +115,7 @@ "All instances UIDs in the series: dict_keys(['1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612', '1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455'])\n", "Instance with UID 1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612 has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612\n", "Instance with index 1 has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n", - "Instance object Instance(uri=/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmps99_wvqt_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm, hashed_uids=False, instance_uid=1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455, series_uid=1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506, study_uid=1.2.826.0.1.3680043.8.498.77805869330689203045629680212005263354, dependencies=[]) has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n" + "Instance object Instance(uri=/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpuog0hqla_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm, hashed_uids=False, instance_uid=1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455, series_uid=1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506, study_uid=1.2.826.0.1.3680043.8.498.77805869330689203045629680212005263354, dependencies=[]) has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n" ] } ], @@ -210,25 +210,23 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 7, "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Generated series thumbnail with shape: (2, 128, 128, 3) ([n_frames, height, width, 3])\n" + "ename": "TypeError", + "evalue": "CODObject.add_metadata_field() got an unexpected keyword argument 'tag_name'", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mTypeError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 7\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m CODObject(datastore_path=datastore_path, \n\u001b[32m 2\u001b[39m client=client, \n\u001b[32m 3\u001b[39m study_uid=instance_a.study_uid(), \n\u001b[32m 4\u001b[39m series_uid=instance_a.series_uid(), \n\u001b[32m 5\u001b[39m lock=\u001b[38;5;28;01mFalse\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m cod_obj:\n\u001b[32m 6\u001b[39m \u001b[38;5;66;03m# generate a thumbnail\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m7\u001b[39m thumbnail = \u001b[43mcod_obj\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget_thumbnail\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdirty\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\n\u001b[32m 8\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mGenerated series thumbnail with shape: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mthumbnail.shape\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m ([n_frames, height, width, 3])\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 9\u001b[39m \u001b[38;5;66;03m# retrieve an instance from its index in the thumbnail (note that after sorting based on InstanceNumber, SliceLocation, etc., instance_a is the second instance in the series)\u001b[39;00m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/utils.py:204\u001b[39m, in \u001b[36mpublic_method..wrapper\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 200\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.lock:\n\u001b[32m 201\u001b[39m logger.warning(\n\u001b[32m 202\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mPerforming dirty operation \u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfunc.\u001b[34m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33m on locked CODObject: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 203\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m204\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/cod_object.py:487\u001b[39m, in \u001b[36mCODObject.get_thumbnail\u001b[39m\u001b[34m(self, generate_if_missing, instance_uid, dirty)\u001b[39m\n\u001b[32m 483\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m generate_if_missing:\n\u001b[32m 484\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[32m 485\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mThumbnail either stale or not found for \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m (and generate_if_missing=False)\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 486\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m487\u001b[39m \u001b[43mgenerate_thumbnail\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcod_obj\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43moverwrite_existing\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\n\u001b[32m 488\u001b[39m thumbnail_metadata = \u001b[38;5;28mself\u001b[39m.get_metadata_field(\u001b[33m\"\u001b[39m\u001b[33mthumbnail\u001b[39m\u001b[33m\"\u001b[39m, dirty=dirty)\n\u001b[32m 489\u001b[39m \u001b[38;5;66;03m# thumbnail metadata guaranteed to be populated at this point\u001b[39;00m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/thumbnail.py:320\u001b[39m, in \u001b[36mgenerate_thumbnail\u001b[39m\u001b[34m(cod_obj, overwrite_existing)\u001b[39m\n\u001b[32m 316\u001b[39m all_frames, thumbnail_metadata = _generate_thumbnail_frames(\n\u001b[32m 317\u001b[39m cod_obj, instances, instance_to_instance_uid\n\u001b[32m 318\u001b[39m )\n\u001b[32m 319\u001b[39m thumbnail_path = _save_thumbnail_to_disk(cod_obj, all_frames)\n\u001b[32m--> \u001b[39m\u001b[32m320\u001b[39m \u001b[43mcod_obj\u001b[49m\u001b[43m.\u001b[49m\u001b[43madd_metadata_field\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 321\u001b[39m \u001b[43m \u001b[49m\u001b[43mtag_name\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mthumbnail\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 322\u001b[39m \u001b[43m \u001b[49m\u001b[43mtag_value\u001b[49m\u001b[43m=\u001b[49m\u001b[43mthumbnail_metadata\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 323\u001b[39m \u001b[43m \u001b[49m\u001b[43moverwrite_existing\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[32m 324\u001b[39m \u001b[43m \u001b[49m\u001b[43mdirty\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdirty\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 325\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 326\u001b[39m \u001b[38;5;66;03m# we just generated the thumbnail, so it is not synced to the datastore\u001b[39;00m\n\u001b[32m 327\u001b[39m cod_obj._thumbnail_synced = \u001b[38;5;28;01mFalse\u001b[39;00m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/utils.py:204\u001b[39m, in \u001b[36mpublic_method..wrapper\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 200\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.lock:\n\u001b[32m 201\u001b[39m logger.warning(\n\u001b[32m 202\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mPerforming dirty operation \u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfunc.\u001b[34m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33m on locked CODObject: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 203\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m204\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[31mTypeError\u001b[39m: CODObject.add_metadata_field() got an unexpected keyword argument 'tag_name'" ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" } ], "source": [ @@ -259,11 +257,11 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ - "# cod_obj.remove_custom_tag(tag_name)\n", + "# cod_obj.remove_metadata_field(tag_name)\n", "\n", "# rename custom tag to metadata field: cod_obj.add_metadata_field(field_name, field_value)\n", "\n", @@ -274,8 +272,8 @@ " study_uid=instance_a.study_uid(), \n", " series_uid=instance_a.series_uid(), \n", " lock=False) as cod_obj:\n", - " cod_obj.add_custom_tag(tag_name=\"my_tag\", tag_value=\"my_value\", dirty=True)\n", - " assert cod_obj.get_custom_tag(tag_name=\"my_tag\", dirty=True) == \"my_value\"\n", + " cod_obj.add_metadata_field(field_name=\"my_field\", field_value=\"my_value\", dirty=True)\n", + " assert cod_obj.get_metadata_field(field_name=\"my_field\", dirty=True) == \"my_value\"\n", " " ] }, @@ -379,7 +377,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [ { From b2a9619bb9d51f52fbda0b79c99f640f9b4cb145 Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Fri, 6 Jun 2025 12:14:41 -0400 Subject: [PATCH 20/24] remove_metadata_field method --- cloud_optimized_dicom/cod_object.py | 9 +++++++++ cloud_optimized_dicom/series_metadata.py | 11 +++++++++++ getting_started.ipynb | 12 +++++------- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/cloud_optimized_dicom/cod_object.py b/cloud_optimized_dicom/cod_object.py index 9b9851d..b0bb849 100644 --- a/cloud_optimized_dicom/cod_object.py +++ b/cloud_optimized_dicom/cod_object.py @@ -455,6 +455,15 @@ def get_metadata_field( """Get a custom field from the metadata. Returns `None` if the field does not exist.""" return self.get_metadata(dirty=dirty).metadata_fields.get(field_name, None) + @public_method + def remove_metadata_field(self, field_name: str, dirty: bool = False): + """Remove a custom field from the metadata""" + field_was_present = self.get_metadata(dirty=dirty)._remove_metadata_field( + field_name + ) + # if the field was present, and we removed it, the metadata is now desynced + self._metadata_synced = not field_was_present + @public_method def get_thumbnail( self, diff --git a/cloud_optimized_dicom/series_metadata.py b/cloud_optimized_dicom/series_metadata.py index 1c30249..8768a70 100644 --- a/cloud_optimized_dicom/series_metadata.py +++ b/cloud_optimized_dicom/series_metadata.py @@ -44,6 +44,17 @@ def _add_metadata_field( ) self.metadata_fields[field_name] = field_value + def _remove_metadata_field(self, field_name: str) -> bool: + """Remove a custom field from the series metadata. + + Returns: + bool: True if the field was present and removed, False if the field was not present. + """ + if field_name not in self.metadata_fields: + return False + del self.metadata_fields[field_name] + return True + def _sort_instances(self): """Sort the instances dict, the same way instances are sorted for the thumbnail. diff --git a/getting_started.ipynb b/getting_started.ipynb index 63a5071..116dcb7 100644 --- a/getting_started.ipynb +++ b/getting_started.ipynb @@ -252,7 +252,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Add custom metadata tags to COD" + "## Working with custom metadata fields" ] }, { @@ -261,19 +261,17 @@ "metadata": {}, "outputs": [], "source": [ - "# cod_obj.remove_metadata_field(tag_name)\n", - "\n", - "# rename custom tag to metadata field: cod_obj.add_metadata_field(field_name, field_value)\n", - "\n", - "# clearer nomenclature\n", - "\n", "with CODObject(datastore_path=datastore_path, \n", " client=client, \n", " study_uid=instance_a.study_uid(), \n", " series_uid=instance_a.series_uid(), \n", " lock=False) as cod_obj:\n", + " # add a custom metadata field\n", " cod_obj.add_metadata_field(field_name=\"my_field\", field_value=\"my_value\", dirty=True)\n", " assert cod_obj.get_metadata_field(field_name=\"my_field\", dirty=True) == \"my_value\"\n", + " # remove the custom metadata field\n", + " cod_obj.remove_metadata_field(field_name=\"my_field\", dirty=True)\n", + " assert cod_obj.get_metadata_field(field_name=\"my_field\", dirty=True) is None\n", " " ] }, From 16d765ab04c823b4c8e212c6b3327c93bdf2861f Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Fri, 6 Jun 2025 12:22:43 -0400 Subject: [PATCH 21/24] better commenting on modifying instance example --- getting_started.ipynb | 51 ++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/getting_started.ipynb b/getting_started.ipynb index 116dcb7..9875b23 100644 --- a/getting_started.ipynb +++ b/getting_started.ipynb @@ -115,7 +115,7 @@ "All instances UIDs in the series: dict_keys(['1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612', '1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455'])\n", "Instance with UID 1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612 has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612\n", "Instance with index 1 has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n", - "Instance object Instance(uri=/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpuog0hqla_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm, hashed_uids=False, instance_uid=1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455, series_uid=1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506, study_uid=1.2.826.0.1.3680043.8.498.77805869330689203045629680212005263354, dependencies=[]) has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n" + "Instance object Instance(uri=/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmptrqawjr4_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm, hashed_uids=False, instance_uid=1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455, series_uid=1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506, study_uid=1.2.826.0.1.3680043.8.498.77805869330689203045629680212005263354, dependencies=[]) has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n" ] } ], @@ -214,19 +214,21 @@ "metadata": {}, "outputs": [ { - "ename": "TypeError", - "evalue": "CODObject.add_metadata_field() got an unexpected keyword argument 'tag_name'", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mTypeError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 7\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m CODObject(datastore_path=datastore_path, \n\u001b[32m 2\u001b[39m client=client, \n\u001b[32m 3\u001b[39m study_uid=instance_a.study_uid(), \n\u001b[32m 4\u001b[39m series_uid=instance_a.series_uid(), \n\u001b[32m 5\u001b[39m lock=\u001b[38;5;28;01mFalse\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m cod_obj:\n\u001b[32m 6\u001b[39m \u001b[38;5;66;03m# generate a thumbnail\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m7\u001b[39m thumbnail = \u001b[43mcod_obj\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget_thumbnail\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdirty\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\n\u001b[32m 8\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mGenerated series thumbnail with shape: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mthumbnail.shape\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m ([n_frames, height, width, 3])\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 9\u001b[39m \u001b[38;5;66;03m# retrieve an instance from its index in the thumbnail (note that after sorting based on InstanceNumber, SliceLocation, etc., instance_a is the second instance in the series)\u001b[39;00m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/utils.py:204\u001b[39m, in \u001b[36mpublic_method..wrapper\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 200\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.lock:\n\u001b[32m 201\u001b[39m logger.warning(\n\u001b[32m 202\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mPerforming dirty operation \u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfunc.\u001b[34m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33m on locked CODObject: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 203\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m204\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/cod_object.py:487\u001b[39m, in \u001b[36mCODObject.get_thumbnail\u001b[39m\u001b[34m(self, generate_if_missing, instance_uid, dirty)\u001b[39m\n\u001b[32m 483\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m generate_if_missing:\n\u001b[32m 484\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[32m 485\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mThumbnail either stale or not found for \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m (and generate_if_missing=False)\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 486\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m487\u001b[39m \u001b[43mgenerate_thumbnail\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcod_obj\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43moverwrite_existing\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\n\u001b[32m 488\u001b[39m thumbnail_metadata = \u001b[38;5;28mself\u001b[39m.get_metadata_field(\u001b[33m\"\u001b[39m\u001b[33mthumbnail\u001b[39m\u001b[33m\"\u001b[39m, dirty=dirty)\n\u001b[32m 489\u001b[39m \u001b[38;5;66;03m# thumbnail metadata guaranteed to be populated at this point\u001b[39;00m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/thumbnail.py:320\u001b[39m, in \u001b[36mgenerate_thumbnail\u001b[39m\u001b[34m(cod_obj, overwrite_existing)\u001b[39m\n\u001b[32m 316\u001b[39m all_frames, thumbnail_metadata = _generate_thumbnail_frames(\n\u001b[32m 317\u001b[39m cod_obj, instances, instance_to_instance_uid\n\u001b[32m 318\u001b[39m )\n\u001b[32m 319\u001b[39m thumbnail_path = _save_thumbnail_to_disk(cod_obj, all_frames)\n\u001b[32m--> \u001b[39m\u001b[32m320\u001b[39m \u001b[43mcod_obj\u001b[49m\u001b[43m.\u001b[49m\u001b[43madd_metadata_field\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 321\u001b[39m \u001b[43m \u001b[49m\u001b[43mtag_name\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mthumbnail\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 322\u001b[39m \u001b[43m \u001b[49m\u001b[43mtag_value\u001b[49m\u001b[43m=\u001b[49m\u001b[43mthumbnail_metadata\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 323\u001b[39m \u001b[43m \u001b[49m\u001b[43moverwrite_existing\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[32m 324\u001b[39m \u001b[43m \u001b[49m\u001b[43mdirty\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdirty\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 325\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 326\u001b[39m \u001b[38;5;66;03m# we just generated the thumbnail, so it is not synced to the datastore\u001b[39;00m\n\u001b[32m 327\u001b[39m cod_obj._thumbnail_synced = \u001b[38;5;28;01mFalse\u001b[39;00m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/utils.py:204\u001b[39m, in \u001b[36mpublic_method..wrapper\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 200\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.lock:\n\u001b[32m 201\u001b[39m logger.warning(\n\u001b[32m 202\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mPerforming dirty operation \u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfunc.\u001b[34m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33m on locked CODObject: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 203\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m204\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[31mTypeError\u001b[39m: CODObject.add_metadata_field() got an unexpected keyword argument 'tag_name'" + "name": "stdout", + "output_type": "stream", + "text": [ + "Generated series thumbnail with shape: (2, 128, 128, 3) ([n_frames, height, width, 3])\n" ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ @@ -257,7 +259,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -284,23 +286,18 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ - "# right now when you open a cod object you are opening it in read mode\n", - "# but if we did want to support write mode, then the backend changes a lot\n", - "# ex: rather than pulling a tar, you would pull the tar and actually extract it so you have the files\n", - "# then you would be able to modify the files directly\n", - "# you would basically not have metadata (the metadata.json would be invalid because you're changing stuff)\n", - "# would be way easier to just have an truncate() call under the hood when the write is over\n", - "\n", - "# simplified: metadata/get_metadata()/get_thumbnail() inherently slow due to having to generate them on the fly\n", - "# you can't write a cod file, but you can write on instances\n", - "\n", - "# cod_obj.get_instance(instance_uid).open(\"w\") -> extract the tar, modify the instance, then truncate()\n", + "# Right now when you open a cod object you are essentially opening it in read mode\n", + "# If we did want to support write mode, then the backend changes a lot\n", + "# Rather than just pulling the tar and referencing instances within the tar, \n", + "# COD would actually extract it, which would enable the user to modify instances directly\n", + "# In write mode, metadata would basically not exist (the metadata.json would be invalid because you're changing stuff)\n", + "# When writing is over COD would truncate() the instances, which calls append(), which would re-generate the metadata\n", "\n", - "# more advanced and efficient would be write mode on the cod object itself, so we're not extracting/truncating for every instance\n", + "# Anyways, the above is a future TODO. For now, modifying an instance looks like this:\n", "\n", "with CODObject(datastore_path=datastore_path, \n", " client=client, \n", From 51bb06562de2ff6eb2a91680fcc7eb590353ee09 Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Fri, 6 Jun 2025 13:25:53 -0400 Subject: [PATCH 22/24] GET implied in dicomweb request --- cloud_optimized_dicom/dicomweb.py | 10 ++-- cloud_optimized_dicom/tests/test_dicomweb.py | 18 +++---- getting_started.ipynb | 52 +++++++++----------- 3 files changed, 36 insertions(+), 44 deletions(-) diff --git a/cloud_optimized_dicom/dicomweb.py b/cloud_optimized_dicom/dicomweb.py index f20f3c7..4453e11 100644 --- a/cloud_optimized_dicom/dicomweb.py +++ b/cloud_optimized_dicom/dicomweb.py @@ -255,12 +255,14 @@ def from_uri(cls, uri: str) -> "DicomwebRequest": @classmethod def from_request(cls, request: str) -> "DicomwebRequest": """ - Parse the request string (e.g. `GET {s}/studies/{study}/series/{series}`) + Parse the request string (e.g. `{s}/studies/{study}/series/{series}`), and return a DicomwebRequest object. + Currently only GET requests are supported, so it is implied that the request starts with `GET` """ - assert request.startswith("GET "), "Only GET requests are currently supported" - uri = request.replace("GET", "").strip() - return cls.from_uri(uri) + assert request.startswith("gs://"), ( + "Expected request to begin with GS URI but got: " + request + ) + return cls.from_uri(request.strip()) # public method to expose (really the only thing that should be used/imported) diff --git a/cloud_optimized_dicom/tests/test_dicomweb.py b/cloud_optimized_dicom/tests/test_dicomweb.py index 7ac58d1..d415d68 100644 --- a/cloud_optimized_dicom/tests/test_dicomweb.py +++ b/cloud_optimized_dicom/tests/test_dicomweb.py @@ -76,8 +76,7 @@ def test_get_study(self): "1.2.826.0.1.3680043.8.498.18783474219392509401504861043428417882", "metadata", ) - request = f"GET {study_uri}" - result = handle_request(request, self.client) + result = handle_request(study_uri, self.client) # we expect a dictionary of metadata self.assertIsInstance(result, dict) # expect all study level tags to be present, and not None @@ -96,8 +95,7 @@ def test_get_series(self): "1.2.826.0.1.3680043.8.498.89840699185761593370876698622882853150", "metadata", ) - request = f"GET {series_uri}" - result = handle_request(request, self.client) + result = handle_request(series_uri, self.client) # we expect a list of instance metadata dictionaries self.assertIsInstance(result, list) # there happen to be 82 instances in this series @@ -122,8 +120,7 @@ def test_get_instance(self): "1.2.826.0.1.3680043.8.498.10368404844741579486264078308290534273", "metadata", ) - request = f"GET {instance_uri}" - result = handle_request(request, self.client) + result = handle_request(instance_uri, self.client) # we expect a dictionary of metadata self.assertIsInstance(result, dict) # check something in the metadata (e.g. series uid) @@ -145,8 +142,7 @@ def test_get_single_frame(self): "frames", "1", ) - request = f"GET {frame_uri}" - result = handle_request(request, self.client) + result = handle_request(frame_uri, self.client) # we expect a non-empty list of bytes self.assertIsInstance(result, list) self.assertEqual(len(result), 1) @@ -168,9 +164,8 @@ def test_get_too_many_frames_errors(self): "frames", "1,2", ) - request = f"GET {frame_uri}" with self.assertRaises(AssertionError): - handle_request(request, self.client) + handle_request(frame_uri, self.client) def test_non_metadata_requests_raise_error(self): """ @@ -185,9 +180,8 @@ def test_non_metadata_requests_raise_error(self): "instances", "1.2.826.0.1.3680043.8.498.10368404844741579486264078308290534273", ) - request = f"GET {request_uri}" with self.assertRaises(AssertionError): - handle_request(request, self.client) + handle_request(request_uri, self.client) if __name__ == "__main__": diff --git a/getting_started.ipynb b/getting_started.ipynb index 9875b23..e4c2e5a 100644 --- a/getting_started.ipynb +++ b/getting_started.ipynb @@ -16,14 +16,13 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "from cloud_optimized_dicom.cod_object import CODObject\n", "from cloud_optimized_dicom.instance import Instance\n", "from cloud_optimized_dicom.utils import delete_uploaded_blobs\n", - "from cloud_optimized_dicom.dicomweb import handle_request\n", "from google.cloud import storage\n", "import pydicom\n", "import tempfile\n", @@ -53,7 +52,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -80,7 +79,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -105,7 +104,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -115,7 +114,7 @@ "All instances UIDs in the series: dict_keys(['1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612', '1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455'])\n", "Instance with UID 1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612 has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612\n", "Instance with index 1 has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n", - "Instance object Instance(uri=/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmptrqawjr4_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm, hashed_uids=False, instance_uid=1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455, series_uid=1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506, study_uid=1.2.826.0.1.3680043.8.498.77805869330689203045629680212005263354, dependencies=[]) has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n" + "Instance object Instance(uri=/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpj4xw8_kr_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm, hashed_uids=False, instance_uid=1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455, series_uid=1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506, study_uid=1.2.826.0.1.3680043.8.498.77805869330689203045629680212005263354, dependencies=[]) has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n" ] } ], @@ -154,7 +153,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -183,7 +182,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -210,7 +209,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -259,7 +258,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -336,28 +335,25 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ - "# series_metadata = CODObject.dicomweb(GET {datastore_path}/studies/{study_uid}/series/{series_uid}/metadata, client)\n", - "\n", - "# lose the GET... later, if supported, can add options for POST, PUT, etc.\n", - "\n", + "from cloud_optimized_dicom.dicomweb import handle_request\n", "# get study-level metadata (returns a dict of study level tags)\n", - "study_metadata = handle_request(f\"GET {datastore_path}/studies/{instance_a.study_uid()}/metadata\", client)\n", + "study_metadata = handle_request(f\"{datastore_path}/studies/{instance_a.study_uid()}/metadata\", client)\n", "assert study_metadata[\"00100020\"][\"Value\"][0] == \"GRDNB4C659BSD9NZ\"\n", "\n", "# get series-level metadata (returns list of instance level tag dicts)\n", - "series_metadata = handle_request(f\"GET {datastore_path}/studies/{instance_a.study_uid()}/series/{instance_a.series_uid()}/metadata\", client)\n", + "series_metadata = handle_request(f\"{datastore_path}/studies/{instance_a.study_uid()}/series/{instance_a.series_uid()}/metadata\", client)\n", "assert series_metadata[0][\"00080018\"][\"Value\"][0] == instance_a.instance_uid()\n", "\n", "# get instance-level metadata (returns instance level tag dict)\n", - "instance_metadata = handle_request(f\"GET {datastore_path}/studies/{instance_a.study_uid()}/series/{instance_a.series_uid()}/instances/{instance_a.instance_uid()}/metadata\", client)\n", + "instance_metadata = handle_request(f\"{datastore_path}/studies/{instance_a.study_uid()}/series/{instance_a.series_uid()}/instances/{instance_a.instance_uid()}/metadata\", client)\n", "assert instance_metadata[\"00080018\"][\"Value\"][0] == instance_a.instance_uid()\n", "\n", "# get a frame from an instance (expect a list of raw bytes of the frame(s))\n", - "frame = handle_request(f\"GET {datastore_path}/studies/{instance_a.study_uid()}/series/{instance_a.series_uid()}/instances/{instance_a.instance_uid()}/frames/1\", client)\n", + "frame = handle_request(f\"{datastore_path}/studies/{instance_a.study_uid()}/series/{instance_a.series_uid()}/instances/{instance_a.instance_uid()}/frames/1\", client)\n", "assert isinstance(frame, list)\n", "assert len(frame) == 1\n", "assert isinstance(frame[0], bytes)" @@ -372,14 +368,14 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "[Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpiie59xs7_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm'\n", + "[Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp9aq__y_2_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm'\n", "Traceback (most recent call last):\n", " File \"/Users/cal/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py\", line 439, in _create_or_append_tar\n", " instance.append_to_series_tar(tar)\n", @@ -391,8 +387,8 @@ " File \"/opt/homebrew/Cellar/python@3.11/3.11.11/Frameworks/Python.framework/Versions/3.11/lib/python3.11/tarfile.py\", line 2067, in gettarinfo\n", " statres = os.lstat(name)\n", " ^^^^^^^^^^^^^^\n", - "FileNotFoundError: [Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpiie59xs7_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm'\n", - "[Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpiie59xs7_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm'\n", + "FileNotFoundError: [Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp9aq__y_2_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm'\n", + "[Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp9aq__y_2_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm'\n", "Traceback (most recent call last):\n", " File \"/Users/cal/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py\", line 439, in _create_or_append_tar\n", " instance.append_to_series_tar(tar)\n", @@ -404,24 +400,24 @@ " File \"/opt/homebrew/Cellar/python@3.11/3.11.11/Frameworks/Python.framework/Versions/3.11/lib/python3.11/tarfile.py\", line 2067, in gettarinfo\n", " statres = os.lstat(name)\n", " ^^^^^^^^^^^^^^\n", - "FileNotFoundError: [Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpiie59xs7_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm'\n" + "FileNotFoundError: [Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp9aq__y_2_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm'\n" ] }, { "ename": "ValueError", - "evalue": "GRADIENT_STATE_LOGS:FAILED_TO_TAR_ALL_INSTANCES:/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpiie59xs7_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm\n/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpiie59xs7_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm", + "evalue": "GRADIENT_STATE_LOGS:FAILED_TO_TAR_ALL_INSTANCES:/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp9aq__y_2_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm\n/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp9aq__y_2_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm", "output_type": "error", "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[11]\u001b[39m\u001b[32m, line 31\u001b[39m\n\u001b[32m 23\u001b[39m i.uid_hash_func = example_hash_function\n\u001b[32m 24\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m CODObject(datastore_path=deid_datastore_path, \n\u001b[32m 25\u001b[39m client=client, \n\u001b[32m 26\u001b[39m study_uid=hashed_study_uid, \n\u001b[32m (...)\u001b[39m\u001b[32m 29\u001b[39m lock=\u001b[38;5;28;01mFalse\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m deid_cod:\n\u001b[32m 30\u001b[39m \u001b[38;5;66;03m# TODO: this doesnt work yet - need to extract the instances from the original tar\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m31\u001b[39m \u001b[43mdeid_cod\u001b[49m\u001b[43m.\u001b[49m\u001b[43mappend\u001b[49m\u001b[43m(\u001b[49m\u001b[43minstances\u001b[49m\u001b[43m=\u001b[49m\u001b[43minstances\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdirty\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\n", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[12]\u001b[39m\u001b[32m, line 31\u001b[39m\n\u001b[32m 23\u001b[39m i.uid_hash_func = example_hash_function\n\u001b[32m 24\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m CODObject(datastore_path=deid_datastore_path, \n\u001b[32m 25\u001b[39m client=client, \n\u001b[32m 26\u001b[39m study_uid=hashed_study_uid, \n\u001b[32m (...)\u001b[39m\u001b[32m 29\u001b[39m lock=\u001b[38;5;28;01mFalse\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m deid_cod:\n\u001b[32m 30\u001b[39m \u001b[38;5;66;03m# TODO: this doesnt work yet - need to extract the instances from the original tar\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m31\u001b[39m \u001b[43mdeid_cod\u001b[49m\u001b[43m.\u001b[49m\u001b[43mappend\u001b[49m\u001b[43m(\u001b[49m\u001b[43minstances\u001b[49m\u001b[43m=\u001b[49m\u001b[43minstances\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdirty\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\n", "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/utils.py:204\u001b[39m, in \u001b[36mpublic_method..wrapper\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 200\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.lock:\n\u001b[32m 201\u001b[39m logger.warning(\n\u001b[32m 202\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mPerforming dirty operation \u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfunc.\u001b[34m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33m on locked CODObject: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 203\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m204\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/cod_object.py:329\u001b[39m, in \u001b[36mCODObject.append\u001b[39m\u001b[34m(self, instances, treat_metadata_diffs_as_same, max_instance_size, max_series_size, delete_local_origin, dirty)\u001b[39m\n\u001b[32m 309\u001b[39m \u001b[38;5;129m@public_method\u001b[39m\n\u001b[32m 310\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mappend\u001b[39m(\n\u001b[32m 311\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 317\u001b[39m dirty: \u001b[38;5;28mbool\u001b[39m = \u001b[38;5;28;01mFalse\u001b[39;00m,\n\u001b[32m 318\u001b[39m ):\n\u001b[32m 319\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Append a list of instances to the COD object.\u001b[39;00m\n\u001b[32m 320\u001b[39m \n\u001b[32m 321\u001b[39m \u001b[33;03m Args:\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 327\u001b[39m \u001b[33;03m dirty: bool - Must be `True` if the CODObject is \"dirty\" (i.e. `lock=False`).\u001b[39;00m\n\u001b[32m 328\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m329\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mappend\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 330\u001b[39m \u001b[43m \u001b[49m\u001b[43mcod_object\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 331\u001b[39m \u001b[43m \u001b[49m\u001b[43minstances\u001b[49m\u001b[43m=\u001b[49m\u001b[43minstances\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 332\u001b[39m \u001b[43m \u001b[49m\u001b[43mdelete_local_origin\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdelete_local_origin\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 333\u001b[39m \u001b[43m \u001b[49m\u001b[43mtreat_metadata_diffs_as_same\u001b[49m\u001b[43m=\u001b[49m\u001b[43mtreat_metadata_diffs_as_same\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 334\u001b[39m \u001b[43m \u001b[49m\u001b[43mmax_instance_size\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmax_instance_size\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 335\u001b[39m \u001b[43m \u001b[49m\u001b[43mmax_series_size\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmax_series_size\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 336\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/cod_object.py:331\u001b[39m, in \u001b[36mCODObject.append\u001b[39m\u001b[34m(self, instances, treat_metadata_diffs_as_same, max_instance_size, max_series_size, delete_local_origin, dirty)\u001b[39m\n\u001b[32m 311\u001b[39m \u001b[38;5;129m@public_method\u001b[39m\n\u001b[32m 312\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mappend\u001b[39m(\n\u001b[32m 313\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 319\u001b[39m dirty: \u001b[38;5;28mbool\u001b[39m = \u001b[38;5;28;01mFalse\u001b[39;00m,\n\u001b[32m 320\u001b[39m ):\n\u001b[32m 321\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Append a list of instances to the COD object.\u001b[39;00m\n\u001b[32m 322\u001b[39m \n\u001b[32m 323\u001b[39m \u001b[33;03m Args:\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 329\u001b[39m \u001b[33;03m dirty: bool - Must be `True` if the CODObject is \"dirty\" (i.e. `lock=False`).\u001b[39;00m\n\u001b[32m 330\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m331\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mappend\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 332\u001b[39m \u001b[43m \u001b[49m\u001b[43mcod_object\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 333\u001b[39m \u001b[43m \u001b[49m\u001b[43minstances\u001b[49m\u001b[43m=\u001b[49m\u001b[43minstances\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 334\u001b[39m \u001b[43m \u001b[49m\u001b[43mdelete_local_origin\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdelete_local_origin\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 335\u001b[39m \u001b[43m \u001b[49m\u001b[43mtreat_metadata_diffs_as_same\u001b[49m\u001b[43m=\u001b[49m\u001b[43mtreat_metadata_diffs_as_same\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 336\u001b[39m \u001b[43m \u001b[49m\u001b[43mmax_instance_size\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmax_instance_size\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 337\u001b[39m \u001b[43m \u001b[49m\u001b[43mmax_series_size\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmax_series_size\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 338\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py:84\u001b[39m, in \u001b[36mappend\u001b[39m\u001b[34m(cod_object, instances, delete_local_origin, treat_metadata_diffs_as_same, max_instance_size, max_series_size)\u001b[39m\n\u001b[32m 82\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m append_result\n\u001b[32m 83\u001b[39m \u001b[38;5;66;03m# handle new\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m84\u001b[39m append_result = \u001b[43m_handle_new\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcod_object\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstate_change\u001b[49m\u001b[43m.\u001b[49m\u001b[43mnew\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mappend_result\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 85\u001b[39m metrics.TAR_SUCCESS_COUNTER.inc()\n\u001b[32m 86\u001b[39m metrics.TAR_BYTES_PROCESSED.inc(os.path.getsize(cod_object.tar_file_path))\n", "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py:391\u001b[39m, in \u001b[36m_handle_new\u001b[39m\u001b[34m(cod_object, new_state_changes, append_result)\u001b[39m\n\u001b[32m 381\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_handle_new\u001b[39m(\n\u001b[32m 382\u001b[39m cod_object: \u001b[33m\"\u001b[39m\u001b[33mCODObject\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 383\u001b[39m new_state_changes: \u001b[38;5;28mlist\u001b[39m[\u001b[38;5;28mtuple\u001b[39m[Instance, Optional[SeriesMetadata], Optional[\u001b[38;5;28mstr\u001b[39m]]],\n\u001b[32m 384\u001b[39m append_result: AppendResult,\n\u001b[32m 385\u001b[39m ) -> AppendResult:\n\u001b[32m 386\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 387\u001b[39m \u001b[33;03m Create/append to tar & upload; add to series metadata & upload.\u001b[39;00m\n\u001b[32m 388\u001b[39m \u001b[33;03m Returns:\u001b[39;00m\n\u001b[32m 389\u001b[39m \u001b[33;03m updated_append_result\u001b[39;00m\n\u001b[32m 390\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m391\u001b[39m instances_added_to_tar = \u001b[43m_handle_create_tar\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcod_object\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnew_state_changes\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 392\u001b[39m _handle_create_metadata(cod_object, instances_added_to_tar)\n\u001b[32m 393\u001b[39m \u001b[38;5;66;03m# update append result\u001b[39;00m\n", "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py:415\u001b[39m, in \u001b[36m_handle_create_tar\u001b[39m\u001b[34m(cod_object, new_state_changes)\u001b[39m\n\u001b[32m 412\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(cod_object._metadata.instances) > \u001b[32m0\u001b[39m:\n\u001b[32m 413\u001b[39m cod_object.pull_tar(dirty=\u001b[38;5;129;01mnot\u001b[39;00m cod_object.lock)\n\u001b[32m--> \u001b[39m\u001b[32m415\u001b[39m instances_added_to_tar = \u001b[43m_create_or_append_tar\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 416\u001b[39m \u001b[43m \u001b[49m\u001b[43mcod_object\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[43mnew\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mnew\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m_\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m_\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mnew_state_changes\u001b[49m\u001b[43m]\u001b[49m\n\u001b[32m 417\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 418\u001b[39m _create_sqlite_index(cod_object)\n\u001b[32m 419\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m instances_added_to_tar\n", "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py:447\u001b[39m, in \u001b[36m_create_or_append_tar\u001b[39m\u001b[34m(cod_object, instances_to_add)\u001b[39m\n\u001b[32m 445\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(instances_added_to_tar) == \u001b[32m0\u001b[39m:\n\u001b[32m 446\u001b[39m uri_str = \u001b[33m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33m\"\u001b[39m.join([instance.dicom_uri \u001b[38;5;28;01mfor\u001b[39;00m instance \u001b[38;5;129;01min\u001b[39;00m instances_to_add])\n\u001b[32m--> \u001b[39m\u001b[32m447\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mGRADIENT_STATE_LOGS:FAILED_TO_TAR_ALL_INSTANCES:\u001b[39m\u001b[38;5;132;01m{\u001b[39;00muri_str\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m 448\u001b[39m logger.info(\n\u001b[32m 449\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mGRADIENT_STATE_LOGS:POPULATED_TAR:\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mcod_object.tar_file_path\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m (\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mos.path.getsize(cod_object.tar_file_path)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m bytes)\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 450\u001b[39m )\n\u001b[32m 451\u001b[39m \u001b[38;5;66;03m# tar has been altered, so it is no longer in sync with the datastore\u001b[39;00m\n", - "\u001b[31mValueError\u001b[39m: GRADIENT_STATE_LOGS:FAILED_TO_TAR_ALL_INSTANCES:/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpiie59xs7_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm\n/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpiie59xs7_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm" + "\u001b[31mValueError\u001b[39m: GRADIENT_STATE_LOGS:FAILED_TO_TAR_ALL_INSTANCES:/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp9aq__y_2_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm\n/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp9aq__y_2_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm" ] } ], From 2a51604214e6f7dd9df1813ab52ca12cf583c904 Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Fri, 6 Jun 2025 13:48:54 -0400 Subject: [PATCH 23/24] extract_locally method for cod obj, instance --- cloud_optimized_dicom/cod_object.py | 9 +++++ cloud_optimized_dicom/instance.py | 18 ++++++++++ .../tests/test_cod_object.py | 36 +++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/cloud_optimized_dicom/cod_object.py b/cloud_optimized_dicom/cod_object.py index b0bb849..38054ec 100644 --- a/cloud_optimized_dicom/cod_object.py +++ b/cloud_optimized_dicom/cod_object.py @@ -599,6 +599,15 @@ def delete_dependencies( logger.info(f"GRADIENT_STATE_LOGS:DELETED:{deleted_dependencies}") return deleted_dependencies + @public_method + def extract_locally(self, dirty: bool = False): + """ + Extract the tar and index to the local temp dir, and set the dicom_uri of each instance to the local path. + """ + self.pull_tar(dirty=dirty) + for instance_uid, instance in self.get_instances(dirty=dirty).items(): + instance._extract_from_local_tar() + @public_method def pull_tar(self, dirty: bool = False): """Pull tar and index from GCS to local temp dir, diff --git a/cloud_optimized_dicom/instance.py b/cloud_optimized_dicom/instance.py index 2c06c09..4875d6e 100644 --- a/cloud_optimized_dicom/instance.py +++ b/cloud_optimized_dicom/instance.py @@ -106,6 +106,24 @@ def fetch(self): self.dicom_uri = self._temp_file_path self.validate() + def _extract_from_local_tar(self): + """ + Extract the instance from a local tar file. + """ + assert self.is_nested_in_tar and not is_remote( + self.dicom_uri + ), f"extract_from_local_tar expected local tar uri but got: {self.dicom_uri}" + # create a temp file to store the instance + with tempfile.NamedTemporaryFile(suffix=".dcm", delete=False) as temp_file: + self._temp_file_path = temp_file.name + # read the instance from the tar into the temp file + with self.open() as f_in: + with open(self._temp_file_path, "wb") as f_out: + f_out.write(f_in.read()) + # after writing, dicom_uri is now local + self.dicom_uri = self._temp_file_path + self.validate() + def validate(self): """Open the instance, read the internal fields, and validate they match hints if provided. diff --git a/cloud_optimized_dicom/tests/test_cod_object.py b/cloud_optimized_dicom/tests/test_cod_object.py index 9882c55..9ff3c50 100644 --- a/cloud_optimized_dicom/tests/test_cod_object.py +++ b/cloud_optimized_dicom/tests/test_cod_object.py @@ -101,6 +101,42 @@ def test_pull_tar(self): self.assertEqual(ds.SeriesInstanceUID, self.test_series_uid) self.assertEqual(ds.SOPInstanceUID, self.test_instance_uid) + def test_extract_locally(self): + """Test that extract_locally extracts the tar and index to the local temp dir, and sets the dicom_uri of each instance to the local path""" + delete_uploaded_blobs(self.client, [self.datastore_path]) + # append and sync an instance + instance = Instance(dicom_uri=self.local_instance_path) + with CODObject( + datastore_path=self.datastore_path, + client=self.client, + study_uid=self.test_study_uid, + series_uid=self.test_series_uid, + lock=True, + ) as cod_obj: + cod_obj.append([instance]) + cod_obj.sync() + cod_obj = CODObject( + datastore_path=self.datastore_path, + client=self.client, + study_uid=self.test_study_uid, + series_uid=self.test_series_uid, + lock=False, + ) + instance = cod_obj.get_metadata(dirty=True).instances[self.test_instance_uid] + # Before we extract, the instance should have a remote URI (it exists in the COD datastore) + self.assertTrue(is_remote(instance.dicom_uri) and instance.is_nested_in_tar) + cod_obj.extract_locally(dirty=True) + # After we extract, the instance should be local and not nested in a tar + self.assertTrue( + not is_remote(instance.dicom_uri) and not instance.is_nested_in_tar + ) + # We should be able to open/read the instance in this state from this local tar file + with instance.open() as f: + ds = dcmread(f) + self.assertEqual(ds.StudyInstanceUID, self.test_study_uid) + self.assertEqual(ds.SeriesInstanceUID, self.test_series_uid) + self.assertEqual(ds.SOPInstanceUID, self.test_instance_uid) + def test_serialize_deserialize(self): """Test serialization and deserialization""" with CODObject( From 829edbe1c66eebab36b4c932fb9e592886097540 Mon Sep 17 00:00:00 2001 From: Cal Nightingale Date: Fri, 6 Jun 2025 13:49:17 -0400 Subject: [PATCH 24/24] form deid from regular cod example works --- getting_started.ipynb | 93 ++++++++++--------------------------------- 1 file changed, 20 insertions(+), 73 deletions(-) diff --git a/getting_started.ipynb b/getting_started.ipynb index e4c2e5a..73bb74e 100644 --- a/getting_started.ipynb +++ b/getting_started.ipynb @@ -16,7 +16,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -52,7 +52,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -79,7 +79,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -104,7 +104,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -114,7 +114,7 @@ "All instances UIDs in the series: dict_keys(['1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612', '1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455'])\n", "Instance with UID 1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612 has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612\n", "Instance with index 1 has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n", - "Instance object Instance(uri=/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpj4xw8_kr_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm, hashed_uids=False, instance_uid=1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455, series_uid=1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506, study_uid=1.2.826.0.1.3680043.8.498.77805869330689203045629680212005263354, dependencies=[]) has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n" + "Instance object Instance(uri=/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmpq3mkwp76_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm, hashed_uids=False, instance_uid=1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455, series_uid=1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506, study_uid=1.2.826.0.1.3680043.8.498.77805869330689203045629680212005263354, dependencies=[]) has SOPInstanceUID: 1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455\n" ] } ], @@ -153,7 +153,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -182,7 +182,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -209,7 +209,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -258,7 +258,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -285,7 +285,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -335,7 +335,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -370,66 +370,15 @@ "cell_type": "code", "execution_count": 12, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp9aq__y_2_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm'\n", - "Traceback (most recent call last):\n", - " File \"/Users/cal/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py\", line 439, in _create_or_append_tar\n", - " instance.append_to_series_tar(tar)\n", - " File \"/Users/cal/work/cloud_optimized_dicom/cloud_optimized_dicom/instance.py\", line 317, in append_to_series_tar\n", - " tar.add(self.dicom_uri, arcname=f\"/instances/{uid_for_uri}.dcm\")\n", - " File \"/opt/homebrew/Cellar/python@3.11/3.11.11/Frameworks/Python.framework/Versions/3.11/lib/python3.11/tarfile.py\", line 2194, in add\n", - " tarinfo = self.gettarinfo(name, arcname)\n", - " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " File \"/opt/homebrew/Cellar/python@3.11/3.11.11/Frameworks/Python.framework/Versions/3.11/lib/python3.11/tarfile.py\", line 2067, in gettarinfo\n", - " statres = os.lstat(name)\n", - " ^^^^^^^^^^^^^^\n", - "FileNotFoundError: [Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp9aq__y_2_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm'\n", - "[Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp9aq__y_2_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm'\n", - "Traceback (most recent call last):\n", - " File \"/Users/cal/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py\", line 439, in _create_or_append_tar\n", - " instance.append_to_series_tar(tar)\n", - " File \"/Users/cal/work/cloud_optimized_dicom/cloud_optimized_dicom/instance.py\", line 317, in append_to_series_tar\n", - " tar.add(self.dicom_uri, arcname=f\"/instances/{uid_for_uri}.dcm\")\n", - " File \"/opt/homebrew/Cellar/python@3.11/3.11.11/Frameworks/Python.framework/Versions/3.11/lib/python3.11/tarfile.py\", line 2194, in add\n", - " tarinfo = self.gettarinfo(name, arcname)\n", - " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " File \"/opt/homebrew/Cellar/python@3.11/3.11.11/Frameworks/Python.framework/Versions/3.11/lib/python3.11/tarfile.py\", line 2067, in gettarinfo\n", - " statres = os.lstat(name)\n", - " ^^^^^^^^^^^^^^\n", - "FileNotFoundError: [Errno 2] No such file or directory: '/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp9aq__y_2_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm'\n" - ] - }, - { - "ename": "ValueError", - "evalue": "GRADIENT_STATE_LOGS:FAILED_TO_TAR_ALL_INSTANCES:/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp9aq__y_2_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm\n/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp9aq__y_2_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[12]\u001b[39m\u001b[32m, line 31\u001b[39m\n\u001b[32m 23\u001b[39m i.uid_hash_func = example_hash_function\n\u001b[32m 24\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m CODObject(datastore_path=deid_datastore_path, \n\u001b[32m 25\u001b[39m client=client, \n\u001b[32m 26\u001b[39m study_uid=hashed_study_uid, \n\u001b[32m (...)\u001b[39m\u001b[32m 29\u001b[39m lock=\u001b[38;5;28;01mFalse\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m deid_cod:\n\u001b[32m 30\u001b[39m \u001b[38;5;66;03m# TODO: this doesnt work yet - need to extract the instances from the original tar\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m31\u001b[39m \u001b[43mdeid_cod\u001b[49m\u001b[43m.\u001b[49m\u001b[43mappend\u001b[49m\u001b[43m(\u001b[49m\u001b[43minstances\u001b[49m\u001b[43m=\u001b[49m\u001b[43minstances\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdirty\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/utils.py:204\u001b[39m, in \u001b[36mpublic_method..wrapper\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 200\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.lock:\n\u001b[32m 201\u001b[39m logger.warning(\n\u001b[32m 202\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mPerforming dirty operation \u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfunc.\u001b[34m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33m on locked CODObject: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 203\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m204\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/cod_object.py:331\u001b[39m, in \u001b[36mCODObject.append\u001b[39m\u001b[34m(self, instances, treat_metadata_diffs_as_same, max_instance_size, max_series_size, delete_local_origin, dirty)\u001b[39m\n\u001b[32m 311\u001b[39m \u001b[38;5;129m@public_method\u001b[39m\n\u001b[32m 312\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mappend\u001b[39m(\n\u001b[32m 313\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 319\u001b[39m dirty: \u001b[38;5;28mbool\u001b[39m = \u001b[38;5;28;01mFalse\u001b[39;00m,\n\u001b[32m 320\u001b[39m ):\n\u001b[32m 321\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Append a list of instances to the COD object.\u001b[39;00m\n\u001b[32m 322\u001b[39m \n\u001b[32m 323\u001b[39m \u001b[33;03m Args:\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 329\u001b[39m \u001b[33;03m dirty: bool - Must be `True` if the CODObject is \"dirty\" (i.e. `lock=False`).\u001b[39;00m\n\u001b[32m 330\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m331\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mappend\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 332\u001b[39m \u001b[43m \u001b[49m\u001b[43mcod_object\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 333\u001b[39m \u001b[43m \u001b[49m\u001b[43minstances\u001b[49m\u001b[43m=\u001b[49m\u001b[43minstances\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 334\u001b[39m \u001b[43m \u001b[49m\u001b[43mdelete_local_origin\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdelete_local_origin\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 335\u001b[39m \u001b[43m \u001b[49m\u001b[43mtreat_metadata_diffs_as_same\u001b[49m\u001b[43m=\u001b[49m\u001b[43mtreat_metadata_diffs_as_same\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 336\u001b[39m \u001b[43m \u001b[49m\u001b[43mmax_instance_size\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmax_instance_size\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 337\u001b[39m \u001b[43m \u001b[49m\u001b[43mmax_series_size\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmax_series_size\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 338\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py:84\u001b[39m, in \u001b[36mappend\u001b[39m\u001b[34m(cod_object, instances, delete_local_origin, treat_metadata_diffs_as_same, max_instance_size, max_series_size)\u001b[39m\n\u001b[32m 82\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m append_result\n\u001b[32m 83\u001b[39m \u001b[38;5;66;03m# handle new\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m84\u001b[39m append_result = \u001b[43m_handle_new\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcod_object\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstate_change\u001b[49m\u001b[43m.\u001b[49m\u001b[43mnew\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mappend_result\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 85\u001b[39m metrics.TAR_SUCCESS_COUNTER.inc()\n\u001b[32m 86\u001b[39m metrics.TAR_BYTES_PROCESSED.inc(os.path.getsize(cod_object.tar_file_path))\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py:391\u001b[39m, in \u001b[36m_handle_new\u001b[39m\u001b[34m(cod_object, new_state_changes, append_result)\u001b[39m\n\u001b[32m 381\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_handle_new\u001b[39m(\n\u001b[32m 382\u001b[39m cod_object: \u001b[33m\"\u001b[39m\u001b[33mCODObject\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 383\u001b[39m new_state_changes: \u001b[38;5;28mlist\u001b[39m[\u001b[38;5;28mtuple\u001b[39m[Instance, Optional[SeriesMetadata], Optional[\u001b[38;5;28mstr\u001b[39m]]],\n\u001b[32m 384\u001b[39m append_result: AppendResult,\n\u001b[32m 385\u001b[39m ) -> AppendResult:\n\u001b[32m 386\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 387\u001b[39m \u001b[33;03m Create/append to tar & upload; add to series metadata & upload.\u001b[39;00m\n\u001b[32m 388\u001b[39m \u001b[33;03m Returns:\u001b[39;00m\n\u001b[32m 389\u001b[39m \u001b[33;03m updated_append_result\u001b[39;00m\n\u001b[32m 390\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m391\u001b[39m instances_added_to_tar = \u001b[43m_handle_create_tar\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcod_object\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnew_state_changes\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 392\u001b[39m _handle_create_metadata(cod_object, instances_added_to_tar)\n\u001b[32m 393\u001b[39m \u001b[38;5;66;03m# update append result\u001b[39;00m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py:415\u001b[39m, in \u001b[36m_handle_create_tar\u001b[39m\u001b[34m(cod_object, new_state_changes)\u001b[39m\n\u001b[32m 412\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(cod_object._metadata.instances) > \u001b[32m0\u001b[39m:\n\u001b[32m 413\u001b[39m cod_object.pull_tar(dirty=\u001b[38;5;129;01mnot\u001b[39;00m cod_object.lock)\n\u001b[32m--> \u001b[39m\u001b[32m415\u001b[39m instances_added_to_tar = \u001b[43m_create_or_append_tar\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 416\u001b[39m \u001b[43m \u001b[49m\u001b[43mcod_object\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[43mnew\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mnew\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m_\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m_\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mnew_state_changes\u001b[49m\u001b[43m]\u001b[49m\n\u001b[32m 417\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 418\u001b[39m _create_sqlite_index(cod_object)\n\u001b[32m 419\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m instances_added_to_tar\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/work/cloud_optimized_dicom/cloud_optimized_dicom/append.py:447\u001b[39m, in \u001b[36m_create_or_append_tar\u001b[39m\u001b[34m(cod_object, instances_to_add)\u001b[39m\n\u001b[32m 445\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(instances_added_to_tar) == \u001b[32m0\u001b[39m:\n\u001b[32m 446\u001b[39m uri_str = \u001b[33m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33m\"\u001b[39m.join([instance.dicom_uri \u001b[38;5;28;01mfor\u001b[39;00m instance \u001b[38;5;129;01min\u001b[39;00m instances_to_add])\n\u001b[32m--> \u001b[39m\u001b[32m447\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mGRADIENT_STATE_LOGS:FAILED_TO_TAR_ALL_INSTANCES:\u001b[39m\u001b[38;5;132;01m{\u001b[39;00muri_str\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m 448\u001b[39m logger.info(\n\u001b[32m 449\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mGRADIENT_STATE_LOGS:POPULATED_TAR:\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mcod_object.tar_file_path\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m (\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mos.path.getsize(cod_object.tar_file_path)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m bytes)\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 450\u001b[39m )\n\u001b[32m 451\u001b[39m \u001b[38;5;66;03m# tar has been altered, so it is no longer in sync with the datastore\u001b[39;00m\n", - "\u001b[31mValueError\u001b[39m: GRADIENT_STATE_LOGS:FAILED_TO_TAR_ALL_INSTANCES:/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp9aq__y_2_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.51559928123146446551440195325528927455.dcm\n/var/folders/c2/zbssspdd0d95m1kv1c2z1tf80000gn/T/tmp9aq__y_2_1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506/1.2.826.0.1.3680043.8.498.53683297893086086544068651189614355506.tar://instances/1.2.826.0.1.3680043.8.498.25686983467200677455391333207792083612.dcm" - ] - } - ], + "outputs": [], "source": [ "def example_hash_function(uid: str) -> str:\n", " \"\"\"\n", " Example hash function that adds 1 to the last part of the uid (i.e 1.2.3.4 becomes 1.2.3.5)\n", " \"\"\"\n", " split_uid = uid.split(\".\")\n", - " last_part = split_uid[-1]\n", - " new_last_part = str(int(last_part) + 1)\n", - " split_uid[-1] = new_last_part\n", + " last_num = int(split_uid[-1])\n", + " split_uid[-1] = str(last_num + 1)\n", " return \".\".join(split_uid)\n", "\n", "with CODObject(datastore_path=datastore_path, \n", @@ -437,24 +386,22 @@ " study_uid=instance_a.study_uid(), \n", " series_uid=instance_a.series_uid(), \n", " lock=False) as orig_cod:\n", - " hashed_study_uid = example_hash_function(orig_cod.study_uid)\n", - " hashed_series_uid = example_hash_function(orig_cod.series_uid)\n", - " orig_cod.pull_tar(dirty=True)\n", + " # call extract_locally() to download and extract the tar (instances are now local, in their own temp files)\n", + " orig_cod.extract_locally(dirty=True)\n", " # get all the instances from the original COD\n", " instances = [i for i in orig_cod.get_metadata(dirty=True).instances.values()]\n", " # provide the instances with the function they should use to hash their UIDs\n", " for i in instances:\n", " i.uid_hash_func = example_hash_function\n", + " # NOTE the use of hashed_uid() methods below, and the use of hashed_uids=True in the CODObject constructor\n", " with CODObject(datastore_path=deid_datastore_path, \n", " client=client, \n", - " study_uid=hashed_study_uid, \n", - " series_uid=hashed_series_uid,\n", + " study_uid=instances[0].hashed_study_uid(), \n", + " series_uid=instances[0].hashed_series_uid(),\n", " hashed_uids=True,\n", " lock=False) as deid_cod:\n", - " # TODO: this doesnt work yet - need to extract the instances from the original tar\n", " deid_cod.append(instances=instances, dirty=True)\n", - " \n", - " " + " # Note: for this series to be reflected in the datastore, you would need to call sync() and have lock=True" ] } ],