def test_polynomial_estimation(): # over-determined tform = estimate_transform('polynomial', SRC, DST, order=10) assert_array_almost_equal(tform(SRC), DST, 6) # via estimate method tform2 = PolynomialTransform() tform2.estimate(SRC, DST, order=10) assert_array_almost_equal(tform2._params, tform._params)
def get_inverse_warp(self, recompute=False): if recompute: self._inverse_warp = None if self._inverse_warp is None: self._inverse_warp = PolynomialTransform() self._inverse_warp.estimate(self.get_targets(), self.get_sources(), self.get_transform_order()) return self._warp
def test_2D_only_implementations(): with pytest.raises(NotImplementedError): _ = PolynomialTransform(dimensionality=3) tf = AffineTransform(dimensionality=3) with pytest.raises(NotImplementedError): _ = tf.rotation with pytest.raises(NotImplementedError): _ = tf.shear
def test_polynomial_init(array_like_input): tform = estimate_transform('polynomial', SRC, DST, order=10) # init with transformation parameters if array_like_input: params = [list(p) for p in tform.params] else: params = tform.params tform2 = PolynomialTransform(params) assert_almost_equal(tform2.params, tform.params)
def test_invalid_input(): with testing.raises(ValueError): ProjectiveTransform(np.zeros((2, 3))) with testing.raises(ValueError): AffineTransform(np.zeros((2, 3))) with testing.raises(ValueError): SimilarityTransform(np.zeros((2, 3))) with testing.raises(ValueError): EuclideanTransform(np.zeros((2, 3))) with testing.raises(ValueError): AffineTransform(matrix=np.zeros((2, 3)), scale=1) with testing.raises(ValueError): SimilarityTransform(matrix=np.zeros((2, 3)), scale=1) with testing.raises(ValueError): EuclideanTransform( matrix=np.zeros((2, 3)), translation=(0, 0)) with testing.raises(ValueError): PolynomialTransform(np.zeros((3, 3))) with testing.raises(ValueError): FundamentalMatrixTransform(matrix=np.zeros((3, 2))) with testing.raises(ValueError): EssentialMatrixTransform(matrix=np.zeros((3, 2))) with testing.raises(ValueError): EssentialMatrixTransform(rotation=np.zeros((3, 2))) with testing.raises(ValueError): EssentialMatrixTransform( rotation=np.zeros((3, 3))) with testing.raises(ValueError): EssentialMatrixTransform( rotation=np.eye(3)) with testing.raises(ValueError): EssentialMatrixTransform(rotation=np.eye(3), translation=np.zeros((2,))) with testing.raises(ValueError): EssentialMatrixTransform(rotation=np.eye(3), translation=np.zeros((2,))) with testing.raises(ValueError): EssentialMatrixTransform( rotation=np.eye(3), translation=np.zeros((3,)))
def test_polynomial_init(): tform = estimate_transform('polynomial', SRC, DST, order=10) # init with transformation parameters tform2 = PolynomialTransform(tform._params) assert_array_almost_equal(tform2._params, tform._params)
def test_union_differing_types(): tform1 = SimilarityTransform() tform2 = PolynomialTransform() with pytest.raises(TypeError): tform1.__add__(tform2)
def test_polynomial_inverse(): with pytest.raises(Exception): PolynomialTransform().inverse(0)
def estimate(*data): return PolynomialTransform.estimate(*data, order=2)
def test_polynomial_inverse(): assert_raises(Exception, PolynomialTransform().inverse, 0)
class CorrespondenceEditor(object): ax: plt.Axes canvas: plt.FigureCanvasBase def __init__(self, ax, on_commit=None, facecolor=None, edgecolor=None, pick_radius=15): self.arrowstyle = "-|>,head_width=2.5, head_length=5" self.facecolor = facecolor or (0, 0.5, 1.0, 1.0) self.edgecolor = edgecolor or (0, 0.5, 1.0, 1.0) self.pick_radius = pick_radius self.scroll_speed = -0.5 # Possible semantics: # An integer > 1 -- fit a polynomial of that order # A float in [0,1] -- Use that fraction of the number of matches as the order self.order = 0.5 self._warp = None self._inverse_warp = None self.on_finish = on_commit self.ax = ax self.canvas = ax.figure.canvas self.matches = [[], []] self._active_point = -1 self._active_arrow = -1 self._arrow_patches = [] self._arrow_tails = [] # Zero-length arrows are iunvisible, so I will put markers at the tails self._arrow_highlight = FancyArrowPatch((0, 0), (0, 0), visible=False, arrowstyle=self.arrowstyle, color='y', linewidth=3) self._arrow_highlight_head = Line2D([0], [0], visible=False, marker='o', markersize=5, markerfacecolor='y', markeredgecolor='y', pickradius=self.pick_radius) self._arrow_highlight_tail = Line2D([0], [0], visible=False, marker='o', markersize=5, markerfacecolor='y', markeredgecolor='y', pickradius=self.pick_radius) self._point_highlight = Line2D([0], [0], visible=False, marker='o', markersize=5, markerfacecolor='r', markeredgecolor='r') self._arrow_highlight = self.ax.add_patch(self._arrow_highlight) self._arrow_highlight_head = self.ax.add_artist(self._arrow_highlight_head) self._arrow_highlight_tail = self.ax.add_artist(self._arrow_highlight_tail) self._point_highlight = self.ax.add_artist(self._point_highlight) self.cids = [] self._history = [] self._future = [] self.save_state() # Initialuze history (to empty doc) self._event = None self._mouse_down = None # Set to the event that started a drag. None if not dragging. self._editing = 0 # Whether we are editing. Counts up and down to allow compound edits. self.key_handlers = { ' ': self.commit, 's': partial(self.set_active_point, 0), 't': partial(self.set_active_point, 1), 'n': partial(self.new_arrow, selected=True, drag_target=False), 'delete': self.delete_arrow, 'e': self.delete_arrow, 'left': partial(self.nudge, dx=-1, dy=0), 'right': partial(self.nudge, dx=1, dy=0), 'up': partial(self.nudge, dx=0, dy=-1), 'down': partial(self.nudge, dx=0, dy=1), 'ctrl+z': self.undo, 'ctrl+Z': self.redo, 'enter': self.commit, } # Create the arrows self.refresh_arrows() self.connect_events() def __len__(self): return len(self.matches[0]) def __getitem__(self, i): return self.matches[0][i], self.matches[1][i] def __iter__(self): for i in range(len(self)): yield self[i] def begin_editing(self): """Call before changing the data""" self._editing += 1 def finish_editing(self): """Call after you are done modifying the data. Saves to history after all edits are complete. """ assert self._editing > 0 self._editing -= 1 if self._editing == 0: self._warp = None # We changed the matched --> the warp is dirty self._inverse_warp = None # We changed the matched --> the warp is dirty self.save_state() self.refresh_highlights() self.canvas.draw_idle() def get_state(self): return self.matches def set_state(self, state): s, t = state self.matches[0][:] = s self.matches[1][:] = t self.refresh_arrows() def save_state(self): # Sometimes I push multiple times for some reason if self._history and self._history[-1] == self.get_state(): return self._history.append(deepcopy(self.get_state())) del self._future[:] def undo(self): if self._history: self._future.append(deepcopy(self.get_state())) self.set_state(self._history.pop()) def redo(self): if self._future: self.set_state(self._future.pop()) if self._history and self.get_state() != self._history[-1]: self._history.append(deepcopy(self.get_state())) def connect(self, event, handler): self.cids.append(self.canvas.mpl_connect(event, handler)) def connect_events(self): self.connect('motion_notify_event', self._on_motion) self.connect('button_press_event', self._on_button_press) self.connect('button_release_event', self._on_button_release) self.connect('key_press_event', self._on_key_press) # self.connect('key_release_event', self._on_key_release) self.connect('scroll_event', self._on_scroll) def disconnect_events(self): """Disconnect all event handlers""" for cid in self.cids: self.canvas.mpl_disconnect(cid) self.cids = [] def ignore(self, event): return event.inaxes != self.ax def zoom(self, amount=1, xy=None): amount = 2 ** (amount) xmin, xmax = self.ax.get_xlim() ymin, ymax = self.ax.get_ylim() if xy is None: x = (xmin + xmax) / 2. y = (ymin + ymax) / 2. else: x, y = xy xmin = x + (xmin - x) * amount ymin = y + (ymin - y) * amount xmax = x + (xmax - x) * amount ymax = y + (ymax - y) * amount self.ax.set_xbound(xmin, xmax) self.ax.set_ybound(ymin, ymax) def _on_scroll(self, event: MouseEvent): x, y = event.xdata, event.ydata self.zoom(event.step * self.scroll_speed, (x, y)) self.canvas.draw_idle() def _on_motion(self, event: MouseEvent): if self.ignore(event): return if self._mouse_down: self.set_point((event.xdata, event.ydata)) else: self.select_arrow(event) def _distance_to_line_segment(self, p0, p1, xy, epsilon=1e-4): p0 = np.array(p0) p1 = np.array(p1) xy = np.array(xy) v = p1-p0 v2 = v@v if v2 > epsilon: q = (v @ (xy-p0))/(v @ v) # Distance along the edge q = np.clip(q, 0, 1) # Make sure we are on the line segment else: q = 0 p = p0 + v*q # Closest point on the line segment return np.linalg.norm(xy-p) def select_arrow(self, event: MouseEvent): patch: FancyArrowPatch min_distance = self.pick_radius argmin_distance = -1 p = np.array((event.xdata, event.ydata)) for i, (s, t) in enumerate(self): d = self._distance_to_line_segment(s, t, p) if d < min_distance: min_distance = d argmin_distance = i self.set_active_arrow(argmin_distance) if 0 <= argmin_distance: s, t = self[argmin_distance] d0 = norm(p-s) d1 = norm(p-t) if d0 < d1: self.set_active_point(0) else: self.set_active_point(1) def _start_drag(self, event: matplotlib.backend_bases.MouseEvent): self._mouse_down = event self.begin_editing() self.set_point((event.xdata, event.ydata)) def _on_button_press(self, event: matplotlib.backend_bases.MouseEvent): if self.ignore(event): return self._event = event if self.has_active_arrow(): if event.button == 1: # There are a couple of ways we can miss a mouse up event... if not self._mouse_down: self._start_drag(event) else: if event.button == 1: self.new_arrow(s=(event.xdata, event.ydata), t=(event.xdata, event.ydata), selected=True, drag_target=True) def _on_button_release(self, event: MouseEvent): if self.ignore(event): return if self._mouse_down: self._mouse_down = None self.finish_editing() def _on_key_press(self, event: KeyEvent): if self.ignore(event): return self._event = event if event.key in self.key_handlers: self.key_handlers[event.key]() def refresh_arrows(self): # Remove the old arrows while self._arrow_patches: self._arrow_patches.pop().remove() # Dont forget I added a point marker to the back of every arrow while self._arrow_tails: self._arrow_tails.pop().remove() # Add the new ones for s, t in zip(*self.matches): self._make_arrow_patch(s, t) # Refresh the highlights self.refresh_highlights() # Schedule a redraw self.canvas.draw_idle() def _set_highlight_arrow(self, s=None, t=None, visible=True): if s is not None and t is not None: self._arrow_highlight.set_positions(s, t) self._arrow_highlight_tail.set_data(s) self._arrow_highlight_head.set_data(t) self._arrow_highlight.set_visible(visible) self._arrow_highlight_head.set_visible(visible) self._arrow_highlight_tail.set_visible(visible) def refresh_highlights(self): if 0 <= self._active_arrow < len(self): self._set_highlight_arrow(self.matches[0][self._active_arrow], self.matches[1][self._active_arrow], visible=True) else: self._set_highlight_arrow(visible=False) if 0 <= self._active_arrow < len(self) and 0 <= self._active_point < 2: self._point_highlight.set_data(*(self.matches[self._active_point][self._active_arrow])) self._point_highlight.set_visible(True) else: self._point_highlight.set_visible(False) self.canvas.draw_idle() def commit(self, _unused=None): if self.on_finish: self.on_finish(self) def get_active_arrow(self): return self._active_arrow def set_active_arrow(self, i): if i != self._active_arrow: self._active_arrow = i self.refresh_highlights() def has_active_arrow(self): return 0 <= self._active_arrow < len(self) def get_active_point(self): return self._active_point def has_active_point(self): return 0 <= self._active_point < 2 def set_active_point(self, i): if i != self._active_point: self._active_point = i self.refresh_highlights() def _make_arrow_patch(self, s, t): a = FancyArrowPatch(s, t, arrowstyle=self.arrowstyle, facecolor=self.facecolor, edgecolor=self.edgecolor) a = self.ax.add_patch(a) self._arrow_patches.append(a) tail = Line2D([s[0]], [s[1]], marker='o', markersize=2.5, markeredgecolor=self.edgecolor, markerfacecolor=self.facecolor, pickradius=self.pick_radius ) tail = self.ax.add_artist(tail) self._arrow_tails.append(tail) @mutator def new_arrow(self, s=None, t=None, selected=True, drag_target=False): # Default to the location of the mouse in most recent event if s is None: if t is None: s = self._event.xdata, self._event.ydata else: s = self.predict_source(t) if t is None: if s is None: t = self._event.xdata, self._event.ydata else: t = self.predict_target(s) # Add the new source and target self.matches[0].append(s) self.matches[1].append(t) # Add a patch for the new arrow self._make_arrow_patch(s, t) # Users expect the new arrow to be selected if selected: self.set_active_arrow(len(self) - 1) self.set_active_point(1) # Presumably we clicked on the tail and we will click on the head next if drag_target: self._start_drag(self._event) # noinspection PyUnusedLocal @mutator def set_point(self, xy, point=None, arrow=None): patch: FancyArrowPatch line: Line2D if arrow is None: arrow = self._active_arrow if point is None: point = self._active_point if not 0 <= arrow < len(self): return # No arrows to select yet self.matches[point][arrow] = xy # Update the plot elements for the arrow patch = self._arrow_patches[arrow] patch.set_positions(self.matches[0][arrow], self.matches[1][arrow]) # And also move the tail self._arrow_tails[arrow].set_data(self.matches[0][arrow]) # Update the highlights if we are moving the selected item if arrow == self._active_arrow: self.refresh_highlights() def ensure_selected_arrow(self): if len(self) == 0: self.new_arrow() if self._active_arrow < 0: self.set_active_arrow(len(self) - 1) def ensure_selected_point(self): self.ensure_selected_arrow() if self._active_point < 0: self.set_active_arrow(len(self) - 1) @mutator def nudge(self, dx=0, dy=0): self.ensure_selected_point() x, y = self.matches[self._active_point][self._active_arrow] self.set_point((x + dx, y + dy)) @mutator def delete_arrow(self, arrow=None): if arrow is None: self.ensure_selected_arrow() arrow = self._active_arrow del self.matches[0][arrow] del self.matches[1][arrow] # Remove the patch from the plot arrow_patch = self._arrow_patches.pop(arrow) arrow_patch.remove() tail = self._arrow_tails.pop(arrow) tail.remove() # Update the active arrow index (it might have shifted) # The behavior if we delete the active arrow should be that # the next arrow is selected. Otherwise the selected arrow # should be the same. if self._active_arrow > arrow: self._active_arrow -= 1 def get_sources(self): return self.matches[0] def get_targets(self): return self.matches[1] def get_transform_order(self): if self.order > 1: order = min(self.order, len(self)) else: order = round(self.order * len(self)) return order def get_warp(self, recompute=False): if recompute: self._warp = None if self._warp is None: self._warp = PolynomialTransform() self._warp.estimate(np.array(self.get_sources()), np.array(self.get_targets()), self.get_transform_order()) return self._warp def get_inverse_warp(self, recompute=False): if recompute: self._inverse_warp = None if self._inverse_warp is None: self._inverse_warp = PolynomialTransform() self._inverse_warp.estimate(self.get_targets(), self.get_sources(), self.get_transform_order()) return self._warp def predict_target(self, s): t = self.get_warp()(np.array([s]))[0] return tuple(t) def predict_source(self, t): s = self.get_inverse_warp()(np.array([t]))[0] return tuple(s)
def test_union_differing_types(): tform1 = SimilarityTransform() tform2 = PolynomialTransform() assert_raises(TypeError, tform1.__add__, tform2)