Beispiel #1
0
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)
Beispiel #2
0
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)
Beispiel #3
0
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()
Beispiel #4
0
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
Beispiel #5
0
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
Beispiel #6
0
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)
Beispiel #7
0
class FakeModel(GenModel):
    j = mdlacc(3)
Beispiel #8
0
class FakeOther(object):
    j = mdlacc(3)
Beispiel #9
0
class SelectToolModel(GenModel):
    vers = mdlacc(SelectByModes.POINT)
Beispiel #10
0
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
Beispiel #11
0
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])