SlicerRt / SlicerRT

Open-source toolkit for radiation therapy research, an extension of 3D Slicer. Features include DICOM-RT import/export, dose volume histogram, dose accumulation, external beam planning (TPS), structure comparison and morphology, isodose line/surface generation, etc.
https://slicerrt.org
128 stars 60 forks source link

Allow loading user defined treatment machines in Room's Eye View #218

Open cpinter opened 1 year ago

cpinter commented 1 year ago

Many times researchers would like to calculate collisions for their machines but they cannot share the treatment machine models. At the same time they would be willing to define the parts if there was a relatively convenient way to do so.

This could be achieved by defining a JSON schema that describes the parts. The currently available treatment machines should be converted to this new method as well. A separate repository could be set up for the publicly available machine definitions.

A project with @ferdymercury

MichaelColonel commented 1 year ago

You can count on me, if you need help with models transform programming and other stuff!

cpinter commented 1 year ago

Thank you :)

cpinter commented 1 year ago

Working on a sample JSON file to describe the existing Varian machine. This is what I have now. Please comment on anything that could need to be added or changed:

{
  "TreatmentMachineName": "Varian TrueBeam STx",
  "$schema": "https://raw.githubusercontent.com/???/???-schema.json#",
  "Part": [
    {
      "Type": "Collimator",
      "File": "Collimator.stl",
      "FileToPartTransformMatrix": [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ]
    },
    {
      "Type": "Gantry",
      "File": "Gantry.stl",
      "FileToPartTransformMatrix": [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ]
    },
    {
      "Type": "ImagingPanelLeft",
      "File": "ImagingPanelLeft.stl",
      "FileToPartTransformMatrix": [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ]
    },
    {
      "Type": "ImagingPanelRight",
      "File": "ImagingPanelRight.stl",
      "FileToPartTransformMatrix": [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ]
    },
    {
      "Type": "LinacBody",
      "File": "LinacBody.stl",
      "FileToPartTransformMatrix": [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ]
    },
    {
      "Type": "PatientSupport",
      "File": "PatientSupport.stl",
      "FileToPartTransformMatrix": [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ]
    },
    {
      "Type": "TableTop",
      "File": "TableTop.stl",
      "FileToPartTransformMatrix": [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ]
    },
  ]
}

One thing that made me thinking is that the movement of the imaging panels is more complex than a simple translation or rotation (first rotates in then starts to translate too). After checking, actually, it seems that the movement of the panels is broken. It seems out of scope for this project to fix that I guess (I may still do it), but certainly worth mentioning that there are some magic numbers in the code that moves the imaging panels, and also worth considering if we want to load that parameter from JSON as well:

https://github.com/SlicerRt/SlicerRT/blob/19572b5d6f598a0d94c75141afe0ea203d4be9e4/RoomsEyeView/Logic/vtkSlicerRoomsEyeViewModuleLogic.cxx#L1030-L1037

ferdymercury commented 1 year ago

Looks very good, thanks for your work!

Some comments below:

cpinter commented 1 year ago

Thanks for the comments! Good that you have more ideas now seeing this file - I only added those that we defined at our meeting. A few things:

Here's the updated sample JSON file. I changed some element names to be more unambiguous (title vs name, active vs visible vs enabled):

{
  "TreatmentMachineName": "Varian TrueBeam STx",
  "$schema": "https://raw.githubusercontent.com/???/???-schema.json#",
  "Part": [
    {
      "Type": "Collimator",
      "Name": "Collimator",
      "FilePath": "Collimator.stl",
      "FileToPartTransformMatrix": [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ],
      "Color": [255, 255, 255],
      "Enabled": true
    },
    {
      "Type": "Gantry",
      "Name": "Gantry",
      "FilePath": "Gantry.stl",
      "FileToPartTransformMatrix": [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ],
      "Color": [255, 255, 255],
      "Enabled": true
    },
    {
      "Type": "ImagingPanelLeft",
      "Name": "Left imaging panel",
      "FilePath": "ImagingPanelLeft.stl",
      "FileToPartTransformMatrix": [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ],
      "Color": [255, 255, 255],
      "Enabled": true
    },
    {
      "Type": "ImagingPanelRight",
      "Name": "Right imaging panel",
      "FilePath": "ImagingPanelRight.stl",
      "FileToPartTransformMatrix": [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ],
      "Color": [255, 255, 255],
      "Enabled": true
    },
    {
      "Type": "LinacBody",
      "Name": "Linac body",
      "FilePath": "LinacBody.stl",
      "FileToPartTransformMatrix": [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ],
      "Color": [255, 255, 255],
      "Enabled": true
    },
    {
      "Type": "PatientSupport",
      "Name": "Patient support",
      "FilePath": "PatientSupport.stl",
      "FileToPartTransformMatrix": [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ],
      "Color": [255, 255, 255],
      "Enabled": true
    },
    {
      "Type": "TableTop",
      "Name": "Table top",
      "FilePath": "TableTop.stl",
      "FileToPartTransformMatrix": [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ],
      "Color": [255, 255, 255],
      "Enabled": true
    }
  ]
}
ferdymercury commented 1 year ago
  • There is no good way to have enum in JSON (at least I did an extensive search yesterday and this was the conclusion), so it needs to be something we check in C++ whether the string used for Type is among the accepted ones or not.

Sounds good.

  • I'd probably add the instructions in the README.md of the repository where we have the publically available machines

Perfect.

  • Indicating author is a good idea, although I wouldn't add URLs pointing to its own repository in the JSON

Agreed.

  • Folder: I'd rather just have it in the File. Maybe rename it to FilePath?

Good idea, too.

  • Degrees of freedom: Yes this could be very useful. I'd suggest we add this after the first simple approach works

Great.

  • Multiple machines in one JSON: I don't see the rationale. It puts a development burden on us, when for the user it would be a minor technicality and maybe 10s of extra work to save it in two files instead of one. Also cleaner to have one JSON for each machine in every sense I can think of

You're right.

  • The file size limit would make more sense in the C++ logic I think, because I don't see a reason for having it different for each machine. But this also seems like a feature we can add later

Perfect.

Thanks so much :)

cpinter commented 1 year ago

The basic loading of treatment machine from JSON descriptor file works now for the default Varian TrueBeam machine. Next steps are implementing color, enabled state, and the transform.

One question I have is what to do with the user interface. So far there has been a combobox with the two default machines. For now, as the easiest solution, I added a third option "From file...", clicking on which a file selector pops up and we can load the machine using that: image

@ferdymercury @gregsharp what do you think the ideal GUI would look like for machine selection?

cpinter commented 1 year ago

@ferdymercury Also an idea as I was thinking about the Enabled state. I can imagine you want to show the model but not include it in the collision detection (also for speedup reasons). Should we include this somehow?

ferdymercury commented 1 year ago

@ferdymercury Also an idea as I was thinking about the Enabled state. I can imagine you want to show the model but not include it in the collision detection (also for speedup reasons).

Sounds good. Maybe it would be worth defining State as a string with three possible options:

ferdymercury commented 1 year ago

@ferdymercury @gregsharp what do you think the ideal GUI would look like for machine selection?

Thanks, it's looking great. Maybe it would be interesting to let the user define a MachineDatabasePath in the UserPreferences of Slicer, or via an env variable, that would prepopulate this combobox with all machines /json files found in this folder.

cpinter commented 1 year ago

Thanks a lot for the answers, I agree with both.

cpinter commented 1 year ago

I implemented all the features discussed related to loading a treatment machine model already included in the JSON files (part name, file path, file to RAS matrix, color, state), and tested with the two default machines. I integrated (https://github.com/SlicerRt/SlicerRT/pull/226) the commits into the upstream to make it easier to try what is ready so far.

This means that tomorrow afternoon (when the build machines in the US are done building and testing all extensions too) you can download the latest Slicer preview, install SlicerRT, and try the latest changes.

A few outstanding things:

I think what we already discussed and is missing are:

Anything else?

ferdymercury commented 1 year ago

I implemented all the features discussed related to loading a treatment machine model already included in the JSON files (part name, file path, file to RAS matrix, color, state), and tested with the two default machines. I integrated (#226) the commits into the upstream to make it easier to try what is ready so far.

This means that tomorrow afternoon (when the build machines in the US are done building and testing all extensions too) you can download the latest Slicer preview, install SlicerRT, and try the latest changes.

Awesome, that was quick. I'll try in a few days then.

A few outstanding things:

* There was a custom scaling applied to the table top displacement for the treatment machines, see https://github.com/SlicerRt/SlicerRT/blob/4c0ceb8900f6c12ab9b66e2ae6e7d1077c99c409/RoomsEyeView/Logic/vtkSlicerRoomsEyeViewModuleLogic.cxx#L1212-L1221
   What should we do with these? Add a special scaling member to the table top part? Or is this displacement scaling factor something that could apply to any part?

Oh, I see, it's for flexible parts that change size. I guess it should be used only for the part of the couch trunk that extends, not for the whole table top. That being said, maybe it's easier from the code-side to implement it for every part in the JSON and not making distinctions in the code depending on type. EDIT: Or maybe better, automatically calculate how much it has to extend. This should be doable by getting it's height. It should be glued from the bottom and only extend or compress vertically the upper part by a dynamic factor, such that the height changes by the same number of centimeters than the table-top is moved vertically.

* The collision detection already did not work for the additional machine parts (e.g. ApplicatorHolder, ElectronApplicator), but now I disabled even loading them, because those should work via their own JSON file as well. So I hid the entire section to avoid confusion. Do we need these custom parts fully working again?

I'd say the applicators should be part of the main JSON file of the machine, not a separate one (unless there is a universal applicator that works on many machines). The "Type" tag can indicate that is an Accessory or sth like that.

Anything else?

ferdymercury commented 1 year ago

This means that tomorrow afternoon (when the build machines in the US are done building and testing all extensions too) you can download the latest Slicer preview, install SlicerRT, and try the latest changes.

Works great with default machines! thanks.

One idea: Maybe (if not too complicated) a button "Remove treatment machine" could be helpful? Or if one loads a second treatment machine, that a prompt appears asking if the previous one should be deleted.

I'll try later with external STL files.

cpinter commented 1 year ago

Thanks for the feedback! I usually just clear the scene. Also it can be deleted from the Data module with two clicks (it is organized into a folder). In normal Slicer modules we usually don't complicate the already complex user interfaces with convenience functions that can be reached so easily. But it you think it's important we can add a button...

ferdymercury commented 1 year ago

Sounds reasonable, no need for an extra button, I agree. But maybe the previous machine can be automatically hidden from the scene if a new one is loaded?

cpinter commented 1 year ago

But maybe the previous machine can be automatically hidden from the scene if a new one is loaded?

Yes this makes complete sense.

ferdymercury commented 1 year ago

Some feedback from my side:

cpinter commented 1 year ago

Are the default machines in the right coordinate system? Shouldn't the couch height be AP instead of IS ?

I believe you're right. I just checked, and the coordinate systems were like this from the start. If we change the coordinate systems to match the patient, then I think we should do it in a way, so that we don't do a half job, that the patient is fixed within the coordinate system, i.e. when moving/rotating the table top, the table top is kept fixed, and the rest of the linac moves instead.

I get these warnings in the console log, but probably they are not important

This is a basic Slicer warning saying that the STL did not specify coordinate system and alerts the user that LPS is assumed. I think this warning was useful because there was a switch from an RAS default to an LPS default not so long ago. You can safely ignore it. If you think this is distracting we can propose removing this warning.

The UI feedback could be improved a bit

Yes, totally agreed. I have seen this too, especially with the linac that had more triangles (the Varian one is a low-detail one but the Siemens linac has many times more triangles and collision detection takes much longer for that). Dynamically assessing whether collision calculation is needed or not may not be so simple. Let me do some investigation.

tell the user what slicer is doing

OK this could be done fairly easily I think.

If for example, there is no imaging panels or collimator loaded, the respective sliders should be disabled in the UI

Good point. Would you prefer hiding or disabling? I think it makes sense also to hide them if not available for the currently loaded linac.

For loading a part of a custom couch that moves L-R, S-A but not I-S, we have to wait until the degrees of freedom functionality is implemented, right?

Yes I think this is what would be needed if we don't want to handle special cases and do some quick&dirty before that.

Concerning scaling factor...

Let's discuss.

As now the basics are done, and what we have at the moment works exactly how it worked before, I propose that we integrate the topic branch into master, and create tickets for the individual issues. That way nothing will fall through the cracks. We could create a special milestone to be able to handle the related issues more easily. If you agree I do the integration and can create the tickets.

ferdymercury commented 1 year ago

that the patient is fixed within the coordinate system, i.e. when moving/rotating the table top, the table top is kept fixed, and the rest of the linac moves instead.

Yep. How i did this in Raystation was to keep indeed the patient fixed. The room was placed around the isocenter defined for the current beam. When changing beam isocenter or angle, the room was moved accordingly. I still gave freedom to move table top a bit as you can put the patient more towards an edge of the table...

If you think this is distracting we can propose removing this warning.

No need. Is fine like it is now.

Good point. Would you prefer hiding or disabling? I think it makes sense also to hide them if not available for the currently loaded linac.

No preference from my side.

As now the basics are done, and what we have at the moment works exactly how it worked before, I propose that we integrate the topic branch into master, and create tickets for the individual issues. That way nothing will fall through the cracks. We could create a special milestone to be able to handle the related issues more easily. If you agree I do the integration and can create the tickets.

Great, agreed. Thanks for your work.

cpinter commented 1 year ago

I have had some time to resume working on this today, sorry for the delay.

Here's the list of tasks based on the conversation so that we have them in one spot (feel free to edit):

I started working on changing the transform logic so that the table top is fixed whichever transform the user is changing (except that the lateral movement of the table top will be allowed as discussed above). It is not so easy, but luckily there is a new implementation in a project I'm involved in, so trying to get inspiration from there to make the transition as lightweight and elegant as possible.

ferdymercury commented 1 year ago

Looks great, thanks so much.

Maybe add (cannot edit your post):

cpinter commented 1 year ago

I made the change that applies a transform to the whole machine so that the table top is kept fixed.

https://github.com/SlicerRt/SlicerRT/assets/1325980/04260deb-c216-4987-99df-76990d35bf29

In this implementation I use the so far unused FixedReferenceToRAS transform that is at the highest level of the transform tree to achieve this.

We still have the problem that the directions are wrong (vertical direction in treatment machine is not PA but IS). I was thinking maybe we could use the same FixedReferenceToRAS transform defined in the JSON files to do this rotation. Then this new code should take that transform and concatenate it with the one that keeps the table top fixed. I think this would fit into the IEC standard, as there is no RAS defined there, and the FixedReference has the Z as we have it currently (see here), so both transforms should go into FixedReferenceToRAS. What do you think?

ferdymercury commented 1 year ago

Hi Csaba. The video looks wonderful.

Concerning the positions PA IS:

What I was doing in RayStation was to define a first rotation that was patient-dependent. If patient CT was FFS or HFS or FFP or HFP i had a separate initial rotation matrix applied there. I guess the same info can be better taken from ImageOrientation DICOM tag matrix.

Wouldn't this be enough, so that we do not need to rely on the information of the JSON file? Or am I misunderstanding sth?

There is a corner case when image was done with HF but treatment delivered with FF, but we cannot cover everything heheh.

cpinter commented 1 year ago

I think you're right, the main thing is to have the orientations right when the beam is specified (and the beam belongs to a plan that belongs to a patient CT), so we can do it like that. Thanks!

cpinter commented 1 year ago

I added your grant acknowledgment to the Room's Eye View source files (see https://github.com/SlicerRt/SlicerRT/commit/c7f64521895a077430f54a853410950aec15db30) and to the module acknowledgment section (see https://github.com/SlicerRt/SlicerRT/commit/52a39001e4439dc841ea34e6f5bdfa7da794625a). Let me know if you'd like to add it anywhere else. We'll add it to the treatment machines repository too, but we don't have that yet.

ferdymercury commented 1 year ago

Perfect! thanks.

cpinter commented 1 year ago

A little progress:

I'll continue with the tasks one by one in the meantime. Thanks!

ferdymercury commented 1 year ago

Sounds good, agreed, thanks!

cpinter commented 1 year ago

Now if a treatment machine is already loaded when the user wants to load a new one, this dialog pops up: image

Or if the user wants to load a machine that is already loaded, this is shown and loading is cancelled: image

ferdymercury commented 1 year ago

Lovely :)

cpinter commented 1 year ago

I started working on this task "Place room around the isocenter defined for the current beam. Move room when isocenter changes" because it is related to an ugly bug related to the FixedReference system jumping from patient-aligned and RAS-aligned depending on what the user changes. Since we made the changes that allow the table top be the fixed part, we have two parallel worlds in terms of the FixedReference coordinate system:

  1. Beam-driven FixedReferenceToRAS: It considers the isocenter and does the transforms to have the A and S directions correct, see https://github.com/SlicerRt/SlicerRT/blob/3ae787b5702caf764336f2d4c52b160a046cf5ff/Beams/Logic/vtkSlicerIECTransformLogic.cxx#L367
  2. Tabletop-driven FixedReferenceToRAS: It considers only the table top, but not the isocenter, or other parameters of the beam, see https://github.com/SlicerRt/SlicerRT/blob/3ae787b5702caf764336f2d4c52b160a046cf5ff/RoomsEyeView/Logic/vtkSlicerRoomsEyeViewModuleLogic.cxx#L980-L981

This means that if the user changes the gantry angle for example, we jump to "world 1", in which the patient is seen correctly oriented with respect to the table and the gantry, but if the user changes a patient support parameter, we jump to "world 2".

I propose that we try to fix this simply by considering the isocenter in the tabletop-driven scenario, and see how the rest of the application behaves (changing beam parameters, changing gantry angle in REV module, etc.). One thing that I think we'll have to do is that if the user changes table top parameters we need to deselect the beam, but not sure about that.

@ferdymercury please let me know if this is OK with you. Thanks!

ferdymercury commented 1 year ago

Oh, I see, thanks for the analysis.

Yep, sounds good. Everything tabletop-driven. Which if I understand correctly, should be similar to patient-beam-isocenter-driven, because the tabletop is glued to the posterior of the patient.

In principle, if a beam isocenter is selected, we should allow to change the couch rotation and gantry angle without having to unselect that beam. Tabletop and patient remain glued at the same position, only walls with gantry rotate around.

Or am I misunderstanding something?

cpinter commented 1 year ago

Thanks for the quick response!

Yes, the solution is basically to merge the two and be both beam and tabletop driven.

In principle, if a beam isocenter is selected, we should allow to change the couch rotation and gantry angle without having to unselect that beam. Tabletop and patient remain glued at the same position, only walls with gantry rotate around. Or am I misunderstanding something?

You're right, I will keep thinking about this part. It seemed that things fall apart a bit when starting to change those, hence I suggested that I do the first fix and see how the rest behaves.

cpinter commented 11 months ago

I implemented a very simple way to prevent lengthy collision detections: if the product of the number of triangles for the two models taking part in collision detection is higher than a threshold, that collision detection is disabled, and the user is warned. This threshold is now 1e10, so for example two models of 100,000 triangles.

This is how the warning looks: image

@ferdymercury Please let me know if this works for now.

ferdymercury commented 11 months ago

Hi Csaba, thanks. I am not sure if this addresses the issue I was seeing. The problem was (I believe) not the algorithm taking too long, but rather that while 'sliding', it was queuing many times the collision detetion for many intermediate angles, is that possible? I think it should 'abort' / throw out of the queue previous calculations if the slider moves faster than the solution of the previous angle.

Concerning the threshold, it sounds good, but maybe there should be an additional button saying 'calculate anyway even if it takes a long time' ?

cpinter commented 11 months ago

I spent the whole day today trying to see the reason for this phenomenon and how to fix it

ferdymercury commented 11 months ago

Ohh, ok, I see. In my Python implementation, the collision check was running on a separate thread that could be aborted, see https://github.com/mghro/rad-collision/blob/main/RayStation/collision_detection.py#L1013 Something along this line would be maybe doable via std::thread ?

Also, we might explore faster (less accurate) collision detection algorithms such as Dice Similarity Coefficient with a threshold.

cpinter commented 11 months ago

This should be a discussion towards a future project. Threading is quite difficult in Slicer, but doable. Calculating DSC is an option, but for that we'd need to ensure that the loaded models are fully closed, otherwise voxelization will fail and collision detection will not work, and I think this is not a viable requirement.

This is the new warning popup I made for disabling collision detection for the too-high-resolution parts: image Would this work for now?

In the future we could also offer automatically creating lower resolution models using Uniform remesh for the purpose of collision detection.

ferdymercury commented 11 months ago

Sounds good, let's keep it like now and write down the improvement ideas for the future :)

cpinter commented 11 months ago

One more thing I did was to indicate in the message if the user chose to disable: image