Module AmpliVision.src.objs
Sub-modules
AmpliVision.src.objs.graph
AmpliVision.src.objs.grid
AmpliVision.src.objs.image
AmpliVision.src.objs.test_analyzer
AmpliVision.src.objs.utils
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.
class Square (tl: int, br: int, index: ast.Tuple, PIN_RATIO: int, PLUS_MINUS: int, img: numpy.ndarray)
-
Square
Class that represents a square in the grid_ds.
Args:
- tl: top left point of the square
- br: bottom right point of the square
- index: index of the square in the grid_ds
Attributes:
- tl: top left point of the square
- br: bottom right point of the square
- index: index of the square in the grid_ds
- block: boolean that indicates if the square is a block
- pin_count: number of pins in the square
Methods:
- add_pin: adds a pin to the square
- draw_pins: draws the pins in the square
- draw_corners: draws the corners of the square
- createImg: creates an image of the square, a cutout of the image around the square
- add_corners: adds the corners of the square to the square object
- is_in_corners: checks if a point is in the corners of the square
- which_corner_is_contour_in: finds which corner of square a contour is in
- get_rgb_avg_of_contour: gets the average RGB of a contour in the image
- get_pins_rgb: gets the average RGB of the pins in the square
Expand source code
class Square: """ ### Square --------------- Class that represents a square in the grid_ds. #### Args: * tl: top left point of the square * br: bottom right point of the square * index: index of the square in the grid_ds #### Attributes: * tl: top left point of the square * br: bottom right point of the square * index: index of the square in the grid_ds * block: boolean that indicates if the square is a block * pin_count: number of pins in the square #### Methods: * add_pin: adds a pin to the square * draw_pins: draws the pins in the square * draw_corners: draws the corners of the square * createImg: creates an image of the square, a cutout of the image around the square * add_corners: adds the corners of the square to the square object * is_in_corners: checks if a point is in the corners of the square * which_corner_is_contour_in: finds which corner of square a contour is in * get_rgb_avg_of_contour: gets the average RGB of a contour in the image * get_pins_rgb: gets the average RGB of the pins in the square """ def __init__(self, tl: int, br: int, index: Tuple, PIN_RATIO: int, PLUS_MINUS: int, img: np.ndarray) -> None: # potential pins self.p_pins = [] # pins self.pins = [] # block or not and type of block self.is_block = False self.block_type = '' # RBG values of the pins in the square (tl, tr, bl, br) self.rgb_sequence = [] # coordinates and index in Grid self.tl = tl self.br = br self.index = index # image and image of the square for visualization if necessary self.img = img.copy() if img is not None: self.sq_img = self.createImg(img.copy()) # corners of the square self.corners = [] self.add_corners(PIN_RATIO, PLUS_MINUS) self.test_area_img = None # rotation of the block. 0 is vertical strip with bkg on bottom. 1 is horizontal strip with bkg on left side. etc. self.rotation = 0 # ratios self.PIN_RATIO = PIN_RATIO self.PLUS_MINUS = PLUS_MINUS ## Get functions ## def get_index(self) -> Tuple: """ Returns the index of the square """ return self.index def get_p_pins(self) -> list[int]: """ Returns the potential pins in the square """ return self.p_pins def get_pins(self) -> list[int]: """ Returns the pins in the square """ return self.pins def get_corners(self) -> list[int]: """ Returns the corners of the square """ return self.corners def get_img(self) -> np.ndarray: """ Returns the image of the square """ return self.img def get_sq_img(self) -> np.ndarray: """ Returns the image of the square """ if self.sq_img is None: self.sq_img = self.createImg(self.img) return self.sq_img def get_test_area_img(self) -> np.ndarray: " Returns the image of squares test area (inner square where test strip can be)" if self.test_area_img is None: self.test_area_img = self.create_test_area_img(self.get_sq_img()) return self.test_area_img def get_block_type(self) -> str: """ Returns the block type of the square """ if self.is_block: return self.block_type else: return "Not a block" def get_rgb_sequence(self) -> list[int]: """ Returns the RGB sequence of the square """ return self.rgb_sequence def createImg(self, img: np.ndarray) -> np.ndarray: """ Creates an image of the square, a cutout of the image around the square""" return img[(self.tl[1]-10):(self.br[1]+10), (self.tl[0]-10):(self.br[0]+10)] def create_test_area_img(self, sq_img: np.ndarray) -> np.ndarray: " Creates an image of the inner test spot" sq_img = self.img corners = self.calculate_corners_pinbased() """ r = sq_img[a:b, c:d] means that the image r is a cutout of the image sq_img. from the top left corner (a, c) to the bottom right corner (b, d). where a, b, c, d are the coordinates of the corners of the square. for example, a = corners[0][1][1] means that a is the y coordinate of the bottom right corner of the top right corner of the square. b = corners[2][0][1] means that b is the y coordinate of the top left corner of the bottom right corner of the square. c = corners[0][1][0] means that c is the x coordinate of the bottom right corner of the top right corner of the square. d = corners[2][0][0] means that d is the x coordinate of the top left corner of the bottom right corner of the square. """ return sq_img[corners[0][1][1]:corners[2][0][1], corners[0][1][0]:corners[2][0][0]] ## Add functions ## def add_pin(self, pin: np.ndarray) -> None: """ Adds a pin to the square """ self.pins.append(pin) def add_p_pin(self, pin: np.ndarray) -> None: """ Adds a potential pin to the square """ self.p_pins.append(pin) def add_corners(self, PIN_RATIO: int, PLUS_MINUS: int, p: int = 3, a: float = 1.8) -> None: """ Adds the corners of the square to the square object #### Args: * PIN_RATIO: ratio of the pin size to the square size * PLUS_MINUS: arbitrary tolerance value * p: "padding" value. Determines size of the corners. * a: skew value. Is the exponential determining how skewed the corners are. """ # top left and bottom right coordinates of the square tl_x, tl_y = self.tl br_x, br_y = self.br # Skewing the corners in relation to the center of the grid to account for perspective. # the further away from the center, the more skewed the corners are (exponential). # Avoiding division by zero SKEW_x, SKEW_y = self.calculate_skew(a) # The following four values: top_right, top_left, bottom_right, bottom_left are the corners of the square. # Each corner contains its top left and bottom right coordinates. # Coordinates are calculated using: # top left and bottom right coordinates of the square, arbitrary plus minus value, the padding value and the skew value. self.corners = self.calculate_corners( tl_x, tl_y, br_x, br_y, PIN_RATIO, PLUS_MINUS, p, SKEW_x, SKEW_y) def calculate_skew(self, a: float) -> Tuple: """ Calculates the skew originated from cellphone cameras |x-4|^a * (x-4)/|x-4| """ if self.index[0] != 4: SKEW_x = int( (abs(self.index[0] - 4) ** a) * ((self.index[0] - 4) / abs(self.index[0] - 4))) else: SKEW_x = 0 # Avoiding division by zero if self.index[1] != 4: SKEW_y = int( (abs(self.index[1] - 4) ** a) * ((self.index[1] - 4) / abs(self.index[1] - 4))) else: SKEW_y = 0 return SKEW_x, SKEW_y def calculate_corners(self, tl_x: int, tl_y: int, br_x: int, br_y: int, PIN_RATIO: int, PLUS_MINUS: int, p: int, SKEW_x: int, SKEW_y: int) -> list[int]: """ Calculates the corners of the square using magic. The "corners" here refer to the space in the ampli block where the pins are located. """ top_right = ( (tl_x - (p*PLUS_MINUS) + SKEW_x, tl_y - (p*PLUS_MINUS) + SKEW_y), (tl_x + PIN_RATIO + (p*PLUS_MINUS) + SKEW_x, tl_y + PIN_RATIO + (p*PLUS_MINUS) + SKEW_y) ) top_left = ( (br_x - PIN_RATIO - (p*PLUS_MINUS) + SKEW_x, tl_y - (p*PLUS_MINUS) + SKEW_y), (br_x + (p*PLUS_MINUS) + SKEW_x, tl_y + PIN_RATIO + (p*PLUS_MINUS) + SKEW_y) ) bottom_right = ( (tl_x - (p*PLUS_MINUS) + SKEW_x, br_y - PIN_RATIO - (p*PLUS_MINUS) + SKEW_y), (tl_x + PIN_RATIO+(p*PLUS_MINUS) + SKEW_x, br_y + (p*PLUS_MINUS) + SKEW_y) ) bottom_left = ( (br_x - PIN_RATIO - (p*PLUS_MINUS) + SKEW_x, br_y - PIN_RATIO - (p*PLUS_MINUS) + SKEW_y), (br_x + (p*PLUS_MINUS) + SKEW_x, br_y + (p*PLUS_MINUS) + SKEW_y) ) return [top_right, top_left, bottom_right, bottom_left] def calculate_corners_pinbased(self) -> list[list[int]]: """ Calculates the corners of the square based on the pins in the square. To be used after the pins have been added to the square. list[[corner_tl, corner_br], ...] in clockwise order starting from top left. """ corners = [] # pin is a list of contours for pin in self.pins: x, y, w, h = cv.boundingRect(pin) # add extra padding to the corners px, py = self.calculate_skew(0.2) px = int(px) py = int(py) # append top left and bottom right points of the test area corners.append([(x-px, y-py), (x+w+px, y+h+py)]) return self.order_corner_points(corners) ## Drawing functions ## def draw_p_pins(self, image: np.ndarray) -> None: """ Draws the potential pins in the square """ for pin in self.p_pins: cv.drawContours(image, pin, -1, (0, 255, 0), 3) def draw_pins(self, image: np.ndarray) -> None: """ Draws the pins in the square """ for pin in self.pins: cv.drawContours(image, pin, -1, (0, 255, 0), 3) def draw_corners(self, img: np.ndarray) -> None: """ Draws the corners of the square """ for corner in self.corners: cv.rectangle(img, corner[0], corner[1], (0, 0, 255), 1) def draw_corners_pinbased(self, img: np.ndarray) -> None: """ Draws the corners of the square based on the pins in the square To be used after the pins have been added to the square.""" # pin is a list of contours for x, y in self.calculate_corners_pinbased(): cv.rectangle(img, x, y, (0, 0, 255), 1) def draw_test_area(self, img: np.ndarray) -> None: "Draws the test area of the square" corners = self.calculate_corners_pinbased() cv.rectangle(img, corners[0][1], corners[2][0], (0, 0, 255), 1) ### Boolean functions ### def is_in_test_bounds(self, x: int, y: int) -> bool: "checks if coordinate is within test bounds (inner square where strip is)" pass def is_in_corners(self, x: int, y: int) -> bool: """ Checks if a point is in the corners of the square. """ # corn = ["top_left", "top_right", "bottom_left", "bottom_right"] i = 0 for corner in self.corners: if x >= corner[0][0] and x <= corner[1][0]: if y >= corner[0][1] and y <= corner[1][1]: # print(corn[i], ": ", round(self.get_rgb_avg_of_contour(contour))) return True i += 1 return False def is_in_corners_skewed(self, x: int, y: int, w: float, h: float) -> bool: """ Checks if a point is in the corners of the square, taking into consideration the skewing that happens.""" return (self.is_in_corners(x, y) or self.is_in_corners(x+int(w), y+int(h)) or self.is_in_corners(x-int(w), y-int(h)) or self.is_in_corners(x+int(w), y-int(h)) or self.is_in_corners(x-int(w), y+int(h))) def which_corner_is_contour_in(self, contour: np.ndarray = None, xy=None) -> str: """ Function that finds which corner of square a contour is in. """ corn = ["top_left", "top_right", "bottom_left", "bottom_right"] if xy is None: x, y = cv.boundingRect(contour)[:2] else: x, y = xy x = int(x) y = int(y) i = 0 for corner in self.corners: if x >= corner[0][0] and x <= corner[1][0]: if y >= corner[0][1] and y <= corner[1][1]: return corn[i] i += 1 # might be unecessary after corner skewing i = 0 for corner in self.corners: if x + (2*self.PLUS_MINUS) >= corner[0][0] and x - (2*self.PLUS_MINUS) <= corner[1][0]: if y + (2*self.PLUS_MINUS) >= corner[0][1] and y - (2*self.PLUS_MINUS) <= corner[1][1]: return corn[i] i += 1 def order_corner_points(self, corners: list[int]) -> list[int]: """ Orders the corners of the square in a clockwise manner starting from the top-left corner. """ # top right, top left, bottom right, bottom left ordered_corners = [None, None, None, None] for xy in corners: mid_x = (xy[0][0] + xy[1][0]) / 2 mid_y = (xy[0][1] + xy[1][1]) / 2 s = self.which_corner_is_contour_in(xy=(mid_x, mid_y)) if s == "top_left": ordered_corners[0] = xy elif s == "top_right": ordered_corners[1] = xy elif s == "bottom_right": ordered_corners[2] = xy elif s == "bottom_left": ordered_corners[3] = xy if None in ordered_corners: print("\nError in ordering corners\n") return None return ordered_corners # set functions def set_rgb_sequence(self) -> None: """ ### Set rgb sequence --------------- Function that sets the rgb sequence of the square. #### Returns: * None """ # get the RGB values of the pins in the square pins_rgb, corner_key = get_pins_rgb(self) # fixing the order from tr,tl,br,bl to clockwise starting from top-right. This might be the ugliest code I've ever written. But it works! set_rgb_sequence_clockwise(self, pins_rgb, corner_key) def set_test_area_img(self, img): " Sets the image of the test area" self.test_area_img = img
Methods
def add_corners(self, PIN_RATIO: int, PLUS_MINUS: int, p: int = 3, a: float = 1.8) ‑> None
-
Adds the corners of the square to the square object
Args:
- PIN_RATIO: ratio of the pin size to the square size
- PLUS_MINUS: arbitrary tolerance value
- p: "padding" value. Determines size of the corners.
- a: skew value. Is the exponential determining how skewed the corners are.
def add_p_pin(self, pin: numpy.ndarray) ‑> None
-
Adds a potential pin to the square
def add_pin(self, pin: numpy.ndarray) ‑> None
-
Adds a pin to the square
def calculate_corners(self, tl_x: int, tl_y: int, br_x: int, br_y: int, PIN_RATIO: int, PLUS_MINUS: int, p: int, SKEW_x: int, SKEW_y: int) ‑> list[int]
-
Calculates the corners of the square using magic. The "corners" here refer to the space in the ampli block where the pins are located.
def calculate_corners_pinbased(self) ‑> list[list[int]]
-
Calculates the corners of the square based on the pins in the square. To be used after the pins have been added to the square. list[[corner_tl, corner_br], …] in clockwise order starting from top left.
def calculate_skew(self, a: float) ‑> ast.Tuple
-
Calculates the skew originated from cellphone cameras
|x-4|^a * (x-4)/|x-4|
def createImg(self, img: numpy.ndarray) ‑> numpy.ndarray
-
Creates an image of the square, a cutout of the image around the square
def create_test_area_img(self, sq_img: numpy.ndarray) ‑> numpy.ndarray
-
Creates an image of the inner test spot
def draw_corners(self, img: numpy.ndarray) ‑> None
-
Draws the corners of the square
def draw_corners_pinbased(self, img: numpy.ndarray) ‑> None
-
Draws the corners of the square based on the pins in the square To be used after the pins have been added to the square.
def draw_p_pins(self, image: numpy.ndarray) ‑> None
-
Draws the potential pins in the square
def draw_pins(self, image: numpy.ndarray) ‑> None
-
Draws the pins in the square
def draw_test_area(self, img: numpy.ndarray) ‑> None
-
Draws the test area of the square
def get_block_type(self) ‑> str
-
Returns the block type of the square
def get_corners(self) ‑> list[int]
-
Returns the corners of the square
def get_img(self) ‑> numpy.ndarray
-
Returns the image of the square
def get_index(self) ‑> ast.Tuple
-
Returns the index of the square
def get_p_pins(self) ‑> list[int]
-
Returns the potential pins in the square
def get_pins(self) ‑> list[int]
-
Returns the pins in the square
def get_rgb_sequence(self) ‑> list[int]
-
Returns the RGB sequence of the square
def get_sq_img(self) ‑> numpy.ndarray
-
Returns the image of the square
def get_test_area_img(self) ‑> numpy.ndarray
-
Returns the image of squares test area (inner square where test strip can be)
def is_in_corners(self, x: int, y: int) ‑> bool
-
Checks if a point is in the corners of the square.
def is_in_corners_skewed(self, x: int, y: int, w: float, h: float) ‑> bool
-
Checks if a point is in the corners of the square, taking into consideration the skewing that happens.
def is_in_test_bounds(self, x: int, y: int) ‑> bool
-
checks if coordinate is within test bounds (inner square where strip is)
def order_corner_points(self, corners: list[int]) ‑> list[int]
-
Orders the corners of the square in a clockwise manner starting from the top-left corner.
def set_rgb_sequence(self) ‑> None
-
Set rgb sequence
Function that sets the rgb sequence of the square.
Returns:
- None
def set_test_area_img(self, img)
-
Sets the image of the test area
def which_corner_is_contour_in(self, contour: numpy.ndarray = None, xy=None) ‑> str
-
Function that finds which corner of square a contour is in.
class TestAnalyzer (block)
-
This class is responsible for getting and analyzing test results a.k.a phase B
Expand source code
class TestAnalyzer: "This class is responsible for getting and analyzing test results a.k.a phase B" def __init__(self, block): self.block = block # look only at the inner test square: self.test_square_img = block.get_test_area_img() # square used in csv export self.grid_index = block.index self.block_type = block.get_block_type() self.strip_sections = { "bkg": StripSection(self.test_square_img, 'bkg', block.rotation), "spot1": StripSection(self.test_square_img, 'spot1', block.rotation), "spot2": StripSection(self.test_square_img, 'spot2', block.rotation) } def analyze_test_result(self, double_thresh = False, display: bool = False): # should I name it main? "gets test results from a block, analyses them, and export them to csv" # find the positive spots with hsv mask # need to think about cases where mask for example return one pixel. # do you check for minimum contour size? do you only look for it manually? food for thought if display: print("rotation: ", self.block.rotation) # thresholds optimized for marker data rgb_spots = ColorContourExtractor.process_image( self.test_square_img, hsv_lower=[0, 40, 20], double_thresh=double_thresh, display=display ) if display: cv.waitKey(100) plt.close() cv.destroyAllWindows() self.add_positives_to_sections(rgb_spots, display=display) # find the negative spots "manually" through ratios self.add_negatives_to_sections(display=display) # get background color noise so we can remove it from other sections self.strip_sections['bkg'].set_total_avg_rgb() bkg_rgb_avg = self.strip_sections['bkg'].total_avg_rgb # remove background noise from other sections corrected_rgbs = [] for section in self.strip_sections.values(): if section.strip_type != 'bkg': if display: print(f"{section.strip_type}AVG RGB: {section.total_avg_rgb}") print("correcting: ", section.strip_type) corrected_rgbs.append(section.subtract_bkg(bkg_rgb_avg)) if display: print("\n") # validate results to catch any potential errors in the test "TODO: adapt validate_results to work with the new strip configuration" # self.validate_results() # export results to csv row = self.create_csv_row(corrected_rgbs) return row def add_positives_to_sections(self, rgb_spots, display: int = 0) -> None: "used to add positive result spots to appropriate strip section" # adds each spot to its strip section for spot in rgb_spots: # display the spot cpy = cv.drawContours(self.test_square_img.copy(), [spot], -1, (0, 255, 0), 1) #plt.imshow(f'TA/add_positives_to_sections', cv.resize(cpy, (400, 400))) for section in self.strip_sections.values(): if section.bounds_contour(spot): section.add_spot(self.block, spot, True, debug=display) # break # only adds to one section def add_negatives_to_sections(self, display: int = 0) -> None: "used to find negative result spots to appropriate strip section" for type, section in zip(self.strip_sections.keys(), self.strip_sections.values()): if len(section.spots) == 0: section.set_spots_manually(self.block, debug=display) def validate_results(self) -> None: "deals with test result potential positive, negative, false positive, error scenarios" results = self.get_section_results() # 1 test is properly positive (bkg, test, and spot2 line rgbs are > threshold) if results[1] & results[2]: print("Test worked properly and result is positive") # 2 test is properly negative (spot2 line rgb is > threshold) elif (not results[1]) & results[2]: print("Test worked properly and result is negative") # 3 spot2 error (bkg, and maybe test line rgbs are > threshold) else: print("Test may have not worked properly") def get_section_results(self) -> list[bool]: "returs a list of booleans representing the result (positive or negative) of each section bkg, test, spot2" results = [] # bkg, test, spot2 for strip in self.strip_sections.values(): strip_result = False # display # strip.print_spots() for spot in strip.spots: if spot["positive"] == True: strip_result = True break results.append(strip_result) print("\n") return results def create_csv_row(self, corrected_rgbs: list[list]) -> str: """ writes the test results to csv file row in format:\n date, time, grid_index, block_type, bkg_r, bkg_g, bkg_b, test_r, test_g, test_b, cntrl_r, cntrl_g, cntrl_b""" # get current date and time now = datetime.now() # format date and time date = now.strftime("%m/%d/%Y") time = now.strftime("%H:%M:%S") # setting all rgb values to None bkg_r = " None" bkg_g, bkg_b = bkg_r, bkg_r spot1_r, spot1_g, spot1_b = bkg_r, bkg_r, bkg_r spot2_r, spot2_g, spot2_b = bkg_r, bkg_r, bkg_r # get rgb values of each section if self.strip_sections['bkg'].total_avg_rgb != None: bkg_b, bkg_g, bkg_r = self.strip_sections['bkg'].total_avg_rgb # print("bkg rgb: ", bkg_r, bkg_g, bkg_b) if self.strip_sections['spot1'].total_avg_rgb != None: spot1_b, spot1_g, spot1_r = self.strip_sections['spot1'].total_avg_rgb #print("spot1 rgb: ", test_r, test_g, test_b) if self.strip_sections["spot2"].total_avg_rgb != None: spot2_b, spot2_g, spot2_r = self.strip_sections['spot2'].total_avg_rgb spot1_corr_b, spot1_corr_g, spot1_corr_r = corrected_rgbs[0] spot2_corr_b, spot2_corr_g, spot2_corr_r = corrected_rgbs[1] # create data to be written to csv data = [ date, time, self.grid_index, self.block_type, spot1_r, spot1_g, spot1_b, spot2_r, spot2_g, spot2_b, bkg_r, bkg_g, bkg_b, spot1_corr_r, spot1_corr_g, spot1_corr_b, spot2_corr_r, spot2_corr_g, spot2_corr_b ] return data def paint_spots(self, rgb_spot_results: dict[list]): """ colors the spots in the image accourding to the results Args: rgb_spot_results (dict[list]): dictionary containing the rgb values of the spots in the image format: {r: [mean1, std1, mean2, std2], g: [mean1, std1, mean2, std2], b: [mean1, std1, mean2, std2]} """ from numpy import random import time # can show improved performance if get_rgb_avg_of_contour TODO is done t = time.time() self.analyze_test_result() #print(f" analyze_test_result in {round(time.time() - t,2)}") image = self.test_square_img image_ = image.copy() for type, section in self.strip_sections.items(): # dont paint bkg if section.strip_type == 'bkg': continue # get the correct index for result i = 0 if type == 'spot1' else 2 rgb = [] means = [] for c in ('b', 'g', 'r'): mean, std = rgb_spot_results[c][i:i+2] means.append(mean) rgb.append(int(random.normal(mean, std))) rgb = tuple(rgb) if means == [0,0,0]: continue #print(f"Paiting {type} with {rgb}") image_ = section.paint_spot(image, rgb, display=False) self.block.set_test_area_img(image_) return self.block
Methods
def add_negatives_to_sections(self, display: int = 0) ‑> None
-
used to find negative result spots to appropriate strip section
def add_positives_to_sections(self, rgb_spots, display: int = 0) ‑> None
-
used to add positive result spots to appropriate strip section
def analyze_test_result(self, double_thresh=False, display: bool = False)
-
gets test results from a block, analyses them, and export them to csv
def create_csv_row(self, corrected_rgbs: list[list]) ‑> str
-
writes the test results to csv file row in format:
date, time, grid_index, block_type, bkg_r, bkg_g, bkg_b, test_r, test_g, test_b, cntrl_r, cntrl_g, cntrl_b
def get_section_results(self) ‑> list[bool]
-
returs a list of booleans representing the result (positive or negative) of each section bkg, test, spot2
def paint_spots(self, rgb_spot_results: dict[list])
-
colors the spots in the image accourding to the results
Args
rgb_spot_results
:dict[list]
- dictionary containing the rgb values of the spots in the image
format
- {r: [mean1, std1, mean2, std2], g: [mean1, std1, mean2, std2], b: [mean1, std1, mean2, std2]}
def validate_results(self) ‑> None
-
deals with test result potential positive, negative, false positive, error scenarios
class TestGraph (blocks)
-
Expand source code
class TestGraph: def __init__(self, blocks): self.graph = self.build_graph(blocks) def build_graph(self, blocks): G = nx.DiGraph() # add nodes (block types) to graph. nodes = [block.block_type for block in blocks] # make sure duplicate names are changed. Ex [wick, wick] -> [wick_1, wick_2] nodes = self.make_unique_names(nodes) G.add_nodes_from(nodes) # add edges (sequence of blocks) to graph positions = [block.index for block in blocks] try: head = nodes.index('sample_block') except ValueError: # block identification error return G edges = self.get_edges(nodes, positions, head) G.add_edges_from(edges) return G def get_edges(self, nodes: list, positions: list, head_node_index: int) -> list: """Finds the sequence of blocks. eg. sample -> test 1 -> test2 -> wick block""" # Initialize the search edges = [] head_node = nodes.pop(head_node_index) head_pos = positions.pop(head_node_index) # Start the recursive search for edges edges.extend(self.search_edges(head_node, head_pos, nodes, positions)) return edges def search_edges(self, current_node, current_pos, remaining_nodes, remaining_positions): edges = [] y, x = current_pos # Define potential neighbors neighbors = [ (y, x + 1), (y, x - 1), (y + 1, x), (y - 1, x) ] for neighbor in neighbors: # list the grid positions. Ex: (5, 2) if neighbor in remaining_positions: # append found edge neighbor_index = remaining_positions.index(neighbor) remaining_positions.remove(neighbor) neighbor_node = remaining_nodes.pop(neighbor_index) edges.append((current_node, neighbor_node)) # recursively search for neighboring edges and add to list edges.extend( self.search_edges( neighbor_node, neighbor, remaining_nodes, remaining_positions ) ) return edges def display(self): G = self.graph pos = nx.spring_layout(G) # Positions for all nodes nx.draw(G, pos, with_labels=True, node_size=2000, node_color="skyblue", font_size=15, font_weight="bold", arrows=True) plt.show() def make_unique_names(self, nodes): count = {} unique_names = [] for name in nodes: if name in count: count[name] += 1 unique_name = f"{name}_{count[name]}" else: count[name] = 0 unique_name = name unique_names.append(unique_name) return unique_names def example_usage(self): # add nodes sequence = [1, 2, 3, 4] G = nx.DiGraph() G.add_nodes_from(sequence) # add edges edges = [(1, 2), (2, 3), (1, 4)] G.add_edges_from(edges) # Draw the graph pos = nx.spring_layout(G) # Positions for all nodes nx.draw(G, pos, with_labels=True, node_size=2000, node_color="skyblue", font_size=15, font_weight="bold", arrows=True) # how to get graph information print(G.nodes) print(G.out_edges) plt.show()
Methods
def build_graph(self, blocks)
def display(self)
def example_usage(self)
def get_edges(self, nodes: list, positions: list, head_node_index: int) ‑> list
-
Finds the sequence of blocks. eg. sample -> test 1 -> test2 -> wick block
def make_unique_names(self, nodes)
def search_edges(self, current_node, current_pos, remaining_nodes, remaining_positions)