dicom-rs icon indicating copy to clipboard operation
dicom-rs copied to clipboard

Add findscu tool

Open troelsarvin opened this issue 3 years ago • 4 comments

It would be great, if a findscu tool be added, along with the existing storescu and echoscu tools. That way, there would be example code demonstrating how to perform query/retrieve (C-Find) operations with the library, and that's something I'm currently struggling with.

troelsarvin avatar Jul 04 '21 10:07 troelsarvin

Thank you for reporting. Yes, it makes sense to continue extending the project's network tool repertoire. And that reminds me that these tools could be accompanied with a high-level API.

Nevertheless, should you have specific questions regarding a potential implementation of the find SCU in DICOM-rs, please let me know.

Enet4 avatar Jul 04 '21 10:07 Enet4

Thanks. I'm at the following stage in my code. As you can see from the concluding comments, I don't know how to proceed building a C-Find request.

use std::path::Path;
use dicom_ul::association::client::ClientAssociationOptions;
use dicom::object::open_file;

// Purpose: Open a file, and see if we can find it in the PACS.

fn main() {
    let myhomedir      = home::home_dir().unwrap();
    let subdir         = "dicom/dicom-example/mod/0000000000019826.dcm";
    let fullpath       = Path::new(&myhomedir).join(subdir);
    let pacs_addr_port = "pacshost:11112";
    let pacs_scp_aet   = "PACSAET";
    let pacs_scu_aet   = "FINDSCU";
    let retrieve_level = "STUDY"; // Expecting this will be needed later

    let obj = open_file(fullpath).unwrap();
    let patient_name = obj.element_by_name("PatientName").unwrap().to_str().unwrap();
    let siuid = obj.element_by_name("StudyInstanceUID").unwrap().to_str().unwrap();

    let mut association = ClientAssociationOptions::new()
        .with_abstract_syntax("1.2.840.10008.5.1.4.1.2.2.1") // Study Root Query/Retrieve Information Model – FIND
        .calling_ae_title(pacs_scu_aet)
        .called_ae_title(pacs_scp_aet)
        .establish(&pacs_addr_port).unwrap();

    let pc_selected_id = association.presentation_contexts().first().unwrap().id;
    println!("patient name: {:?}", patient_name);
    println!("siuid: {:?}", siuid);
    println!("assoc: {:?}", association);
    println!("pc_selected_id: {:?}", pc_selected_id);

    let writer = association.send_pdata(pc_selected_id);

    // Here I'm lost: It looks like a PDU with PData needs to be buit. And that PDU is then
    // to be used with the writer.

    // PData is a vector of PDataValue elements, but how are those
    // defined?
    // https://docs.rs/dicom-ul/0.3.0/dicom_ul/pdu/enum.Pdu.html
}

troelsarvin avatar Jul 04 '21 11:07 troelsarvin

So, the way to fulfil the C-FIND protocol from the SCU end would be:

  1. Send a C-FIND-RQ message http://dicom.nema.org/medical/dicom/current/output/chtml/part07/sect_9.3.2.html
  2. Send a DICOM query in compliance with the DIMSE-C C-FIND service http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_Q.4.html
  3. In a loop, read PDUs from the Find SCP until matching is complete or some error occurs. For as long as the status code is PENDING (either 0xFF00 or 0xFF01), then the SCP will continue sending a matched DICOM object after the C-FIND-RSP message.
  4. Release association

Steps 1 and 2 can be done by sending separate PDU's, one for each, in a similar way as in C-STORE. The command follows a strict set of attributes, and should always fit in a single PData value. The second object contains the attributes to match against the data indexed by the provider. In either case, creating and sending a single PData manually would be enough. send_pdata provides an API that is more convenient when sending a large amount of data, which may need to be split into multiple PDUs.

let pdu = Pdu::PData {
    data: vec![PDataValue {
        presentation_context_id,
        value_type: PDataValueType::Command,
        is_last: true,
        data: cmd_data,
    }],
};
scu.send(&pdu)?;

let pdu = Pdu::PData {
    data: vec![PDataValue {
        presentation_context_id: pc_selected_id,
        value_type: PDataValueType::Data,
        is_last: true,
        data: iod_data,
    }],
};
scu.send(&pdu)?;

(where cmd_data and iod_data are DICOM objects already encoded into Vec<u8>)

I managed to scribble a rather crude implementation of the Find SCU. It does not handle some edge cases, but it could be a starting point for a more complete findscu tool. In this case, the file input is the query object, which can be constructed with the help of other tools. For example, a valid input can be made by taking the dump below and using DCMTK's dump2dcm to convert it to DICOM.

(0008,0052) CS [PATIENT] # QueryRetrieveLevel
(0010,0010) PN [] # PatientsName
(0010,0020) LO [] # PatientID

Enet4 avatar Jul 04 '21 15:07 Enet4

Thanks, Eduardo! I'm afraid it's still very low-level compared to what I'm used to (Python's pynetdicom module), and I'm not sure I'll be able to be successful. But I'll try.

troelsarvin avatar Jul 04 '21 20:07 troelsarvin