Unverified Commit 2d7e81b3 authored by jlaura's avatar jlaura Committed by GitHub
Browse files

Subpixel edge (#493)

* affine centered on the ROI

* Updates for comments

* remove missed phase args in func signature

* Updates for passing tests

* Updates with tests

* Update for comments.

* Updates for additional comments.

* comments addressed

* Adds an edge check for the correlation template.

* Adds edge check to subpixel_template

* Updates ROI to identify when null pixels exist

* Updates gitignore to skip DS_Store entries

* Updates test fixture for ROI tests

* Updates subpixel to use the new ROI capabilities

* Fixes edge check for all edges

* removes extra comment

* Reverts pg ports to be corret for remote CI

* Update subpixel.py
parent 49cc0ac0
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -2,6 +2,10 @@
/.ipynb*
*.ipynb*

# OS X
.DS_Store
._.DS_Store

# Notebooks dir
notebooks/*
!notebooks/*.ipynb
+32 −12
Original line number Diff line number Diff line
import json
from math import modf, floor
import numpy as np
import warnings

from skimage.feature import register_translation
from skimage import transform as tf
@@ -312,6 +313,9 @@ def subpixel_transformed_template(sx, sy, dx, dy,
    s_roi = roi.Roi(s_img, sx, sy, size_x=image_size[0], size_y=image_size[1])
    d_roi = roi.Roi(d_img, dx, dy, size_x=template_size_x, size_y=template_size_y)

    if not s_roi.is_valid or not d_roi.is_valid:
        return [None] * 4

    try:
        s_image_dtype = isis2np_types[pvl.load(s_img.file_name)["IsisCube"]["Core"]["Pixels"]["Type"]]
    except:
@@ -360,6 +364,12 @@ def subpixel_transformed_template(sx, sy, dx, dy,
    # Apply the matcher on the transformed array
    shift_x, shift_y, metrics, corrmap = func(bytescale(buffered_template), s_image, **kwargs)

    # Hard check here to see if we are on the absolute edge of the template
    max_coord = np.unravel_index(corrmap.argmax(), corrmap.shape)
    if 0 in max_coord or corrmap.shape[0]-1 == max_coord[0] or corrmap.shape[1]-1 == max_coord[1]:
        warnings.warn('Maximum correlation is at the edge of the template. Results are ambiguous.', UserWarning)
        return [None] * 4

    if verbose:
        axs[2].imshow(transformed_roi, cmap='Greys')
        axs[2].set_title('Affine Transformed Source')
@@ -462,6 +472,9 @@ def subpixel_template(sx, sy, dx, dy,
    s_roi = roi.Roi(s_img, sx, sy, size_x=image_size[0], size_y=image_size[1])
    d_roi = roi.Roi(d_img, dx, dy, size_x=template_size_x, size_y=template_size_y)

    if not s_roi.is_valid or not d_roi.is_valid:
        return [None] * 4
        
    try:
        s_image_dtype = isis2np_types[pvl.load(s_img.file_name)["IsisCube"]["Core"]["Pixels"]["Type"]]
    except:
@@ -475,6 +488,12 @@ def subpixel_template(sx, sy, dx, dy,
    s_image = bytescale(s_roi.clip(dtype=s_image_dtype))
    d_template = bytescale(d_roi.clip(dtype=d_template_dtype))
    
    if (s_image is None) or (d_template is None):
        return None, None, None, None

    # Apply the matcher function
    shift_x, shift_y, metrics, corrmap = func(d_template, s_image, **kwargs)

    if verbose:
        fig, axs = plt.subplots(1, 3, figsize=(20,10))
        axs[0].imshow(s_image, cmap='Greys')
@@ -482,11 +501,11 @@ def subpixel_template(sx, sy, dx, dy,
        axs[3].imshow(corrmap)
        plt.show()

    if (s_image is None) or (d_template is None):
        return None, None, None, None

    # Apply the matcher function
    shift_x, shift_y, metrics, corrmap = func(d_template, s_image, **kwargs)
    # Hard check here to see if we are on the absolute edge of the template
    max_coord = np.unravel_index(corrmap.argmax(), corrmap.shape)
    if 0 in max_coord or corrmap.shape[0]-1 == max_coord[0] or corrmap.shape[1]-1 == max_coord[1]:
        warnings.warn('Maximum correlation is at the edge of the template. Results are ambiguous.', UserWarning)
        return [None] * 4

    # Apply the shift to the center of the ROI object
    dx = d_roi.x - shift_x
@@ -781,13 +800,13 @@ def geom_match(destination_cube,
                                                verbose=verbose,
                                                **template_kwargs)

    x, y, metric, temp_corrmap = restemplate
    x, y, metric, corrmap = restemplate
    
    if x is None or y is None:
        return None, None, None, None, None

    dist = np.linalg.norm([center_x-x, center_y-y])
    return x, y, dist, metric, temp_corrmap
    return x, y, dist, metric, corrmap


def subpixel_register_measure(measureid,
@@ -971,7 +990,7 @@ def subpixel_register_point(pointid,

            print('geom_match image:', res.path)
            try:
                new_x, new_y, dist, metric,  _ = geom_match(source_node.geodata, destination_node.geodata,
                new_x, new_y, dist, metric, corrmap = geom_match(source_node.geodata, destination_node.geodata,
                                                                 source.sample, source.line,
                                                                 template_kwargs=subpixel_template_kwargs,
                                                                 phase_kwargs=iterative_phase_kwargs,
@@ -1001,6 +1020,7 @@ def subpixel_register_point(pointid,

            cost = cost_func(measure.template_shift, measure.template_metric)

            # Check to see if the cost function requirement has been met
            if cost <= threshold:
                measure.ignore = True # Threshold criteria not met
                currentlog['status'] = f'Cost failed. Distance shifted: {measure.template_shift}. Metric: {measure.template_metric}.'
+66 −12
Original line number Diff line number Diff line
@@ -62,6 +62,32 @@ def test_subpixel_template(apollo_subsets):
    assert nx == 50.5
    assert ny == 52.4375

@pytest.mark.parametrize("loc, failure", [((0,4), True),
                                          ((4,0), True),
                                          ((1,1), False)])
def test_subpixel_template_at_edge(apollo_subsets, loc, failure):
    a = apollo_subsets[0]
    b = apollo_subsets[1]

    def func(*args, **kwargs):
        corr = np.zeros((10,10))
        corr[loc[0], loc[1]] = 10
        return 0, 0, 0, corr

    with patch('autocnet.matcher.subpixel.clip_roi', side_effect=clip_side_effect):
        if failure:
            with pytest.warns(UserWarning, match=r'Maximum correlation \S+'):
                nx, ny, strength, _ = sp.subpixel_template(a.shape[1]/2, a.shape[0]/2,
                                                        b.shape[1]/2, b.shape[0]/2,
                                                        a, b, upsampling=16,
                                                        func=func)
        else:
            nx, ny, strength, _ = sp.subpixel_template(a.shape[1]/2, a.shape[0]/2,
                                                        b.shape[1]/2, b.shape[0]/2,
                                                        a, b, upsampling=16,
                                                        func=func)
            assert nx == 50.5

def test_estimate_affine_transformation():
    a = [[0,1], [0,0], [1,0], [1,1], [0,1]]
    b = [[1, 2], [1, 1], [2, 1], [2, 2], [1, 2]]
@@ -81,6 +107,34 @@ def test_subpixel_transformed_template(apollo_subsets):
    assert nx == pytest.approx(51.18894)
    assert ny == pytest.approx(54.36261)


@pytest.mark.parametrize("loc, failure", [((0,4), True),
                                          ((4,0), True),
                                          ((1,1), False)])
def test_subpixel_transformed_template_at_edge(apollo_subsets, loc, failure):
    a = apollo_subsets[0]
    b = apollo_subsets[1]

    def func(*args, **kwargs):
        corr = np.zeros((5,5))
        corr[loc[0], loc[1]] = 10
        return 0, 0, 0, corr

    transform = tf.AffineTransform(rotation=math.radians(1), scale=(1.1,1.1))
    with patch('autocnet.matcher.subpixel.clip_roi', side_effect=clip_side_effect):
        if failure:
            with pytest.warns(UserWarning, match=r'Maximum correlation \S+'):
                nx, ny, strength, _ = sp.subpixel_transformed_template(a.shape[1]/2, a.shape[0]/2,
                                                        b.shape[1]/2, b.shape[0]/2,
                                                        a, b, transform, upsampling=16,
                                                        func=func)
        else:
            nx, ny, strength, _ = sp.subpixel_transformed_template(a.shape[1]/2, a.shape[0]/2,
                                                        b.shape[1]/2, b.shape[0]/2,
                                                        a, b, transform, upsampling=16,
                                                        func=func)
            assert nx == 50.5

@pytest.mark.parametrize("convergence_threshold, expected", [(2.0, (50.49, 52.08, (0.039507, -9.5e-20)))])
def test_iterative_phase(apollo_subsets, convergence_threshold, expected):
    a = apollo_subsets[0]
@@ -105,13 +159,13 @@ def test_check_image_size(data, expected):
    assert sp.check_image_size(data) == expected

@pytest.mark.parametrize("x, y, x1, y1, image_size, template_size, expected",[
    (4, 3, 3, 2, (3,3), (3,3), (3,2)),
    (4, 3, 3, 2, (7,7), (3,3), (3,2)),  # Increase the search image size
    (4, 3, 3, 2, (7,7), (5,5), (3,2)), # Increase the template size
    (4, 3, 2, 2, (7,7), (3,3), (3,2)), # Move point in the x-axis
    (4, 3, 4, 3, (7,7), (3,3), (3,2)), # Move point in the other x-direction
    (4, 3, 3, 1, (7,7), (3,3), (3,2)), # Move point negative in the y-axis
    (4, 3, 3, 3, (7,7), (3,3), (3,2))  # Move point positive in the y-axis
    (4, 3, 4, 2, (5,5), (3,3), (4,2)),
    (4, 3, 4, 2, (7,7), (3,3), (4,2)),  # Increase the search image size
    (4, 3, 4, 2, (7,7), (5,5), (4,2)), # Increase the template size
    (4, 3, 3, 2, (7,7), (3,3), (4,2)), # Move point in the x-axis
    (4, 3, 5, 3, (7,7), (3,3), (4,2)), # Move point in the other x-direction
    (4, 3, 4, 1, (7,7), (3,3), (4,2)), # Move point negative in the y-axis
    (4, 3, 4, 3, (7,7), (3,3), (4,2))  # Move point positive in the y-axis

])
def test_subpixel_template_cooked(x, y, x1, y1, image_size, template_size, expected):
@@ -130,11 +184,11 @@ def test_subpixel_template_cooked(x, y, x1, y1, image_size, template_size, expec
                           (0, 0, 0, 0, 0, 0, 1, 1, 1)), dtype=np.uint8)

    # Should yield (-3, 3) offset from image center
    t_shape = np.array(((0, 0, 0, 0, 0, 0, 0),
                        (0, 0, 1, 1, 1, 0, 0),
                        (0, 0, 0, 1, 0, 0, 0),
                        (0, 0, 0, 1, 0, 0, 0),
                        (0, 0, 0, 0, 0, 0, 0)), dtype=np.uint8)
    t_shape = np.array(((0, 0, 0, 0, 0, 0, 0, 0, 0),
                        (0, 0, 0, 1, 1, 1, 0, 0, 0),
                        (0, 0, 0, 0, 1, 0, 0, 0, 0),
                        (0, 0, 0, 0, 1, 0, 0, 0, 0),
                        (0, 0, 0, 0, 0, 0, 0, 0, 0)), dtype=np.uint8)

    dx, dy, corr, corrmap = sp.subpixel_template(x, y, x1, y1, 
                                                 test_image, t_shape,
+55 −12
Original line number Diff line number Diff line
@@ -11,6 +11,8 @@ class Roi():

    Attributes
    ----------
    data : ndarray/object
           An ndarray or an object with a raster_size attribute

    x : float
        The x coordinate in image space
@@ -36,11 +38,12 @@ class Roi():
    bottom_y : int
               The bottom image coordinate in imge space
    """
    def __init__(self, data, x, y, size_x=200, size_y=200):
    def __init__(self, data, x, y, size_x=200, size_y=200, dtype=None, ndv=None):
        self.data = data

        self.ndv = ndv
        self.x = x
        self.y = y
        self.dtype = dtype
        self.size_x = size_x
        self.size_y = size_y

@@ -60,6 +63,21 @@ class Roi():
    def y(self, y):
        self.ayr, self._y = modf(y)

    @property
    def ndv(self):
        """
        The no data value of the ROI. Used by the is_valid
        property to determine if the ROI contains any null
        pixels.
        """
        if hasattr(self.data, 'no_data_value'):
            self._ndv = self.data.no_data_value   
        return self._ndv

    @ndv.setter
    def ndv(self, ndv):
        self._ndv = ndv

    @property
    def image_extent(self):
        """
@@ -95,21 +113,46 @@ class Roi():
        ie = self.image_extent
        return (ie[1] - ie[0])/2, (ie[3]-ie[2])/2

    def clip(self, dtype=None):
    @property
    def is_valid(self):
        """
        True if all elements in the clipped ROI are valid, i.e., 
        no null pixels (as defined by the no data value (ndv)) are
        present.
        """
        return self.ndv not in self.array

    @property
    def array(self):
        """
        The clopped array associated with this ROI.
        """
        pixels = self.image_extent
        if isinstance(self.data, np.ndarray):
            array = self.data[pixels[2]:pixels[3]+1,
                                         pixels[0]:pixels[1]+1]
             return self.data[pixels[2]:pixels[3]+1,pixels[0]:pixels[1]+1]
        else:
            # Have to reformat to [xstart, ystart, xnumberpixels, ynumberpixels]
            pixels = [pixels[0], pixels[2], pixels[1]-pixels[0], pixels[3]-pixels[2]]
            array = self.data.read_array(pixels=pixels, dtype=dtype)

        return array
            return self.data.read_array(pixels=pixels, dtype=self.dtype)

    def transform(self, x, y):
    def clip(self, dtype=None):
        """
        Convert arbitrary coordinates from the ROI coordinate system
        to the full image coordinate system.
        Compatibility function that makes a call to the array property. 
        
        Warning: The dtype passed in via this function resets the dtype attribute of this
        instance. 

        Parameters
        ----------
        dtype : str
                The datatype to be used when reading the ROI information if the read 
                occurs through the data object using the read_array method. When using
                this object when the data are a numpy array the dtype has not effect.

        Returns
        -------
         : ndarray
           The array attribute of this object.
        """
        pass
        self.dtype = dtype
        return self.array
+24 −0
Original line number Diff line number Diff line
@@ -3,6 +3,30 @@ import pytest

from autocnet.transformation.roi import Roi

@pytest.fixture
def array_with_nodata():
    arr = np.ones((10,10))
    arr[5,5] = 0
    return arr

def test_geodata_with_ndv_is_valid(geodata_a):
    roi = Roi(geodata_a, 5, 5)
    assert roi.is_valid == False

def test_geodata_is_valid(geodata_b):
    roi = Roi(geodata_b, 5, 5)
    assert roi.is_valid == True

def test_center(array_with_nodata):
    roi = Roi(array_with_nodata, 5, 5)
    assert roi.center == (5,5)

@pytest.mark.parametrize("ndv, truthy", [(None, True),
                                         (0, False)])
def test_is_valid(array_with_nodata, ndv, truthy):
    roi = Roi(array_with_nodata, 2.5, 2.5, ndv=ndv)
    assert roi.is_valid == truthy

@pytest.mark.parametrize("x, y, axr, ayr",[
                         (10.1, 10.1, .1, .1),
                         (10.5, 10.5, .5, .5),
Loading