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

Ticket/425/motion/correction/tiffs #433

Merged
merged 46 commits into from
Feb 8, 2022
Merged

Conversation

danielsf
Copy link
Contributor

@danielsf danielsf commented Feb 3, 2022

During the 2/2/2022 meeting with the SSF team, the scientists requested that we produce spatiotemporally downsampled TIFF files to support their QC of motion correction. This PR modifies our existing ophys_etl/modules/video modules to produce those files. Examples of the output can be found in

/allen/programs/mindscope/workgroups/surround/motion_correction_labeling_2022/**/*tiff

Jun Zhuang has looked at examples of these files and is happy with the output.

@codecov
Copy link

codecov bot commented Feb 3, 2022

Codecov Report

Merging #433 (534a109) into main (5159c07) will increase coverage by 0.34%.
The diff coverage is 98.33%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #433      +/-   ##
==========================================
+ Coverage   90.23%   90.57%   +0.34%     
==========================================
  Files          88       89       +1     
  Lines        5426     5456      +30     
==========================================
+ Hits         4896     4942      +46     
+ Misses        530      514      -16     
Flag Coverage Δ
not_container_tests 90.57% <98.33%> (+0.34%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
src/ophys_etl/modules/video/utils.py 97.29% <97.77%> (+1.56%) ⬆️
src/ophys_etl/modules/video/cli_mixins.py 100.00% <100.00%> (ø)
src/ophys_etl/modules/video/schemas.py 97.56% <100.00%> (+25.22%) ⬆️
src/ophys_etl/modules/video/side_by_side_video.py 87.50% <100.00%> (+5.14%) ⬆️
src/ophys_etl/modules/video/single_video.py 86.66% <100.00%> (+5.41%) ⬆️

@danielsf danielsf marked this pull request as ready for review February 4, 2022 00:25
Copy link
Contributor

@morriscb morriscb left a comment

Choose a reason for hiding this comment

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

Found one thing that I have questions about. Also, there seems to be a lot of duplicated code in this module/ticket. Is there a way of possibly eliminating some of the duplicated code?

src/ophys_etl/modules/video/utils.py Outdated Show resolved Hide resolved
@@ -16,8 +17,23 @@ class VideoBaseSchema(argschema.ArgSchema):
required=False,
allow_none=True,
default=3,
description=("Radius of median filter kernel; "
"if None, no median filter applied"))
description=("Size of spatial filter kernel; "
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you instead add a new argument use_spatial_filter to control whether to apply spatial filtering? Passing None or 0 here is a little awkward, imo.

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 am of the opposite opinion. I don't like the idea of adding more parameters to the schema. It clutters up --help and leaves more places for users to screw up (setting use_spatial_filter=False, kernel_size=2 when they intended to use a kernel of size 2). I like the idea that you can just turn filtering off by setting the kernel size to nothing. We can talk about this, if you want.

Copy link
Contributor

Choose a reason for hiding this comment

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

In that case you could just issue a warning that use_spatial_filter is False but kernel_size is not None

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To me, that seems just as complicated as the present

        use_kernel = False
        if self.args['kernel_size'] is not None:
            if self.args['kernel_size'] > 0:
                use_kernel = True

I'm going to leave this.

quantiles = (0.0, 1.0)

use_kernel = False
if self.args['kernel_size'] is not None:
Copy link
Contributor

Choose a reason for hiding this comment

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

Adding a new argument use_spatial_filter will simplify this logic

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 know, I just think the simplicity in the CLI is worth it.

@@ -30,13 +28,14 @@ def create_downsampled_video(
input_hz: float,
video_path: pathlib.Path,
output_hz: float,
kernel_size: Optional[int],
spatial_filter: Optional[Callable[[np.ndarray], np.ndarray]],
Copy link
Contributor

@aamster aamster Feb 7, 2022

Choose a reason for hiding this comment

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

Can create_downsampled_video_h5 instead convert the spatial_filter argument as a string into a function rather than the CLI constructing it and passing a function around?

If we wanted to use this without going through the CLI, we would have to replicate that logic.

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 actually like that these methods accept callables. It means that, outside of the CLI, we can quickly experiment with new spatial filters by just passing in a function without having to implement the if/else block necessary to parse a string into a function.

Again: open to discussion, but I do see this as a feature, rather than a bug.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure, then you allow this function to accept string or callable. If string, it converts it into a callable.
This would not force the user to go through the CLI to be able to properly pull the correct function for "mean" or "median" filters and would allow your use case of passing a custom callable for experimenting.

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'm going to leave this. Your proposal would require adding a kernel_size arg to create_downsampled_video_h5. I'm not a huge fan of increasing the number of arguments more than we have to. When/if we move to class-based video manipulation, I suspect the apply_spatial_filter() method will be general enough to accept an object that is a spatial filter (I know I'm talking like someone who lives in C++-ville; in python, it would just be a callable). I still like the generalizability of that. We can revisit this conversation when we move to class-based manipulation of videos.

src/ophys_etl/modules/video/utils.py Outdated Show resolved Hide resolved
src/ophys_etl/modules/video/utils.py Outdated Show resolved Hide resolved
src/ophys_etl/modules/video/utils.py Outdated Show resolved Hide resolved
src/ophys_etl/modules/video/utils.py Outdated Show resolved Hide resolved
src/ophys_etl/modules/video/utils.py Show resolved Hide resolved
"""

downsampled_video = skimage_measure.block_reduce(
video,
Copy link
Contributor

Choose a reason for hiding this comment

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

It's a bummer we can't combine our median filter with mean filter somehow. They are both doing the same thing except they use different functions and do/don't do downsampling

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Probably the responsible thing to do would be to consolidate these in a video_utils module. Do you mind if we leave that work fro #436 (if you agree, I'll add this as an explicit bullet to that ticket)?

src/ophys_etl/modules/video/single_video.py Outdated Show resolved Hide resolved
@danielsf
Copy link
Contributor Author

danielsf commented Feb 8, 2022

@aamster @morriscb I think this is ready for a re-review.

Copy link
Contributor

@aamster aamster 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, left some nitpicks.

@@ -16,8 +17,23 @@ class VideoBaseSchema(argschema.ArgSchema):
required=False,
allow_none=True,
default=3,
description=("Radius of median filter kernel; "
"if None, no median filter applied"))
description=("Size of spatial filter kernel; "
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry can you please make it clear that these kernels apply specifically downsampling . It is possible to apply a sliding kernel with no downsampling

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Our default median filter actually doesn't apply downsampling.

https://github.com/AllenInstitute/ophys_etl_pipelines/blob/main/src/ophys_etl/modules/median_filtered_max_projection/utils.py#L12-L41

The size of the frame coming in is the size of the frame coming out

>>> import numpy as np
>>> import scipy.ndimage as scipy_ndimage
>>> f = np.random.random((25, 25))
>>> 
>>> f2 = scipy_ndimage.median_filter(f, size=3, mode='reflect')
>>> f.shape
(25, 25)
>>> f2.shape
(25, 25)

(because this isn't complicated enough...)

allow_none=False,
default='median',
validation=OneOf(('median', 'mean')),
description=("Type of spatial smoothing kernel to be "
Copy link
Contributor

@aamster aamster Feb 8, 2022

Choose a reason for hiding this comment

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

Same thoughts above about mentioning "downsampling". Also I'm assuming the "downsampling" mentioned already here is the temporal downsampling

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 modified the docstring to explicitly call out that the "mean" filter downsamples the video frames while the "median" filter does not.

quantiles = (self.args['lower_quantile'],
self.args['upper_quantile'])
else:
quantiles = (0.0, 1.0)
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you instead change the default to 0 and 1 instead of None? Then this logic wouldn't be needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -16,8 +17,23 @@ class VideoBaseSchema(argschema.ArgSchema):
required=False,
allow_none=True,
default=3,
description=("Radius of median filter kernel; "
"if None, no median filter applied"))
description=("Size of spatial filter kernel; "
Copy link
Contributor

Choose a reason for hiding this comment

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

In that case you could just issue a warning that use_spatial_filter is False but kernel_size is not None

@@ -0,0 +1,44 @@
from ophys_etl.modules.video.utils import (
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you please make it clear that this is specific to the CLI? Ie by calling this module cli_mixin or by placing it in a cli package

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -30,13 +28,14 @@ def create_downsampled_video(
input_hz: float,
video_path: pathlib.Path,
output_hz: float,
kernel_size: Optional[int],
spatial_filter: Optional[Callable[[np.ndarray], np.ndarray]],
Copy link
Contributor

Choose a reason for hiding this comment

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

Sure, then you allow this function to accept string or callable. If string, it converts it into a callable.
This would not force the user to go through the CLI to be able to properly pull the correct function for "mean" or "median" filters and would allow your use case of passing a custom callable for experimenting.

kernel_size: Optional[int]
Size of the median filter kernel to be applied to the downsampled
movie (if None, no median filter will be applied)
spatial_filter: Optional[Callable[[np.ndarray], np.ndarray]]
Copy link
Contributor

Choose a reason for hiding this comment

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

My thinking is this -- if what you are modeling can be thought of as a thing, then it should use a class-based design. Video is a thing, therefore it should use a class-based design. I don't think we necessarily need to carry around the whole video in memory with this approach.

src/ophys_etl/modules/video/utils.py Show resolved Hide resolved
dtype=float)

mgr = multiprocessing.Manager()
output_lock = mgr.Lock()
validity_dict = mgr.dict()
process_list = []

print(f'spatial filter {spatial_filter}')
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe you meant to use the logger?

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 think I actually meant to remove that print statement. I added that when I was implementing spatial_filter to make sure it got passed through. Thanks for catching that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants