def header_getter(field, header): data_element = header.get_data_element(field) dtype = data_element.VALUE_REPRESENTATION.value data = data_element.value if ("String" in dtype and "Decimal" not in dtype and "Code" not in dtype or dtype == "Unknown"): dtype = "STRING" elif "Code String" in dtype: dtype = "LIST" else: dtype = "NORMAL" if isinstance(data, (tuple, list)): data = ([ScanningSequence(elem).name for elem in data] if field == "ScanningSequence" or field == (0x0018, 0x0020) else [SequenceVariant(elem).name for elem in data] if field == "SequenceVariant" or field == (0x0018, 0x0021) else data) else: data = (ScanningSequence(data).name if field == "ScanningSequence" or field == (0x0018, 0x0020) else SequenceVariant(data).name if field == "SequenceVariant" or field == (0x0018, 0x0021) else data) return data or None, dtype
def test_sequence_variant_base_choices(self): """ `Sequence Variant`_ is a `Code String (CS)`_ data element and therfores has a limited number of possible values. .. _Sequence Variant: https://dicom.innolitics.com/ciods/mr-image/mr-image/00180021 .. _Code String (CS): http://northstar-www.dartmouth.edu/doc/idl/html_6.2/Value_Representations.html """ field = self.series._meta.get_field("sequence_variant") self.assertEqual(field.base_field.choices, SequenceVariant.choices())
class Series(DicomEntity): """ A model to represent a single instance of the Series_ entity. .. _Series: http://dicom.nema.org/dicom/2013/output/chtml/part03/chapter_A.html """ #: `Series Instance UID #: <https://dicom.innolitics.com/ciods/mr-image/general-series/0020000e>`_ #: value. uid = models.CharField( max_length=64, unique=True, validators=[digits_and_dots_only], verbose_name="Series Instance UID", help_text=help_text.SERIES_UID, ) #: `Series Number #: <https://dicom.innolitics.com/ciods/mr-image/general-series/00200011>`_ #: value. number = models.PositiveIntegerField( blank=True, null=True, verbose_name="Series Number", help_text=help_text.SERIES_NUMBER, ) #: `Series Description #: <https://dicom.innolitics.com/ciods/mr-image/general-series/0008103e>`_ #: value. description = models.CharField( max_length=64, blank=True, null=True, verbose_name="Series Description", help_text=help_text.SERIES_DESCRIPTION, ) #: `Series Date #: <https://dicom.innolitics.com/ciods/mr-image/general-series/00080021>`_ #: value. date = models.DateField( blank=True, null=True, help_text=help_text.SERIES_DATE ) #: `Series Time #: <https://dicom.innolitics.com/ciods/mr-image/general-series/00080021>`_ #: value. time = models.TimeField( blank=True, null=True, help_text=help_text.SERIES_TIME ) #: `Echo Time #: <https://dicom.innolitics.com/ciods/mr-image/mr-image/00180081>`_ #: value. echo_time = models.FloatField( blank=True, null=True, validators=[MinValueValidator(0)], help_text=help_text.ECHO_TIME, ) #: `Echo Train Length #: <https://dicom.innolitics.com/ciods/mr-image/mr-image/00180091>`_ #: value. echo_train_length = models.PositiveIntegerField( blank=True, null=True, help_text=help_text.ECHO_TRAIN_LENGTH ) #: `Inversion Time #: <https://dicom.innolitics.com/ciods/mr-image/mr-image/00180082>`_ #: value. inversion_time = models.FloatField( blank=True, null=True, validators=[MinValueValidator(0)], help_text=help_text.INVERSION_TIME, ) #: `Repetition Time #: <https://dicom.innolitics.com/ciods/mr-image/mr-image/00180080>`_ #: value. repetition_time = models.FloatField( blank=True, null=True, validators=[MinValueValidator(0)], help_text=help_text.REPETITION_TIME, ) #: `Scanning Sequence #: <https://dicom.innolitics.com/ciods/mr-image/mr-image/00180020>`_ #: value. scanning_sequence = ChoiceArrayField( models.CharField(max_length=2, choices=ScanningSequence.choices()), size=5, help_text=help_text.SCANNING_SEQUENCE, blank=True, null=True, ) #: `Sequence Variant #: <https://dicom.innolitics.com/ciods/mr-image/mr-image/00180021>`_ #: value. sequence_variant = ChoiceArrayField( models.CharField(max_length=4, choices=SequenceVariant.choices()), size=6, help_text=help_text.SEQUENCE_VARIANT, blank=True, null=True, ) #: `Pixel Spacing #: <https://dicom.innolitics.com/ciods/mr-image/image-plane/00280030>`_ #: value. pixel_spacing = ArrayField( models.FloatField(validators=[MinValueValidator(0)]), size=2, help_text=help_text.PIXEL_SPACING, blank=True, null=True, ) #: `Slice Thickness #: <https://dicom.innolitics.com/ciods/mr-image/image-plane/00180050>`_ #: value. slice_thickness = models.FloatField( validators=[MinValueValidator(0)], help_text=help_text.SLICE_THICKNESS, blank=True, null=True, ) #: `Manufacturer #: <https://dicom.innolitics.com/ciods/mr-image/device/00500010/00080070>`_ #: value. manufacturer = models.CharField( max_length=64, blank=True, null=True, help_text=help_text.MANUFACTURER ) #: `Manufacturer's Model Name #: <https://dicom.innolitics.com/ciods/mr-image/device/00500010/00081090>`_ #: value. manufacturer_model_name = models.CharField( max_length=64, blank=True, null=True, help_text=help_text.MANUFACTURER_MODEL_NAME, ) #: `Magnetic Field Strength #: <https://dicom.innolitics.com/ciods/mr-image/mr-image/00180087>`_ #: value. magnetic_field_strength = models.FloatField( null=True, blank=True, validators=[MinValueValidator(0)], help_text=help_text.MAGNETIC_FIELD_STRENGTH, ) #: `Device Serial Number #: <https://dicom.innolitics.com/ciods/mr-image/device/00500010/00181000>`_ #: value. device_serial_number = models.CharField( max_length=64, blank=True, null=True, help_text=help_text.DEVICE_SERIAL_NUMBER, ) #: `Body Part Examined #: <https://dicom.innolitics.com/ciods/mr-image/general-series/00180015>`_ #: value. body_part_examined = models.CharField( max_length=16, blank=True, null=True, help_text=help_text.BODY_PART_EXAMINED, ) #: `Patient Position #: <https://dicom.innolitics.com/ciods/mr-image/general-series/00185100>`_ #: value. patient_position = models.CharField( max_length=4, choices=PatientPosition.choices(), blank=True, null=True, help_text=help_text.PATIENT_POSITION, ) #: `Modality #: <https://dicom.innolitics.com/ciods/mr-image/general-series/00080060>`_ #: value. modality = models.CharField( max_length=10, choices=Modality.choices(), help_text=help_text.MODALITY ) #: `Institution Name #: <https://dicom.innolitics.com/ciods/mr-image/general-equipment/00080080>`_ #: value. institution_name = models.CharField( max_length=64, blank=True, null=True, help_text=help_text.INSTITUTE_NAME, ) #: `Operator's Name #: <https://dicom.innolitics.com/ciods/mr-image/general-series/00081070>`_ #: value. operators_name = models.JSONField( blank=True, null=True, help_text=help_text.OPERATORS_NAME ) #: `Protocol Name #: <https://dicom.innolitics.com/ciods/mr-image/general-series/00181030>`_ #: value. protocol_name = models.CharField( max_length=64, blank=True, null=True, help_text=help_text.PROTOCOL_NAME ) #: `Flip Angle #: <https://dicom.innolitics.com/ciods/mr-image/mr-image/00181314>`_ #: value. flip_angle = models.FloatField( null=True, blank=True, help_text=help_text.FLIP_ANGLE ) #: `Pulse Sequence Name #: <https://dicom.innolitics.com/ciods/enhanced-mr-color-image/mr-pulse-sequence/00189005>`_ #: value. pulse_sequence_name = models.CharField( max_length=64, blank=True, null=True, help_text=help_text.PULSE_SEQUENCE_NAME, ) #: `Sequence Name #: <https://dicom.innolitics.com/ciods/mr-image/mr-image/00180024>`_ #: value. sequence_name = models.CharField( max_length=64, blank=True, null=True, help_text=help_text.SEQUENCE_NAME ) #: `MR Acquisition Type #: <https://dicom.innolitics.com/ciods/mr-image/mr-image/00180023>`_ #: value. MR_ACQUISITION_2D = "2D" MR_ACQUISITION_3D = "3D" MR_ACQUISITION_TYPE_CHOICES = ( (MR_ACQUISITION_2D, "2D"), (MR_ACQUISITION_3D, "3D"), ) mr_acquisition_type = models.CharField( max_length=2, choices=MR_ACQUISITION_TYPE_CHOICES, blank=True, null=True, help_text=help_text.MR_ACQUISITION_TYPE, verbose_name="MR Acquisition Type", ) #: The :class:`~django_dicom.models.study.Study` instance to which this #: series belongs. study = models.ForeignKey( "django_dicom.Study", on_delete=models.PROTECT, blank=True, null=True ) #: The :class:`~django_dicom.models.patient.Patient` instance to which this #: series belongs. patient = models.ForeignKey( "django_dicom.Patient", on_delete=models.PROTECT, blank=True, null=True ) #: A dictionary of DICOM data element keywords to be used to populate #: a created instance's fields. FIELD_TO_HEADER = { "uid": "SeriesInstanceUID", "date": "SeriesDate", "time": "SeriesTime", "description": "SeriesDescription", "number": "SeriesNumber", "mr_acquisition_type": "MRAcquisitionType", "pulse_sequence_name": (0x0019, 0x109C), } # Cached :class:`~dicom_parser.series.Series` instance. _instance = None logger = logging.getLogger("data.dicom.series") class Meta: ordering = ( "-date", "time", "number", ) verbose_name_plural = "Series" indexes = [ models.Index(fields=["uid"]), models.Index(fields=["date", "time"]), ] def __str__(self) -> str: """ Returns the str representation of this instance. Returns ------- str This instance's string representation """ return self.uid def get_absolute_url(self) -> str: """ Returns the absolute URL for this instance. For more information see the `Django documentation`_. .. _Django documentation: https://docs.djangoproject.com/en/3.0/ref/models/instances/#get-absolute-url Returns ------- str This instance's absolute URL path """ return reverse("dicom:series-detail", args=[str(self.id)]) def save(self, *args, **kwargs) -> None: """ Overrides :meth:`~django_dicom.models.dicom_entity.DicomEntity.save` to create any missing related DICOM entities if required. """ header = kwargs.get("header") if header and self.missing_relation: if not self.patient: self.patient, _ = header.get_or_create_patient() if not self.study: self.study, _ = header.get_or_create_study() super().save(*args, **kwargs) def get_path(self) -> Path: """ Returns the base directory containing the images composing this series. Returns ------- str This series's base directory path """ sample_image = self.image_set.first() dcm_path = ( sample_image.dcm.name if os.getenv("USE_S3") else sample_image.dcm.path ) return Path(dcm_path).parent def get_scanning_sequence_display(self) -> list: """ Returns the display valuse of this instance's :attr:`~django_dicom.models.series.Series.scanning_sequence` attribute. Returns ------- list Verbose scanning sequence values """ if self.scanning_sequence: return [ ScanningSequence[sequence].value if getattr(ScanningSequence, sequence, False) else sequence for sequence in self.scanning_sequence ] def get_sequence_variant_display(self) -> list: """ Returns the display valuse of this instance's :attr:`~django_dicom.models.series.Series.sequence_variant` attribute. Returns ------- list Verbose sequence variant values """ if self.sequence_variant: return [ SequenceVariant[variant].value if getattr(SequenceVariant, variant, False) else variant for variant in self.sequence_variant ] @property def path(self) -> Path: """ Returns the base path of this series' images. Returns ------- :class:`pathlib.Path` Series directory path """ return self.get_path() @property def instance(self) -> DicomSeries: """ Caches the created :class:`dicom_parser.series.Series` instance to prevent multiple reades. Returns ------- :class:`dicom_parser.series.Series` Series information """ if not isinstance(self._instance, DicomSeries): self._instance = DicomSeries(self.path) return self._instance @property def data(self) -> np.ndarray: """ Returns the :attr:`dicom_parser.series.Series.data` property's value. Returns ------- :class:`np.ndarray` Series data """ return self.instance.data @property def datetime(self) -> datetime: """ Returns a :class:`datetime.datetime` object by combining the values of the :attr:`~django_dicom.models.series.Series.date` and :attr:`~django_dicom.models.series.Series.time` fields. Returns ------- :class:`datetime.datetime` Series datetime """ time = self.time or datetime.min.time() if self.date: return datetime.combine(self.date, time, tzinfo=pytz.UTC) @property def missing_relation(self) -> bool: """ Returns whether this instance misses an associated :class:`~django_dicom.models.patient.Patient` or :class:`~django_dicom.models.study.Study`. Returns ------- bool Whether this instance has missing relationships """ return not (self.patient and self.study) @property def spatial_resolution(self) -> tuple: """ Returns the 3D spatial resolution of the instance by combining the values of the :attr:`~django_dicom.models.series.Series.pixel_spacing` and :attr:`~django_dicom.models.series.Series.slice_thickness` fields. Returns ------- tuple (x, y, z) resolution in millimeters """ if self.pixel_spacing and self.slice_thickness: return tuple(self.pixel_spacing + [self.slice_thickness]) elif self.pixel_spacing: return tuple(self.pixel_spacing)
class SeriesFilter(filters.FilterSet): """ Provides filtering functionality for the :class:`~django_dicom.views.series.SeriesViewSet`. Available filters are: * *id*: Primary key * *uid*: Series Instance UID * *patient_id*: Related :class:`~django_dicom.models.patient.Patient` instance's primary key * *study_uid*: Related :class:`~django_dicom.models.study.Study` instance's :attr:`~django_dicom.models.study.Study.uid` value * *study_description*: Related :class:`~django_dicom.models.study.Study` instance's :attr:`~django_dicom.models.study.Study.description` value (in-icontains) * *modality*: Any of the values defined in :class:`~dicom_parser.utils.code_strings.modality.Modality` * *description*: Series description value (contains, icontains, or exact) * *number*: Series number value * *protocol_name*: Protocol name value (contains) * *scanning_sequence*: Any combination of the values defined in :class:`~dicom_parser.utils.code_strings.scanning_sequence.ScanningSequence` * *sequence_variant*: Any combination of the values defined in :class:`~dicom_parser.utils.code_strings.sequence_variant.SequenceVariant` * *echo_time*: :attr:`~django_dicom.models.series.Series.echo_time` value * *inversion_time*: :attr:`~django_dicom.models.series.Series.inversion_time` value * *repetition_time*: :attr:`~django_dicom.models.series.Series.repetition_time` value * *flip_angle*: Any of the existing :attr:`~django_dicom.models.series.Series.flip_angle` in the database * *created_after_date*: Create after date * *date*: Exact :attr:`~django_dicom.models.series.Series.date` value * *created_before_date*: Create before date * *created_after_time*: Create after time * *created_before_time*: Create before time * *manufacturer*: Any of the existing :attr:`~django_dicom.models.series.Series.manufacturer` in the database * *manufacturer_model_name*: Any of the existing :attr:`~django_dicom.models.series.Series.manufacturer_model_name` in the database * *device_serial_number*: Any of the existing :attr:`~django_dicom.models.series.Series.device_serial_number` in the database * *institution_name*: Any of the existing :attr:`~django_dicom.models.series.Series.institution_name` in the database * *pulse_sequence_name*: :attr:`~django_dicom.models.series.Series.pulse_sequence_name` value (in-icontains) * *sequence_name*: :attr:`~django_dicom.models.series.Series.sequence_name` value (in-icontains) """ study_uid = filters.CharFilter("study__uid", lookup_expr="exact", label="Study UID") study_description = CharInFilter( field_name="study__description", lookup_expr="in", label="Study description icontains", method=filter_in_string, ) modality = filters.ChoiceFilter("modality", choices=Modality.choices()) description = filters.LookupChoiceFilter(lookup_choices=[ ("contains", "Contains (case-sensitive)"), ("icontains", "Contains (case-insensitive)"), ("exact", "Exact"), ]) protocol_name = filters.CharFilter("protocol_name", lookup_expr="contains") scanning_sequence = filters.MultipleChoiceFilter( "scanning_sequence", choices=ScanningSequence.choices(), conjoined=True, method=filter_array, ) sequence_variant = filters.MultipleChoiceFilter( "sequence_variant", choices=SequenceVariant.choices(), conjoined=True, method=filter_array, ) flip_angle = filters.AllValuesFilter("flip_angle") created_after_date = filters.DateFilter("date", lookup_expr="gte") date = filters.DateFilter("date") created_before_date = filters.DateFilter("date", lookup_expr="lte") created_after_time = filters.TimeFilter("time", lookup_expr="gte") created_before_time = filters.TimeFilter("time", lookup_expr="lte") manufacturer = filters.AllValuesFilter("manufacturer") manufacturer_model_name = filters.AllValuesFilter( "manufacturer_model_name") magnetic_field_strength = filters.AllValuesFilter( "magnetic_field_strength") device_serial_number = filters.AllValuesFilter("device_serial_number") institution_name = filters.AllValuesFilter("institution_name") pulse_sequence_name = CharInFilter( field_name="pulse_sequence_name", lookup_expr="icontains", method=filter_in_string, ) sequence_name = CharInFilter( field_name="sequence_name", lookup_expr="icontains", method=filter_in_string, ) pixel_spacing = filters.RangeFilter("pixel_spacing__0") slice_thickness = filters.RangeFilter("slice_thickness") repetition_time = filters.RangeFilter("repetition_time") inversion_time = filters.RangeFilter("inversion_time") echo_time = filters.RangeFilter("echo_time") header_fields = filters.CharFilter("image", method=filter_header) class Meta: model = Series fields = ( "id", "uid", "patient_id", "study_uid", "study_description", "modality", "description", "protocol_name", "number", "created_after_date", "created_before_date", "created_after_time", "created_before_time", "echo_time", "inversion_time", "repetition_time", "slice_thickness", "pixel_spacing", "scanning_sequence", "sequence_variant", "flip_angle", "manufacturer", "manufacturer_model_name", "magnetic_field_strength", "device_serial_number", "institution_name", "patient__id", "pulse_sequence_name", "sequence_name", )