feat(core): rework DICOM / series relationship in Sight
Description
Currently, our Series
(ImageSeries
, ModelSeries
, ActivitySeries
, DicomSeries
, ...) are special data objects that store some (a really small portion) of common DICOM tags (patient name, study date, equipement, ...). Most of the time, theses fields are even not used, but when they are, only DicomSeries
keep the link between them and the real DICOM file. DicomSeries
is a kind of "glue" between "Sight" and "DICOM", but in the "Sight" world, it is DICOM Readers/Writers that have the responsibility to make the real DICOM tags and the Series
data members to match.
This design seems a bit complicated and sub-optimal:
- We need to manage ourself the relation between
Series
internal members and real DICOM tags which are really well managed by underlying libraries like GDCM. We have to write wrappers, readers/writers, constantly convert them between both world, with the risk of altering values. - The data itself need to be serialized two times, one time in the data object, another in the real DICOM file.
- Even if we are not opening a DICOM file, if we still use disguised DICOM tags, there is no reason to not use a real DICOM container.
- We are currently limited by our choice of the hard coded "Series" members, which are indeed, fake DICOM tags. DICOM manage thousand of tags.
- Everything is a Series. Even the inheritance is questionably here. A
ModelSeries
have almost nothing in common with anActivitySeries
, they should not have a sibling relation. - Since everything is a series, the same data (ex: "Patient name") is duplicated in all series instance, even when they indeed work with the same "Patient". Even worse, the same data is also duplicated in the real DICOM files. This duplication have a cost, in performance, and, of course, in safety.
Proposal
- Have only one class that store / give access to the real DICOM container. Use it by aggregation rather than inheritance, everywhere.
- old "Series" class members should be replaced by the corresponding DICOM tag (ex: "Patient name"). No wrapper, data conversion.
- Old "Series" class members will be serialized as a DICOM file. Meshes from ModelSeries, Images from ImageSeries, etc.. should also be serialized inside.
- Collection (
SeriesDB
,Vector
,Composite
...) code will be factorized as a templated class. Even ifList
will be removed, readding it or adding a new one can be done in a couple of lines.
Functional specifications
As a reminder, a DICOM file is structured as such:
classDiagram
class Patient{
PatientID [0x0010][0x0020]
}
class Study{
StudyInstanceUID [0x0020][0x000d]
}
class Series{
SeriesInstanceUID [0x0020][0x000e]
}
class Instance{
SOPInstanceUID [0x0008][0x0018]
}
Patient "1" o.. "n" Study
Study "1" o.. "n" Series
Series "1" o.. "n" Instance
Current class diagram
Also as a reminder, the current (simplified) class diagram looks like:
classDiagram
SeriesDB "1" o-- "n" Series
Series <|-- ActivitySeries
Series <|-- CameraSeries
Series <|-- DicomSeries
Series <|-- ImageSeries
Series <|-- ModelSeries
Series o-- Patient
Series o-- Study
Series o-- Equipement
ActivitySeries o-- Composite
CameraSeries "1" o-- "n" Camera
DicomSeries "1" o-- "n" BufferObject
DICOM <.. BufferObject
ImageSeries o-- Image
ImageSeries o-- DicomSeries
Image o-- Array
Array o-- BufferObject
ModelSeries "1" o-- "n" Reconstruction
ModelSeries o-- DicomSeries
Reconstruction o-- Image
Reconstruction o-- Mesh
Reconstruction o-- Material
class Series {
String Modality
String InstanceUID
...
}
class DicomSeries{
std::map ComputedTag
}
class ImageSeries{
String ContrastAgent
String ContrastRoute
...
}
class Patient{
String Name
String PatientId
...
}
class Study{
String StudyID
String PatientAge
...
}
class Equipement{
String InstitutionName
}
Classes that will change or be removed
-
ActivitySeries
andCameraSeries
The inheritance from Series
of ActivitySeries
and CameraSeries
classes will be removed and the classes renamed respectively Activity
and Cameras
. Nothing justify that an activity or a list of camera to be a series. It is even wrongly designed, since it would disallow us to make an application / activity that work on two series at the same time, like comparing the evolution of a tumor from two distinct DICOM datasets. Removing unnecessary coupling will make the code safer, more readable and more extensible.
-
Series
mutation
Series
will be changed from a stand-alone data object to a trivial ISeries
interface. Since there is no generic Series
Information Object Definition (IOD) in DICOM, there is no value to make it a concrete data object. Furthermore, being a simple interface will allow ImageSeries
inheritance from Image
, which will simplify the workflow and avoid Image
extraction from an ImageSeries
.
The ISeries
interface will define access methods to and from underlying DicomInstance
which kinda replace DicomSeries
or more precisely, will implement the bridge between Sight
data object and DICOM properties.
-
Patient
,Study
,Equipement
will rest in Oblivion
This data objects have no other (clear) purpose than keeping some DICOM like properties. There is no reason to keep them, since properties will be accessed trough ISeries
interface, which will in turn, forward the call to DicomInstance
.
-
DicomSeries
removed and somewhat replaced byDicomInstance
DicomInstance
will reference a DICOM instance and will have access to all DICOM properties from that instance using GDCM
library (is there another solution ?). Since a DICOM series is a collection of DICOM instance, an object that implements ISeries
like ImageSeries
will have access to a vector of DicomInstance
, that, in the case of a CT Image, reference each slices (CT Image IOD stores each slice in separated instances). This allows to conserve more meta-data, which generally won't change between slice, but could from time to time (which we did not manage well!).
To be clear, DicomSeries
will be removed as its features are, in fact, bogus. The BufferObject
used to reference the original DICOM file cannot be a Series
since in DICOM, normally, each instances (ie a slice) are stored in separate files. Some IOD allows multiple frames in the same file (most notably US Multi-frame Image), but this is generally not the case. In short, a Series
must references 1 to n DICOM files.
When we are working with empty data, a correct initialization must be found (like asking the user the IOD, he wants to work with...). We can imagine an initialization service would look like:
-
Image
inheritance forImageSeries
This will make the ImageSeries::getImage()
obsolete. Same for the new service SGetImage
, which need to be removed from all config. That means, of course that all AppConfig / Services that extracted the Image
from the ImageSeries
, need to be modified.
-
SeriesDB
replaced by a simplesight::data::vector
or any collection
It will allow us to store anything inside, not only Series. Reference to the object itself will use 'DataSet' as name.
Serialization
Most notable change will be remaining Series
: ImageSeries
and ModelSeries
. Since we use a DICOM context to manage their proprieties, we will also serialize them as DICOM files. No more VTK. For the session archive, all series will be serialized as standalone DICOM series, stored in the session archive. All properties, if available as a DICOM properties, must be serialized as DICOM. Other properties, which exist only in Sight (like fields
), should be serialized as usual, in json.
ModelSeries
, ImageSeries
Reader / Writer
This will need to be reworked to match the new class diagram. Again, the goal is to use DICOM to read them and DICOM to write them. It must even be possible to use same code as serialization (or at least to share lots of code).
For Reader, it should be advisable to show a "Series" selection dialog, after a quick scan of a folder (or when selecting a DICOMDIR, to choose the right series to load, without fully loading all DICOM files. It should be, of course only shown when there is more than one series available, and maybe configurable (select automatically when a IOD is found, when a tag is present, etc..)
New class diagram
We would like to go to:
classDiagram
SeriesSet "1" o-- "n" ISeries
ActivitySet "1" o-- "n" Activity
CameraSet "1" o-- "n" Camera
ISeries "1" o-- "n" DicomInstance
DicomInstance *-- DICOM
ISeries <|-- ModelSeries
ModelSeries "1" o-- "n" Reconstruction
Reconstruction o-- Mesh
Reconstruction o-- Material
Reconstruction o-- Image
ISeries <|-- ImageSeries
Image <|-- ImageSeries
class DicomInstance{
get_value(tag) value
}
class ISeries{
get_dicom_instances() vector
get_patient_name()
get_study_date()
get_...()
}
Almost everything inherits from data::Object, but, for clarity, we only display it for
ActivitySet
,Camera
and allSeriesSet
Technical specifications
-
ISeries
ISeries
will be a strict interface class, no inheritance, allowing ImageSeries
inheritance from Image
. It will define a DicomInstance
vector getter (to allow complex tag search) and several often used DICOM properties getter. We can imagine, as an example, to get the patient name, a developer will call ISeries::get_patient_name()
, which will call the underlying DicomInstance::get_value(0x0010,0x0010)
.
ISeries
must allow to change the underlying IOD of the DicomInstance
, so a Reader, not necessarily related to DICOM, that reads an Image or a Mesh, could set to the right IOD. Another way to do this is to implement a service::IXMLParser
so the configuration could be done in XML like:
<object uid="..." type="data::ImageSeries" >
<iod>CT Image</iod>
</object>
-
ImageSeries
andModelSeries
Serialization code of Image
, ImageSeries
, ModelSeries
, Reconstruction
, Meash
, Material
need to be updated. they will use referenced DicomInstances
to create back a valid DICOM dataset inside the Session
archive, using GDCM
and io::session
classes. The code, if possible, should reuse the code from the DICOM Readers/Writers
For the reader: to let the user chose the right Series to display, we will look at DicomExplorer and possibly reuse the code.
A new templated collection will be implemented (with the associated helpers) using the std container (std::vector, std::set, std::map) as template parameter