-
Notifications
You must be signed in to change notification settings - Fork 68
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
PickShiftedUDF #1635
base: master
Are you sure you want to change the base?
PickShiftedUDF #1635
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1635 +/- ##
==========================================
+ Coverage 69.85% 69.89% +0.03%
==========================================
Files 325 325
Lines 27255 27299 +44
Branches 3139 3147 +8
==========================================
+ Hits 19039 19080 +41
Misses 7647 7647
- Partials 569 572 +3 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a good idea and a good start!
For changelog you can add a file in docs/source/changelog/features/
. There are already two, you can just follow the pattern. :-)
Parameters | ||
---------- | ||
regression_coefficients : (3, 2) matrix | ||
A vector of regression coefficients for x- and y-directions. | ||
The regression coefficients make up two planes that describe | ||
the descan and are used to remove it. | ||
The coefficients are arranged in the following manner: | ||
((y, x), (dy/dy, dx/dy), (dy/dx, dx/dx)) where y and x are | ||
the shifts at coordinates (0, 0). The displayed image will be | ||
shifted in x- and y-directions by these parameters. By default | ||
`None`, which corresponds to no shifts. | ||
''' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about supporting the same options as CoMUDF
, or even use the CoMParams
class:
LiberTEM/src/libertem/udf/com.py
Lines 30 to 44 in 59529c1
class CoMParams(NamedTuple): | |
""" | |
Used to instantiate :class:`~libertem.udf.com.CoMUDF`. | |
See :meth:`~libertem.udf.com.CoMUDF.with_params` | |
for descriptions of each parameter. In most cases use this | |
method rather than instantiating this class directly. | |
""" | |
cy: Optional[float] = None | |
cx: Optional[float] = None | |
r: float = float('inf') | |
ri: Union[float, None] = 0. | |
scan_rotation: float = 0. | |
flip_y: bool = False | |
regression: RegressionOptionsT = RegressionOptions.NO_REGRESSION |
scan_rotation
and flip_y
would apply perfectly for this use case and should be easy to implement with the scipy.ndimage
functions.
In particular when dealing with DPC, it can help if in the picked frames everything points in the right directions relative to the scan! By using the same parameters, one can use them for both. As a side effect one could use this to create a corrected dataset! Could be merged with corresponding code in pyxem as a perspective?
def get_results(self): | ||
|
||
intensity = self.results.get_buffer('intensity').data | ||
|
||
regression_coefficients = np.array(self.params.regression_coefficients) | ||
# Only do shifts if nonzero regression coefficients | ||
if regression_coefficients.any(): | ||
|
||
coordinates = self.results.get_buffer('coordinates').raw_data | ||
|
||
prefix_ones = np.ones((*coordinates.shape[:-1], 1)) | ||
coordinates = np.concatenate((prefix_ones, coordinates), axis=-1) | ||
shifts = np.dot(coordinates, self.params.regression_coefficients) | ||
|
||
shifted = [] | ||
for intens, shift in zip(intensity, shifts): | ||
shifted.append(scipy.ndimage.shift(intens, -shift, mode="constant")) | ||
|
||
if shifted: | ||
intensity = np.stack(shifted) | ||
|
||
return { | ||
"intensity": intensity, | ||
"coordinates": self.results.get_buffer('coordinates').raw_data | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about using process_frame()
and doing all this there? It would be easier to read, and the processing would be performed on the workers, not on the main process.
Later one could look into using process_partition()
or postprocess()
to process larger chunks, which would be beneficial for using GPUs.
|
||
|
||
def test_pick_shifted(lt_ctx, delayed_ctx): | ||
data = _mk_random(size=(16, 16, 16, 16), dtype="float32") | ||
# data = np.ones((16, 16, 16, 16), dtype="float32") | ||
# data = np.arange(0, 16*16*16*16, dtype="float32").reshape((16, 16, 16, 16)) | ||
dataset = MemoryDataSet(data=data, tileshape=(3, 7, 16), | ||
num_partitions=7, sig_dims=2) | ||
roi = np.random.choice([True, False], size=dataset.shape.nav) | ||
roi[0] = True | ||
|
||
regression_coefficients = np.array([(3, 3), (0, 0), (0, 0)]) | ||
|
||
udf = PickShiftedUDF(regression_coefficients=regression_coefficients) | ||
res = lt_ctx.run_udf(dataset=dataset, udf=udf, roi=roi) | ||
res_delayed = delayed_ctx.run_udf(dataset=dataset, udf=udf, roi=roi) | ||
|
||
assert np.allclose(data[roi][..., 3:, 3:], res['intensity'].data[..., :-3, :-3], 1e-15) | ||
assert np.allclose(data[roi][..., 3:, 3:], res_delayed['intensity'].data[..., :-3, :-3], 1e-15) | ||
|
||
assert data.dtype == res['intensity'].data.dtype | ||
assert data.dtype == res_delayed['intensity'].data.dtype | ||
|
||
|
||
def test_pick_shifted_empty_roi(lt_ctx, delayed_ctx): | ||
data = _mk_random(size=(16, 16, 16, 16), dtype="float32") | ||
dataset = MemoryDataSet(data=data, tileshape=(3, 7, 7), | ||
num_partitions=7, sig_dims=2) | ||
roi = np.zeros(dataset.shape.nav, dtype=bool) | ||
|
||
regression_coefficients = np.array([(3, 3), (0, 0), (0, 0)]) | ||
|
||
udf = PickShiftedUDF(regression_coefficients=regression_coefficients) | ||
res = lt_ctx.run_udf(dataset=dataset, udf=udf, roi=roi) | ||
res_delayed = delayed_ctx.run_udf(dataset=dataset, udf=udf, roi=roi) | ||
|
||
assert np.allclose(data[roi], res['intensity'].data) | ||
assert np.allclose(data[roi], res_delayed['intensity'].data) | ||
|
||
assert data[roi].shape == res['intensity'].data.shape | ||
assert data[roi].shape == res_delayed['intensity'].data.shape | ||
|
||
assert data.dtype == res['intensity'].data.dtype | ||
assert data.dtype == res_delayed['intensity'].data.dtype |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regression coefficients should all be different to make sure we are not mixing them anywhere. In particular, the dy/dx ... coefficients require tests.
We should also confirm that the coefficients act the same as in CoMUDF
, in particular if we support more parameters, as suggested above. A simple and comprehensive test would be comparing CoMUDF
on an uncorrected dataset vs. CoMUDF
on a corrected dataset.
|
||
|
||
class PickShiftedUDF(UDF): | ||
''' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
''' | |
''' | |
.. versionadded:: 0.15.0 | |
src/libertem/udf/raw.py
Outdated
def __init__(self): | ||
super().__init__() | ||
def __init__(self, **kwargs): | ||
super().__init__(**kwargs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, why this change? The PickShiftedUDF
is subclassed from UDF
, so PickUDF
doesn't need to pass parameters through. If one wanted a common subclass to share code, it would be cleaner to have a BasePickUDF
with the common parts, and derive both PickUDF
and PickShiftedUDF
from that, each with a final constructor. That way the parameters are clear for autocompletion etc.
/azp run libertem.libertem-data |
Azure Pipelines successfully started running 1 pipeline(s). |
Contributor Checklist:
Reviewer Checklist:
/azp run libertem.libertem-data
passedThis is an addition of a new PickUDF that is able to shift the data that is retrieved. This is useful because it makes it possible to correct raw data for unintended shifts, for example descan which can obstruct electromagnetic signal in STEM-DPC. This functions as the previous PickUDF, but you can give corrective regression parameters indicating how much to shift the data. This can be found for example by using the CoMUDF:
The way this is implemented is with
scipy.ndimage.shift
, done as a postprocessing step in the UDF. The shifts are found with theself.meta.coordinates
attribute, which is passed around as a result buffer. So perhaps there is a cleaner way of doing this. The UDF seems to work well for my practical purposes and I have set up some accompanying tests.