def test_shift(original, dx, dy): # Rotation center for all translation tests. image_center = np.array(original.shape) / 2.0 - 0.5 # No rotation for all translation tests. rmatrix = np.array([[1.0, 0.0], [0.0, 1.0]]) # Check a shifted shape against expected outcome expected = np.roll(np.roll(original, dx, axis=1), dy, axis=0) rcen = image_center - np.array([dx, dy]) shift = affine_transform(original, rmatrix=rmatrix, recenter=True, image_center=rcen) ymin, ymax = max([0, dy]), min([original.shape[1], original.shape[1] + dy]) xmin, xmax = max([0, dx]), min([original.shape[0], original.shape[0] + dx]) assert compare_results(expected[ymin:ymax, xmin:xmax], shift[ymin:ymax, xmin:xmax]) # Check shifted and unshifted shape against original image rcen = image_center + np.array([dx, dy]) unshift = affine_transform(shift, rmatrix=rmatrix, recenter=True, image_center=rcen) # Need to ignore the portion of the image cut off by the first shift ymin, ymax = max([0, -dy]), min([original.shape[1], original.shape[1] - dy]) xmin, xmax = max([0, -dx]), min([original.shape[0], original.shape[0] - dx]) assert compare_results(original[ymin:ymax, xmin:xmax], unshift[ymin:ymax, xmin:xmax])
def test_all(angle, dx, dy, scale_factor): """ Tests to make sure that combinations of scaling, shifting and rotation produce the expected output. """ k = int(angle / 90) angle = np.radians(angle) image_center = np.array(original.shape) / 2.0 - 0.5 # Check a shifted, rotated and scaled shape against expected outcome c = np.cos(angle) s = np.sin(angle) rmatrix = np.array([[c, -s], [s, c]]) scale = tf.rescale(original / original.max(), scale_factor, order=4, mode='constant', multichannel=False, anti_aliasing=False) * original.max() new = np.zeros(original.shape) # Old width and new center of image w = np.array(original.shape[0])/2.0 - 0.5 new_c = (np.array(scale.shape[0])/2.0 - 0.5) upper = int(w+new_c+1) if scale_factor > 1: lower = int(new_c-w) new = scale[lower:upper, lower:upper] else: lower = int(w-new_c) new[lower:upper, lower:upper] = scale disp = np.array([dx, dy]) rcen = image_center + disp rot = np.rot90(new, k=k) shift = np.roll(np.roll(rot, dx, axis=1), dy, axis=0) expected = shift rotscaleshift = affine_transform(original, rmatrix=rmatrix, scale=scale_factor, recenter=True, image_center=rcen) w = np.array(expected.shape[0])/2.0 - 0.5 new_c = (np.array(rotscaleshift.shape[0])/2.0 - 0.5) upper = int(w+new_c+1) if scale_factor > 1: lower = int(new_c-w) expected = rotscaleshift[lower:upper, lower:upper] else: lower = int(w-new_c) expected[lower:upper, lower:upper] = rotscaleshift compare_results(expected, rotscaleshift) # Check a rotated/shifted and restored image against original transformed = affine_transform(original, rmatrix=rmatrix, scale=1.0, recenter=True, image_center=rcen) rcen = image_center - np.dot(rmatrix, np.array([dx, dy])) dx, dy = np.asarray(np.dot(rmatrix, disp), dtype=int) rmatrix = np.array([[c, s], [-s, c]]) inverse = affine_transform(transformed, rmatrix=rmatrix, scale=1.0, recenter=True, image_center=rcen) # Need to ignore the portion of the image cut off by the first shift # (which isn't the portion you'd expect, because of the rotation) ymin, ymax = max([0, -dy]), min([original.shape[1], original.shape[1]-dy]) xmin, xmax = max([0, -dx]), min([original.shape[0], original.shape[0]-dx]) compare_results(original[ymin:ymax, xmin:xmax], inverse[ymin:ymax, xmin:xmax])
def test_clipping(rot30): # Generates a plot to test the clipping the output image to the range of the input image image = np.ones((20, 20)) image[4:-4, 4:-4] = 2 num_methods = len(_rotation_registry.keys()) fig = Figure(figsize=(12, 2 * num_methods)) axs = fig.subplots(nrows=num_methods, ncols=5) for i, method in enumerate(_rotation_registry.keys()): axs[i, 0].imshow(image, vmin=0, vmax=3) axs[i, 1].imshow(affine_transform(image, rot30, clip=False, method=method, missing=0), vmin=0, vmax=3) axs[i, 2].imshow(affine_transform(image, rot30, clip=True, method=method, missing=0), vmin=0, vmax=3) axs[i, 3].imshow(affine_transform(image, rot30, clip=True, method=method, missing=2), vmin=0, vmax=3) axs[i, 4].imshow(affine_transform(image, rot30, clip=True, method=method, missing=np.nan), vmin=0, vmax=3) axs[i, 0].set_ylabel(method) axs[0, 0].set_title('Original') axs[0, 1].set_title('no clip & missing=0') axs[0, 2].set_title('clip & missing=0') axs[0, 3].set_title('clip & missing=2') axs[0, 4].set_title('clip & missing=NaN') return fig
def test_scipy_rotation(angle, k): # Test rotation against expected outcome angle = np.radians(angle) c = np.cos(angle); s = np.sin(angle) rmatrix = np.array([[c, -s], [s, c]]) expected = np.rot90(original, k=k) rot = affine_transform(original, rmatrix=rmatrix, use_scipy=True) assert compare_results(expected, rot, allclose=False) # TODO: Check incremental 360 degree rotation against original image # Check derotated image against original derot_matrix = np.array([[c, s], [-s, c]]) derot = affine_transform(rot, rmatrix=derot_matrix, use_scipy=True) assert compare_results(original, derot, allclose=False)
def test_scale(original, scale_factor): # No rotation for all scaling tests. rmatrix = np.array([[1.0, 0.0], [0.0, 1.0]]) # Check a scaled image against the expected outcome # When we depend on SciPy 1.6, we can replace this with scipy.ndimage.zoom(..., grid_mode=True) newim = tf.rescale(original / original.max(), scale_factor, order=1, mode='constant', anti_aliasing=False) * original.max() # Old width and new center of image w = original.shape[0] / 2.0 - 0.5 new_c = (newim.shape[0] / 2.0) - 0.5 expected = np.zeros(original.shape) upper = int(w + new_c + 1) if scale_factor > 1: lower = int(new_c - w) expected = newim[lower:upper, lower:upper] else: lower = int(w - new_c) expected[lower:upper, lower:upper] = newim scale = affine_transform(original, rmatrix=rmatrix, scale=scale_factor, order=1, missing=0) assert compare_results(expected, scale)
def test_nan_scipy(identity): # Test replacement of NaN values for scipy rotation in_arr = np.array([[np.nan]]) with pytest.warns(SunpyUserWarning, match='Setting NaNs to 0 for SciPy rotation.'): out_arr = affine_transform(in_arr, rmatrix=identity, use_scipy=True) assert not np.all(np.isnan(out_arr))
def test_int(identity): # Test casting of integer array to float array in_arr = np.array([[100]], dtype=int) with pytest.warns(SunpyUserWarning, match='Integer input data has been cast to float64'): out_arr = affine_transform(in_arr, rmatrix=identity) assert np.issubdtype(out_arr.dtype, np.floating)
def test_scale(original, scale_factor): # No rotation for all scaling tests. rmatrix = np.array([[1.0, 0.0], [0.0, 1.0]]) # Check a scaled image against the expected outcome newim = tf.rescale(original / original.max(), scale_factor, order=4, mode='constant', multichannel=False, anti_aliasing=False) * original.max() # Old width and new center of image w = original.shape[0] / 2.0 - 0.5 new_c = (newim.shape[0] / 2.0) - 0.5 expected = np.zeros(original.shape) upper = int(w + new_c + 1) if scale_factor > 1: lower = int(new_c - w) expected = newim[lower:upper, lower:upper] else: lower = int(w - new_c) expected[lower:upper, lower:upper] = newim scale = affine_transform(original, rmatrix=rmatrix, scale=scale_factor, order=4) assert compare_results(expected, scale)
def test_nan_skimage_high(identity): # Test replacement of NaN values for scikit-image rotation with order >=4 in_arr = np.array([[np.nan]]) with pytest.warns( SunpyUserWarning, match='Setting NaNs to 0 for higher-order scikit-image rotation.'): out_arr = affine_transform(in_arr, rmatrix=identity, order=4) assert not np.all(np.isnan(out_arr))
def test_rotation(angle, k): # Test rotation against expected outcome angle = np.radians(angle) c = np.cos(angle); s = np.sin(angle) rmatrix = np.array([[c, -s], [s, c]]) expected = np.rot90(original, k=k) #Run the tests at order 4 as it produces more accurate 90 deg rotations rot = affine_transform(original, order=4, rmatrix=rmatrix) assert compare_results(expected, rot) # TODO: Check incremental 360 degree rotation against original image # Check derotated image against original derot_matrix = np.array([[c, s], [-s, c]]) derot = affine_transform(rot, order=4, rmatrix=derot_matrix) assert compare_results(original, derot)
def test_nan_scipy(identity): # Test preservation of NaN values for scipy rotation in_arr = np.array([[np.nan, 0]]) out_arr = affine_transform(in_arr, rmatrix=identity, order=0, method='scipy') assert np.isnan(out_arr[0, 0])
def test_endian(method, order, rot30): if order not in _rotation_registry[method].allowed_orders: return # Test that the rotation output values do not change with input byte order native = np.ones((10, 10)) swapped = native.byteswap().newbyteorder() rot_native = affine_transform(native, rot30, order=order, method=method, missing=0) rot_swapped = affine_transform(swapped, rot30, order=order, method=method, missing=0) assert compare_results(rot_native, rot_swapped)
def test_deprecated_args(identity): in_arr = np.array([[100]]) with pytest.warns(SunpyDeprecationWarning, match="The 'use_scipy' argument is deprecated"): out_arr = affine_transform(in_arr, rmatrix=identity, use_scipy=True) with pytest.warns(SunpyDeprecationWarning, match="The 'use_scipy' argument is deprecated"): out_arr = affine_transform(in_arr, rmatrix=identity, use_scipy=False) with pytest.raises(ValueError, match="Method blah not in supported methods"): out_arr = affine_transform(in_arr, rmatrix=identity, method='blah') with pytest.warns(SunpyUserWarning, match="Using scipy instead of skimage for rotation"): out_arr = affine_transform(in_arr, rmatrix=identity, use_scipy=True, method='skimage')
def test_shift(dx, dy): # Rotation center for all translation tests. image_center = np.array(original.shape)/2.0 - 0.5 # No rotation for all translation tests. rmatrix = np.array([[1.0, 0.0], [0.0, 1.0]]) # Check a shifted shape against expected outcome expected = np.roll(np.roll(original, dx, axis=1), dy, axis=0) rcen = image_center + np.array([dx, dy]) shift = affine_transform(original, rmatrix=rmatrix, recenter=True, image_center=rcen) ymin, ymax = max([0, dy]), min([original.shape[1], original.shape[1]+dy]) xmin, xmax = max([0, dx]), min([original.shape[0], original.shape[0]+dx]) compare_results(expected[ymin:ymax, xmin:xmax], shift[ymin:ymax, xmin:xmax]) # Check shifted and unshifted shape against original image rcen = image_center - np.array([dx, dy]) unshift = affine_transform(shift, rmatrix=rmatrix, recenter=True, image_center=rcen) # Need to ignore the portion of the image cut off by the first shift ymin, ymax = max([0, -dy]), min([original.shape[1], original.shape[1]-dy]) xmin, xmax = max([0, -dx]), min([original.shape[0], original.shape[0]-dx]) compare_results(original[ymin:ymax, xmin:xmax], unshift[ymin:ymax, xmin:xmax])
def test_nans(rot30): # Generates a plot to test the preservation and expansions of NaNs by the rotation image_with_nans = np.ones((23, 23)) image_with_nans[4:-4, 4:-4] = 2 image_with_nans[9:-9, 9:-9] = np.nan num_methods = len(_rotation_registry.keys()) fig = Figure(figsize=(16, 2 * num_methods)) axs = fig.subplots(nrows=num_methods, ncols=7) axs[0, 0].set_title('Original (NaNs are white)') for j in range(6): axs[0, j + 1].set_title(f'order={j}') for i, method in enumerate(_rotation_registry.keys()): axs[i, 0].imshow(image_with_nans, vmin=-1.1, vmax=1.1) for j in range(6): if j not in _rotation_registry[method].allowed_orders: with pytest.raises(ValueError): affine_transform(image_with_nans, rot30, order=j, method=method, missing=np.nan) axs[i, j + 1].remove() else: axs[i, j + 1].imshow(affine_transform(image_with_nans, rot30, order=j, method=method, missing=np.nan), vmin=-1.1, vmax=1.1) axs[i, 0].set_ylabel(method) return fig
def mapRotation(self, zobs, datarec): theta = self.__getEtaMinusP(datarec) print(theta) #### 10/04/2018 #### #### correção do calculo da rotacao para radianos ### rmatrix = np.ndarray(shape=(2, 2), dtype=float) rmatrix[0][0] = m.cos(theta * (np.pi / 180)) rmatrix[0][1] = m.sin(theta * (np.pi / 180)) rmatrix[1][0] = -m.sin(theta * (np.pi / 180)) rmatrix[1][1] = m.cos(theta * (np.pi / 180)) maprotated = affine_transform(zobs, rmatrix) return maprotated
def test_scale(scale_factor): # No rotation for all scaling tests. rmatrix = np.array([[1.0, 0.0], [0.0, 1.0]]) # Check a scaled image against the expected outcome newim = tf.rescale(original/original.max(), scale_factor, order=4, mode='constant') * original.max() # Old width and new center of image w = original.shape[0]/2.0 - 0.5 new_c = (newim.shape[0]/2.0) - 0.5 expected = np.zeros(original.shape) upper = w+new_c+1 if scale_factor > 1: lower = new_c-w expected = newim[lower:upper, lower:upper] else: lower = w-new_c expected[lower:upper, lower:upper] = newim scale = affine_transform(original, rmatrix=rmatrix, scale=scale_factor) compare_results(expected, scale)
def test_nan_scipy(identity): # Test replacement of NaN values for scipy rotation in_arr = np.array([[np.nan]]) out_arr = affine_transform(in_arr, rmatrix=identity, use_scipy=True) assert not np.all(np.isnan(out_arr))
def test_int(identity): # Test casting of integer array to float array in_arr = np.array([[100]], dtype=int) out_arr = affine_transform(in_arr, rmatrix=identity) assert np.issubdtype(out_arr.dtype, np.float)
def rotate(self, angle=None, rmatrix=None, order=4, scale=1.0, rotation_center=(0,0), recenter=False, missing=0.0, use_scipy=False): """ Returns a new rotated and rescaled map. Specify either a rotation angle or a rotation matrix, but not both. If neither an angle or a rotation matrix are specified, the map will be rotated by the rotation angle in the metadata. Also updates the rotation_matrix attribute and any appropriate header data so that they correctly describe the new map. Parameters ---------- angle : float The angle (degrees) to rotate counterclockwise. rmatrix : 2x2 Linear transformation rotation matrix. order : int 0-5 Interpolation order to be used. When using scikit-image this parameter is passed into :func:`skimage.transform.warp` (e.g., 4 corresponds to bi-quartic interpolation). When using scipy it is passed into :func:`scipy.ndimage.interpolation.affine_transform` where it controls the order of the spline. Faster performance may be obtained at the cost of accuracy by using lower values. Default: 4 scale : float A scale factor for the image, default is no scaling rotation_center : tuple The axis of rotation in data coordinates Default: the origin in the data coordinate system recenter : bool If True, position the axis of rotation at the center of the new map Default: False missing : float The numerical value to fill any missing points after rotation. Default: 0.0 use_scipy : bool If True, forces the rotation to use :func:`scipy.ndimage.interpolation.affine_transform`, otherwise it uses the :func:`skimage.transform.warp`. Default: False, unless scikit-image can't be imported Returns ------- out : Map A new Map instance containing the rotated and rescaled data of the original map. See Also -------- sunpy.image.transform.affine_transform : The routine this method calls for the rotation. Notes ----- This function will remove old CROTA keywords from the header. This function will also convert a CDi_j matrix to a PCi_j matrix. See :func:`sunpy.image.transform.affine_transform` for details on the transformations, situations when the underlying data is modified prior to rotation, and differences from IDL's rot(). """ if angle is not None and rmatrix is not None: raise ValueError("You cannot specify both an angle and a matrix") elif angle is None and rmatrix is None: rmatrix = self.rotation_matrix # Interpolation parameter sanity if order not in range(6): raise ValueError("Order must be between 0 and 5") # Copy Map new_map = deepcopy(self) if angle is not None: # Calulate the parameters for the affine_transform c = np.cos(np.deg2rad(angle)) s = np.sin(np.deg2rad(angle)) rmatrix = np.matrix([[c, -s], [s, c]]) # Calculate the shape in pixels to contain all of the image data extent = np.max(np.abs(np.vstack((new_map.shape * rmatrix, new_map.shape * rmatrix.T))), axis=0) # Calculate the needed padding or unpadding diff = np.asarray(np.ceil((extent - new_map.shape) / 2)).ravel() # Pad the image array pad_x = np.max((diff[1], 0)) pad_y = np.max((diff[0], 0)) new_map.data = np.pad(new_map.data, ((pad_y, pad_y), (pad_x, pad_x)), mode='constant', constant_values=(missing, missing)) new_map.meta['crpix1'] += pad_x new_map.meta['crpix2'] += pad_y # map_center is swapped compared to the x-y convention array_center = (np.array(new_map.data.shape)-1)/2.0 # pixel_center is swapped compared to the x-y convention if recenter: # Convert the axis of rotation from data coordinates to pixel coordinates x = new_map.data_to_pixel(rotation_center[0], 'x') y = new_map.data_to_pixel(rotation_center[1], 'y') pixel_center = (y, x) else: pixel_center = array_center # Apply the rotation to the image data new_map.data = affine_transform(new_map.data.T, np.asarray(rmatrix), order=order, scale=scale, image_center=pixel_center, recenter=recenter, missing=missing, use_scipy=use_scipy).T # Calculate new reference pixel and coordinate at the center of the # image. if recenter: new_center = rotation_center else: # Retrieve old coordinates for the center of the array old_center = np.asarray(new_map.pixel_to_data(array_center[1], array_center[0])) # Calculate new coordinates for the center of the array new_center = rotation_center - np.dot(rmatrix, rotation_center - old_center) new_center = np.asarray(new_center)[0] # Define a new reference pixel in the rotated space new_map.meta['crval1'] = new_center[0] new_map.meta['crval2'] = new_center[1] new_map.meta['crpix1'] = array_center[1] + 1 # FITS counts pixels from 1 new_map.meta['crpix2'] = array_center[0] + 1 # FITS counts pixels from 1 # Unpad the array if necessary unpad_x = -np.min((diff[1], 0)) if unpad_x > 0: new_map.data = new_map.data[:, unpad_x:-unpad_x] new_map.meta['crpix1'] -= unpad_x unpad_y = -np.min((diff[0], 0)) if unpad_y > 0: new_map.data = new_map.data[unpad_y:-unpad_y, :] new_map.meta['crpix2'] -= unpad_y # Calculate the new rotation matrix to store in the header by # "subtracting" the rotation matrix used in the rotate from the old one # That being calculate the dot product of the old header data with the # inverse of the rotation matrix. pc_C = np.dot(new_map.rotation_matrix, rmatrix.I) new_map.meta['PC1_1'] = pc_C[0,0] new_map.meta['PC1_2'] = pc_C[0,1] new_map.meta['PC2_1'] = pc_C[1,0] new_map.meta['PC2_2'] = pc_C[1,1] # Update pixel size if image has been scaled. if scale != 1.0: new_map.meta['cdelt1'] = new_map.scale['x'] / scale new_map.meta['cdelt2'] = new_map.scale['y'] / scale # Remove old CROTA kwargs because we have saved a new PCi_j matrix. new_map.meta.pop('CROTA1', None) new_map.meta.pop('CROTA2', None) # Remove CDi_j header new_map.meta.pop('CD1_1', None) new_map.meta.pop('CD1_2', None) new_map.meta.pop('CD2_1', None) new_map.meta.pop('CD2_2', None) return new_map
def test_nan_skimage_high(identity): # Test replacement of NaN values for scikit-image rotation with order >=4 in_arr = np.array([[np.nan]]) out_arr = affine_transform(in_arr, rmatrix=identity, order=4) assert not np.all(np.isnan(out_arr))
def test_int(identity): # Test casting of integer array to float array in_arr = np.array([[100]], dtype=int) with pytest.warns(SunpyUserWarning, match='Input data has been cast to float64.'): out_arr = affine_transform(in_arr, rmatrix=identity) assert np.issubdtype(out_arr.dtype, np.floating)
def test_flat(identity): # Test that a flat array can be rotated using scikit-image in_arr = np.array([[100]]) out_arr = affine_transform(in_arr, rmatrix=identity) assert np.allclose(in_arr, out_arr, rtol=rtol)
def rotate(self, angle=None, rmatrix=None, order=3, scale=1.0, image_center=None, recenter=False, missing=0.0, use_scipy=False): """ Returns a new rotated and rescaled map. Specify either a rotation angle or a rotation matrix, but not both. If neither an angle or a rotation matrix are specified, the map will be rotated by the rotation angle in the metadata. Also updates the rotation_matrix attribute and any appropriate header data so that they correctly describe the new map. Parameters ---------- angle : float The angle (degrees) to rotate counterclockwise. rmatrix : 2x2 Linear transformation rotation matrix. order : int 0-5 Interpolation order to be used. When using scikit-image this parameter is passed into :func:`skimage.transform.warp`. When using scipy it is passed into :func:`scipy.ndimage.interpolation.affine_transform` where it controls the order of the spline. scale : float A scale factor for the image, default is no scaling image_center : tuple The axis of rotation in pixel coordinates Default: the origin in the data coordinate system recenter : bool If True, position the axis of rotation at the center of the new map Default: False missing : float The numerical value to fill any missing points after rotation. Default: 0.0 use_scipy : bool If True, forces the rotation to use :func:`scipy.ndimage.interpolation.affine_transform`, otherwise it uses the :class:`skimage.transform.AffineTransform` class and :func:`skimage.transform.warp`. The function will also automatically fall back to :func:`scipy.ndimage.interpolation.affine_transform` if scikit-image can't be imported. Default: False Returns ------- out : Map A new Map instance containing the rotated and rescaled data of the original map. Notes ----- This function will remove old CROTA keywords from the header. This function will also convert a CDi_j matrix to a PCi_j matrix. This function is not numerically equalivalent to IDL's rot() see the :func:`sunpy.image.transform.affine_transform` documentation for a detailed description of the differences. """ if angle is not None and rmatrix is not None: raise ValueError("You cannot specify both an angle and a matrix") elif angle is None and rmatrix is None: rmatrix = self.rotation_matrix # Interpolation parameter sanity if order not in range(6): raise ValueError("Order must be between 0 and 5") # Copy Map new_map = deepcopy(self) if angle is not None: #Calulate the parameters for the affine_transform c = np.cos(np.deg2rad(angle)) s = np.sin(np.deg2rad(angle)) rmatrix = np.matrix([[c, -s], [s, c]]) if image_center is None: # FITS pixels count from 1 (curse you, FITS!) image_center = (self.shape[1] - self.reference_pixel['x'] + 1, self.reference_pixel['y']) # Because map data has the origin at the bottom left not the top left # as is convention for images vertically flip the image for the # transform and then flip it back again. new_map.data = np.flipud(affine_transform(np.flipud(new_map.data), np.array(rmatrix), order=order, scale=scale, image_center=image_center, recenter=recenter, missing=missing, use_scipy=use_scipy)) map_center = (np.array(self.shape)/2.0) + 0.5 # Calculate the new rotation matrix to store in the header by # "subtracting" the rotation matrix used in the rotate from the old one # That being calculate the dot product of the old header data with the # inverse of the rotation matrix. pc_C = np.dot(self.rotation_matrix, rmatrix.I) new_map.meta['PC1_1'] = pc_C[0,0] new_map.meta['PC1_2'] = pc_C[0,1] new_map.meta['PC2_1'] = pc_C[1,0] new_map.meta['PC2_2'] = pc_C[1,1] # Update pixel size if image has been scaled. if scale != 1.0: new_map.meta['cdelt1'] = self.scale['x'] / scale new_map.meta['cdelt2'] = self.scale['y'] / scale if recenter: # Move the reference pixel based on the image shift. # The y coordinate is inverted due to the map having the origin in # the lower left rather than the upper left. shift = image_center - map_center new_map.meta['crpix1'] += shift[0] new_map.meta['crpix2'] -= shift[1] # Remove old CROTA kwargs because we have saved a new PCi_j matrix. new_map.meta.pop('CROTA1', None) new_map.meta.pop('CROTA2', None) # Remove CDi_j header new_map.meta.pop('CD1_1', None) new_map.meta.pop('CD1_2', None) new_map.meta.pop('CD2_1', None) new_map.meta.pop('CD2_2', None) return new_map
def test_nan_skimage_high(identity): # Test replacement of NaN values for scikit-image rotation with order >=4 in_arr = np.array([[np.nan]]) with pytest.warns(SunpyUserWarning, match='Setting NaNs to 0 for higher-order scikit-image rotation.'): out_arr = affine_transform(in_arr, rmatrix=identity, order=4) assert not np.all(np.isnan(out_arr))
def test_nan_skimage_low(identity): # Test non-replacement of NaN values for scikit-image rotation with order <= 3 in_arr = np.array([[np.nan]]) out_arr = affine_transform(in_arr, rmatrix=identity, order=3) assert np.all(np.isnan(out_arr))
def test_flat(identity): # Test that a flat array can be rotated using scikit-image in_arr = np.array([[100]], dtype=np.float64) out_arr = affine_transform(in_arr, rmatrix=identity) assert np.allclose(in_arr, out_arr, rtol=RTOL)
def test_all(original, angle, dx, dy, scale_factor): """ Tests to make sure that combinations of scaling, shifting and rotation produce the expected output. """ k = int(angle / 90) angle = np.radians(angle) image_center = np.array(original.shape) / 2.0 - 0.5 # Check a shifted, rotated and scaled shape against expected outcome c = np.round(np.cos(angle)) s = np.round(np.sin(angle)) rmatrix = np.array([[c, -s], [s, c]]) scale = tf.rescale(original / original.max(), scale_factor, order=4, mode='constant', multichannel=False, anti_aliasing=False) * original.max() new = np.zeros(original.shape) disp = np.array([dx, dy]) dxs, dys = np.asarray(disp * scale_factor, dtype=int) # Old width and new center of image w = np.array(original.shape[0]) / 2.0 - 0.5 new_c = (np.array(scale.shape[0]) / 2.0 - 0.5) upper = int(w + new_c + 1) if scale_factor > 1: lower = int(new_c - w) new = scale[lower - dys:upper - dys, lower - dxs:upper - dxs] else: lower = int(w - new_c) new[lower + dys:upper + dys, lower + dxs:upper + dxs] = scale rcen = image_center - disp expected = np.rot90(new, k=k) rotscaleshift = affine_transform(original, rmatrix=rmatrix, scale=scale_factor, order=4, recenter=True, image_center=rcen) assert compare_results(expected, rotscaleshift) # Check a rotated/shifted and restored image against original transformed = affine_transform(original, rmatrix=rmatrix, scale=1.0, order=4, recenter=True, image_center=rcen) inv_rcen = image_center + np.dot(rmatrix.T, np.array([dx, dy])) inverse = affine_transform(transformed, rmatrix=rmatrix.T, scale=1.0, order=4, recenter=True, image_center=inv_rcen) # Need to ignore the portion of the image cut off by the first shift ymin, ymax = max([0, -dy]), min([original.shape[1], original.shape[1] - dy]) xmin, xmax = max([0, -dx]), min([original.shape[0], original.shape[0] - dx]) assert compare_results(original[ymin:ymax, xmin:xmax], inverse[ymin:ymax, xmin:xmax])
def test_float32(identity): # Check that float32 input remains as float32 output # Test casting of integer array to float array in_arr = np.array([[100]], dtype=np.float32) out_arr = affine_transform(in_arr, rmatrix=identity) assert np.issubdtype(out_arr.dtype, np.float32)
def rotate(self, angle=None, rmatrix=None, order=3, scale=1.0, image_center=(0,0), recenter=False, missing=0.0, use_scipy=False): """ Returns a new rotated and rescaled map. Specify either a rotation angle or a rotation matrix, but not both. If neither an angle or a rotation matrix are specified, the map will be rotated by the rotation angle in the metadata. Also updates the rotation_matrix attribute and any appropriate header data so that they correctly describe the new map. Parameters ---------- angle : float The angle (degrees) to rotate counterclockwise. rmatrix : 2x2 Linear transformation rotation matrix. order : int 0-5 Interpolation order to be used. When using scikit-image this parameter is passed into :func:`skimage.transform.warp`. When using scipy it is passed into :func:`scipy.ndimage.interpolation.affine_transform` where it controls the order of the spline. Higher accuracy may be obtained at the cost of performance by using higher values. scale : float A scale factor for the image, default is no scaling image_center : tuple The axis of rotation in data coordinates Default: the origin in the data coordinate system recenter : bool If True, position the axis of rotation at the center of the new map Default: False missing : float The numerical value to fill any missing points after rotation. Default: 0.0 use_scipy : bool If True, forces the rotation to use :func:`scipy.ndimage.interpolation.affine_transform`, otherwise it uses the :class:`skimage.transform.AffineTransform` class and :func:`skimage.transform.warp`. The function will also automatically fall back to :func:`scipy.ndimage.interpolation.affine_transform` if scikit-image can't be imported. Default: False Returns ------- out : Map A new Map instance containing the rotated and rescaled data of the original map. See Also -------- sunpy.image.transform.affine_transform : The routine this method calls for the rotation. Notes ----- This function will remove old CROTA keywords from the header. This function will also convert a CDi_j matrix to a PCi_j matrix. The scikit-image and scipy affine_transform routines do not use the same algorithm, see :func:`sunpy.image.transform.affine_transform` for details. This function is not numerically equalivalent to IDL's rot() see the :func:`sunpy.image.transform.affine_transform` documentation for a detailed description of the differences. """ if angle is not None and rmatrix is not None: raise ValueError("You cannot specify both an angle and a matrix") elif angle is None and rmatrix is None: rmatrix = self.rotation_matrix # Interpolation parameter sanity if order not in range(6): raise ValueError("Order must be between 0 and 5") # Copy Map new_map = deepcopy(self) if angle is not None: #Calulate the parameters for the affine_transform c = np.cos(np.deg2rad(angle)) s = np.sin(np.deg2rad(angle)) rmatrix = np.matrix([[c, -s], [s, c]]) # map_center is swapped compared to the x-y convention array_center = (np.array(self.data.shape)-1)/2.0 # rotation_center is swapped compared to the x-y convention if recenter: # Convert the axis of rotation from data coordinates to pixel coordinates x = self.data_to_pixel(image_center[0], 'x') y = self.data_to_pixel(image_center[1], 'y') rotation_center = (y, x) else: rotation_center = array_center #Return a new map #Copy Header new_map = deepcopy(self) new_map.data = affine_transform(new_map.data.T, np.asarray(rmatrix), order=order, scale=scale, image_center=rotation_center, recenter=recenter, missing=missing, use_scipy=use_scipy).T # Calculate new reference pixel and coordinate at the center of the # image. if recenter: new_center = image_center else: # Retrieve old coordinates for the center of the array old_center = np.asarray(self.pixel_to_data(array_center[1], array_center[0])) # Calculate new coordinates for the center of the array new_center = image_center - np.dot(rmatrix, image_center - old_center) new_center = np.asarray(new_center)[0] # Define a new reference pixel in the rotated space new_map.meta['crval1'] = new_center[0] new_map.meta['crval2'] = new_center[1] new_map.meta['crpix1'] = array_center[1] + 1 # FITS counts pixels from 1 new_map.meta['crpix2'] = array_center[0] + 1 # FITS counts pixels from 1 # Calculate the new rotation matrix to store in the header by # "subtracting" the rotation matrix used in the rotate from the old one # That being calculate the dot product of the old header data with the # inverse of the rotation matrix. pc_C = np.dot(self.rotation_matrix, rmatrix.I) new_map.meta['PC1_1'] = pc_C[0,0] new_map.meta['PC1_2'] = pc_C[0,1] new_map.meta['PC2_1'] = pc_C[1,0] new_map.meta['PC2_2'] = pc_C[1,1] # Update pixel size if image has been scaled. if scale != 1.0: new_map.meta['cdelt1'] = self.scale['x'] / scale new_map.meta['cdelt2'] = self.scale['y'] / scale # Remove old CROTA kwargs because we have saved a new PCi_j matrix. new_map.meta.pop('CROTA1', None) new_map.meta.pop('CROTA2', None) # Remove CDi_j header new_map.meta.pop('CD1_1', None) new_map.meta.pop('CD1_2', None) new_map.meta.pop('CD2_1', None) new_map.meta.pop('CD2_2', None) return new_map
def rotate(self, angle=None, rmatrix=None, order=3, scale=1.0, image_center=(0, 0), recenter=False, missing=0.0, use_scipy=False): """ Returns a new rotated and rescaled map. Specify either a rotation angle or a rotation matrix, but not both. If neither an angle or a rotation matrix are specified, the map will be rotated by the rotation angle in the metadata. Also updates the rotation_matrix attribute and any appropriate header data so that they correctly describe the new map. Parameters ---------- angle : float The angle (degrees) to rotate counterclockwise. rmatrix : 2x2 Linear transformation rotation matrix. order : int 0-5 Interpolation order to be used. When using scikit-image this parameter is passed into :func:`skimage.transform.warp`. When using scipy it is passed into :func:`scipy.ndimage.interpolation.affine_transform` where it controls the order of the spline. Higher accuracy may be obtained at the cost of performance by using higher values. scale : float A scale factor for the image, default is no scaling image_center : tuple The axis of rotation in data coordinates Default: the origin in the data coordinate system recenter : bool If True, position the axis of rotation at the center of the new map Default: False missing : float The numerical value to fill any missing points after rotation. Default: 0.0 use_scipy : bool If True, forces the rotation to use :func:`scipy.ndimage.interpolation.affine_transform`, otherwise it uses the :class:`skimage.transform.AffineTransform` class and :func:`skimage.transform.warp`. The function will also automatically fall back to :func:`scipy.ndimage.interpolation.affine_transform` if scikit-image can't be imported. Default: False Returns ------- out : Map A new Map instance containing the rotated and rescaled data of the original map. See Also -------- sunpy.image.transform.affine_transform : The routine this method calls for the rotation. Notes ----- This function will remove old CROTA keywords from the header. This function will also convert a CDi_j matrix to a PCi_j matrix. The scikit-image and scipy affine_transform routines do not use the same algorithm, see :func:`sunpy.image.transform.affine_transform` for details. This function is not numerically equalivalent to IDL's rot() see the :func:`sunpy.image.transform.affine_transform` documentation for a detailed description of the differences. """ if angle is not None and rmatrix is not None: raise ValueError("You cannot specify both an angle and a matrix") elif angle is None and rmatrix is None: rmatrix = self.rotation_matrix # Interpolation parameter sanity if order not in range(6): raise ValueError("Order must be between 0 and 5") # Copy Map new_map = deepcopy(self) if angle is not None: #Calulate the parameters for the affine_transform c = np.cos(np.deg2rad(angle)) s = np.sin(np.deg2rad(angle)) rmatrix = np.matrix([[c, -s], [s, c]]) # map_center is swapped compared to the x-y convention array_center = (np.array(self.data.shape) - 1) / 2.0 # rotation_center is swapped compared to the x-y convention if recenter: # Convert the axis of rotation from data coordinates to pixel coordinates x = self.data_to_pixel(image_center[0], 'x') y = self.data_to_pixel(image_center[1], 'y') rotation_center = (y, x) else: rotation_center = array_center #Return a new map #Copy Header new_map = deepcopy(self) new_map.data = affine_transform(new_map.data.T, np.asarray(rmatrix), order=order, scale=scale, image_center=rotation_center, recenter=recenter, missing=missing, use_scipy=use_scipy).T # Calculate new reference pixel and coordinate at the center of the # image. if recenter: new_center = image_center else: # Retrieve old coordinates for the center of the array old_center = np.asarray( self.pixel_to_data(array_center[1], array_center[0])) # Calculate new coordinates for the center of the array new_center = image_center - np.dot(rmatrix, image_center - old_center) new_center = np.asarray(new_center)[0] # Define a new reference pixel in the rotated space new_map.meta['crval1'] = new_center[0] new_map.meta['crval2'] = new_center[1] new_map.meta[ 'crpix1'] = array_center[1] + 1 # FITS counts pixels from 1 new_map.meta[ 'crpix2'] = array_center[0] + 1 # FITS counts pixels from 1 # Calculate the new rotation matrix to store in the header by # "subtracting" the rotation matrix used in the rotate from the old one # That being calculate the dot product of the old header data with the # inverse of the rotation matrix. pc_C = np.dot(self.rotation_matrix, rmatrix.I) new_map.meta['PC1_1'] = pc_C[0, 0] new_map.meta['PC1_2'] = pc_C[0, 1] new_map.meta['PC2_1'] = pc_C[1, 0] new_map.meta['PC2_2'] = pc_C[1, 1] # Update pixel size if image has been scaled. if scale != 1.0: new_map.meta['cdelt1'] = self.scale['x'] / scale new_map.meta['cdelt2'] = self.scale['y'] / scale # Remove old CROTA kwargs because we have saved a new PCi_j matrix. new_map.meta.pop('CROTA1', None) new_map.meta.pop('CROTA2', None) # Remove CDi_j header new_map.meta.pop('CD1_1', None) new_map.meta.pop('CD1_2', None) new_map.meta.pop('CD2_1', None) new_map.meta.pop('CD2_2', None) return new_map
def rotate(self, angle=None, rmatrix=None, order=4, scale=1.0, recenter=False, missing=0.0, use_scipy=False): """ Returns a new rotated and rescaled map. Specify either a rotation angle or a rotation matrix, but not both. If neither an angle or a rotation matrix are specified, the map will be rotated by the rotation angle in the metadata. The map will be rotated around the reference coordinate defined in the meta data. Also updates the rotation_matrix attribute and any appropriate header data so that they correctly describe the new map. Parameters ---------- angle : `~astropy.units.Quantity` The angle (degrees) to rotate counterclockwise. rmatrix : 2x2 Linear transformation rotation matrix. order : int 0-5 Interpolation order to be used. When using scikit-image this parameter is passed into :func:`skimage.transform.warp` (e.g., 4 corresponds to bi-quartic interpolation). When using scipy it is passed into :func:`scipy.ndimage.interpolation.affine_transform` where it controls the order of the spline. Faster performance may be obtained at the cost of accuracy by using lower values. Default: 4 scale : float A scale factor for the image, default is no scaling recenter : bool If True, position the axis of rotation at the center of the new map Default: False missing : float The numerical value to fill any missing points after rotation. Default: 0.0 use_scipy : bool If True, forces the rotation to use :func:`scipy.ndimage.interpolation.affine_transform`, otherwise it uses the :func:`skimage.transform.warp`. Default: False, unless scikit-image can't be imported Returns ------- out : Map A new Map instance containing the rotated and rescaled data of the original map. See Also -------- sunpy.image.transform.affine_transform : The routine this method calls for the rotation. Notes ----- This function will remove old CROTA keywords from the header. This function will also convert a CDi_j matrix to a PCi_j matrix. See :func:`sunpy.image.transform.affine_transform` for details on the transformations, situations when the underlying data is modified prior to rotation, and differences from IDL's rot(). """ if angle is not None and rmatrix is not None: raise ValueError("You cannot specify both an angle and a matrix") elif angle is None and rmatrix is None: rmatrix = self.rotation_matrix # This is out of the quantity_input decorator. To allow the angle=None # case. See https://github.com/astropy/astropy/issues/3734 if angle: try: equivalent = angle.unit.is_equivalent(u.deg) if not equivalent: raise u.UnitsError("Argument '{0}' to function '{1}'" " must be in units convertable to" " '{2}'.".format('angle', 'rotate', u.deg.to_string())) # Either there is no .unit or no .is_equivalent except AttributeError: if hasattr(angle, "unit"): error_msg = "a 'unit' attribute without an 'is_equivalent' method" else: error_msg = "no 'unit' attribute" raise TypeError("Argument '{0}' to function '{1}' has {2}. " "You may want to pass in an astropy Quantity instead." .format('angle', 'rotate', error_msg)) # Interpolation parameter sanity if order not in range(6): raise ValueError("Order must be between 0 and 5") # The FITS-WCS transform is by definition defined around the # reference coordinate in the header. rotation_center = u.Quantity([self.reference_coordinate.x, self.reference_coordinate.y]) # Copy Map new_map = deepcopy(self) if angle is not None: # Calulate the parameters for the affine_transform c = np.cos(np.deg2rad(angle)) s = np.sin(np.deg2rad(angle)) rmatrix = np.matrix([[c, -s], [s, c]]) # Calculate the shape in pixels to contain all of the image data extent = np.max(np.abs(np.vstack((new_map.data.shape * rmatrix, new_map.data.shape * rmatrix.T))), axis=0) # Calculate the needed padding or unpadding diff = np.asarray(np.ceil((extent - new_map.data.shape) / 2)).ravel() # Pad the image array pad_x = np.max((diff[1], 0)) pad_y = np.max((diff[0], 0)) new_map.data = np.pad(new_map.data, ((pad_y, pad_y), (pad_x, pad_x)), mode='constant', constant_values=(missing, missing)) new_map.meta['crpix1'] += pad_x new_map.meta['crpix2'] += pad_y # All of the following pixel calculations use a pixel origin of 0 pixel_array_center = (np.flipud(new_map.data.shape) - 1) / 2.0 # Convert the axis of rotation from data coordinates to pixel coordinates pixel_rotation_center = u.Quantity(new_map.data_to_pixel(*rotation_center, origin=0)).value if recenter: pixel_center = pixel_rotation_center else: pixel_center = pixel_array_center # Apply the rotation to the image data new_map.data = affine_transform(new_map.data.T, np.asarray(rmatrix), order=order, scale=scale, image_center=np.flipud(pixel_center), recenter=recenter, missing=missing, use_scipy=use_scipy).T if recenter: new_reference_pixel = pixel_array_center else: # Calculate new pixel coordinates for the rotation center new_reference_pixel = pixel_center + np.dot(rmatrix, pixel_rotation_center - pixel_center) new_reference_pixel = np.array(new_reference_pixel).ravel() # Define the new reference_pixel new_map.meta['crval1'] = rotation_center[0].value new_map.meta['crval2'] = rotation_center[1].value new_map.meta['crpix1'] = new_reference_pixel[0] + 1 # FITS pixel origin is 1 new_map.meta['crpix2'] = new_reference_pixel[1] + 1 # FITS pixel origin is 1 # Unpad the array if necessary unpad_x = -np.min((diff[1], 0)) if unpad_x > 0: new_map.data = new_map.data[:, unpad_x:-unpad_x] new_map.meta['crpix1'] -= unpad_x unpad_y = -np.min((diff[0], 0)) if unpad_y > 0: new_map.data = new_map.data[unpad_y:-unpad_y, :] new_map.meta['crpix2'] -= unpad_y # Calculate the new rotation matrix to store in the header by # "subtracting" the rotation matrix used in the rotate from the old one # That being calculate the dot product of the old header data with the # inverse of the rotation matrix. pc_C = np.dot(new_map.rotation_matrix, rmatrix.I) new_map.meta['PC1_1'] = pc_C[0,0] new_map.meta['PC1_2'] = pc_C[0,1] new_map.meta['PC2_1'] = pc_C[1,0] new_map.meta['PC2_2'] = pc_C[1,1] # Update pixel size if image has been scaled. if scale != 1.0: new_map.meta['cdelt1'] = (new_map.scale.x / scale).value new_map.meta['cdelt2'] = (new_map.scale.y / scale).value # Remove old CROTA kwargs because we have saved a new PCi_j matrix. new_map.meta.pop('CROTA1', None) new_map.meta.pop('CROTA2', None) # Remove CDi_j header new_map.meta.pop('CD1_1', None) new_map.meta.pop('CD1_2', None) new_map.meta.pop('CD2_1', None) new_map.meta.pop('CD2_2', None) return new_map