class SIPModel(GenModel): def __init__(self): super(SIPModel, self).__init__() changed = QtCore.Signal() pin_count = mdlacc(14) pin_space = mdlacc(units.IN_10) pad_diameter = mdlacc(units.MM * 1.6)
class AlignmentViewModel(GenModel): def __init__(self, image): GenModel.__init__(self) self.ra = RectAlignmentModel(image) self.kp = KeypointAlignmentModel(image) align_by = mdlacc(ALIGN_BY_DIMENSIONS) view_mode = mdlacc(VIEW_MODE_UNALIGNED)
class PassiveModel(GenModel): def __init__(self): super(PassiveModel, self).__init__() self.well_known = None self.__pin_d = 0.3 * units.IN self.__body_corner = Point2(0.15 * units.IN, 0.05 * units.IN) self.__pin_corner = Point2(0.05 * units.IN, 0.05 * units.IN) changed = QtCore.Signal() snap_well = mdlacc(True) sym_type = mdlacc(PassiveSymType.TYPE_RES) body_type = mdlacc(PassiveBodyType.CHIP) @property def pin_d(self): if self.well_known: return self.well_known.pin_d return self.__pin_d @pin_d.setter def pin_d(self, v): self.__pin_d = v self.changed.emit() @property def body_corner_vec(self): if self.well_known: return self.well_known.body_size return self.__body_corner @body_corner_vec.setter def body_corner_vec(self, v): self.__body_corner = v self.changed.emit() @property def pin_corner_vec(self): if self.well_known: return self.well_known.pad_size return self.__pin_corner @pin_corner_vec.setter def pin_corner_vec(self, v): self.__pin_corner = v self.changed.emit()
class ComponentModel(GenModel): def __init__(self): super(ComponentModel, self).__init__() self.model_instances = {} for t, meta in mdl_meta.items(): i = self.model_instances[t] = meta.cons() i.changed.connect(self.changed) cmptype = mdlacc(MDL_TYPE_BASICSMD) center = mdlacc(Point2(0, 0)) theta = mdlacc(0) def get_selected_model(self): return self.model_instances[self.cmptype] def get_model_name(self): return mdl_meta[self.cmptype].text def get_mpe(self): return self.get_selected_model().multi_editor
class KeypointAlignmentModel(GenModel): """ Full model including the keypoint alignment state """ def __init__(self, image: ImageLayer) -> None: GenModel.__init__(self) self.combo_adapter = KeypointAlignmentModelQAI(self) self.__keypoints: List[AlignKeypoint] = [] self.__image = image # Currently selected keypoint index. None for no selection selected_idx = mdlacc(None) def load(self, project: 'Project') -> None: # Initialize all the known keypoints for pkp in project.imagery.keypoints: idx = self.add_keypoint() obj = self.keypoints[idx] # Link to existing keypoint obj._orig_kp = pkp # World position = copy of existing position (allocate new mutable obj) obj.world = pkp.world_position.dup() for kp_layer, position in pkp.layer_positions: if kp_layer is self.__image: obj.layer = position.dup() obj.use = True else: obj.is_new = False def save(self, project: 'Project') -> None: al = KeyPointAlignment() # Remove all keypoints that were deleted still_exist_pkp = set(i._orig_kp for i in self.keypoints if hasattr(i, "_orig_kp")) for pkp in list(project.imagery.keypoints): if not pkp in still_exist_pkp: project.imagery.del_keypoint(pkp) # Now for all remaining keypoints, recreate for kp in self.keypoints: # Create or update the world kp if kp._orig_kp is None: pkp = KeyPoint(project, kp.world) kp._orig_kp = pkp project.imagery.add_keypoint(pkp) else: pkp = kp._orig_kp pkp.world_position = kp.world for kp in self.keypoints: if kp.use: if kp._orig_kp is not None: al.set_keypoint_position(kp._orig_kp, kp.layer) # Now, save the changes to the layer self.__image.set_alignment(al) self.__image.transform_matrix = self.image_matrix def set_keypoint_world(self, idx: int, pos: Vec2) -> None: """ set the world coordinates of a keypoint by index idx must be within the range of known keypoints :param idx: :param pos: :return: """ # Refuse to move existing keypoints in world-space if not self.keypoints[idx].is_new: return self.__keypoints[idx].world = pos self.change() def set_keypoint_px(self, idx: int, pos: Vec2) -> None: """ set the keypoint position on a layer. Pos=None to remove :param idx: :param pos: :return: """ self.__keypoints[idx].layer = pos self.change() def set_keypoint_used(self, idx: int, use: bool) -> None: """ Set whether a keypoint is used in the current layer solution :param idx: :param use: :return: """ self.__keypoints[idx].use = use self.change() def insert_keypoint(self, idx: int) -> None: """ Returns a new keypoint inserted at index idx. Used for undos :param idx: :return: """ self.combo_adapter.beginInsertRows(QtCore.QModelIndex(), idx, idx) self.__keypoints.insert(idx, AlignKeypoint()) self.combo_adapter.endInsertRows() self.change() def add_keypoint(self) -> int: """ Adds a new keypoint. Returns the index of the new keypoint :return: """ idx = len(self.__keypoints) self.combo_adapter.beginInsertRows(QtCore.QModelIndex(), idx, idx) self.__keypoints.append(AlignKeypoint()) self.combo_adapter.endInsertRows() self.change() return idx def del_keypoint(self, idx: int) -> None: """ Delete a keypoint by index :param idx: :return: """ # Refuse to delete existing keypoints if not self.keypoints[idx].is_new: return self.combo_adapter.beginRemoveRows(QtCore.QModelIndex(), idx, idx) del self.__keypoints[idx] self.combo_adapter.endRemoveRows() self.change() @property def keypoints(self) -> List[AlignKeypoint]: """ View-only copy of the keypoints. Do not edit the objects returned by this property Use the model-level getter/setter functions :return: list of keypoints """ return list(self.__keypoints) @property def _image_transform_info( self) -> Tuple[Constraint, 'npt.NDArray[numpy.float64]']: """ Calculate the constraint level and the transform (if possible) :return: constraint_level, transform_matrix """ relevant_keypoints = [i for i in self.keypoints if i.use] # Num keypoints = 0 - can't compute a transform if len(relevant_keypoints) == 0: return Constraint.Unconstrained, numpy.identity(3) # Num keypoints = 1, just do a simple translate elif len(relevant_keypoints) == 1: kp = relevant_keypoints[0] # Pixel in worldspace layer_world = self.__image.p2n(kp.layer) # Just basic translation in worldspace vec = kp.world - layer_world return Constraint.Translation, translate(vec.x, vec.y) # Calculate either translate-rotate, or translate-rotate-scale transforms elif len(relevant_keypoints) in (2, 3): # Construct a system of equations to solve the positioning # Basic Ax = b, where x is the non-homologous terms of the translation matrix # 'A' is made from "rows" rows = [] b = [] # Add all of the keypoints as constraints for kp in relevant_keypoints: # normalized image / world coordinates for the keypoint lw = self.__image.p2n(kp.layer) w = kp.world # a b c d e f # rows.append([ lw.x, lw.y, 1, 0, 0, 0, ]) rows.append([ 0, 0, 0, lw.x, lw.y, 1, ]) b.append(w.x) b.append(w.y) # If we only have two keypoints, constrain scale to equal on both axes if len(relevant_keypoints) == 2: rows.append([1, 0, 0, 0, -1, 0]) rows.append([0, 1, 0, 1, 0, 0]) b.extend((0, 0)) # Try to solve the system of equations a = numpy.vstack(rows) try: x = numpy.linalg.solve(a, b) except numpy.linalg.LinAlgError: # Cant solve, constructed matrix is singular return Constraint.Singular, numpy.identity(3) # Depending on the number of keypoints, we're either constrainted to translate/rotate/equal-scale # or full Orthogonal projection constraint = Constraint.Orthogonal if len(relevant_keypoints) == 2: constraint = Constraint.Rotate_Scale return constraint, numpy.vstack((x.reshape(2, 3), (0, 0, 1))) elif len(relevant_keypoints) == 4: # Perspective transform # TODO: replace this with a LMS solver for overconstrained cases # plus provide error estimations and visualize src = numpy.ones((4, 2), dtype=numpy.float32) dst = numpy.ones((4, 2), dtype=numpy.float32) src[:, :2] = [ self.__image.p2n(kp.layer) for kp in relevant_keypoints ] dst[:, :2] = [kp.world for kp in relevant_keypoints] persp = cv2.getPerspectiveTransform(src, dst) return Constraint.Perspective, cast('npt.NDArray[numpy.float64]', persp) else: # Temporarily refuse to solve overconstrained alignments return Constraint.Overconstrained, numpy.identity(3) @property def image_matrix(self) -> 'npt.NDArray[numpy.float64]': """ return the transform matrix (from normalized image coordinates to world space) :return: """ _, matrix = self._image_transform_info return matrix @property def image_matrix_inv(self) -> 'npt.NDArray[numpy.float64]': """ returns the world-space to normalized image coordinate matrix :return: """ return cast('npt.NDArray[numpy.float64]', numpy.linalg.inv(self.image_matrix)) @property def constraint_info(self) -> Constraint: """ Get the constraint info for the current solution (IE, one of the Constraint.* constants) :return: """ constraint, _ = self._image_transform_info return constraint
class BasicSMDICModel(GenModel): def __init__(self): super(BasicSMDICModel, self).__init__() changed = QtCore.Signal() side1_pins = mdlacc(25) side2_pins = mdlacc(25) side3_pins = mdlacc(25) side4_pins = mdlacc(25) dim_1_body = mdlacc(14000) dim_1_pincenter = mdlacc(16000) dim_2_body = mdlacc(14000) dim_2_pincenter = mdlacc(16000) pin_contact_length = mdlacc(600) pin_contact_width = mdlacc(220) pin_spacing = mdlacc(500)
class FakeModel(GenModel): j = mdlacc(3)
class FakeOther(object): j = mdlacc(3)
class SelectToolModel(GenModel): vers = mdlacc(SelectByModes.POINT)
class RectAlignmentControllerView(BaseToolController, GenModel): changed = QtCore.Signal() def __init__(self, parent, model): super(RectAlignmentControllerView, self).__init__() self._parent = parent self.model_overall = model self.model = model.ra self.__init_interaction() self.gls = None self.active = False idx_handle_sel = mdlacc(None) idx_handle_hover = mdlacc(None) #sel_mode = mdlacc(SEL_MODE_NONE) behave_mode = mdlacc(MODE_NONE) ghost_handle = mdlacc(None) def change(self): self.changed.emit() def __init_interaction(self): # Selection / drag handling self.idx_handle_sel = None #self.sel_mode = SEL_MODE_NONE self.behave_mode = MODE_NONE # ghost handle for showing placement self.ghost_handle = None def initialize(self): self.__init_interaction() self.active = True def finalize(self): self.active = False def initializeGL(self, gls): self.gls = gls # Basic solid-color program self.prog = self.gls.shader_cache.get("vert2", "frag1") self.mat_loc = GL.glGetUniformLocation(self.prog, "mat") self.col_loc = GL.glGetUniformLocation(self.prog, "color") # Build a VBO for rendering square "drag-handles" self.vbo_handles_ar = numpy.ndarray(4, dtype=[("vertex", numpy.float32, 2)]) self.vbo_handles_ar["vertex"] = numpy.array(corners) * HANDLE_HALF_SIZE self.vbo_handles = VBO(self.vbo_handles_ar, GL.GL_STATIC_DRAW, GL.GL_ARRAY_BUFFER) self.vao_handles = VAO() with self.vbo_handles, self.vao_handles: vbobind(self.prog, self.vbo_handles_ar.dtype, "vertex").assign() # Build a VBO/VAO for the perimeter # We don't initialize it here because it is updated every render # 4 verticies for outside perimeter # 6 verticies for each dim self.vbo_per_dim_ar = numpy.zeros(16, dtype=[("vertex", numpy.float32, 2)]) self.vbo_per_dim = VBO(self.vbo_per_dim_ar, GL.GL_DYNAMIC_DRAW, GL.GL_ARRAY_BUFFER) self.vao_per_dim = VAO() with self.vao_per_dim, self.vbo_per_dim: vbobind(self.prog, self.vbo_per_dim_ar.dtype, "vertex").assign() def im2V(self, pt): """Translate Image coordinates to viewport coordinates""" if self.model_overall.view_mode: ph = projectPoint(self.model.image_matrix, pt) return self.viewState.tfW2V(ph) else: return self.viewState.tfW2V(pt) def V2im(self, pt): """ Translate viewport coordinates to image coordinates :param pt: :return: """ world = self.viewState.tfV2W(pt) if self.model_overall.view_mode: inv = numpy.linalg.inv(self.model.image_matrix) return projectPoint(inv, world) else: return Vec2(world) #inv = numpy.linalg.inv(self.model.image_matrix) #return Vec2(inv.dot(pt)[:2]) def gen_dim(self, idx, always_above = True): """ Generate rendering data for the dimension-lines :param idx: :return: """ a = self.im2V(self.model.dim_handles[0 + idx]) b = self.im2V(self.model.dim_handles[1 + idx]) d = b-a delta = (b-a).norm() normal = Point2(rotate(math.pi / 2)[:2,:2].dot(delta)) if always_above: if numpy.cross(Vec2(1,0), normal) > 0: normal = -normal res = numpy.array([ a + normal * 8, a + normal * 20, a + normal * 15, b + normal * 15, b + normal * 8, b + normal * 20, ]) return res def render(self, vs): self.viewState = vs disabled = not self.active or self.model_overall.view_mode # Perimeter is defined by the first 4 handles self.vbo_per_dim_ar["vertex"][:4] = [self.im2V(pt) for pt in self.model.align_handles[:4]] # Generate the dimension lines. For ease of use, we always draw the dim-lines above when dims are manual # or below when dims are unlocked self.vbo_per_dim_ar["vertex"][4:10] = self.gen_dim(0, not self.model.dims_locked) self.vbo_per_dim_ar["vertex"][10:16] = self.gen_dim(2, not self.model.dims_locked) self.vbo_per_dim.set_array(self.vbo_per_dim_ar) # Ugh..... PyOpenGL isn't smart enough to bind the data when it needs to be copied with self.vbo_per_dim: self.vbo_per_dim.copy_data() GL.glDisable(GL.GL_BLEND) # ... and draw the perimeter with self.vao_per_dim, self.prog: GL.glUniformMatrix3fv(self.mat_loc, 1, True, self.viewState.glWMatrix.astype(numpy.float32)) # Draw the outer perimeter if disabled: GL.glUniform4f(self.col_loc, 0.8, 0.8, 0.8, 1) else: GL.glUniform4f(self.col_loc, 0.8, 0.8, 0, 1) GL.glDrawArrays(GL.GL_LINE_LOOP, 0, 4) # Draw the dimensions GL.glUniform4f(self.col_loc, 0.8, 0.0, 0.0, 1) GL.glDrawArrays(GL.GL_LINES, 4, 6) GL.glUniform4f(self.col_loc, 0.0, 0.0, 0.8, 1) GL.glDrawArrays(GL.GL_LINES, 10, 6) if disabled: return # Now draw a handle at each corner with self.vao_handles, self.prog: for n, i in enumerate(self.model.align_handles): # skip nonexistent handles if i is None: continue is_anchor = IDX_IS_ANCHOR(n) corner_pos = self.im2V(i) if disabled: color = [0.8, 0.8, 0.8, 1] elif self.idx_handle_sel == n: color = [1, 1, 1, 1] elif self.idx_handle_hover == n: color = [1, 1, 0, 1] else: color = [0.8, 0.8, 0, 0.5] self.render_handle(corner_pos, color, is_anchor, True) if self.idx_handle_sel == n: self.render_handle(corner_pos, [0,0,0,1], is_anchor, False) if self.ghost_handle is not None: self.render_handle(self.ghost_handle,[0.8, 0.8, 0, 0.5], True) if not self.model.dims_locked: for n, i in enumerate(self.model.dim_handles): handle_pos = self.im2V(i) if n == self.idx_handle_sel: color = [1, 1, 1, 1] if n < 2: color = [0.8, 0.0, 0.0, 1] else: color = [0.0, 0.0, 0.8, 1] self.render_handle(handle_pos, color, False, True) if self.idx_handle_sel == n: self.render_handle(corner_pos, [0,0,0,1], is_anchor, False) def render_handle(self, position, color, diagonal=False, filled=False): if diagonal: r = rotate(math.pi/4) else: r = numpy.identity(3) m = self.viewState.glWMatrix.dot(translate(*position).dot(r)) GL.glUniformMatrix3fv(self.mat_loc, 1, True, m.astype(numpy.float32)) GL.glUniform4f(self.col_loc, *color) GL.glDrawArrays(GL.GL_TRIANGLE_FAN if filled else GL.GL_LINE_LOOP, 0, 4) def get_handle_for_mouse(self, pos): for n, handle in enumerate(self.model.all_handles()): if handle is None: continue # get the pix-wise BBOX of the handle p = self.im2V(handle) r = QtCore.QRect(p[0], p[1], 0, 0) r.adjust(-HANDLE_HALF_SIZE, -HANDLE_HALF_SIZE, HANDLE_HALF_SIZE, HANDLE_HALF_SIZE) # If event inside the bbox if r.contains(pos): return n return None def get_line_query_for_mouse(self, pos): for n, (p1, p2) in enumerate(self.model.line_iter()): p1_v = self.im2V(p1) p2_v = self.im2V(p2) p, d = project_point_line(pos, p1_v, p2_v) if p is not None and d < HANDLE_HALF_SIZE: return n, p return None, None def mousePressEvent(self, event): disabled = not self.active or self.model_overall.view_mode if disabled: return False handle = self.get_handle_for_mouse(event.pos()) if event.button() == QtCore.Qt.LeftButton and event.modifiers() & ADD_MODIFIER: idx, p = self.get_line_query_for_mouse(event.pos()) if idx is not None: anchors = self.model.get_anchors(idx) if len(anchors) < 2: p = self.V2im(p) idx = new_anchor_index(self.model, idx) cmd = cmd_set_handle_position(self.model, idx, p) self._parent.undoStack.push(cmd) self.idx_handle_sel = idx self.idx_handle_hover = None elif event.button() == QtCore.Qt.LeftButton and event.modifiers() & DEL_MODIFIER and ( handle is not None and handle >= 4): cmd = cmd_set_handle_position(self.model, handle, None) self._parent.undoStack.push(cmd) #self.model.set_handle(handle, None) self.idx_handle_sel = None self.idx_handle_hover = None elif event.button() == QtCore.Qt.LeftButton: self.idx_handle_sel = handle self.idx_handle_hover = None if handle is not None: self.behave_mode = MODE_DRAGGING else: return False return True def mouseReleaseEvent(self, event): disabled = not self.active or self.model_overall.view_mode if disabled: return False if event.button() == QtCore.Qt.LeftButton and self.behave_mode == MODE_DRAGGING: self.behave_mode = MODE_NONE else: return False return True def mouseMoveEvent(self, event): disabled = not self.active or self.model_overall.view_mode if disabled: return False needs_update = False idx = self.get_handle_for_mouse(event.pos()) if self.ghost_handle is not None: self.ghost_handle = None if self.behave_mode == MODE_NONE: if idx is not None: self.idx_handle_hover = idx else: self.idx_handle_hover = None if event.modifiers() & ADD_MODIFIER: line_idx, pos = self.get_line_query_for_mouse(event.pos()) if line_idx is not None: self.ghost_handle = pos elif self.behave_mode == MODE_DRAGGING: w_pos = self.V2im(Vec2(event.pos())) cmd = cmd_set_handle_position(self.model, self.idx_handle_sel, w_pos, merge=True) self._parent.undoStack.push(cmd) #self.model.set_handle(self.idx_handle_sel, w_pos) return False def focusOutEvent(self, evt): self.idx_handle_sel = None def keyPressEvent(self, evt): disabled = not self.active or self.model_overall.view_mode if disabled: return False if evt.key() == QtCore.Qt.Key_Escape: self.idx_handle_sel = None elif self.idx_handle_sel is not None: if evt.key() in (QtCore.Qt.Key_Delete, QtCore.Qt.Key_Backspace) and IDX_IS_ANCHOR(self.idx_handle_sel): cmd = cmd_set_handle_position(self.model, self.idx_handle_sel, None) self._parent.undoStack.push(cmd) #self.model.set_handle(self.idx_handle_sel, None) self.idx_handle_sel = None # Basic 1-px nudging elif evt.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Right, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down): nudge = { QtCore.Qt.Key_Left: (-1, 0), QtCore.Qt.Key_Right: ( 1, 0), QtCore.Qt.Key_Up: ( 0, -1), QtCore.Qt.Key_Down: ( 0, 1), }[evt.key()] current = Vec2(self.im2V(self.model.align_handles[self.idx_handle_sel])) viewspace = self.V2im(current + nudge) cmd = cmd_set_handle_position(self.model, self.idx_handle_sel, viewspace) self._parent.undoStack.push(cmd) #self.model.set_handle(self.idx_handle_sel, viewspace) self.ghost_handle = None def next_prev_child(self, next): all_handles = self.model.all_handles() step = -1 if next: step = 1 if self.behave_mode == MODE_DRAGGING: return True else: idx = self.idx_handle_sel if idx == None: return False while 1: idx = (idx + step) % len(all_handles) if all_handles[idx] is not None: self.idx_handle_sel = idx return True
class RectAlignmentModel(GenModel): def __init__(self, image): GenModel.__init__(self) self.__image = image # 4 corner handles, 2 (potential) anchor handles per line ini_shape = image.decoded_image.shape max_dim = float(max(ini_shape)) x = ini_shape[1] / max_dim y = ini_shape[0] / max_dim self.__align_handles = [Vec2(-x, -y), Vec2(x, -y), Vec2(x, y), Vec2(-x, y)] + [None] * 8 self.__dim_handles = [self.__align_handles[0], self.__align_handles[1], self.align_handles[1], self.align_handles[2]] self.__placeholder_dim_values = [100, 100] self.__dim_values = [None, None] self.update_matricies() translate_x = mdlacc(0) translate_y = mdlacc(0) origin_idx = mdlacc(0) persp_matrix = mdlacc(numpy.identity(3)) scale_matrix = mdlacc(numpy.identity(3)) rotate_theta = mdlacc(0.0) flip_x = mdlacc(False) flip_y = mdlacc(False) dims_locked = mdlacc(True, on=lambda self: self.update_matricies) def load(self, project): ra = self.__image.alignment assert isinstance(ra, RectAlignment) assert len(ra.dim_handles) == 4 assert len(ra.handles) == 12 # Handles self.__dim_handles = [point2_or_none(p) for p in ra.dim_handles] self.__align_handles = [point2_or_none(p) for p in ra.handles] # Dims self.dims_locked = ra.dims_locked self.dim_values[0] = ra.dims[0] self.dim_values[1] = ra.dims[1] # Center/Origin self.translate_x = ra.origin_center.x self.translate_y = ra.origin_center.y self.origin_idx = ra.origin_corner # Flips self.flip_x = ra.flip_x self.flip_y = ra.flip_y def save(self, project): align = RectAlignment(self.__align_handles, self.__dim_handles, self.__active_dims(), self.dims_locked, Point2(self.translate_x, self.translate_y), self.origin_idx, self.flip_x, self.flip_y) self.__image.set_alignment(align) self.__image.transform_matrix = self.image_matrix @property def flip_rotate_matrix(self): return rotate(self.rotate_theta).dot(scale(-1 if self.flip_x else 1, -1 if self.flip_y else 1)) @property def translate_matrix(self): pt = self.scale_matrix.dot(self.persp_matrix.dot(self.align_handles[self.origin_idx].homol())) pt /= pt[2] return translate(self.translate_x - pt[0], self.translate_y - pt[1]) @property def placeholder_dim_values(self): return RectAlignmentModel._lprox(self, self.__placeholder_dim_values, coerce=None) @property def dim_values(self): return RectAlignmentModel._lprox(self, self.__dim_values, coerce=None) @property def image_matrix(self): return self.translate_matrix.dot(self.flip_rotate_matrix.dot(self.scale_matrix.dot(self.persp_matrix))) def all_handles(self): if self.dims_locked: return self.align_handles else: return self.align_handles + self.dim_handles def set_handle(self, handle_id, pos): if IDX_IS_ANCHOR(handle_id) or IDX_IS_HANDLE(handle_id): if self.align_handles[handle_id] is None: self.align_handles[handle_id] = pos self.__update_line_pos(handle_id, pos) elif IDX_IS_DIM(handle_id): idx = handle_id - ANCHOR_MAX self.dim_handles[idx] = pos self.update_matricies() class _lprox(object): def __init__(self, par, backing, n = None, start = 0, coerce=Point2): self.par = par self.backing = backing self.n = n if n is None: self.n = len(self.backing) self.start = start self.coerce = coerce def __getitem__(self, item): if isinstance(item, int): if item >= self.n or item < 0: raise IndexError("index %s is not valid" % item) return self.backing[self.start + item] elif isinstance(item, slice): if item.start is None: new_start = self.start else: new_start = item.start + self.start if item.stop is None: new_stop = self.start + self.n else: new_stop = item.stop + self.start if (new_start < 0 or new_start >= self.n or (item.step != 1 and item.step is not None) or new_stop <= new_start or new_stop > self.n): raise IndexError("slice %s is not valid" % item) return RectAlignmentModel._lprox(self.par, self.backing, new_stop - new_start, new_start) raise TypeError def __setitem__(self, key, value): if value is not None and self.coerce is not None: value = self.coerce(value) if not isinstance(key, int): raise TypeError old = self.backing[self.start + key] self.backing[self.start + key] = value if none_compare(old, value): self.par.change() def __iter__(self): return iter(self.backing[self.start : self.start + self.n]) def __len__(self): return self.n def __add__(self, other): return list(self) + list(other) @property def align_handles(self): return RectAlignmentModel._lprox(self, self.__align_handles) @property def dim_handles(self): if self.dims_locked: return [self.__align_handles[0], self.__align_handles[1], self.align_handles[1], self.align_handles[2]] else: return RectAlignmentModel._lprox(self, self.__dim_handles, 4) def line_iter(self): return list(zip(self.align_handles[:PERIM_HANDLE_MAX], self.align_handles[1:PERIM_HANDLE_MAX] + self.align_handles[0:1])) def get_anchors(self, idx): assert IDX_IS_HANDLE(idx) base = 2 * idx + PERIM_HANDLE_MAX return [i for i in self.align_handles[base:base + 2] if i is not None] def __active_dims(self): if self.dim_values[0] is not None and self.dim_values[1] is not None: return self.dim_values return self.placeholder_dim_values def __update_line_pos(self, h_idx, pos): """ :param h_idx: index of the moved handle :param pos: point it was moved to :return: """ if pos is None: self.align_handles[h_idx] = None return pos = Vec2(pos) # If we're moving an endpoint if h_idx < 4: anchors_ahead = self.get_anchors(h_idx) anchors_prev = self.get_anchors((h_idx - 1) % 4) ahead_idx_2 = (h_idx + 1) % 4 line_ahead_2 = self.line_iter()[(h_idx + 1) % 4] pt_ahead_2 = None line_behind_2 = self.line_iter()[(h_idx - 2) % 4] behind_idx_2 = (h_idx - 1) % 4 pt_behind_2 = None if len(anchors_ahead) == 2 and len(anchors_prev) == 2: # Our lines are anchored on both sides by double-anchors, so we can't move at all return # If the ahead of prev line constrain us, then project the point to the line for motion elif len(anchors_ahead) == 2: pos, _ = project_point_line(pos, anchors_ahead[0], anchors_ahead[1], False) elif len(anchors_prev) == 2: pos, _ = project_point_line(pos, anchors_prev[0], anchors_prev[1], False) if len(anchors_ahead) == 1: itype, pt_ahead_2 = line_intersect(pos, anchors_ahead[0], line_ahead_2[0], line_ahead_2[1]) if itype != INTERSECT_NORMAL: return if len(anchors_prev) == 1: itype, pt_behind_2 = line_intersect(pos, anchors_prev[0], line_behind_2[0], line_behind_2[1]) if itype != INTERSECT_NORMAL: return if pt_ahead_2 is not None: self.align_handles[ahead_idx_2] = pt_ahead_2 if pt_behind_2 is not None: self.align_handles[behind_idx_2] = pt_behind_2 self.align_handles[h_idx] = pos else: line_idx = (h_idx - 4) // 2 o_idx = (h_idx & ~1) | (h_idx ^ 1) this_anchor = Vec2(self.align_handles[h_idx]) other_anchor = self.align_handles[o_idx] lines = list(self.line_iter()) this_line = lines[line_idx] prev_line = lines[(line_idx - 1) % 4] next_line = lines[(line_idx + 1) % 4] if other_anchor is None: if this_anchor is None: delta = Vec2(0,0) else: # One anchor, move the whole line by pos - this_anchor delta = pos - this_anchor pt_a = Vec2(this_line[0]) + delta pt_b = Vec2(this_line[1]) + delta else: pt_a = pos pt_b = Vec2(other_anchor) # Recalculate the endpoints intersect_cond, pt_prev = line_intersect(pt_a, pt_b, prev_line[0], prev_line[1]) if intersect_cond != INTERSECT_NORMAL: return intersect_cond, pt_next = line_intersect(pt_a, pt_b, next_line[0], next_line[1]) if intersect_cond != INTERSECT_NORMAL: return self.align_handles[line_idx] = pt_prev self.align_handles[(line_idx + 1) % 4] = pt_next # We can always move an anchor self.align_handles[h_idx] = pos def update_matricies(self): # Build compatible arrays for cv2.getPerspectiveTransform src = numpy.ones((4,2), dtype=numpy.float32) dst = numpy.ones((4,2), dtype=numpy.float32) src[:, :2] = self.align_handles[:4] dst[:, :2] = corners # And update the perspective transform self.persp_matrix = cv2.getPerspectiveTransform(src, dst) # Now, calculate the scale factor da = self.dim_handles[1] - self.dim_handles[0] db = self.dim_handles[3] - self.dim_handles[2] ma = da.mag() mb = db.mag() sf = 100.0/max(ma, mb) self.placeholder_dim_values[0] = sf * ma * MM self.placeholder_dim_values[1] = sf * mb * MM dims = self.__active_dims() # Perspective transform handles - convert to handles_pp = [] for handle in self.dim_handles: p1 = self.persp_matrix.dot(handle.homol()) p1 /= p1[2] handles_pp.append(p1[:2]) da = handles_pp[1] - handles_pp[0] db = handles_pp[3] - handles_pp[2] A = numpy.vstack([da**2, db**2]) B = numpy.array(dims) ** 2 res = numpy.abs(numpy.linalg.solve(A, B)) ** .5 self.scale_matrix = scale(res[0], res[1])