233 lines
6.9 KiB
Python
233 lines
6.9 KiB
Python
import numpy as np
|
|
|
|
from ..util import img_as_float
|
|
from .._shared.utils import (
|
|
_supported_float_type,
|
|
check_nD,
|
|
)
|
|
|
|
|
|
class FeatureDetector:
|
|
def __init__(self):
|
|
self.keypoints_ = np.array([])
|
|
|
|
def detect(self, image):
|
|
"""Detect keypoints in image.
|
|
|
|
Parameters
|
|
----------
|
|
image : 2D array
|
|
Input image.
|
|
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
|
|
class DescriptorExtractor:
|
|
def __init__(self):
|
|
self.descriptors_ = np.array([])
|
|
|
|
def extract(self, image, keypoints):
|
|
"""Extract feature descriptors in image for given keypoints.
|
|
|
|
Parameters
|
|
----------
|
|
image : 2D array
|
|
Input image.
|
|
keypoints : (N, 2) array
|
|
Keypoint locations as ``(row, col)``.
|
|
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
|
|
def plot_matched_features(
|
|
image0,
|
|
image1,
|
|
*,
|
|
keypoints0,
|
|
keypoints1,
|
|
matches,
|
|
ax,
|
|
keypoints_color='k',
|
|
matches_color=None,
|
|
only_matches=False,
|
|
alignment='horizontal',
|
|
):
|
|
"""Plot matched features between two images.
|
|
|
|
.. versionadded:: 0.23
|
|
|
|
Parameters
|
|
----------
|
|
image0 : (N, M [, 3]) array
|
|
First image.
|
|
image1 : (N, M [, 3]) array
|
|
Second image.
|
|
keypoints0 : (K1, 2) array
|
|
First keypoint coordinates as ``(row, col)``.
|
|
keypoints1 : (K2, 2) array
|
|
Second keypoint coordinates as ``(row, col)``.
|
|
matches : (Q, 2) array
|
|
Indices of corresponding matches in first and second sets of
|
|
descriptors, where `matches[:, 0]` (resp. `matches[:, 1]`) contains
|
|
the indices in the first (resp. second) set of descriptors.
|
|
ax : matplotlib.axes.Axes
|
|
The Axes object where the images and their matched features are drawn.
|
|
keypoints_color : matplotlib color, optional
|
|
Color for keypoint locations.
|
|
matches_color : matplotlib color or sequence thereof, optional
|
|
Single color or sequence of colors for each line defined by `matches`,
|
|
which connect keypoint matches. See [1]_ for an overview of supported
|
|
color formats. By default, colors are picked randomly.
|
|
only_matches : bool, optional
|
|
Set to True to plot matches only and not the keypoint locations.
|
|
alignment : {'horizontal', 'vertical'}, optional
|
|
Whether to show the two images side by side (`'horizontal'`), or one above
|
|
the other (`'vertical'`).
|
|
|
|
References
|
|
----------
|
|
.. [1] https://matplotlib.org/stable/users/explain/colors/colors.html#specifying-colors
|
|
|
|
Notes
|
|
-----
|
|
To make a sequence of colors passed to `matches_color` work for any number of
|
|
`matches`, you can wrap that sequence in :func:`itertools.cycle`.
|
|
"""
|
|
image0 = img_as_float(image0)
|
|
image1 = img_as_float(image1)
|
|
|
|
new_shape0 = list(image0.shape)
|
|
new_shape1 = list(image1.shape)
|
|
|
|
if image0.shape[0] < image1.shape[0]:
|
|
new_shape0[0] = image1.shape[0]
|
|
elif image0.shape[0] > image1.shape[0]:
|
|
new_shape1[0] = image0.shape[0]
|
|
|
|
if image0.shape[1] < image1.shape[1]:
|
|
new_shape0[1] = image1.shape[1]
|
|
elif image0.shape[1] > image1.shape[1]:
|
|
new_shape1[1] = image0.shape[1]
|
|
|
|
if new_shape0 != image0.shape:
|
|
new_image0 = np.zeros(new_shape0, dtype=image0.dtype)
|
|
new_image0[: image0.shape[0], : image0.shape[1]] = image0
|
|
image0 = new_image0
|
|
|
|
if new_shape1 != image1.shape:
|
|
new_image1 = np.zeros(new_shape1, dtype=image1.dtype)
|
|
new_image1[: image1.shape[0], : image1.shape[1]] = image1
|
|
image1 = new_image1
|
|
|
|
offset = np.array(image0.shape)
|
|
if alignment == 'horizontal':
|
|
image = np.concatenate([image0, image1], axis=1)
|
|
offset[0] = 0
|
|
elif alignment == 'vertical':
|
|
image = np.concatenate([image0, image1], axis=0)
|
|
offset[1] = 0
|
|
else:
|
|
mesg = (
|
|
f"`plot_matched_features` accepts either 'horizontal' or 'vertical' for "
|
|
f"alignment, but '{alignment}' was given. See "
|
|
f"https://scikit-image.org/docs/dev/api/skimage.feature.html#skimage.feature.plot_matched_features "
|
|
f"for details."
|
|
)
|
|
raise ValueError(mesg)
|
|
|
|
if not only_matches:
|
|
ax.scatter(
|
|
keypoints0[:, 1],
|
|
keypoints0[:, 0],
|
|
facecolors='none',
|
|
edgecolors=keypoints_color,
|
|
)
|
|
ax.scatter(
|
|
keypoints1[:, 1] + offset[1],
|
|
keypoints1[:, 0] + offset[0],
|
|
facecolors='none',
|
|
edgecolors=keypoints_color,
|
|
)
|
|
|
|
ax.imshow(image, cmap='gray')
|
|
ax.axis((0, image0.shape[1] + offset[1], image0.shape[0] + offset[0], 0))
|
|
|
|
number_of_matches = matches.shape[0]
|
|
|
|
from matplotlib.colors import is_color_like
|
|
|
|
if matches_color is None:
|
|
rng = np.random.default_rng(seed=0)
|
|
colors = [rng.random(3) for _ in range(number_of_matches)]
|
|
elif is_color_like(matches_color):
|
|
colors = [matches_color for _ in range(number_of_matches)]
|
|
elif hasattr(matches_color, "__len__") and len(matches_color) == number_of_matches:
|
|
# No need to check each color, matplotlib does so for us
|
|
colors = matches_color
|
|
else:
|
|
error_message = (
|
|
'`matches_color` needs to be a single color '
|
|
'or a sequence of length equal to the number of matches.'
|
|
)
|
|
raise ValueError(error_message)
|
|
|
|
for i, match in enumerate(matches):
|
|
idx0, idx1 = match
|
|
ax.plot(
|
|
(keypoints0[idx0, 1], keypoints1[idx1, 1] + offset[1]),
|
|
(keypoints0[idx0, 0], keypoints1[idx1, 0] + offset[0]),
|
|
'-',
|
|
color=colors[i],
|
|
)
|
|
|
|
|
|
def _prepare_grayscale_input_2D(image):
|
|
image = np.squeeze(image)
|
|
check_nD(image, 2)
|
|
image = img_as_float(image)
|
|
float_dtype = _supported_float_type(image.dtype)
|
|
return image.astype(float_dtype, copy=False)
|
|
|
|
|
|
def _prepare_grayscale_input_nD(image):
|
|
image = np.squeeze(image)
|
|
check_nD(image, range(2, 6))
|
|
image = img_as_float(image)
|
|
float_dtype = _supported_float_type(image.dtype)
|
|
return image.astype(float_dtype, copy=False)
|
|
|
|
|
|
def _mask_border_keypoints(image_shape, keypoints, distance):
|
|
"""Mask coordinates that are within certain distance from the image border.
|
|
|
|
Parameters
|
|
----------
|
|
image_shape : (2,) array_like
|
|
Shape of the image as ``(rows, cols)``.
|
|
keypoints : (N, 2) array
|
|
Keypoint coordinates as ``(rows, cols)``.
|
|
distance : int
|
|
Image border distance.
|
|
|
|
Returns
|
|
-------
|
|
mask : (N,) bool array
|
|
Mask indicating if pixels are within the image (``True``) or in the
|
|
border region of the image (``False``).
|
|
|
|
"""
|
|
|
|
rows = image_shape[0]
|
|
cols = image_shape[1]
|
|
|
|
mask = (
|
|
((distance - 1) < keypoints[:, 0])
|
|
& (keypoints[:, 0] < (rows - distance + 1))
|
|
& ((distance - 1) < keypoints[:, 1])
|
|
& (keypoints[:, 1] < (cols - distance + 1))
|
|
)
|
|
|
|
return mask
|