Skip to content

Support downsampling of image data with NaNs#3061

Merged
j9ac9k merged 7 commits intopyqtgraph:masterfrom
nup002:feat_imageitem_nan_downsample
Jul 18, 2024
Merged

Support downsampling of image data with NaNs#3061
j9ac9k merged 7 commits intopyqtgraph:masterfrom
nup002:feat_imageitem_nan_downsample

Conversation

@nup002
Copy link
Copy Markdown
Contributor

@nup002 nup002 commented Jun 10, 2024

This PR adds tolerance for NaNs in ImageItem autodownsampling.

In the original code, NaNs in the image data would dominate after autodownsampling. With these changes, NaNs are ignored during downsampling.

Copy link
Copy Markdown
Member

@j9ac9k j9ac9k left a comment

Choose a reason for hiding this comment

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

The has_nans keyword argument is propagated into the recursive call on line 1776 in functions.py

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jun 11, 2024

Hi @nup002

I posted a couple of comments, the other thing I'm not sure about is if we want the keyword argument to be has_nans. It's a bit late now, but I'd be curious to know if there is anything similar in the library that we can have the keyword match. If nothing inside pyqtgraph, then is there anything in Qt that's equivalent and if not there, anything in numpy/scipy.

@nup002
Copy link
Copy Markdown
Contributor Author

nup002 commented Jun 11, 2024

Hi @nup002

I posted a couple of comments, the other thing I'm not sure about is if we want the keyword argument to be has_nans. It's a bit late now, but I'd be curious to know if there is anything similar in the library that we can have the keyword match. If nothing inside pyqtgraph, then is there anything in Qt that's equivalent and if not there, anything in numpy/scipy.

The only thing I can think of which is similar is Scipy's nan_policy: https://docs.scipy.org/doc/scipy/dev/api-dev/nan_policy.html
We can implement this instead of the simpler has_nans for finer control of the desired behaviour of downsample. But I am not sure I think this makes much sense since when downsampling the desired behaviour is very likely that NaNs must not propagate.

@NilsNemitz
Copy link
Copy Markdown
Contributor

If there is a potential discussion on the desired behavior, then isn't the parameter really skip_nans instead of has_nans?

@nup002
Copy link
Copy Markdown
Contributor Author

nup002 commented Jun 11, 2024

Yes, but skip_nans implies a NaN check within downsample, and this is expensive to do every time it is called. Instead, the proposed implementation uses the self._imageHasNans attribute of ImageItem to avoid repeated NaN checks. I also do not feel that permitting NaN propagation in the downsampling makes much sense, except in certain edge cases.

@NilsNemitz
Copy link
Copy Markdown
Contributor

NilsNemitz commented Jun 11, 2024

Sorry, I misunderstood the discussion on the keyword naming, I thought this is exposed to the user. But the change is really that the existing 'has_nans' information is now passed on to the downsampling function which takes on a different behaviour then.

It seems to me that you are introducing a new downsampleMethod, like we already have in PlotDataItem:

downsampleMethod ``str``, default ``'peak'``

Would it be reasonable to add a similar switch here, creating a user choice between downsampleMethod='mean' and downsampleMethod='nanmean'?

I tend to agree that the current behaviour is probably not the most helpful in most of the cases where NaN values might appear in the image, but it does seem at least mathematically valid and well defined.

@nup002
Copy link
Copy Markdown
Contributor Author

nup002 commented Jun 12, 2024

I propose that we expose an optional keyword argument to the user in ImageItem.__init__ named 'nan_policy' that defines how to handle NaNs during downsampling. The valid values would be 'propagate' which uses np.mean and 'ignore' which uses np.nanmean. We should at the same time add this functionality to PlotDataItem, which has the same limitation with regards to NaNs propagating into the downsampled data.

@nup002
Copy link
Copy Markdown
Contributor Author

nup002 commented Jun 15, 2024

I have implemented nanPolicy as an optional keyword argument for ImageItem which defaults to 'ignore', and which can be set with a the method ImageItem.setNanPolicy:

Control how NaN values are handled during downsampling for this ImageItem.

Parameters
----------
nanPolicy : str
    Must be one of ['ignore`, 'propagate']. If 'nanPolicy' is 'ignore', NaNs
    are automatically ignored during downsampling, at the expense of
    performance. If 'nanPolicy' is 'propagate', NaNs are kept during
    downsampling. ImageItem is instantiated with nanPolicy set to 'ignore'.

If this feature request is approved, I will work on adding matching functionality to PlotDataItem so that it also can ignore NaNs during downsampling.



def downsample(data, n, axis=0, xvals='subsample'):
def downsample(data, n, axis=0, xvals='subsample', *, nanPolicy='ignore'):
Copy link
Copy Markdown
Contributor

@NilsNemitz NilsNemitz Jul 2, 2024

Choose a reason for hiding this comment

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

renaming 'nan_policy' to 'nanPolicy' seems like a good idea for matching our Qt-inspired case style.
Shouldn't 'ignore' be 'omit' if we are aiming to resemble SciPy conventions?

Do we have any additional opinions for what should be the default policy?
Although I agree that 'omit' is probably the most commonly useful policy, maybe we should stick with 'propagate' as the default to better match existing behavior.

Copy link
Copy Markdown
Contributor

@NilsNemitz NilsNemitz left a comment

Choose a reason for hiding this comment

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

Dear @nup002 , sorry for the delay.

I like the approach of matching the SciPy solution, and I think that's a fantastic upgrade for ImageItem.

I am not usually doing code review, but I've added a few comments.

I think the main items are that 'ignore' is 'omit' in the SciPy convention, and that it is probably best to leave the default behavior on 'propagate' for backwards compatibility and possibly speed.

The documentation comments are not too serious, ImageItem will need a documentation pass in the future. If you want someplace to look for reference, the most up-to-date piece of documentation style we have is probably PlotDataItem after PRs #3039 and #3057 .

elif nanPolicy == 'propagate':
d2 = d1.mean(axis+1)
else:
raise ValueError(f"Keyword argument {nanPolicy=} must be one of ['propagate', 'ignore'].")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@j9ac9k has been looking into documentation styles recently and is clearer on the conventional markups, but I think this might be "Keyword argument 'nanPolicy' must be one of 'propagate' or 'ignore'."
And this might not need an f string?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If a keyword only argument can be one of several things (in this case string) the syntax is:

{ 'option_1', 'option_2' }

You can see an example of it here (but you have to scroll down a bit) https://numpydoc.readthedocs.io/en/latest/format.html#parameters

self.levels = None ## [min, max] or [[redMin, redMax], ...]
self.lut = None
self.autoDownsample = False
self.nanPolicy = 'ignore'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

default to 'propagate' ?

Parameters
----------
nanPolicy : str
Must be one of ['ignore`, 'propagate']. If 'nanPolicy' is 'ignore', NaNs
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we have control strings like this marked up elsewhere in the style of

Must be one of ``'propagate'`` or ``'omit'``. For ``nanPolicy='omit'``, NaNs are automatically ignored...

I would suggest changing the markup to that. But the docstyle is currently in flux and not very clearly defined yet.

Copy link
Copy Markdown
Member

@j9ac9k j9ac9k Jul 11, 2024

Choose a reason for hiding this comment

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

I think it should be:

Parameters
----------
nanPolicy : { 'ignore', 'propagate' }
    Put here a description of each option.

Must be one of ['ignore`, 'propagate']. If 'nanPolicy' is 'ignore', NaNs
are automatically ignored during downsampling, at the expense of
performance. If 'nanPolicy' is 'propagate', NaNs are kept during
downsampling. ImageItem is instantiated with nanPolicy set to 'ignore'.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You've actually added a switch to make a new ImageItem with any nanPolicy. Maybe:

Unless a different policy was specified, a new ImageItem is created with ``nanPolicy='propagate'``.

"""
assert nanPolicy in ['propagate', 'ignore'], (f"{nanPolicy=} must be one "
f"of ['propagate', 'ignore']")
self.nanPolicy = nanPolicy
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since this is catching user errors, it should probably raise a ValueError (instead of using assert).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Agreed if nanPolicy is not in the above, we can raise a ValueError, let's not have an assert check here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also rename the attribute to ._nanPolicy, see comment below about getter/setter.

if 'autoDownsample' in kwargs:
self.setAutoDownsample(kwargs['autoDownsample'])
if 'nanPolicy' in kwargs:
self.nanPolicy = kwargs['nanPolicy']
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we want to call self.setNanPolicy(kwargs['nanPolicy']).

self.nanPolicy should not be an attribute, but should be a method that gets the currently stored value. I would suggest renaming to self._nanPolicy and have another method

def nanPolicy(self):
    return self._nanPolicy

That will be more in line w/ Qt's getters and setters.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jul 11, 2024

Hi @nup002

My apologies for sitting on this for so long before commenting. I recently underwent a major international move, and it's taken me a while to get sorted out in my current home.

I agree with @NilsNemitz 's comments. I have a couple of nit-pick items

  1. self.nanPolicy should not be an attribute, but a method. The attribute should be self._nanPolicy and the getter method for that attribute should be self.nanPolicy(). This lines up with our API and the API of Qt.
  2. There is an assert check to ensure the nanPolicy string is one of the expected kids, as assert checks can easily be turned off, I avoid having them outside of testing code. Instead I suggest having an if-statement, where if the value of nanPolicy is not one of the expected kinds, it raises a ValueError.

@nup002
Copy link
Copy Markdown
Contributor Author

nup002 commented Jul 11, 2024

@j9ac9k All of your suggestions, and those of @NilsNemitz, are reasonable. I will get on them once I can find some time.

Copy link
Copy Markdown
Contributor Author

@nup002 nup002 left a comment

Choose a reason for hiding this comment

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

All suggestions implemented.

@nup002 nup002 requested a review from j9ac9k July 17, 2024 14:40
@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jul 18, 2024

Thanks @nup002 for this PR, this looks good to me, merging. I really appreciate your patience on this!

@j9ac9k j9ac9k merged commit a50a5fc into pyqtgraph:master Jul 18, 2024
@nup002 nup002 deleted the feat_imageitem_nan_downsample branch October 4, 2024 12:27
j9ac9k pushed a commit to j9ac9k/pyqtgraph that referenced this pull request Mar 25, 2025
* np.nanmean downsampling of image data with NaNs.

* Make has_nans keyword argument only. Propagate has_nans in recursive call.

* Implement nanPolicy for use control of NaNs during downsampling.

* Change default nanPolicy to 'propagate'

* Change nanPolicy option 'ignore' to 'omit'

* Implement getter method for NanPolicy

* Rename method GetNanPolicy to NanPolicy
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