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

Add transforms for Digital Pathology #100

Merged
merged 36 commits into from
Sep 29, 2021
Merged

Add transforms for Digital Pathology #100

merged 36 commits into from
Sep 29, 2021

Conversation

shekhardw
Copy link
Contributor

@shekhardw shekhardw commented Sep 12, 2021

Implement transforms for color jitter, image scale intensity, image flip and rotate included for batched RGB images.

This will be used exclusively for digital pathology training pipelines.

closes #105

@shekhardw shekhardw requested a review from a team as a code owner September 12, 2021 18:18
@shekhardw shekhardw added feature request New feature or request non-breaking Introduces a non-breaking change labels Sep 12, 2021
@gigony
Copy link
Contributor

gigony commented Sep 13, 2021

______________________________________________________________________________________________________________________________________ ERROR collecting src/cucim/core/operations/color/tests/test_color_jitter.py _______________________________________________________________________________________________________________________________________
src/cucim/core/operations/color/tests/test_color_jitter.py:6: in <module>
    img = Image.open('1.png')
/home/gbae/.pyenv/versions/3.8.10/envs/cucim-3.8/lib/python3.8/site-packages/PIL/Image.py:2912: in open
    fp = builtins.open(filename, "rb")
E   FileNotFoundError: [Errno 2] No such file or directory: '1.png'
_______________________________________________________________________________________________________________________________________ ERROR collecting src/cucim/core/operations/intensity/tests/test_scaling.py _______________________________________________________________________________________________________________________________________
src/cucim/core/operations/intensity/tests/test_scaling.py:7: in <module>
    img = Image.open('1.png')
/home/gbae/.pyenv/versions/3.8.10/envs/cucim-3.8/lib/python3.8/site-packages/PIL/Image.py:2912: in open
    fp = builtins.open(filename, "rb")
E   FileNotFoundError: [Errno 2] No such file or directory: '1.png'
_________________________________________________________________________________________________________________________________________ ERROR collecting src/cucim/core/operations/spatial/tests/test_flip.py __________________________________________________________________________________________________________________________________________
src/cucim/core/operations/spatial/tests/test_flip.py:6: in <module>
    img = Image.open('1.png')
/home/gbae/.pyenv/versions/3.8.10/envs/cucim-3.8/lib/python3.8/site-packages/PIL/Image.py:2912: in open
    fp = builtins.open(filename, "rb")
E   FileNotFoundError: [Errno 2] No such file or directory: '1.png'
________________________________________________________________________________________________________________________________________ ERROR collecting src/cucim/core/operations/spatial/tests/test_rotate.py _________________________________________________________________________________________________________________________________________
src/cucim/core/operations/spatial/tests/test_rotate.py:6: in <module>
    img = Image.open('1.png')
/home/gbae/.pyenv/versions/3.8.10/envs/cucim-3.8/lib/python3.8/site-packages/PIL/Image.py:2912: in open
    fp = builtins.open(filename, "rb")
E   FileNotFoundError: [Errno 2] No such file or directory: '1.png'
----------------------------------------------------------------------------------------------------------------------------------------------- generated xml file: /home/gbae/repo/cucim/junit-cucim.xml

@shekhardw Please specify the absolute path to load the test data.

(pytest is run in root of the repository so that relative paths are not available).
You may want to use __file__ to get absolute path.

How to reproduce

pushed python/cucim
pip install -e .
popd

./run test python unit skimage # or `./run test_python`

Copy link
Contributor

@gigony gigony left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check/auto-format using black/isort.

pip install black
python -m black python/cucim/src/cucim/core/ --check  # check
python -m black python/cucim/src/cucim/core/ # autofix

pip install isort
python -m isort python/cucim/src/cucim/core/ --check  # check
python -m isort python/cucim/src/cucim/core/ # autofix

Copy link
Contributor

@gigony gigony left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.

Copy link
Contributor

@grlee77 grlee77 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @shekhardw,
In general the kernels look nice and performance is great, but input validations, docstrings and test cases are very minimal.

For example, it is not always clear from the docstrings:
1.) which dtypes are supported
2.) whether RGB, RGBA and/or arbitrary number of "channels" are supported
3.) whether an additional "batch" dimension is supported.

I have made a number of suggestions, but it could probably use another pass after these are addressed.

I did not review the C++ code for all individual CUDA kernels, but the ones I looked at seemed good to me.

from typing import Any, List, Optional, Tuple

import numpy as np
import scipy.ndimage as ndimage
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick, but to be consistent with the cucim.skimage and upstream scikit-image, I would use

import scipy.ndimage as ndi

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes made

python/cucim/src/cucim/core/operations/color/jitter.py Outdated Show resolved Hide resolved
python/cucim/src/cucim/core/operations/color/jitter.py Outdated Show resolved Hide resolved
python/cucim/src/cucim/core/operations/color/jitter.py Outdated Show resolved Hide resolved
diff_im = np_output - arr
diff_total_value = np.abs(np.sum(diff_im))

assert diff_total_value > 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This very minimal test coverage that only verifies that the kernels run without crashing and that some change to the image has occurred.

Why not place this in a named test function, though, so pytest would report it as a test case?

You could then add additional tests in separate functions. Additional simple tests would be:

  • a test case with all factors set to 0, verifying that the image is unmodified in that case.
  • a test with a batch image, e.g. using cupy_arr_batch = cupy.stack((cupy_arr, ) * 8, axis=0). In this case since all elements in the batch are identical we could also verify that for the output.
  • a test verifying that providing numpy.ndarray input results in a numpy.ndarray output
  • same output with C or Fortran-ordered inputs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test cases included for all factors 0, batch image, cupy input, numpy input. Color jitter picks a random sequence to apply 4 operations. Test functions included in all test files.

R = np.random.RandomState()

if R.rand() < prob:
return image_rotate_90(img, spatial_axis)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return image_rotate_90(img, spatial_axis)
return image_flip(img, spatial_axis)

wrong function!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected.


import cucim.core.operations.spatial as spt

img = Image.open(os.path.join(os.path.abspath(os.path.dirname(__file__)), "1.png"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the source of this dice image? (i.e. is it public domain?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is any rights issue it should be easy enough to just use one of the images bundled with scikit-image.

e.g. for an RGB image:
skimage.data.astronaut() (will need to transpose dimensions to get the color axis from last to first)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test image updated to use astronaut image from scikit-image. Additional test cases included.

Copy link
Contributor

@gigony gigony left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @grlee77 for the review! @shekhardw would address your comments.

# See the License for the specific language governing permissions and
# limitations under the License.

from cucim.core.operations.color import color_jitter as color_jitter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @grlee77 . Let me chime in this question.

We didn't decide what/how to expose APIs from cucim.core.operations and it looks like MONAI's transforms require APIs in a specific form and prone to change.
For this reason, Shekhar and I decided to use cucim.core.operations.expose.transform for exposing APIs needed by MONAI transforms, without messing up cucim.core.operations.xxx namespaces.

We need to discuss how to use cucim.core.operations namespace in detail, as a group, in the next cuCIM bi-weekly meeting.

@shekhardw
Copy link
Contributor Author

@grlee77 @gigony Thanks for your valuable feedback and code review. I have pushed updated test cases. Please let me know if there are any additional suggestions/feedback.

Copy link
Contributor

@gigony gigony left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good to me.
please format the updated code so that it can pass CI/CD checks.

@grlee77 Please help review and approval the changes.

return arr_o

def test_rand_flip_numpy_input():

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: blank line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corrected.

assert np.allclose(output,flip_arr)

def test_rand_flip_zero_prob():

Copy link
Contributor

@gigony gigony Sep 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: blank line

please check other parts of test methods that start with a blank line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corrected and all test functions checked.

@gigony gigony changed the title Initial Transforms for digital pathology [run style] Initial Transforms for digital pathology Sep 17, 2021
@gigony gigony changed the title [run style] Initial Transforms for digital pathology Initial Transforms for digital pathology Sep 17, 2021
@gigony
Copy link
Contributor

gigony commented Sep 17, 2021

rerun tests

Copy link
Contributor

@grlee77 grlee77 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the comments @shekhardw. The tests are looking a lot better now and can always be expanded further as needed if issues crop up.

Thanks for also adding the dimension info to the docstrings.

Out of curiosity, what is the source of the 1.png image used in the color tests? (it appears as somewhat random colors). Is there a particular reason for testing with this image?

# limitations under the License.

from cucim.core.operations.color import color_jitter as color_jitter
from cucim.core.operations.intensity import rand_zoom as rand_zoom
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from cucim.core.operations.intensity import rand_zoom as rand_zoom
from cucim.core.operations.intensity import rand_zoom

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@grlee77 1.png was created out of random array. I will change it with astronaut image (to be consistent).

@gigony gigony changed the title Initial Transforms for digital pathology Add Transforms for digital pathology Sep 21, 2021
@gigony gigony changed the title Add Transforms for digital pathology Add transforms for Digital Pathology Sep 21, 2021
Copy link
Contributor

@grlee77 grlee77 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also change import skimage to import skimage.data in the tests as suggested in the recently added comments. I was surprised the former works and I think it may not work across all older scikit-image versions!

It happens to work because there is a from .data import data_dir in skimage.__init__ which apparently loads data into the skimage namespace (see here). It is safer not to rely on this incidental import of data and just import it explicitly.

@shekhardw
Copy link
Contributor Author

Thanks @grlee77 test cases updated.

Copy link
Member

@jakirkham jakirkham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Shekhar! 😄

Had a couple questions/suggestion below. For simplicity and clearer discussion focused only on one case of the pattern for discussion, but the same could be applied in other places where that pattern is used

Comment on lines +111 to +117
def get_params(brightness: Optional[List[float]],
contrast: Optional[List[float]],
saturation: Optional[List[float]],
hue: Optional[List[float]]
) -> Tuple[np.ndarray, Optional[float],
Optional[float], Optional[float],
Optional[float]]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary for these to be closures? Or could we define module level functions for these tasks?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shekhardw, any thoughts on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose to fix this in a separate MR.

Comment on lines +298 to +300
_logger.error("[cucim] " + str(e), exc_info=True)
_logger.info("Error executing color jitter on GPU")
raise
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to handle logging within cuCIM? Typically this is the sort of thing that would be configured in a user application (unless for some reason the exception would never be seen)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shekhardw, any thoughts on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose to fix this in a separate MR.

Comment on lines +16 to +17
extern "C" {
__global__ void brightnessjitter_kernel(unsigned char *input_rgb, \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to put these in C file? Might make this is easier to read/lint/test/etc. and also use directly if desired. From the Python side we could always load the file into a str for use as needed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shekhardw, any thoughts on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose to fix this in a separate MR.

Comment on lines 13 to 17
def test_exposed_transforms():
assert color_jitter is not None
assert image_flip is not None
assert image_rotate_90 is not None
assert scale_intensity is not None
assert zoom is not None
assert rand_zoom is not None
assert rand_image_flip is not None
assert rand_image_rotate_90 is not None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the goal just to test that these just exist or use them in some way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just the existence for exposing. Each of the transforms has its own set of test cases.

@shekhardw
Copy link
Contributor Author

@jakirkham Thanks for your valuable feedback. I have updated expose related changes.
Can we address the kernel, logging and closure related changes in another merge request?

@jakirkham
Copy link
Member

Thanks Shekhar! 😄

There's a couple comments above that may have been missed. Bumped them above. Please let me know your thoughts on those

@shekhardw
Copy link
Contributor Author

Hi @jakirkham, I propose to address the 3 remaining commments on closure, logging and cuda kernel in a separate MR. Are you fine with that?

@jakirkham
Copy link
Member

In that case could you please raise an issue tracking these?

shekhardw and others added 20 commits September 29, 2021 13:19
./src/cucim/core/operations/expose/transform.py:18:1: F40
'cucim.core.operations.spatial.rand_image_rotate_90' imported but unused
@jakirkham
Copy link
Member

@gpucibot merge

@rapids-bot rapids-bot bot merged commit 629f207 into branch-21.10 Sep 29, 2021
@jakirkham jakirkham deleted the dp_transforms branch September 29, 2021 22:02
@jakirkham
Copy link
Member

Thanks Shekhar for the PR and Gigon and Greg for the reviews! 😄

ajschmidt8 pushed a commit that referenced this pull request Jun 6, 2022
closes #114

Removes the try/except wrappers with logging of errors from the digital pathology transforms added in #100. 

Also renames a variable `to_cupy` to `to_numpy` instead to reflect its actual function.

**For reviewers**: There appear to be many lines changed here, but it is mostly a change in indentation due to removal of `try/except` blocks with `_logger` calls. Also some nested function definitions within `color_jitter` were moved outside of that function.

Authors:
   - Gregory Lee (https://github.com/grlee77)

Approvers:
   - Gigon Bae (https://github.com/gigony)
   - https://github.com/jakirkham
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request New feature or request non-breaking Introduces a non-breaking change
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add Transforms for Digital Pathology
5 participants