Module AmpliVision.src.objs.grid

Sub-modules

AmpliVision.src.objs.grid.Square
AmpliVision.src.objs.grid.grid

Classes

class Grid (img: numpy.ndarray)
Expand source code
class Grid:
    def __init__(self, img: np.ndarray):
        # scanned image
        self.img = img.copy()

        # setup ratios used in the grid
        # such as the plus minus, etc.
        self.setup_ratios()

        # saving blocks here and in grid creates 2 sources of truth.
        # This list should keep index or something.
        self.blocks = []

        # represents the grid in the image as a 2D array of squares. Initialized as 2D array of None to represent empty.
        self.grid = [
            [None for _ in range(self.MAX_INDEX + 1)]
            for _ in range(self.MAX_INDEX + 1)
        ]

        self.create_grid()

    ## Setup functions ##
    def setup_ratios(self):
        """
        ### Setup rations
        Function that sets up the ratios used in the grid.
        """
        # max x and y coordinates of the image
        self.MAX_XY = self.img.shape[0]  # assumes image is square

        # ratios measured experimentally as a percentage of the grid
        # size of pin diameter/grid size
        self.PIN_RATIO = int(self.MAX_XY * 0.012)
        # size of edge/grid size. Edge is the "lines" around squares
        self.EDGE_RATIO = int(self.MAX_XY * 0.01)
        # squares are the places where you can insert the ampli blocks
        self.SQUARE_RATIO = int(self.MAX_XY * 0.089)
        # an arbitrary general tolerance
        self.PLUS_MINUS = int(self.MAX_XY * 0.005)
        self.SQUARE_LENGTH = self.SQUARE_RATIO + self.EDGE_RATIO
        self.MAX_INDEX = 9  # assumes grid is square 10x10

    def create_grid(self):
        """ 
        ### Create grid
        Function that creates the grid of squares in the image.
        """
        # These are the stop values for the for loops.
        STOP_XY = self.MAX_XY - self.EDGE_RATIO
        STEP = self.SQUARE_LENGTH

        # iterate through the grid by moving in steps of SQUARE_LENGTH
        # until the max x and y values are reached.
        # x and y are the top left points of the squares
        # x_index and y_index are the index of the square in the grid
        for y, x in itertools.product(range(0, STOP_XY, STEP), range(0, STOP_XY, STEP)):

            # get the corresponding index of the square in the grid
            # flipped coordinates to make it (row, column)
            y_index, x_index = Utils.xy_to_index(self, x, y)

            # coordinates of the top left and bottom right points of the square
            top_left = (
                x + (self.EDGE_RATIO),
                y + (self.EDGE_RATIO)
            )
            bottom_right = (
                x + self.SQUARE_RATIO + (self.EDGE_RATIO),
                y + self.SQUARE_RATIO + (self.EDGE_RATIO)
            )

            # create a square object sq
            sq = Square(
                top_left,
                bottom_right,
                (x_index, y_index),
                self.PIN_RATIO,
                self.PLUS_MINUS,
                self.img
            )

            # add the square to the grid list
            self.grid[x_index][y_index] = sq

    ### Find functions ###

    def find_pins(self, contours: list[np.ndarray]):
        """
        ### Find potential pins
        ---------------
        Function that finds the pins and adds them to their bounding squares.
        The pins are found by finding the square structures in the image.

        #### Args:
        * contours: list of contours around non-grayscale (colorful) edges in image
        """

        # Square structures are 4 points (in this case pins),
        # arranged in the shape of a square
        square_structures, pin_list = self.get_square_structures(contours)

        # adds the 4 potential pins structured as a square shape to the
        # square in the grid where the middle of the structure is located
        for square_structure, pins in zip(square_structures, pin_list):

            # get the middle of the structure
            center = find_center_of_points(square_structure)

            # get the index of sq based on the center of the structure
            # flipped coordinates to make it (row, column)
            y_index, x_index = Utils.xy_to_index(self, center[0], center[1])

            # add pins to the appropriate square in the grid
            for pin in pins:
                self.grid[x_index][y_index].add_pin(pin)

    def find_blocks(self, contours: list[np.ndarray]):
        """
        ### Find blocks
        ---------------
        Function that determines which squares are blocks in the grid.
        It does this by finding the potential pins (p_pins) 
        then checking if the pin is in the corners of the square and adding it to the square if it is.

        #### Args:
        * contours: list of contours around non-grayscale (colorful) edges in image

        #### Returns:
        None
        """

        # finds the potential pins (p_pins)
        # and adds them to their bounding squares.
        self.find_pins(contours)

        # checks if the potential pins are in one of the corners of square.
        # adds potential pin as a pin to the square if it is.
        self.process_pins()

        # checks if the square has x or more pins
        # if it does, it is considered a block.
        self.add_blocks()

    ## Helper functions ##
    def process_pins(self):
        """
        checks if the potential pins are in one of the corners of square.
        Adds potential pin as a pin to the square if it is.
        """
        for sq in itertools.chain(*self.grid):
            if len(sq.get_p_pins()) < 4:
                continue

            for p_pin in sq.get_p_pins():
                x, y, w, h = cv.boundingRect(p_pin)

                # checks if top left or bottom right point of pin
                # is inside corner of square within error range
                if sq.is_in_corners_skewed(x, y, w, h):
                    sq.add_pin(p_pin)

    ### Get functions ###

    def get_blocks(self):
        return self.blocks

    def get_contour_centers(self, contours: list[np.ndarray]):
        """
        Function that finds the center point of each contour

        #### Args:
        * contours: list of contours around non-grayscale (colorful) edges in image

        #### Returns:
        * center_to_contour_index: dictionary with center points as keys and their corresponding contour indices as values
        """
        center_to_contour_index = {}
        for i, contour in enumerate(contours):
            center = find_center_of_contour(contour)
            if center is not None:
                center_to_contour_index[center] = i

        # save the indexes bounding the centers of the
        # contours in a list and remove None values
        centers = list(center_to_contour_index.keys())
        centers = [x for x in centers if x != None]

        return center_to_contour_index, centers

    def get_square_structures(self, contours: list[np.ndarray]):
        """
        ### Square structures
        ---------------
        Function that finds the square structures in the image.  
        A square structure is defined as 4 points (in this case potential pins) arranged in the shape of a square.

        #### Args:
        * contours: list of contours around non-grayscale (colorful) edges in image

        #### Returns:
        * square_structures: list of square structures
        * p_pins: list of p_pins
        """
        square_structures = []
        pins = []

        # find the center of each contour
        center_to_contour_index, centers = self.get_contour_centers(contours)

        # Find all combinations of four points
        combinations = list(itertools.combinations(centers, 4))

        # useful values for debuggin
        """
        point0_step = math.comb(len(centers)-1, 3)
        point1_step = math.comb(len(centers)-2, 2)
        point2_step = math.comb(len(centers)-3, 1)"""
        # print("centers:", len(centers), "combinations:", len(combinations))
        # print("point0_step:", point0_step, "point1_step:", point1_step, "point2_step:", point2_step)
        index = 0
        debug_flag = 0
        #step_filter = point0_step

        # iterate through the combinations of points
        for comb in combinations:
            # (previously) missing block @ 6,4 in image 6066
            '''if index == 1171074: #(1179520 - point1_step - (point2_step*45) - 1):
                print("special: ", index)
                debug_flag = True

            else:
                debug_flag = False
            '''
            if is_arranged_as_square(comb, self.img, self.SQUARE_LENGTH, recursion_flag=0, debug=debug_flag):

                # Add the square to the list of
                # combinations if it is arranged as a square
                square_structures.append(list(comb))

                # Find the indices of the contours that form the square
                contour_indices = [center_to_contour_index[point]
                                   for point in comb]
                pins.append([contours[i] for i in contour_indices])

            index += 1

        return square_structures, pins

    ## Add functions ##

    def add_blocks(self):
        for sq in itertools.chain(*self.grid):
            if len(sq.get_pins()) >= 4:
                sq.is_block = True
                self.blocks.append(sq)

    def add_artificial_block(self, index: tuple[int, int], img, sq_img: np.ndarray):
        """
        ### Add artificial block
        ---------------
        Function that adds an artificial block to the grid.

        #### Args:
        * index: index of the square in the grid
        * sq_img: image of the square
        """

        # get the square in the grid
        i, j = index
        sq = self.grid[i][j]

        # No need to worry about all the square parameters,
        # After the colage, we can pass it through phase 1 again
        # sq.is_block = True, sq.block_type = ...
        return self.paste_block(img, sq_img, sq.tl, sq.br)

    ### Draw functions ###

    def paste_test_area(self, block):
        """ replaces the pixels at the block's location with the block's test area image """
        
        
        # get the top left and bottom right points of the block
        tl, br = block.tl, block.br

        # get the images
        img = self.img
        test_area_img = block.test_area_img

        # pixel coordinates
        corners = block.calculate_corners_pinbased()
        y_min = corners[0][1][1]
        y_max = corners[2][0][1]
        x_min = corners[0][1][0]
        x_max = corners[2][0][0]

        # paste the image of the block's test area on the grid        
        img[y_min:y_max, x_min:x_max] = test_area_img

        self.img = img

    def paste_block(self, img, sq_img, tl, br):
        " pastes the image of the block with transparent bkg on the grid "

        # paste the image of the square on the grid
        # at the top left and bottom right points of the square
        # tl and br are the top left and bottom right points of the square
        center_pt = self.calculate_center(tl, br)
        # sq_img = cv.resize(sq_img, sq_size)

        img = self.add_transparent_image(img, sq_img, center_pt)

        return img

    def add_transparent_image(self, background, foreground, center_pt):
        "Source: https://stackoverflow.com/questions/40895785/using-opencv-to-overlay-transparent-image-onto-another-image"

        bg_h, bg_w, bg_channels = background.shape
        fg_h, fg_w, fg_channels = foreground.shape

        assert bg_channels == 3, f"background image should have exactly 3 channels (RGB). found:{bg_channels}"
        assert fg_channels == 4, f"foreground image should have exactly 4 channels (RGBA). found:{fg_channels}"

        # center the foreground image on the background image according to the center point
        x_offset = center_pt[0] - fg_w // 2
        y_offset = center_pt[1] - fg_h // 2

        w = min(fg_w, bg_w, fg_w + x_offset, bg_w - x_offset)
        h = min(fg_h, bg_h, fg_h + y_offset, bg_h - y_offset)

        if w < 1 or h < 1:
            return

        # clip foreground and background images to the overlapping regions
        bg_x = max(0, x_offset)
        bg_y = max(0, y_offset)
        fg_x = max(0, x_offset * -1)
        fg_y = max(0, y_offset * -1)
        foreground = foreground[fg_y:fg_y + h, fg_x:fg_x + w]
        background_subsection = background[bg_y:bg_y + h, bg_x:bg_x + w]

        # separate alpha and color channels from the foreground image
        foreground_colors = foreground[:, :, :3]
        alpha_channel = foreground[:, :, 3] / 255  # 0-255 => 0.0-1.0

        # construct an alpha_mask that matches the image shape
        alpha_mask = np.dstack((alpha_channel, alpha_channel, alpha_channel))

        # combine the background with the overlay image weighted by alpha
        composite = background_subsection * \
            (1 - alpha_mask) + foreground_colors * alpha_mask

        # overwrite the section of the background image that has been updated
        background[bg_y:bg_y + h, bg_x:bg_x + w] = composite

        return background

    def draw_gridLines(self, img: np.ndarray):
        """
        ### draws grid lines
        ---------------
        Function that draws the grid lines on the image.
        """

        # draw grid lines
        start = 0 + self.EDGE_RATIO
        stop = self.MAX_XY - self.EDGE_RATIO + self.PLUS_MINUS
        step = self.SQUARE_RATIO + self.EDGE_RATIO

        for i, j in itertools.product(range(start, stop, step), repeat=2):
            # vertical lines
            cv.line(img, (i, 0), (i, self.MAX_XY), (0, 255, 0), 2)
            cv.line(img, (i + self.SQUARE_RATIO, 0),
                    (i + self.SQUARE_RATIO, self.MAX_XY), (0, 255, 0), 2)

            # horizontal lines
            cv.line(img, (0, i), (self.MAX_XY, i), (0, 255, 0), 2)
            cv.line(img, (0, i + self.SQUARE_RATIO),
                    (self.MAX_XY, i + self.SQUARE_RATIO), (0, 255, 0), 2)

    def draw_blocks(self, image_copy: np.ndarray, show_pins=False, show_corners=False):
        """
        Function that shows image with pins and corners drawn

        #### Args:
        * image_copy: copy of the original image
        * show_pins: boolean to show pins
        * show_corners: boolean to show corners
        """
        for blk in self.blocks:
            blk.draw_pins(image_copy) if show_pins else None
            blk.draw_corners(image_copy) if show_corners else None
            cv.rectangle(image_copy, blk.tl, blk.br, (0, 0, 255), 3)

    def calculate_center(self, lft_top, rgt_bot):
        """
        ### Calculate center
        ---------------
        Function that calculates the center of a square.

        #### Args:
        * lft_top: top left point of the square
        * rgt_bot: bottom right point of the square

        #### Returns:
        * center: center point of the square
        """
        x = (lft_top[0] + rgt_bot[0]) // 2
        y = (lft_top[1] + rgt_bot[1]) // 2
        return (x, y)

    def get_square(self, index):
        """
        ### Get square
        ---------------
        Function that gets the square at the given index.

        #### Args:
        * index: index of the square in the grid

        #### Returns:
        * square: square at the given index
        """
        return self.grid[index[0]][index[1]]

    def set_square(self, index, square):
        """
        ### Set square
        ---------------
        Function that sets the square at the given index.

        #### Args:
        * index: index of the square in the grid
        * square: square to set
        """
        self.grid[index[0]][index[1]] = square

Methods

def add_artificial_block(self, index: tuple[int, int], img, sq_img: numpy.ndarray)

Add artificial block


Function that adds an artificial block to the grid.

Args:

  • index: index of the square in the grid
  • sq_img: image of the square
def add_blocks(self)
def add_transparent_image(self, background, foreground, center_pt)
def calculate_center(self, lft_top, rgt_bot)

Calculate center


Function that calculates the center of a square.

Args:

  • lft_top: top left point of the square
  • rgt_bot: bottom right point of the square

Returns:

  • center: center point of the square
def create_grid(self)

Create grid

Function that creates the grid of squares in the image.

def draw_blocks(self, image_copy: numpy.ndarray, show_pins=False, show_corners=False)

Function that shows image with pins and corners drawn

Args:

  • image_copy: copy of the original image
  • show_pins: boolean to show pins
  • show_corners: boolean to show corners
def draw_gridLines(self, img: numpy.ndarray)

draws grid lines


Function that draws the grid lines on the image.

def find_blocks(self, contours: list[numpy.ndarray])

Find blocks


Function that determines which squares are blocks in the grid. It does this by finding the potential pins (p_pins) then checking if the pin is in the corners of the square and adding it to the square if it is.

Args:

  • contours: list of contours around non-grayscale (colorful) edges in image

Returns:

None

def find_pins(self, contours: list[numpy.ndarray])

Find potential pins


Function that finds the pins and adds them to their bounding squares. The pins are found by finding the square structures in the image.

Args:

  • contours: list of contours around non-grayscale (colorful) edges in image
def get_blocks(self)
def get_contour_centers(self, contours: list[numpy.ndarray])

Function that finds the center point of each contour

Args:

  • contours: list of contours around non-grayscale (colorful) edges in image

Returns:

  • center_to_contour_index: dictionary with center points as keys and their corresponding contour indices as values
def get_square(self, index)

Get square


Function that gets the square at the given index.

Args:

  • index: index of the square in the grid

Returns:

  • square: square at the given index
def get_square_structures(self, contours: list[numpy.ndarray])

Square structures


Function that finds the square structures in the image.
A square structure is defined as 4 points (in this case potential pins) arranged in the shape of a square.

Args:

  • contours: list of contours around non-grayscale (colorful) edges in image

Returns:

  • square_structures: list of square structures
  • p_pins: list of p_pins
def paste_block(self, img, sq_img, tl, br)

pastes the image of the block with transparent bkg on the grid

def paste_test_area(self, block)

replaces the pixels at the block's location with the block's test area image

def process_pins(self)

checks if the potential pins are in one of the corners of square. Adds potential pin as a pin to the square if it is.

def set_square(self, index, square)

Set square


Function that sets the square at the given index.

Args:

  • index: index of the square in the grid
  • square: square to set
def setup_ratios(self)

Setup rations

Function that sets up the ratios used in the grid.