qurit / rt-utils

A minimal Python library to facilitate the creation and manipulation of DICOM RTStructs.
MIT License
181 stars 56 forks source link

Gaussian smoothing #90

Open mathiser opened 1 year ago

mathiser commented 1 year ago

Dear everyone, A thing that has bugged me when using this (otherwise wonderful) package, is the jagged, unsmooth and a bit too small contours which are returned, when converting binary masks into rtss.

In this PR I propose implementation of how smoothing can be added as an option. Essentially, it is iterations of applying the following filters.

  1. Upscaling image in X and Y direction with a factor
  2. Gaussian smoothing
  3. Binary threshold

How to run it All you have to do is to run the usual add_roi() with another parameter:

rtstruct.add_roi(
    mask=arr,
    name=name,
    color=[0,248,255],
   apply_smoothing="2d")

Furthermore, you can tweak the smoothing by parsing a dict with parameters. The setup is a little curious, and there is room for improvement, but essentially it looks like this:

 smoothing_parameters = {
         "crop_margins": [20, 20, 1],  # To improve memory efficiency - make sure to keep is well away from the gaussian kernel
      "scaling_iterations": integer,  # Number of times to run everything below
      "np_kron": {"scaling_factor": scale},
      "filter_iterations": integer,  # The number of times gaussian and threshold is run at each scaling level
      "ndimage_gaussian_filter": {"sigma": sigma, "radius": radius},  # Are passed ad kwargs, so any parameter from [the filter](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.gaussian_filter.html#scipy.ndimage.gaussian_filter) can be set. 
      "threshold": {"threshold": threshold},  # Threshold between 0 and 1. 
 }

The above dict can be used by doing:

rtstruct.add_roi(
    mask=arr,
    name=name,
    color=[0,248,255],
    apply_smoothing="2d",
    smoothing_parameters=smoothing_parameters
)

I have added the possibility to choose running the filters in either 3d or 2d. I have done most parameter tuning in 2d, and have come up with two sets, which works well, so if

Furthermore, this is a nice entry point to add other smoothing algorithms like:

def awesome_smoothing(mask: np.ndarray, apply_smoothing: str, smoothing_parameters: Dict):
    return ItsAKindOfMagicalArray

rtstruct.add_roi(
    mask=arr,
    name=name,
    color=[0,248,255],
    apply_smoothing="2d",
    smoothing_parameters=smoothing_parameters,
    smoothing_function=awesome_smoothing
)

Other things in the code In order to be able to add upscaled masks, there is a check in rtstruct.addroi():

        ## If upscaled coords are given, they should be adjusted accordingly
        rows = self.series_data[0][0x00280010].value
        scaling_factor = int(mask.shape[0] / rows)

This determines the scale in X, Y which will then be taken into account in image_helper.find_mask_contours():

    for i, contour in enumerate(contours):
        contours[i] = [[(contour[i][0][0] / scaling_factor), (contour[i][0][1] / scaling_factor)] for i in
                       range(0, len(contour))]

Show case Because I work with clinical data, I do not show the underlying images. It does not matter for the impression, I guess. Below is a image where red is the initial dicom rtss and in green is the same converted to binary nifti masks in CT-grid. GroundTruth in dicom and nifti

Converting the green nifti directly back to dicom rtss with the current main branch gives this, where red is ground truth and blue is the converted rtss: Vanilla vs GT

With default parameters of this PR the same conversion yields the following, where red is ground truth and blue is conversion: Dicom GT and smooth consversion

Purpose of the PR I do not know what the maintainers thinks about implementing this into the standard package, but to me and my work, well-working smoothing is a game-changer. I hope we can discuss if other algorithms should be implemented too or if the interface should look different.

Looking forward to hear your thoughts, Mathis

mathiser commented 1 year ago

Really?! Not a single comment from maintainers? Perhaps it is time to expand the group of maintainers to make the repo a bit more active....

awtkns commented 1 year ago

Hi @mathiser sorry about this. Thanks for such a high quality write up on this.

Unfortunately @asim-shrestha and I have been tied up as of late working on our own startup (we actually left the qurit team close to three years ago). As such this project has seemed to fall through the cracks, which is a shame.

Tagging the qurit team so this can get reviewed hopefully @carluri @ivankzn @qurit-frizi :)

awtkns commented 1 year ago

I do agree that this project needs an expanded set of maintainers so that great features like this can get in

mathiser commented 1 year ago

Thanks for your reply, @awtkns. I did suspect that the situation was something like this, which I totally understand. The thing is that rt_utils is (AFAIK) still the only tool out there to perform this task in a sober and simple manner. Lots of out work rely on the package There is so much potential in this and a variety of features to be added.

BTW,this PR was also opened in the hope that people could share some insights in the post-processing contours. It is quite likely that the style of coding is not appropriate in this repo - and I haven't added any tests either....

mathiser commented 1 year ago

Lastly, I have done some more parameter tweaking and nursing on the code. I won't bother updating the PR if the interest is limited...

plesqui commented 1 year ago

Hi @mathiser ,

I have just joined the team and will be helping with maintenance and developing new features. It will take me a couple of weeks to get up to speed but I hope to start reviewing and supporting this project as much as possible.

Thanks again for your contributions and for your patience. We really appreciate it! I'll be back into this PR soon.