scaffold for sub-pixel drift correction for time series#1493
Conversation
|
Hi Yujin, a few ideas:
If we are not doing 3D correction, it might be elegant to implement as a class XYZTCDriftCorrectSource(XYZTCDataSource):
moduleName = 'DriftCorrectDataSource'
def __init__(self, datasource, x_mapping, y_mapping, x_scale=1.0, y_scale=1.0):
if (not isinstance(datasource, XYZTCDataSource)) and (not datasource.ndim == 5) :
datasource = XYZTCWrapper.auto_promote(datasource)
self._datasource = datasource
size_z, size_t, size_c = datasource.shape[2:]
self._x_map = x_mapping # a piecewise mapping object
self._x_scale = x_scale #allows conversion between different camera pixel units. TODO - make it an affine transformation matrix to allow for rotation as well.
self._y_map = y_mapping
self._y_scale = y_scale
XYZTCDataSource.__init__(self, input_order=datasource._input_order, size_z=size_z, size_t=size_t, size_c=size_c)
def getSlice(self, ind):
sl = self._datasource.getSlice(ind)
return ndimage.shift(sl, [self._x_scale*self._x_map(ind), self._y_scale*self._y_map(ind)])
# proxy original data source attributes
def __getattr__(self, item):
return getattr(self._datasource, item)
def getSliceShape(self):
return self._datasource.getSliceShape()
def getNumSlices(self):
return self._datasource.getNumSlices()
def getEvents(self):
return self._datasource.getEvents()
@property
def is_complete(self):
return self._datasource.is_complete()The advantages of this method is that it should work regardless of the dimensionality and dimension order of the input (i.e. it should work for z stacks and time sequences etc ... without any futzing around). The potential caveat is that it is lazily computed (i.e. computed on access). This means that it will return pretty much instantly, but there will be a lag when each frame is accessed - depending on how fast ds = XYZTCDriftCorrectSource(...)
ImageStack(ds.data_xyztc[:,:,:,:,:], mdh=ds.mdh)rather than ImageStack(XYZTCDriftCorrectSource(...)) |
| (Ynm + ShiftMeasure_positions[idx][1]*vy - y0)/vy, | ||
| (Znm + ShiftMeasure_positions[idx][2]*1000 - z0)/vz], | ||
| mode='nearest', order=3) | ||
| drift_corrected.append(corrected) |
There was a problem hiding this comment.
Rather than making a list, it would be more efficient to pre-allocate an output array and write into that.
| drift_corrected = np.asarray(drift_corrected) | ||
| drift_corrected.shape = (self.image.data_xyztc.shape[0], self.image.data_xyztc.shape[1], self.image.data_xyztc.shape[2], idx, 1) | ||
| #print(drift_corrected.shape) | ||
| im = BaseDataSource.XYZTCWrapper(ArrayDataSource.ArrayDataSource(drift_corrected), 'XYZTC', self.image.data_xyztc.shape[2], drift_corrected.shape[3], 1) |
There was a problem hiding this comment.
This is an anti-pattern - in any new code we should be using 5 dimensional datasources throughout, and not using the wrappers. They exist solely for backwards compatibility. Go straight for ArrayDataSource.XYZTCArrayDataSource, or just pass the 5d np array to ImageStack
There was a problem hiding this comment.
I now see where this was copied from - the MemoryBackend. For a bit of context, the usage in MemoryBackend is deliberate as raw data comes from the camera as essentially XYT and anything else needs to be put over the top of this as an interpretation. The goal is that analysis code does not need to worry about what order data comes in or the low level stuff - i.e. memory backend is the only place we should be doing this.
|
You guys will probably call it nitpicking but I would vote against any |
|
@csoeller - definitely a good idea to limit / phase out
although that one will pick up both additions and deletions ... |
|
@csoeller - maybe |
|
No worries re print etc. More generally in relation to this PR, why would one want to drift correct the source images. Presumably this is not for images eventually intended for SMLM? |
|
The application here is a bit complicated, but essentially boils down to taking simultaneous (or interleaved) SMLM and OIDIC images, and correcting the non SMLM channel. In principle it could also be used for widefield fluorescence / SIM / etc ... |
|
Thanks @David-Baddeley @csoeller for the suggestions! It is definitely necessary to limit |
|
Hi David, since my fluorescence channel has scaling and rotation relative to the OI-DIC (also the drift tracking) channel, I modified the code to generalize the correction using an affine transformation as suggested in |
| # self.eventCharts = eventCharts | ||
| # self.ev_mappings = ev_mappings | ||
|
|
||
| if b'PYME2ShiftMeasure' in evKeyNames: |
There was a problem hiding this comment.
| if b'PYME2ShiftMeasure' in evKeyNames: | |
| elif b'PYME2ShiftMeasure' in evKeyNames: | |
| # Standard ShiftMeasure events should have priority (i.e. this should only execute if we haven't found any standard ShiftMeasure events | |
| # TODO - find a way to remove the need for this special case / event type. |
| """ | ||
|
|
||
| input = Input('input') | ||
| drift_tracking_channel_pixel_size_x = Float(64.5) |
There was a problem hiding this comment.
In many cases the drift-tracking camera will be flipped as well as rotated with respect to the imaging camera. The general case (as is used for point-data) is to specify an affine transformation matrix, rather than pixel sizes and rotations, although that is arguably a bit less user-friendly.
I'd need to double check, but it's likely that the necessary transformation can be spoofed with the pixel-size and angle if we allow negative pixel sizes. If this is the case we need to think about whether we want to be consistent with the point data, or whether the pixel size and angle specification is sufficiently more user friendly to make it a better choice.
| x1 = x0*self._xx_scale*np.cos(self._theta) + y0*self._yx_scale*np.sin(self._theta) | ||
| y1 = y0*self._yy_scale*np.cos(self._theta) - x0*self._xy_scale*np.sin(self._theta) | ||
| #return ndimage.shift(sl, [-self._x_scale*self._x_map(np.array([ind+1])), -self._y_scale*self._y_map(np.array([ind+1]))], order=3, mode='nearest') | ||
| return ndimage.shift(sl, [-x1, -y1], order=3, mode='nearest') |
There was a problem hiding this comment.
Maybe compute the affine transformation matrix in __init__ and just apply here?? Also, we need to specify a rotation origin as well as pixel sizes and rotation.
The affine transformation should really be a 3x2 (2x3?) matrix .... applied to [x, y, 1]
|
Hi @Yujin-Bao - sorry it's taken so long to have a good look at this. Main potential issues (as mentioned in comments on the code) are as follows:
|
Is this a bugfix or an enhancement?
Enhancement
Proposed changes:
Sub-pixel drift correction for time series. This is a work in progress, currently only showing a scaffold of the idea. It runs but: the result is problematic; the speed is slow; the code is system-specific for the microscope I am building. Would like to see if there would be any ideas.
@David-Baddeley
Checklist:
The below is a list of things what will be considered when reviewing PRs. It is not prescriptive, and does not
imply that PRs which touch any of these will be rejected but gives a rough indication of where there is a potential
for hangups (i.e. factors which could turn a 5 min review into a half hour or longer and shunt it to the bottom
of the TODO list).
much simpler if this is kept separate from functional changes. The auto-formatting performed by some editors is particulaly egregious and can lead to files with thousands
of non-functional changes with a few functional changes scattered amoungst them]
If an enhancement (or non-trivial bugfix):
PYMEImageby clicking the drop down menu Modules-->drift_correct and then clicking the drop down menu Processing-->Sub-pixel Drift Correction. The drift corrected image will be shown in a new window.