99 PHOSPHOR_DEFAULT ,
1010)
1111from hexrd .core .instrument .physics_package import AbstractPhysicsPackage
12+ import numba
1213import numpy as np
1314
1415from hexrd .core import constants as ct
@@ -1122,11 +1123,9 @@ def interpolate_nearest(self, xy, img, pad_with_nans=True):
11221123
11231124 def interpolate_bilinear (
11241125 self ,
1125- xy ,
1126- img ,
1127- pad_with_nans = True ,
1128- clip_to_panel = True ,
1129- on_panel : Optional [np .ndarray ] = None ,
1126+ xy : np .ndarray ,
1127+ img : np .ndarray ,
1128+ pad_with_nans : bool = True ,
11301129 ):
11311130 """
11321131 Interpolate an image array at the specified cartesian points.
@@ -1137,13 +1136,10 @@ def interpolate_bilinear(
11371136 Array of cartesian coordinates in the image plane at which
11381137 to evaluate intensity.
11391138 img : array_like
1140- 2-dimensional image array.
1139+ 2-dimensional image array. The shape must match (rows, cols).
11411140 pad_with_nans : bool, optional
11421141 Toggle for assigning NaN to points that fall off the detector.
11431142 The default is True.
1144- on_panel : np.ndarray, optional
1145- If you want to skip clip_to_panel() for performance reasons,
1146- just provide an array of which pixels are on the panel.
11471143
11481144 Returns
11491145 -------
@@ -1155,28 +1151,30 @@ def interpolate_bilinear(
11551151 -----
11561152 TODO: revisit normalization in here?
11571153 """
1154+ fill_value = np .nan if pad_with_nans else 0
1155+ int_xy = np .full (len (xy ), fill_value , dtype = float )
11581156
1159- is_2d = img .ndim == 2
1160- right_shape = img .shape [0 ] == self .rows and img .shape [1 ] == self .cols
1161- assert (
1162- is_2d and right_shape
1163- ), "input image must be 2-d with shape (%d, %d)" % (
1164- self .rows ,
1165- self .cols ,
1166- )
1157+ # clip away points too close to or off the detector edges
1158+ xy_clip , on_panel = self .clip_to_panel (xy , buffer_edges = True )
11671159
1168- # initialize output with nans
1169- if pad_with_nans :
1170- int_xy = np .nan * np .ones (len (xy ))
1171- else :
1172- int_xy = np .zeros (len (xy ))
1160+ # Generate the interpolation dict
1161+ interp_dict = self ._generate_bilinear_interp_dict (xy_clip )
11731162
1174- if on_panel is None :
1175- # clip away points too close to or off the edges of the detector
1176- xy_clip , on_panel = self .clip_to_panel (xy , buffer_edges = True )
1177- else :
1178- xy_clip = xy [on_panel ]
1163+ # Set the output and return
1164+ int_xy [on_panel ] = _interpolate_bilinear (img , ** interp_dict )
1165+ return int_xy
11791166
1167+ def _generate_bilinear_interp_dict (
1168+ self ,
1169+ xy_clip : np .ndarray ,
1170+ ) -> dict [str , np .ndarray ]:
1171+ """Compute bilinear interpolation multipliers and indices for the panel
1172+
1173+ If you are going to be using the same panel settings and performing
1174+ interpolation on multiple images, it is advised to run this beforehand
1175+ to precompute the interpolation parameters, so you can use them
1176+ repeatedly.
1177+ """
11801178 # grab fractional pixel indices of clipped points
11811179 ij_frac = self .cartToPixel (xy_clip )
11821180
@@ -1196,20 +1194,24 @@ def interpolate_bilinear(
11961194 j_ceil = j_floor + 1
11971195 j_ceil_img = _fix_indices (j_ceil , 0 , self .cols - 1 )
11981196
1199- # first interpolate at top/bottom rows
1200- row_floor_int = (j_ceil - ij_frac [:, 1 ]) * img [
1201- i_floor_img , j_floor_img
1202- ] + (ij_frac [:, 1 ] - j_floor ) * img [i_floor_img , j_ceil_img ]
1203- row_ceil_int = (j_ceil - ij_frac [:, 1 ]) * img [
1204- i_ceil_img , j_floor_img
1205- ] + (ij_frac [:, 1 ] - j_floor ) * img [i_ceil_img , j_ceil_img ]
1206-
1207- # next interpolate across cols
1208- int_vals = (i_ceil - ij_frac [:, 0 ]) * row_floor_int + (
1209- ij_frac [:, 0 ] - i_floor
1210- ) * row_ceil_int
1211- int_xy [on_panel ] = int_vals
1212- return int_xy
1197+ # Compute differences between raw coordinates to use for interpolation
1198+ j_ceil_sub = j_ceil - ij_frac [:, 1 ]
1199+ j_floor_sub = ij_frac [:, 1 ] - j_floor
1200+ i_ceil_sub = i_ceil - ij_frac [:, 0 ]
1201+ i_floor_sub = ij_frac [:, 0 ] - i_floor
1202+
1203+ return {
1204+ # Compute interpolation multipliers for every pixel
1205+ 'cc' : j_ceil_sub * i_ceil_sub ,
1206+ 'fc' : j_floor_sub * i_ceil_sub ,
1207+ 'cf' : j_ceil_sub * i_floor_sub ,
1208+ 'ff' : j_floor_sub * i_floor_sub ,
1209+ # Store needed pixel indices
1210+ 'i_floor_img' : i_floor_img ,
1211+ 'j_floor_img' : j_floor_img ,
1212+ 'i_ceil_img' : i_ceil_img ,
1213+ 'j_ceil_img' : j_ceil_img ,
1214+ }
12131215
12141216 def make_powder_rings (
12151217 self ,
@@ -2138,3 +2140,63 @@ def _row_edge_vec(rows, pixel_size_row):
21382140
21392141def _col_edge_vec (cols , pixel_size_col ):
21402142 return pixel_size_col * (np .arange (cols + 1 ) - 0.5 * cols )
2143+
2144+
2145+ @numba .njit (nogil = True , cache = True )
2146+ def _interpolate_bilinear (
2147+ img : np .ndarray ,
2148+ cc : np .ndarray ,
2149+ fc : np .ndarray ,
2150+ cf : np .ndarray ,
2151+ ff : np .ndarray ,
2152+ i_floor_img : np .ndarray ,
2153+ j_floor_img : np .ndarray ,
2154+ i_ceil_img : np .ndarray ,
2155+ j_ceil_img : np .ndarray ,
2156+ ) -> np .ndarray :
2157+ # The math is faster and uses the GIL less (which is more
2158+ # multi-threading friendly) when we run this code in numba.
2159+ result = np .zeros (i_floor_img .shape [0 ], dtype = img .dtype )
2160+ on_panel_idx = np .arange (i_floor_img .shape [0 ])
2161+ _interpolate_bilinear_in_place (
2162+ img ,
2163+ cc ,
2164+ fc ,
2165+ cf ,
2166+ ff ,
2167+ i_floor_img ,
2168+ j_floor_img ,
2169+ i_ceil_img ,
2170+ j_ceil_img ,
2171+ on_panel_idx ,
2172+ result ,
2173+ )
2174+ return result
2175+
2176+
2177+ @numba .njit (nogil = True , cache = True )
2178+ def _interpolate_bilinear_in_place (
2179+ img : np .ndarray ,
2180+ cc : np .ndarray ,
2181+ fc : np .ndarray ,
2182+ cf : np .ndarray ,
2183+ ff : np .ndarray ,
2184+ i_floor_img : np .ndarray ,
2185+ j_floor_img : np .ndarray ,
2186+ i_ceil_img : np .ndarray ,
2187+ j_ceil_img : np .ndarray ,
2188+ on_panel_idx : np .ndarray ,
2189+ output_img : np .ndarray ,
2190+ ):
2191+ # The math is faster and uses the GIL less (which is more
2192+ # multi-threading friendly) when we run this code in numba.
2193+ # Running in-place eliminates some intermediary arrays for
2194+ # even faster performance.
2195+ for i in range (on_panel_idx .shape [0 ]):
2196+ idx = on_panel_idx [i ]
2197+ output_img [idx ] += (
2198+ cc [i ] * img [i_floor_img [i ], j_floor_img [i ]] +
2199+ fc [i ] * img [i_floor_img [i ], j_ceil_img [i ]] +
2200+ cf [i ] * img [i_ceil_img [i ], j_floor_img [i ]] +
2201+ ff [i ] * img [i_ceil_img [i ], j_ceil_img [i ]]
2202+ )
0 commit comments