Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A library to help to read, manipulate and save ciod files #553

Open
qarmin opened this issue Aug 6, 2024 · 5 comments
Open

A library to help to read, manipulate and save ciod files #553

qarmin opened this issue Aug 6, 2024 · 5 comments
Labels
A-lib Area: library new This provides a new, mostly independent feature

Comments

@qarmin
Copy link

qarmin commented Aug 6, 2024

Recently in company we needed to create a tool that could:

  • read dicom files
  • write data to the appropriate CIOD structure and validate it to a basic degree(types, whether it is required, quantity )
  • write data to a file/memory in dcm format

So we created for internal use a library that makes it much easier to manage this.

The code looks similar to this(there are defined attributes i.e. CS, UI etc. and separate modules in a separate file):

#[derive(Clone, TypedBuilder)]
pub struct OphtalmicVisualFieldStaticPerimetryMeasurements {
    #[builder(setter(into))]
    pub patient: Patient,
    #[builder(setter(into))]
    pub general_study: GeneralStudy,
    ...
}


impl OphtalmicVisualFieldStaticPerimetryMeasurements {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        instance_uid: &str,
        character_set: SpecificCharacterSet,
        patient: Patient,
        study: GeneralStudy,
    ) -> Self {
        let sop_common = SopCommon::builder()
            .specific_character_set(character_set)
            .sop_class_uid(OPHTHALMIC_VISUAL_FIELD_STATIC_PERIMETRY_MEASUREMENTS_STORAGE)
            .sop_instance_uid(instance_uid)
            .build();

        OphtalmicVisualFieldStaticPerimetryMeasurements::builder()
            .sop_common(sop_common)
            .patient(patient)
            .general_study(study)
            .build()
    }
}

impl DicomCIOD for OphtalmicVisualFieldStaticPerimetryMeasurements {
    fn prepare(&self, media_storage_sop_instance_uid: &str) -> Result<FileDicomObject<InMemDicomObject>, Error> {
        let meta_builder = FileMetaTableBuilder::new()
            .transfer_syntax(EXPLICIT_VR_LITTLE_ENDIAN)
            .media_storage_sop_class_uid(OPHTHALMIC_VISUAL_FIELD_STATIC_PERIMETRY_MEASUREMENTS_STORAGE)
            .media_storage_sop_instance_uid(media_storage_sop_instance_uid);

        InMemDicomObject::from_element_iter(self.dataset().elements)
            .with_meta(meta_builder)
            .map_err(|e| Error::ErrorBuildingMetadata(e.to_string()))
    }

    fn create(obj: &InMemDicomObject) -> Result<Self, Error> {
        Ok(OphtalmicVisualFieldStaticPerimetryMeasurements::builder()
            .patient(Patient::create(obj)?)
            .general_study(GeneralStudy::create(obj)?)
            .sop_common(SopCommon::create(obj)?)
            .build())
    }
}

Specific modules are implemented in similar structure:

#[derive(Clone, PartialEq, TypedBuilder)]
pub struct OphthalmicPhotographyParameters {
    #[builder(setter(into))]
    pub detector_type: DetectorType,
    #[builder(setter(into))]
    pub acquisition_device_type_code_sequence: DicomSequence<GenericCodeSequenceItem>,
    #[builder(default, setter(into))]
    pub illumination_type_code_sequence: Option<DicomSequence<GenericCodeSequenceItem>>,
    #[builder(default, setter(into))]
    pub camera_angle_of_view: Option<f32>,
    ...
}

objects can be created by loading a file from memory like OphtalmicVisualFieldStaticPerimetryMeasurements::load(&file_path) or manually by creating them within rust:

        let patient = Patient::builder()
            .patient_name(patient_name)
            .patient_id("Abce-123".to_string())
            .patient_sex(Sex::Male)
            .patient_birth_date(NaiveDate::from_ymd_opt(1970, 5, 15))
            .patient_age(Age::Years(54))
            .build();
        let background_illumination_color_code_sequence = DicomSequence::new(vec![GenericCodeSequenceItem::builder()
            .code_value("TesB".to_string())
            .code_meaning("TesB2".to_string())
            .build()]);
            
            
        let doc = OphtalmicVisualFieldStaticPerimetryMeasurements::new(
            "1.202.4.4",
            SpecificCharacterSet::IsoIR192,
            background_illumination_color_code_sequence
	);

        doc.prepare("124.0.0.0.1")
            .unwrap()
            .write_to_file("tests-resources/ovfspm_out.dcm")
            .unwrap();

        let file = OpenFileOptions::new()
            .open_file("tests-resources/ovfspm_out.dcm")
            .unwrap();
        let meta = file.meta().clone();
        let obj = file.into_inner();
        assert_eq!(
            meta.media_storage_sop_class_uid(),
            OPHTHALMIC_VISUAL_FIELD_STATIC_PERIMETRY_MEASUREMENTS_STORAGE
        );
        let item = OphtalmicVisualFieldStaticPerimetryMeasurements::create(&obj).unwrap();
        assert_eq!(item.dataset().problems, vec![]);

So my questions:

  • would such a library be useful as a new separate module in dicom-rs? We are considering whether to make it available, as this would help improve the quality of it
  • is there a page/document that is easy to parse and would allow to extract all information about ciod/modules and their tags with all information(i.e. ValueMultiplicity or number of elements in a sequence), to be able to generate code basic structure for most ciods automatically via script?
@qarmin qarmin changed the title A library to help read, manipulate and save ciod files A library to help to read, manipulate and save ciod files Aug 6, 2024
@Enet4
Copy link
Owner

Enet4 commented Aug 6, 2024

Thank you for sharing!

  • would such a library be useful as a new separate module in dicom-rs? We are considering whether to make it available, as this would help improve the quality of it

That idea is aligned with the plans for an IOD/module API for the most part, so it is already on the roadmap and would indeed be great that it be part of the DICOM-rs ecosystem. I would be grateful with that contribution to the project, and I would provide the maintenance and synergy that the project has in place for all covered crates.

We can go into further detail about the feature set here or on Zulip.

  • is there a page/document that is easy to parse and would allow to extract all information about ciod/modules and their tags with all information(i.e. ValueMultiplicity or number of elements in a sequence), to be able to generate code basic structure for most ciods automatically via script?

Part 3 is where one would find all the definitions (XML version here). Some details might only be written in prose or provided in other parts of the standard, but we already have a data element dictionary that can be depended on for code generation. There is also an example of parsing the UID table from PS3.6 in dicom-dictionary-builder.

@Enet4 Enet4 added A-lib Area: library new This provides a new, mostly independent feature labels Aug 6, 2024
@purepani
Copy link

Is there an api in mind for this IOD api? I might want to work on making this exist if there's an idea of what it would look like

@Enet4
Copy link
Owner

Enet4 commented Aug 21, 2024

Primarily, I would envision IOD (or Module)-like structs offering methods to 1) read from different sources (file, raw bytes, in-mem DICOM object) into a value of type Self; and 2) write as a DICOM data set, all of this done automatically via derive. Part 1) is more interesting, with methods such as the following:

  fn open_file(path: impl AsRef<Path>) -> Result<Self>;
  fn read_data(path: impl std::io::Read) -> Result<Self>;
  fn from_obj(dicom_object: &InMemDicomObject) -> Result<Self>;

Whether this would require implementing more traits to facilitate the process of generating the implementation is an implementation detail.

A good choice of methods may also allow us to provide default implementations (e.g. open_file can read the whole file into memory then call from_obj). I also wouldn't worry about making the trait object safe for the time being.

Here's an idea of how it would work to the API user.

use dicom_iod::Module; // import trait & derive macro

#[derive(Module)]
struct PatientModule {
    #[iod(...)] // <-- whichever parameters required here
    patient_name: String,
    #[iod(...)]
    patient_id: Option<String>,
    #[iod(...)]
    patient_birthdate: Option<String>,
    // etc
}

// read enough DICOM data from a file to populate a `PatientModule`
let mut data = PatientModule::open_file("0001.dcm")?;
println!("{} (ID: {})", data.patient_name, data.patient_id);

// e.g. remove birth date
data.patient_birthdate = None;

// encode to byte vector
let mut buf = Vec::<u8>::new();
let ts = dicom_transfer_syntax_registry::entries::EXPLICIT_VR_LITTLE_ENDIAN.erased();
data.write_dataset_with_ts(&mut buf, &ts)?;

So a baseline implementation of a potential dicom_iod and dicom_iod_derive crate pair would offer a module trait with a few methods such as the ones above and the ability to derive it. Later on, we could think of certain extras which can be planned and developed on top of the baseline.

  • PatientModule was declared in the same module in the example, but the project could offer pre-generated (and pre-expanded) module definitions from the standard, so that common modules such as the patient module wouldn't have to be written every time.
  • A potential extra would be making this derive macro also derive additional traits such as ApplyOp and DicomObject, so that those structs can seamlessly interoperate with these APIs.

@qarmin
Copy link
Author

qarmin commented Aug 23, 2024

Currently, we have implemented CIOD classes and all the required modules for them(without most of the optional ones):

  • EncapsulatedPdf
  • OphthalmicPhotohraphy8BitImage
  • OphtalmicVisualFieldStaticPerimetryMeasurements
  • SecondaryCaptureImage

We are currently still tweaking a few things so we should be releasing the first test version soon.

As time goes by it looks like full automatic generation of new ciods/modules is less and less likely due to quite specific validation rules

@Enet4
Copy link
Owner

Enet4 commented Aug 23, 2024

As time goes by it looks like full automatic generation of new ciods/modules is less and less likely due to quite specific validation rules

Right, we might end up with a hybrid approach, where the baseline is generated from the standard but the modules committed to the crate are adjusted by hand to cover the necessary validation rules.

It would also be nice if we could feature-gate the IODs that the developer might need, so that the unused ones do not have to be compiled. This might make a greater difference as the list of classes expands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-lib Area: library new This provides a new, mostly independent feature
Projects
None yet
Development

No branches or pull requests

3 participants