|
1 | 1 | import numpy as np |
2 | 2 |
|
3 | 3 | from hexrd import constants |
| 4 | +from hexrd.instrument.detector import _interpolate_bilinear_in_place |
4 | 5 | from hexrd.material.crystallography import PlaneData |
5 | 6 | from hexrd.xrdutil.utils import ( |
6 | 7 | _project_on_detector_cylinder, |
@@ -77,13 +78,15 @@ def __init__(self, plane_data, instrument, |
77 | 78 | self._instrument = instrument |
78 | 79 |
|
79 | 80 | self._coordinate_mapping = None |
| 81 | + self._nan_mask = None |
80 | 82 | self._cache_coordinate_map = cache_coordinate_map |
81 | 83 | if cache_coordinate_map: |
82 | 84 | # It is important to generate the cached map now, rather than |
83 | 85 | # later, because this object might be sent to other processes |
84 | 86 | # for parallelization, and it will be faster if the mapping |
85 | 87 | # is already generated. |
86 | 88 | self._coordinate_mapping = self._generate_coordinate_mapping() |
| 89 | + self._nan_mask = self._generate_nan_mask(self._coordinate_mapping) |
87 | 90 |
|
88 | 91 | @property |
89 | 92 | def instrument(self): |
@@ -261,13 +264,21 @@ def warp_image(self, image_dict, pad_with_nans=False, |
261 | 264 | if self.cache_coordinate_map: |
262 | 265 | # The mapping should have already been generated. |
263 | 266 | mapping = self._coordinate_mapping |
| 267 | + nan_mask = self._nan_mask |
264 | 268 | else: |
265 | 269 | # Otherwise, we must generate it every time |
266 | 270 | mapping = self._generate_coordinate_mapping() |
| 271 | + # FIXME: this performs a bilinear interpolation |
| 272 | + # each time. Maybe it doesn't matter that much |
| 273 | + # since the interpolation is very fast now, but |
| 274 | + # it'd be nice if we could figure out another |
| 275 | + # way to do it. |
| 276 | + nan_mask = self._generate_nan_mask(mapping) |
267 | 277 |
|
268 | 278 | return self._warp_image_from_coordinate_map( |
269 | 279 | image_dict, |
270 | 280 | mapping, |
| 281 | + nan_mask, |
271 | 282 | pad_with_nans=pad_with_nans, |
272 | 283 | do_interpolation=do_interpolation, |
273 | 284 | ) |
@@ -312,66 +323,104 @@ def _generate_coordinate_mapping(self) -> dict[str, dict[str, np.ndarray]]: |
312 | 323 | xypts[on_plane, :] = valid_xys |
313 | 324 |
|
314 | 325 | _, on_panel = panel.clip_to_panel(xypts, buffer_edges=True) |
| 326 | + on_panel_idx = np.where(on_panel)[0] |
| 327 | + xy_clip = xypts[on_panel_idx] |
| 328 | + |
| 329 | + bilinear_interp_dict = panel._generate_bilinear_interp_dict( |
| 330 | + xy_clip, |
| 331 | + ) |
315 | 332 |
|
316 | 333 | mapping[detector_id] = { |
317 | 334 | 'xypts': xypts, |
318 | | - 'on_panel': on_panel, |
| 335 | + 'on_panel_idx': on_panel_idx, |
| 336 | + 'bilinear_interp_dict': bilinear_interp_dict, |
319 | 337 | } |
320 | 338 |
|
321 | 339 | return mapping |
322 | 340 |
|
323 | | - def _warp_image_from_coordinate_map( |
324 | | - self, |
325 | | - image_dict: dict[str, np.ndarray], |
326 | | - coordinate_map: dict[str, dict[str, np.ndarray]], |
327 | | - pad_with_nans: bool = False, |
328 | | - do_interpolation=True) -> np.ma.MaskedArray: |
329 | | - |
330 | | - panel_buffer_fill_value = np.nan |
331 | | - img_dict = dict.fromkeys(self.detectors) |
332 | | - nan_mask = None |
333 | | - for detector_id, panel in self.detectors.items(): |
334 | | - # Make a copy since we may modify |
335 | | - img = image_dict[detector_id].copy() |
| 341 | + def _generate_nan_mask( |
| 342 | + self, |
| 343 | + coordinate_map: dict[str, dict[str, np.ndarray]], |
| 344 | + ) -> np.ndarray: |
| 345 | + """Generate the nan mask |
336 | 346 |
|
337 | | - # Before warping, mask out any pixels that are invalid, |
338 | | - # so that they won't affect the results. |
| 347 | + This saves time during repeated calls to warp_image(), |
| 348 | + since the nan mask should stay the same and not change. |
| 349 | + """ |
| 350 | + mapping = coordinate_map |
| 351 | + # Generate the nan mask that we will use |
| 352 | + nan_mask = np.ones(self.shape, dtype=bool).flatten() |
| 353 | + for detector_id, panel in self.detectors.items(): |
| 354 | + on_panel_idx = mapping[detector_id]['on_panel_idx'] |
| 355 | + xypts = mapping[detector_id]['xypts'] |
| 356 | + interp_dict = mapping[detector_id]['bilinear_interp_dict'] |
| 357 | + |
| 358 | + # To reproduce old behavior, perform a bilinear |
| 359 | + # interpolation so that if any point has neighboring |
| 360 | + # pixels that are nan, that point will also be excluded. |
| 361 | + dummy_img = np.zeros(panel.shape) |
339 | 362 | buffer = panel_buffer_as_2d_array(panel) |
340 | | - if (np.issubdtype(type(panel_buffer_fill_value), np.floating) and |
341 | | - not np.issubdtype(img.dtype, np.floating)): |
342 | | - # Convert to float. This is especially important |
343 | | - # for nan, since it is a float... |
344 | | - img = img.astype(float) |
| 363 | + dummy_img[~buffer] = np.nan |
| 364 | + |
| 365 | + output = np.full(len(xypts), np.nan) |
| 366 | + output[on_panel_idx] = 0 |
| 367 | + _interpolate_bilinear_in_place( |
| 368 | + dummy_img, |
| 369 | + **interp_dict, |
| 370 | + on_panel_idx=on_panel_idx, |
| 371 | + output_img=output, |
| 372 | + ) |
345 | 373 |
|
346 | | - img[~buffer] = panel_buffer_fill_value |
| 374 | + nan_mask[~np.isnan(output)] = False |
347 | 375 |
|
348 | | - xypts = coordinate_map[detector_id]['xypts'] |
349 | | - on_panel = coordinate_map[detector_id]['on_panel'] |
| 376 | + return nan_mask.reshape(self.shape) |
| 377 | + |
| 378 | + def _warp_image_from_coordinate_map( |
| 379 | + self, |
| 380 | + image_dict: dict[str, np.ndarray], |
| 381 | + coordinate_map: dict[str, dict[str, np.ndarray]], |
| 382 | + nan_mask: np.ndarray, |
| 383 | + pad_with_nans: bool = False, |
| 384 | + do_interpolation=True, |
| 385 | + ) -> np.ma.MaskedArray: |
| 386 | + first_det = next(iter(self.detectors)) |
| 387 | + # This is a flat image. We'll reshape at the end. |
| 388 | + summed_img = np.zeros(len(coordinate_map[first_det]['xypts'])) |
| 389 | + for detector_id, panel in self.detectors.items(): |
| 390 | + img = image_dict[detector_id] |
| 391 | + panel_map = coordinate_map[detector_id] |
| 392 | + |
| 393 | + xypts = panel_map['xypts'] |
| 394 | + on_panel_idx = panel_map['on_panel_idx'] |
| 395 | + interp_dict = panel_map['bilinear_interp_dict'] |
350 | 396 |
|
351 | 397 | if do_interpolation: |
352 | | - this_img = panel.interpolate_bilinear( |
353 | | - xypts, img, |
354 | | - pad_with_nans=pad_with_nans, |
355 | | - on_panel=on_panel).reshape(self.shape) |
356 | | - else: |
357 | | - this_img = panel.interpolate_nearest( |
358 | | - xypts, img, |
359 | | - pad_with_nans=pad_with_nans).reshape(self.shape) |
360 | | - |
361 | | - # It is faster to keep track of the global nans like this |
362 | | - # rather than the previous way we were doing it... |
363 | | - img_nans = np.isnan(this_img) |
364 | | - if nan_mask is None: |
365 | | - nan_mask = img_nans |
| 398 | + # It's faster if we do _interpolate_bilinear ourselves, |
| 399 | + # since we already have all appropriate options set up. |
| 400 | + _interpolate_bilinear_in_place( |
| 401 | + img, |
| 402 | + **interp_dict, |
| 403 | + on_panel_idx=on_panel_idx, |
| 404 | + output_img=summed_img, |
| 405 | + ) |
366 | 406 | else: |
367 | | - nan_mask = np.logical_and(img_nans, nan_mask) |
| 407 | + summed_img += panel.interpolate_nearest( |
| 408 | + xypts, |
| 409 | + img, |
| 410 | + # DON'T pad with nans, so we can sum images together |
| 411 | + # correctly. We'll pad with nans later. |
| 412 | + pad_with_nans=False, |
| 413 | + ) |
| 414 | + |
| 415 | + # Now reshape the image to the appropriate shape |
| 416 | + output_img = summed_img.reshape(self.shape) |
368 | 417 |
|
369 | | - this_img[img_nans] = 0 |
370 | | - img_dict[detector_id] = this_img |
| 418 | + if pad_with_nans: |
| 419 | + # We pad with nans manually here |
| 420 | + output_img[nan_mask] = np.nan |
371 | 421 |
|
372 | | - summed_img = np.sum(list(img_dict.values()), axis=0) |
373 | 422 | return np.ma.masked_array( |
374 | | - data=summed_img, mask=nan_mask, fill_value=0. |
| 423 | + data=output_img, mask=nan_mask, fill_value=0. |
375 | 424 | ) |
376 | 425 |
|
377 | 426 | def tth_to_pixel(self, tth): |
|
0 commit comments