Skip to content

Series

pyorthanc.Series

Bases: Resource

Represent a series that is in an Orthanc server

This object has many getters that allow the user to retrieve metadata or the entire DICOM file of the Series

Source code in pyorthanc/_resources/series.py
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
class Series(Resource):
    """Represent a series that is in an Orthanc server

    This object has many getters that allow the user to retrieve metadata
    or the entire DICOM file of the Series
    """

    @property
    def instances(self) -> List[Instance]:
        """Get series instance"""
        if self._lock_children:
            if self._child_resources is None:
                instances_ids = self.get_main_information()['Instances']
                self._child_resources = [Instance(i, self.client, self._lock_children) for i in instances_ids]

            return self._child_resources

        instances_ids = self.get_main_information()['Instances']

        return [Instance(i, self.client) for i in instances_ids]

    @property
    def uid(self) -> str:
        """Get SeriesInstanceUID"""
        return self._get_main_dicom_tag_value('SeriesInstanceUID')

    def get_main_information(self) -> Dict:
        """Get series main information

        Returns
        -------
        Dict
            Dictionary of series information
        """
        return self.client.get_series_id(self.id_)

    @property
    def manufacturer(self) -> str:
        """Get the manufacturer"""
        return self._get_main_dicom_tag_value('Manufacturer')

    @property
    def study_identifier(self) -> str:
        """Get the parent study identifier"""
        return self.get_main_information()['ParentStudy']

    @property
    def parent_study(self) -> Study:
        from . import Study
        return Study(self.study_identifier, self.client)

    @property
    def parent_patient(self) -> Patient:
        return self.parent_study.parent_patient

    @property
    def date(self) -> datetime:
        """Get series datetime

        The date have precision to the second (if available).

        Returns
        -------
        datetime
            Series date
        """
        date_string = self._get_main_dicom_tag_value('SeriesDate')
        try:
            time_string = self._get_main_dicom_tag_value('SeriesTime')
        except errors.TagDoesNotExistError:
            time_string = None

        return util.make_datetime_from_dicom_date(date_string, time_string)

    @property
    def modality(self) -> str:
        """Get series modality"""
        return self._get_main_dicom_tag_value('Modality')

    @property
    def series_number(self) -> int:
        return int(self._get_main_dicom_tag_value('SeriesNumber'))

    @property
    def performed_procedure_step_description(self) -> str:
        return self._get_main_dicom_tag_value('PerformedProcedureStepDescription')

    @property
    def protocol_name(self) -> str:
        return self._get_main_dicom_tag_value('ProtocolName')

    @property
    def station_name(self) -> str:
        return self._get_main_dicom_tag_value('StationName')

    @property
    def description(self) -> str:
        return self._get_main_dicom_tag_value('SeriesDescription')

    @property
    def body_part_examined(self) -> str:
        return self._get_main_dicom_tag_value('BodyPartExamined')

    @property
    def sequence_name(self) -> str:
        return self._get_main_dicom_tag_value('SequenceName')

    @property
    def cardiac_number_of_images(self) -> int:
        return int(self._get_main_dicom_tag_value('CardiacNumberOfImages'))

    @property
    def images_in_acquisition(self) -> int:
        return int(self._get_main_dicom_tag_value('ImagesInAcquisition'))

    @property
    def number_of_temporal_positions(self) -> int:
        return int(self._get_main_dicom_tag_value('NumberOfTemporalPositions'))

    @property
    def number_of_slices(self) -> int:
        return int(self._get_main_dicom_tag_value('NumberOfSlices'))

    @property
    def number_of_time_slices(self) -> int:
        return int(self._get_main_dicom_tag_value('NumberOfTimeSlices'))

    @property
    def image_orientation_patient(self) -> List[float]:
        orientation = self._get_main_dicom_tag_value('ImageOrientationPatient')

        return [float(i) for i in orientation.split('\\')]

    @property
    def series_type(self) -> str:
        return self._get_main_dicom_tag_value('SeriesType')

    @property
    def operators_name(self) -> str:
        return self._get_main_dicom_tag_value('OperatorsName')

    @property
    def acquisition_device_processing_description(self) -> str:
        return self._get_main_dicom_tag_value('AcquisitionDeviceProcessingDescription')

    @property
    def contrast_bolus_agent(self) -> str:
        return self._get_main_dicom_tag_value('ContrastBolusAgent')

    @property
    def is_stable(self) -> bool:
        return self.get_main_information()['IsStable']

    @property
    def last_update(self) -> datetime:
        last_updated_date_and_time = self.get_main_information()['LastUpdate'].split('T')
        date = last_updated_date_and_time[0]
        time = last_updated_date_and_time[1]

        return util.make_datetime_from_dicom_date(date, time)

    @property
    def labels(self) -> List[str]:
        return self.get_main_information()['Labels']

    def add_label(self, label: str) -> None:
        self.client.put_series_id_labels_label(self.id_, label)

    def remove_label(self, label):
        self.client.delete_series_id_labels_label(self.id_, label)

    def anonymize(self, remove: List = None, replace: Dict = None, keep: List = None,
                  force: bool = False, keep_private_tags: bool = False,
                  keep_source: bool = True, priority: int = 0, permissive: bool = False,
                  private_creator: str = None, dicom_version: str = None) -> 'Series':
        """Anonymize series

        If no error has been raise, return an anonymous series.
        Documentation: https://book.orthanc-server.com/users/anonymization.html

        Notes
        -----
        This method might be long to run, especially on large series or when multiple
        series are anonymized. In those cases, it is recommended to use the `.anonymize_as_job()`

        Parameters
        ----------
        remove
            List of tag to remove
        replace
            Dictionary of {tag: new_content}
        keep
            List of tag to keep unchanged
        force
            Some tags can't be changed without forcing it (e.g. SeriesID) for security reason
        keep_private_tags
            If True, keep the private tags from the DICOM instances.
        keep_source
            If False, instructs Orthanc to the remove original resources.
            By default, the original resources are kept in Orthanc.
        priority
            Priority of the job. The lower the value, the higher the priority.
        permissive
            If True, ignore errors during the individual steps of the job.
        private_creator
            The private creator to be used for private tags in replace.
        dicom_version
            Version of the DICOM standard to be used for anonymization.
            Check out configuration option DeidentifyLogsDicomVersion for possible values.

        Returns
        -------
        Series
            A New anonymous series.

        Examples
        --------
        ```python
        new_series = series.anonymize()

        new_series_with_specific_series_id = series.anonymize(
            replace={'SeriesDescription': 'A description'}
        )
        """
        remove = [] if remove is None else remove
        replace = {} if replace is None else replace
        keep = [] if keep is None else keep

        data = {
            'Asynchronous': False,
            'Remove': remove,
            'Replace': replace,
            'Keep': keep,
            'Force': force,
            'KeepPrivateTags': keep_private_tags,
            'KeepSource': keep_source,
            'Priority': priority,
            'Permissive': permissive,
        }
        if private_creator is not None:
            data['PrivateCreator'] = private_creator
        if dicom_version is not None:
            data['DicomVersion'] = dicom_version

        try:
            anonymous_series = self.client.post_series_id_anonymize(self.id_, data)
        except ReadTimeout:
            raise ReadTimeout(
                'Series anonymization is too long to process. '
                'Use `.anonymize_as_job` or increase client.timeout.'
            )

        return Series(anonymous_series['ID'], self.client)

    def anonymize_as_job(self, remove: List = None, replace: Dict = None, keep: List = None,
                         force: bool = False, keep_private_tags: bool = False,
                         keep_source: bool = True, priority: int = 0, permissive: bool = False,
                         private_creator: str = None, dicom_version: str = None) -> Job:
        """Anonymize series and return a job

        Launch an anonymization job.
        Documentation: https://book.orthanc-server.com/users/anonymization.html

        Notes
        -----
        This method is useful when anonymizing large series or launching many
        anonymization jobs. The jobs are sent to Orthanc and processed according
        to the priority.

        Parameters
        ----------
        remove
            List of tag to remove
        replace
            Dictionary of {tag: new_content}
        keep
            List of tag to keep unchanged
        force
            Some tags can't be changed without forcing it (e.g. SeriesInstanceUID) for security reason
        keep_private_tags
            If True, keep the private tags from the DICOM instances.
        keep_source
            If False, instructs Orthanc to the remove original resources.
            By default, the original resources are kept in Orthanc.
        priority
            Priority of the job. The lower the value, the higher the priority.
        permissive
            If True, ignore errors during the individual steps of the job.
        private_creator
            The private creator to be used for private tags in replace.
        dicom_version
            Version of the DICOM standard to be used for anonymization.
            Check out configuration option DeidentifyLogsDicomVersion for possible values.

        Returns
        -------
        Job
            Return a Job object of the anonymization job.

        Examples
        --------
        For large series (recommended)
        ```python
        job = series.anonymize_as_job()
        job.state  # You can follow the job state

        job.wait_until_completion() # Or just wait on its completion
        new_series = Series(job.content['ID'], orthanc)
        ```
        """
        remove = [] if remove is None else remove
        replace = {} if replace is None else replace
        keep = [] if keep is None else keep

        data = {
            'Asynchronous': True,
            'Remove': remove,
            'Replace': replace,
            'Keep': keep,
            'Force': force,
            'KeepPrivateTags': keep_private_tags,
            'KeepSource': keep_source,
            'Priority': priority,
            'Permissive': permissive,
        }
        if private_creator is not None:
            data['PrivateCreator'] = private_creator
        if dicom_version is not None:
            data['DicomVersion'] = dicom_version

        job_info = self.client.post_series_id_anonymize(self.id_, data)

        return Job(job_info['ID'], self.client)

    def modify(self, remove: List = None, replace: Dict = None, keep: List = None,
               force: bool = False, remove_private_tags: bool = False,
               keep_source: bool = True, priority: int = 0, permissive: bool = False,
               private_creator: str = None) -> 'Series':
        """Modify series

        If no error has been raise, then create a modified version of the series.
        If keep=['SeriesInstanceUID'] and force=True are use, then the series itself is changed.
        Documentation: https://book.orthanc-server.com/users/anonymization.html

        Notes
        -----
        This method might be long to run, especially on large series or when multiple
        series are modified. In those cases, it is recommended to use the `.modify_as_job()`

        Parameters
        ----------
        remove
            List of tag to remove
        replace
            Dictionary of {tag: new_content}
        keep
            Keep the original value of the specified tags, to be chosen among the SeriesInstanceUID,
            SeriesInstanceUID and SOPInstanceUID tags. Avoid this feature as much as possible,
            as this breaks the DICOM model of the real world.
        force
            Some tags can't be changed without forcing it (e.g. SeriesInstanceUID) for security reason
        remove_private_tags
            If True, remove the private tags from the DICOM instances.
        keep_source
            If False, instructs Orthanc to the remove original resources.
            By default, the original resources are kept in Orthanc.
        priority
            Priority of the job. The lower the value, the higher the priority.
        permissive
            If True, ignore errors during the individual steps of the job.
        private_creator
            The private creator to be used for private tags in Replace.

        Returns
        -------
        Series
            Returns a new modified series or returns itself if keep=['SeriesInstanceUID']
            (in this case, the series itself is modified).

        Examples
        --------
        ```python
        # Create a modified series
        modified_series = series.modify(replace={'SeriesInstanceUID': '1.2.840.113745.101000.1008000.38048.4626.5933732'}, force=True)
        assert modified_series.uid == '1.2.840.113745.101000.1008000.38048.4626.5933732'

        # Modify itself
        series.modify(replace={'ReferringPhysicianName': 'last^first'}, keep=['SeriesInstanceUID'], force=True)
        assert series.referring_physician_name == 'last^first'
        ```
        """
        remove = [] if remove is None else remove
        replace = {} if replace is None else replace
        keep = [] if keep is None else keep

        if 'SeriesInstanceUID' in replace and not force:
            raise errors.ModificationError('If SeriesInstanceUID is replaced, `force` must be `True`')

        data = {
            'Asynchronous': False,
            'Remove': remove,
            'Replace': replace,
            'Keep': keep,
            'Force': force,
            'RemovePrivateTags': remove_private_tags,
            'KeepSource': keep_source,
            'Priority': priority,
            'Permissive': permissive,
        }
        if private_creator is not None:
            data['PrivateCreator'] = private_creator

        try:
            modified_series = self.client.post_series_id_modify(self.id_, data)
        except ReadTimeout:
            raise ReadTimeout(
                'Series modification is too long to process. '
                'Use `.modify_as_job` or increase client.timeout.'
            )

        # Reset cache since a main DICOM tag may have be changed
        self._main_dicom_tags = None

        # if 'SeriesInstanceUID' is not affected, the modified_series['ID'] is the same as self.id_
        return Series(modified_series['ID'], self.client)

    def modify_as_job(self, remove: List = None, replace: Dict = None, keep: List = None,
                      force: bool = False, remove_private_tags: bool = False,
                      keep_source: bool = True, priority: int = 0, permissive: bool = False,
                      private_creator: str = None) -> Job:
        """Modify series and return a job

        Launch a modification job. If keep=['SeriesInstanceUID'] (with `force=True`),
        then modified this series. If the SeriesInstanceUID is not keeped, this creates
        a new modified series.
        Documentation: https://book.orthanc-server.com/users/anonymization.html

        Notes
        -----
        This method is useful when modifying large series or launching many
        modification jobs. The jobs are sent to Orthanc and processed according
        to the priority.

        Parameters
        ----------
        remove
            List of tag to remove
        replace
            Dictionary of {tag: new_content}
        keep
            Keep the original value of the specified tags, to be chosen among the SeriesInstanceUID,
            SeriesInstanceUID and SOPInstanceUID tags. Avoid this feature as much as possible,
            as this breaks the DICOM model of the real world.
        force
            Allow the modification of tags related to DICOM identifiers, at the risk of breaking
            the DICOM model of the real world.
        remove_private_tags
            If True, remove the private tags from the DICOM instances.
        keep_source
            If False, instructs Orthanc to the remove original resources.
            By default, the original resources are kept in Orthanc.
        priority
            Priority of the job. The lower the value, the higher the priority.
        permissive
            If True, ignore errors during the individual steps of the job.
        private_creator
            The private creator to be used for private tags in Replace.

        Returns
        -------
        Job
            Return a Job object of the modification job.

        Examples
        --------
        For large series (recommended)
        ```python
        job = series.modify_as_job(replace={'SeriesDescription': 'a description'})
        job.state  # You can follow the job state

        job.wait_until_completion() # Or just wait on its completion
        modified_series = Series(job.content['ID'], client)
        assert modified_series.description == 'a description'
        ```
        Or modify the SeriesInstanceUID
        ```python
        job = series.modify_as_job(
            replace={'SeriesInstanceUID': '1.2.840.113745.101000.1008000.38048.4626.5933732'},
            force=True
        )
        job.wait_until_completion() # Or just wait on its completion

        modified_series = Series(job.content['ID'], client)
        modified_series.uid == '1.2.840.113745.101000.1008000.38048.4626.5933732'
        ```
        Or keep the SeriesInstanceUID
        ```python
        job = series.modify_as_job(
            replace={'SeriesDescription': 'a description'},
            keep=['SeriesInstanceUID'],
            force=True
        )
        job.wait_until_completion()

        assert series.description == 'a description'
        ```
        """
        remove = [] if remove is None else remove
        replace = {} if replace is None else replace
        keep = [] if keep is None else keep

        if 'SeriesInstanceUID' in replace and not force:
            raise errors.ModificationError('If SeriesInstanceUID is affected, `force` must be `True`')

        data = {
            'Asynchronous': True,
            'Remove': remove,
            'Replace': replace,
            'Keep': keep,
            'Force': force,
            'RemovePrivateTags': remove_private_tags,
            'KeepSource': keep_source,
            'Priority': priority,
            'Permissive': permissive,
        }
        if private_creator is not None:
            data['PrivateCreator'] = private_creator

        job_info = self.client.post_series_id_modify(self.id_, data)

        # Reset cache since a main DICOM tag may have be changed
        self._main_dicom_tags = None

        return Job(job_info['ID'], self.client)

    def get_zip(self) -> bytes:
        """Get the bytes of the zip file

        Get the .zip file.

        Returns
        -------
        bytes
            Bytes of Zip file of the series.

        Examples
        --------
        ```python
        from pyorthanc import Orthanc, Series
        a_series = Series('SERIES_IDENTIFIER', Orthanc('http://localhost:8042'))

        bytes_content = a_series.get_zip()
        with open('series_zip_file_path.zip', 'wb') as file_handler:
            file_handler.write(bytes_content)
        ```

        """
        return self.client.get_series_id_archive(self.id_)

    def download(self, filepath: Union[str, BinaryIO], with_progres: bool = False) -> None:
        """Download the zip file to a target path or buffer

        This method is an alternative to the `.get_zip()` method for large files.
        The `.get_zip()` method will pull all the data in a single GET call,
        while `.download()` stream the data to a file or a buffer.
        Favor the `.download()` method to avoid timeout and memory issues.

        Examples
        --------
        ```python
        from pyorthanc import Orthanc, Series
        a_series = Series('SERIES_IDENTIFIER', Orthanc('http://localhost:8042'))

        # Download a zip
        a_series.download('series.zip')

        # Download a zip and show progress
        a_series.download('series.zip', with_progres=True)

        # Or download in a buffer in memory
        buffer = io.BytesIO()
        a_series.download(buffer)
        # Now do whatever you want to do
        buffer.seek(0)
        zip_bytes = buffer.read()
        ```
        """
        self._download_file(f'{self.client.url}/series/{self.id_}/archive', filepath, with_progres)

    def get_shared_tags(self, simplify: bool = False, short: bool = False) -> Dict:
        """Retrieve the shared tags of the series"""
        params = self._make_response_format_params(simplify, short)

        return dict(self.client.get_series_id_shared_tags(
            self.id_,
            params=params
        ))

    @property
    def shared_tags(self) -> Dict:
        return self.get_shared_tags(simplify=True)

    def remove_empty_instances(self) -> None:
        if self._child_resources is not None:
            self._child_resources = [i for i in self._child_resources if i is not None]

acquisition_device_processing_description: str property

body_part_examined: str property

cardiac_number_of_images: int property

contrast_bolus_agent: str property

date: datetime property

Get series datetime

The date have precision to the second (if available).

Returns:

Type Description
datetime

Series date

description: str property

image_orientation_patient: List[float] property

images_in_acquisition: int property

instances: List[Instance] property

Get series instance

is_stable: bool property

labels: List[str] property

last_update: datetime property

manufacturer: str property

Get the manufacturer

modality: str property

Get series modality

number_of_slices: int property

number_of_temporal_positions: int property

number_of_time_slices: int property

operators_name: str property

parent_patient: Patient property

parent_study: Study property

performed_procedure_step_description: str property

protocol_name: str property

sequence_name: str property

series_number: int property

series_type: str property

shared_tags: Dict property

station_name: str property

study_identifier: str property

Get the parent study identifier

uid: str property

Get SeriesInstanceUID

add_label(label)

Source code in pyorthanc/_resources/series.py
182
183
def add_label(self, label: str) -> None:
    self.client.put_series_id_labels_label(self.id_, label)

anonymize(remove=None, replace=None, keep=None, force=False, keep_private_tags=False, keep_source=True, priority=0, permissive=False, private_creator=None, dicom_version=None)

Anonymize series

If no error has been raise, return an anonymous series. Documentation: https://book.orthanc-server.com/users/anonymization.html

Notes

This method might be long to run, especially on large series or when multiple series are anonymized. In those cases, it is recommended to use the .anonymize_as_job()

Parameters:

Name Type Description Default
remove List

List of tag to remove

None
replace Dict

Dictionary of {tag: new_content}

None
keep List

List of tag to keep unchanged

None
force bool

Some tags can't be changed without forcing it (e.g. SeriesID) for security reason

False
keep_private_tags bool

If True, keep the private tags from the DICOM instances.

False
keep_source bool

If False, instructs Orthanc to the remove original resources. By default, the original resources are kept in Orthanc.

True
priority int

Priority of the job. The lower the value, the higher the priority.

0
permissive bool

If True, ignore errors during the individual steps of the job.

False
private_creator str

The private creator to be used for private tags in replace.

None
dicom_version str

Version of the DICOM standard to be used for anonymization. Check out configuration option DeidentifyLogsDicomVersion for possible values.

None

Returns:

Type Description
Series

A New anonymous series.

Examples:

```python new_series = series.anonymize()

new_series_with_specific_series_id = series.anonymize( replace={'SeriesDescription': 'A description'} )

Source code in pyorthanc/_resources/series.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def anonymize(self, remove: List = None, replace: Dict = None, keep: List = None,
              force: bool = False, keep_private_tags: bool = False,
              keep_source: bool = True, priority: int = 0, permissive: bool = False,
              private_creator: str = None, dicom_version: str = None) -> 'Series':
    """Anonymize series

    If no error has been raise, return an anonymous series.
    Documentation: https://book.orthanc-server.com/users/anonymization.html

    Notes
    -----
    This method might be long to run, especially on large series or when multiple
    series are anonymized. In those cases, it is recommended to use the `.anonymize_as_job()`

    Parameters
    ----------
    remove
        List of tag to remove
    replace
        Dictionary of {tag: new_content}
    keep
        List of tag to keep unchanged
    force
        Some tags can't be changed without forcing it (e.g. SeriesID) for security reason
    keep_private_tags
        If True, keep the private tags from the DICOM instances.
    keep_source
        If False, instructs Orthanc to the remove original resources.
        By default, the original resources are kept in Orthanc.
    priority
        Priority of the job. The lower the value, the higher the priority.
    permissive
        If True, ignore errors during the individual steps of the job.
    private_creator
        The private creator to be used for private tags in replace.
    dicom_version
        Version of the DICOM standard to be used for anonymization.
        Check out configuration option DeidentifyLogsDicomVersion for possible values.

    Returns
    -------
    Series
        A New anonymous series.

    Examples
    --------
    ```python
    new_series = series.anonymize()

    new_series_with_specific_series_id = series.anonymize(
        replace={'SeriesDescription': 'A description'}
    )
    """
    remove = [] if remove is None else remove
    replace = {} if replace is None else replace
    keep = [] if keep is None else keep

    data = {
        'Asynchronous': False,
        'Remove': remove,
        'Replace': replace,
        'Keep': keep,
        'Force': force,
        'KeepPrivateTags': keep_private_tags,
        'KeepSource': keep_source,
        'Priority': priority,
        'Permissive': permissive,
    }
    if private_creator is not None:
        data['PrivateCreator'] = private_creator
    if dicom_version is not None:
        data['DicomVersion'] = dicom_version

    try:
        anonymous_series = self.client.post_series_id_anonymize(self.id_, data)
    except ReadTimeout:
        raise ReadTimeout(
            'Series anonymization is too long to process. '
            'Use `.anonymize_as_job` or increase client.timeout.'
        )

    return Series(anonymous_series['ID'], self.client)

anonymize_as_job(remove=None, replace=None, keep=None, force=False, keep_private_tags=False, keep_source=True, priority=0, permissive=False, private_creator=None, dicom_version=None)

Anonymize series and return a job

Launch an anonymization job. Documentation: https://book.orthanc-server.com/users/anonymization.html

Notes

This method is useful when anonymizing large series or launching many anonymization jobs. The jobs are sent to Orthanc and processed according to the priority.

Parameters:

Name Type Description Default
remove List

List of tag to remove

None
replace Dict

Dictionary of {tag: new_content}

None
keep List

List of tag to keep unchanged

None
force bool

Some tags can't be changed without forcing it (e.g. SeriesInstanceUID) for security reason

False
keep_private_tags bool

If True, keep the private tags from the DICOM instances.

False
keep_source bool

If False, instructs Orthanc to the remove original resources. By default, the original resources are kept in Orthanc.

True
priority int

Priority of the job. The lower the value, the higher the priority.

0
permissive bool

If True, ignore errors during the individual steps of the job.

False
private_creator str

The private creator to be used for private tags in replace.

None
dicom_version str

Version of the DICOM standard to be used for anonymization. Check out configuration option DeidentifyLogsDicomVersion for possible values.

None

Returns:

Type Description
Job

Return a Job object of the anonymization job.

Examples:

For large series (recommended)

job = series.anonymize_as_job()
job.state  # You can follow the job state

job.wait_until_completion() # Or just wait on its completion
new_series = Series(job.content['ID'], orthanc)
Source code in pyorthanc/_resources/series.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
def anonymize_as_job(self, remove: List = None, replace: Dict = None, keep: List = None,
                     force: bool = False, keep_private_tags: bool = False,
                     keep_source: bool = True, priority: int = 0, permissive: bool = False,
                     private_creator: str = None, dicom_version: str = None) -> Job:
    """Anonymize series and return a job

    Launch an anonymization job.
    Documentation: https://book.orthanc-server.com/users/anonymization.html

    Notes
    -----
    This method is useful when anonymizing large series or launching many
    anonymization jobs. The jobs are sent to Orthanc and processed according
    to the priority.

    Parameters
    ----------
    remove
        List of tag to remove
    replace
        Dictionary of {tag: new_content}
    keep
        List of tag to keep unchanged
    force
        Some tags can't be changed without forcing it (e.g. SeriesInstanceUID) for security reason
    keep_private_tags
        If True, keep the private tags from the DICOM instances.
    keep_source
        If False, instructs Orthanc to the remove original resources.
        By default, the original resources are kept in Orthanc.
    priority
        Priority of the job. The lower the value, the higher the priority.
    permissive
        If True, ignore errors during the individual steps of the job.
    private_creator
        The private creator to be used for private tags in replace.
    dicom_version
        Version of the DICOM standard to be used for anonymization.
        Check out configuration option DeidentifyLogsDicomVersion for possible values.

    Returns
    -------
    Job
        Return a Job object of the anonymization job.

    Examples
    --------
    For large series (recommended)
    ```python
    job = series.anonymize_as_job()
    job.state  # You can follow the job state

    job.wait_until_completion() # Or just wait on its completion
    new_series = Series(job.content['ID'], orthanc)
    ```
    """
    remove = [] if remove is None else remove
    replace = {} if replace is None else replace
    keep = [] if keep is None else keep

    data = {
        'Asynchronous': True,
        'Remove': remove,
        'Replace': replace,
        'Keep': keep,
        'Force': force,
        'KeepPrivateTags': keep_private_tags,
        'KeepSource': keep_source,
        'Priority': priority,
        'Permissive': permissive,
    }
    if private_creator is not None:
        data['PrivateCreator'] = private_creator
    if dicom_version is not None:
        data['DicomVersion'] = dicom_version

    job_info = self.client.post_series_id_anonymize(self.id_, data)

    return Job(job_info['ID'], self.client)

download(filepath, with_progres=False)

Download the zip file to a target path or buffer

This method is an alternative to the .get_zip() method for large files. The .get_zip() method will pull all the data in a single GET call, while .download() stream the data to a file or a buffer. Favor the .download() method to avoid timeout and memory issues.

Examples:

from pyorthanc import Orthanc, Series
a_series = Series('SERIES_IDENTIFIER', Orthanc('http://localhost:8042'))

# Download a zip
a_series.download('series.zip')

# Download a zip and show progress
a_series.download('series.zip', with_progres=True)

# Or download in a buffer in memory
buffer = io.BytesIO()
a_series.download(buffer)
# Now do whatever you want to do
buffer.seek(0)
zip_bytes = buffer.read()
Source code in pyorthanc/_resources/series.py
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def download(self, filepath: Union[str, BinaryIO], with_progres: bool = False) -> None:
    """Download the zip file to a target path or buffer

    This method is an alternative to the `.get_zip()` method for large files.
    The `.get_zip()` method will pull all the data in a single GET call,
    while `.download()` stream the data to a file or a buffer.
    Favor the `.download()` method to avoid timeout and memory issues.

    Examples
    --------
    ```python
    from pyorthanc import Orthanc, Series
    a_series = Series('SERIES_IDENTIFIER', Orthanc('http://localhost:8042'))

    # Download a zip
    a_series.download('series.zip')

    # Download a zip and show progress
    a_series.download('series.zip', with_progres=True)

    # Or download in a buffer in memory
    buffer = io.BytesIO()
    a_series.download(buffer)
    # Now do whatever you want to do
    buffer.seek(0)
    zip_bytes = buffer.read()
    ```
    """
    self._download_file(f'{self.client.url}/series/{self.id_}/archive', filepath, with_progres)

get_main_information()

Get series main information

Returns:

Type Description
Dict

Dictionary of series information

Source code in pyorthanc/_resources/series.py
43
44
45
46
47
48
49
50
51
def get_main_information(self) -> Dict:
    """Get series main information

    Returns
    -------
    Dict
        Dictionary of series information
    """
    return self.client.get_series_id(self.id_)

get_shared_tags(simplify=False, short=False)

Retrieve the shared tags of the series

Source code in pyorthanc/_resources/series.py
606
607
608
609
610
611
612
613
def get_shared_tags(self, simplify: bool = False, short: bool = False) -> Dict:
    """Retrieve the shared tags of the series"""
    params = self._make_response_format_params(simplify, short)

    return dict(self.client.get_series_id_shared_tags(
        self.id_,
        params=params
    ))

get_zip()

Get the bytes of the zip file

Get the .zip file.

Returns:

Type Description
bytes

Bytes of Zip file of the series.

Examples:

from pyorthanc import Orthanc, Series
a_series = Series('SERIES_IDENTIFIER', Orthanc('http://localhost:8042'))

bytes_content = a_series.get_zip()
with open('series_zip_file_path.zip', 'wb') as file_handler:
    file_handler.write(bytes_content)
Source code in pyorthanc/_resources/series.py
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
def get_zip(self) -> bytes:
    """Get the bytes of the zip file

    Get the .zip file.

    Returns
    -------
    bytes
        Bytes of Zip file of the series.

    Examples
    --------
    ```python
    from pyorthanc import Orthanc, Series
    a_series = Series('SERIES_IDENTIFIER', Orthanc('http://localhost:8042'))

    bytes_content = a_series.get_zip()
    with open('series_zip_file_path.zip', 'wb') as file_handler:
        file_handler.write(bytes_content)
    ```

    """
    return self.client.get_series_id_archive(self.id_)

modify(remove=None, replace=None, keep=None, force=False, remove_private_tags=False, keep_source=True, priority=0, permissive=False, private_creator=None)

Modify series

If no error has been raise, then create a modified version of the series. If keep=['SeriesInstanceUID'] and force=True are use, then the series itself is changed. Documentation: https://book.orthanc-server.com/users/anonymization.html

Notes

This method might be long to run, especially on large series or when multiple series are modified. In those cases, it is recommended to use the .modify_as_job()

Parameters:

Name Type Description Default
remove List

List of tag to remove

None
replace Dict

Dictionary of {tag: new_content}

None
keep List

Keep the original value of the specified tags, to be chosen among the SeriesInstanceUID, SeriesInstanceUID and SOPInstanceUID tags. Avoid this feature as much as possible, as this breaks the DICOM model of the real world.

None
force bool

Some tags can't be changed without forcing it (e.g. SeriesInstanceUID) for security reason

False
remove_private_tags bool

If True, remove the private tags from the DICOM instances.

False
keep_source bool

If False, instructs Orthanc to the remove original resources. By default, the original resources are kept in Orthanc.

True
priority int

Priority of the job. The lower the value, the higher the priority.

0
permissive bool

If True, ignore errors during the individual steps of the job.

False
private_creator str

The private creator to be used for private tags in Replace.

None

Returns:

Type Description
Series

Returns a new modified series or returns itself if keep=['SeriesInstanceUID'] (in this case, the series itself is modified).

Examples:

# Create a modified series
modified_series = series.modify(replace={'SeriesInstanceUID': '1.2.840.113745.101000.1008000.38048.4626.5933732'}, force=True)
assert modified_series.uid == '1.2.840.113745.101000.1008000.38048.4626.5933732'

# Modify itself
series.modify(replace={'ReferringPhysicianName': 'last^first'}, keep=['SeriesInstanceUID'], force=True)
assert series.referring_physician_name == 'last^first'
Source code in pyorthanc/_resources/series.py
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
def modify(self, remove: List = None, replace: Dict = None, keep: List = None,
           force: bool = False, remove_private_tags: bool = False,
           keep_source: bool = True, priority: int = 0, permissive: bool = False,
           private_creator: str = None) -> 'Series':
    """Modify series

    If no error has been raise, then create a modified version of the series.
    If keep=['SeriesInstanceUID'] and force=True are use, then the series itself is changed.
    Documentation: https://book.orthanc-server.com/users/anonymization.html

    Notes
    -----
    This method might be long to run, especially on large series or when multiple
    series are modified. In those cases, it is recommended to use the `.modify_as_job()`

    Parameters
    ----------
    remove
        List of tag to remove
    replace
        Dictionary of {tag: new_content}
    keep
        Keep the original value of the specified tags, to be chosen among the SeriesInstanceUID,
        SeriesInstanceUID and SOPInstanceUID tags. Avoid this feature as much as possible,
        as this breaks the DICOM model of the real world.
    force
        Some tags can't be changed without forcing it (e.g. SeriesInstanceUID) for security reason
    remove_private_tags
        If True, remove the private tags from the DICOM instances.
    keep_source
        If False, instructs Orthanc to the remove original resources.
        By default, the original resources are kept in Orthanc.
    priority
        Priority of the job. The lower the value, the higher the priority.
    permissive
        If True, ignore errors during the individual steps of the job.
    private_creator
        The private creator to be used for private tags in Replace.

    Returns
    -------
    Series
        Returns a new modified series or returns itself if keep=['SeriesInstanceUID']
        (in this case, the series itself is modified).

    Examples
    --------
    ```python
    # Create a modified series
    modified_series = series.modify(replace={'SeriesInstanceUID': '1.2.840.113745.101000.1008000.38048.4626.5933732'}, force=True)
    assert modified_series.uid == '1.2.840.113745.101000.1008000.38048.4626.5933732'

    # Modify itself
    series.modify(replace={'ReferringPhysicianName': 'last^first'}, keep=['SeriesInstanceUID'], force=True)
    assert series.referring_physician_name == 'last^first'
    ```
    """
    remove = [] if remove is None else remove
    replace = {} if replace is None else replace
    keep = [] if keep is None else keep

    if 'SeriesInstanceUID' in replace and not force:
        raise errors.ModificationError('If SeriesInstanceUID is replaced, `force` must be `True`')

    data = {
        'Asynchronous': False,
        'Remove': remove,
        'Replace': replace,
        'Keep': keep,
        'Force': force,
        'RemovePrivateTags': remove_private_tags,
        'KeepSource': keep_source,
        'Priority': priority,
        'Permissive': permissive,
    }
    if private_creator is not None:
        data['PrivateCreator'] = private_creator

    try:
        modified_series = self.client.post_series_id_modify(self.id_, data)
    except ReadTimeout:
        raise ReadTimeout(
            'Series modification is too long to process. '
            'Use `.modify_as_job` or increase client.timeout.'
        )

    # Reset cache since a main DICOM tag may have be changed
    self._main_dicom_tags = None

    # if 'SeriesInstanceUID' is not affected, the modified_series['ID'] is the same as self.id_
    return Series(modified_series['ID'], self.client)

modify_as_job(remove=None, replace=None, keep=None, force=False, remove_private_tags=False, keep_source=True, priority=0, permissive=False, private_creator=None)

Modify series and return a job

Launch a modification job. If keep=['SeriesInstanceUID'] (with force=True), then modified this series. If the SeriesInstanceUID is not keeped, this creates a new modified series. Documentation: https://book.orthanc-server.com/users/anonymization.html

Notes

This method is useful when modifying large series or launching many modification jobs. The jobs are sent to Orthanc and processed according to the priority.

Parameters:

Name Type Description Default
remove List

List of tag to remove

None
replace Dict

Dictionary of {tag: new_content}

None
keep List

Keep the original value of the specified tags, to be chosen among the SeriesInstanceUID, SeriesInstanceUID and SOPInstanceUID tags. Avoid this feature as much as possible, as this breaks the DICOM model of the real world.

None
force bool

Allow the modification of tags related to DICOM identifiers, at the risk of breaking the DICOM model of the real world.

False
remove_private_tags bool

If True, remove the private tags from the DICOM instances.

False
keep_source bool

If False, instructs Orthanc to the remove original resources. By default, the original resources are kept in Orthanc.

True
priority int

Priority of the job. The lower the value, the higher the priority.

0
permissive bool

If True, ignore errors during the individual steps of the job.

False
private_creator str

The private creator to be used for private tags in Replace.

None

Returns:

Type Description
Job

Return a Job object of the modification job.

Examples:

For large series (recommended)

job = series.modify_as_job(replace={'SeriesDescription': 'a description'})
job.state  # You can follow the job state

job.wait_until_completion() # Or just wait on its completion
modified_series = Series(job.content['ID'], client)
assert modified_series.description == 'a description'

Or modify the SeriesInstanceUID

job = series.modify_as_job(
    replace={'SeriesInstanceUID': '1.2.840.113745.101000.1008000.38048.4626.5933732'},
    force=True
)
job.wait_until_completion() # Or just wait on its completion

modified_series = Series(job.content['ID'], client)
modified_series.uid == '1.2.840.113745.101000.1008000.38048.4626.5933732'

Or keep the SeriesInstanceUID

job = series.modify_as_job(
    replace={'SeriesDescription': 'a description'},
    keep=['SeriesInstanceUID'],
    force=True
)
job.wait_until_completion()

assert series.description == 'a description'
Source code in pyorthanc/_resources/series.py
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
def modify_as_job(self, remove: List = None, replace: Dict = None, keep: List = None,
                  force: bool = False, remove_private_tags: bool = False,
                  keep_source: bool = True, priority: int = 0, permissive: bool = False,
                  private_creator: str = None) -> Job:
    """Modify series and return a job

    Launch a modification job. If keep=['SeriesInstanceUID'] (with `force=True`),
    then modified this series. If the SeriesInstanceUID is not keeped, this creates
    a new modified series.
    Documentation: https://book.orthanc-server.com/users/anonymization.html

    Notes
    -----
    This method is useful when modifying large series or launching many
    modification jobs. The jobs are sent to Orthanc and processed according
    to the priority.

    Parameters
    ----------
    remove
        List of tag to remove
    replace
        Dictionary of {tag: new_content}
    keep
        Keep the original value of the specified tags, to be chosen among the SeriesInstanceUID,
        SeriesInstanceUID and SOPInstanceUID tags. Avoid this feature as much as possible,
        as this breaks the DICOM model of the real world.
    force
        Allow the modification of tags related to DICOM identifiers, at the risk of breaking
        the DICOM model of the real world.
    remove_private_tags
        If True, remove the private tags from the DICOM instances.
    keep_source
        If False, instructs Orthanc to the remove original resources.
        By default, the original resources are kept in Orthanc.
    priority
        Priority of the job. The lower the value, the higher the priority.
    permissive
        If True, ignore errors during the individual steps of the job.
    private_creator
        The private creator to be used for private tags in Replace.

    Returns
    -------
    Job
        Return a Job object of the modification job.

    Examples
    --------
    For large series (recommended)
    ```python
    job = series.modify_as_job(replace={'SeriesDescription': 'a description'})
    job.state  # You can follow the job state

    job.wait_until_completion() # Or just wait on its completion
    modified_series = Series(job.content['ID'], client)
    assert modified_series.description == 'a description'
    ```
    Or modify the SeriesInstanceUID
    ```python
    job = series.modify_as_job(
        replace={'SeriesInstanceUID': '1.2.840.113745.101000.1008000.38048.4626.5933732'},
        force=True
    )
    job.wait_until_completion() # Or just wait on its completion

    modified_series = Series(job.content['ID'], client)
    modified_series.uid == '1.2.840.113745.101000.1008000.38048.4626.5933732'
    ```
    Or keep the SeriesInstanceUID
    ```python
    job = series.modify_as_job(
        replace={'SeriesDescription': 'a description'},
        keep=['SeriesInstanceUID'],
        force=True
    )
    job.wait_until_completion()

    assert series.description == 'a description'
    ```
    """
    remove = [] if remove is None else remove
    replace = {} if replace is None else replace
    keep = [] if keep is None else keep

    if 'SeriesInstanceUID' in replace and not force:
        raise errors.ModificationError('If SeriesInstanceUID is affected, `force` must be `True`')

    data = {
        'Asynchronous': True,
        'Remove': remove,
        'Replace': replace,
        'Keep': keep,
        'Force': force,
        'RemovePrivateTags': remove_private_tags,
        'KeepSource': keep_source,
        'Priority': priority,
        'Permissive': permissive,
    }
    if private_creator is not None:
        data['PrivateCreator'] = private_creator

    job_info = self.client.post_series_id_modify(self.id_, data)

    # Reset cache since a main DICOM tag may have be changed
    self._main_dicom_tags = None

    return Job(job_info['ID'], self.client)

remove_empty_instances()

Source code in pyorthanc/_resources/series.py
619
620
621
def remove_empty_instances(self) -> None:
    if self._child_resources is not None:
        self._child_resources = [i for i in self._child_resources if i is not None]

remove_label(label)

Source code in pyorthanc/_resources/series.py
185
186
def remove_label(self, label):
    self.client.delete_series_id_labels_label(self.id_, label)