예제 #1
0
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
예제 #2
0
    def test_scanning_sequence_base_choices(self):
        """
        `Scanning Sequence`_ is a `Code String (CS)`_ data element and therfores
        has a limited number of possible values.

        .. _Scanning Sequence: https://dicom.innolitics.com/ciods/mr-image/mr-image/00180020
        .. _Code String (CS): http://northstar-www.dartmouth.edu/doc/idl/html_6.2/Value_Representations.html

        """

        field = self.series._meta.get_field("scanning_sequence")
        self.assertEqual(field.base_field.choices, ScanningSequence.choices())
예제 #3
0
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)
예제 #4
0
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",
        )