Source code for hammoudeh_puzzle.puzzle_piece

"""
Created by Zayd Hammoudeh (zayd.hammoudeh@sjsu.edu)
"""
import random

from enum import Enum
import numpy
import cv2  # OpenCV


[docs]class Location(object): """ Location Object Used to represent any two dimensional location in matrix row/column notation. """ def __init__(self, (row, column)): self.row = row self.column = column
[docs]class PuzzlePieceRotation(Enum): """Puzzle Piece PieceRotation Enumerated type for representing the amount of rotation for a puzzle piece. Note: Pieces can only be rotated in 90 degree increments. """ degree_0 = 0 # No rotation degree_90 = 90 # 90 degree rotation degree_180 = 180 # 180 degree rotation degree_270 = 270 # 270 degree rotation degree_360 = 360 @staticmethod
[docs] def all_rotations(): """ All Rotation Accessor Gets a list of all supported rotations for a puzzle piece. The list is ascending from 0 degrees to 270 degrees increasing. Returns ([PuzzlePieceRotation]): List of all puzzle rotations. """ return [PuzzlePieceRotation.degree_0, PuzzlePieceRotation.degree_90, PuzzlePieceRotation.degree_180, PuzzlePieceRotation.degree_270]
@staticmethod
[docs] def random_rotation(): """ Random Rotation Generates and returns a random rotation. Returns (PuzzlePieceRotation): A random puzzle piece rotation """ return random.choice(PuzzlePieceRotation.all_rotations())
[docs]class PuzzlePieceSide(Enum): """Puzzle Piece Side Enumerated type for representing the four sides of the a puzzle piece. Note: Pieces can only be rotated in 90 degree increments. """ top = 0 right = 1 bottom = 2 left = 3 @staticmethod
[docs] def get_numb_sides(): """ Accessor for the number of sizes for a puzzle piece. Returns (int): Since these are rectangular pieces, it returns size 4. This is currently fixed. """ return 4
@staticmethod
[docs] def get_all_sides(): """ Static method to extract all the sides of a piece. Returns ([PuzzlePieceSide]): List of all sides of a puzzle piece starting at the top and moving clockwise. """ return [PuzzlePieceSide.top, PuzzlePieceSide.right, PuzzlePieceSide.bottom, PuzzlePieceSide.left]
@property def complementary_side(self): """ Determines and returns the complementary side of this implicit side parameter. For example, if this side is "left" then the function returns "right" and vice versa. Returns (PuzzlePieceSide): Complementary side of the piece. """ if self == PuzzlePieceSide.top: return PuzzlePieceSide.bottom if self == PuzzlePieceSide.right: return PuzzlePieceSide.left if self == PuzzlePieceSide.bottom: return PuzzlePieceSide.top if self == PuzzlePieceSide.left: return PuzzlePieceSide.right @property def side_name(self): """ Gets the name of a puzzle piece side without the class name Returns (str): The name of the side as a string """ return str(self).split(".")[1]
[docs]class SolidColor(Enum): """ Solid color in Blue, Green, Red (BGR) format. """ black = (0, 0, 0) white = (255, 255, 255)
[docs]class PuzzlePiece(object): """ Puzzle Piece Object. It is a very simple object that stores the puzzle piece's pixel information in a NumPY array. It also stores the piece's original information (e.g. X/Y location and puzzle ID) along with what was determined by the solver. """ # Represents L-A-B dimensions in the LAB color space NUMB_LAB_COLORSPACE_DIMENSIONS = 3 _PERFORM_ASSERTION_CHECKS = True # Use predicted values for edge borders for speed up _USE_STORED_PREDICTED_VALUE_SPEED_UP = True # When drawing the image results, optionally draw a border around the pieces to # make piece differences more evident. _ADD_RESULTS_IMAGE_BORDER = True _WHITE_BORDER_THICKNESS = 2 # pixels def __init__(self, puzzle_id, location, lab_img, piece_id=None, puzzle_grid_size=None): """ Puzzle Piece Constructor. Args: puzzle_id (int): Puzzle identification number location ([int]): (row, column) location of this piece. lab_img: Image data in the form of a numpy array. piece_id (int): Piece identification number. puzzle_grid_size ([int]): Grid size of the puzzle """ # Verify the piece id information if piece_id is None and puzzle_grid_size is not None: raise ValueError("Using the puzzle grid size is not supported if piece id is \"None\".") # Piece ID is left to the solver to set self._orig_piece_id = piece_id self._assigned_piece_id = None self._orig_puzzle_id = puzzle_id self._assigned_puzzle_id = None # Store the original location of the puzzle piece and initialize a placeholder x/y location. self._orig_loc = location self._assigned_loc = None # Optionally calculate the identification numbers of the piece neighbors self._actual_neighbor_ids = None if puzzle_grid_size is not None: self.calculate_actual_neighbor_id_numbers(puzzle_grid_size) # Store the image data self._img = lab_img (length, width, dim) = self._img.shape if width != length: raise ValueError("Only square puzzle pieces are supported at this time.") if dim != PuzzlePiece.NUMB_LAB_COLORSPACE_DIMENSIONS: raise ValueError("This image does not appear to be in the LAB colorspace as it does not have 3 dimensions") self._width = width # For some debug images, we may want to see a solid image instead of the original image. # This property stores that color. self._results_image_coloring = None # Used to speed up piece to piece calculations self._border_average_color = None self._predicted_border_values = [None] * PuzzlePieceSide.get_numb_sides() self._calculate_border_color_average() # Rotation gets set later. self._rotation = None
[docs] def calculate_actual_neighbor_id_numbers(self, puzzle_grid_size): """ Neighbor ID Calculator Given a grid size, this function calculates the identification number of this piece's neighbors. If a piece has no neighbor, then location associated with that puzzle piece is filled with "None". Args: puzzle_grid_size ([int]): Grid size (number of rows, number of columns) for this piece's puzzle. """ # Only need to calculate the actual neighbor id information once if self._actual_neighbor_ids is not None: return # Initialize actual neighbor id information self._actual_neighbor_ids = [] # Extract the information on the puzzle grid size (numb_rows, numb_cols) = puzzle_grid_size # Check the top location first # If the row is 0, then it has no top neighbor if self._orig_loc[0] == 0: neighbor_id = None else: neighbor_id = self._orig_piece_id - numb_cols self._actual_neighbor_ids.append((neighbor_id, PuzzlePieceSide.top)) # Check the right side # If in the last column, it has no right neighbor if self._orig_loc[1] + 1 == numb_cols: neighbor_id = None else: neighbor_id = self._orig_piece_id + 1 self._actual_neighbor_ids.append((neighbor_id, PuzzlePieceSide.right)) # Check the bottom side # If in the last column, it has no right neighbor if self._orig_loc[0] + 1 == numb_rows: neighbor_id = None else: neighbor_id = self._orig_piece_id + numb_cols self._actual_neighbor_ids.append((neighbor_id, PuzzlePieceSide.bottom)) # Check the right side # If in the last column, it has no left neighbor if self._orig_loc[1] == 0: neighbor_id = None else: neighbor_id = self._orig_piece_id - 1 self._actual_neighbor_ids.append((neighbor_id, PuzzlePieceSide.left)) # Convert the list to a tuple since it is immutable self._actual_neighbor_ids = tuple(self._actual_neighbor_ids)
def _calculate_border_color_average(self): """ Calculate the average color for each border to expedite calculations of puzzle piece side """ # Top side border sum # noinspection PyListCreation border_color = [numpy.sum(self.get_row_pixels(0))] # Right side border_color.append(numpy.sum(self.get_column_pixels(self._width - 1))) # Bottom side border_color.append(numpy.sum(self.get_row_pixels(self._width - 1))) # Left side border_color.append(numpy.sum(self.get_column_pixels(0))) # convert to average for i in xrange(0, len(border_color)): border_color[i] = 1.0 * border_color[i] / (self.width * PuzzlePiece.NUMB_LAB_COLORSPACE_DIMENSIONS) # Convert to a tuple self._border_average_color = tuple(border_color)
[docs] def border_average_color(self, side): """ Border Average Color Accessor Gets the average color for a border piece. Args: side (PuzzlePieceSide): Side of the puzzle piece whose average value will be returned. Returns (float): The average pixel value for the puzzle piece border. """ return self._border_average_color[side.value]
@property def original_neighbor_id_numbers_and_sides(self): """ Neighbor Identification Number Property In a puzzle, each piece has up to four neighbors. This function access that identification number information. Returns (List[int, PuzzlePieceSide]): Identification number for the puzzle piece on the specified side of the original object. """ # Verify that the array containing the neighbor id numbers is not none if PuzzlePiece._PERFORM_ASSERTION_CHECKS: assert self._actual_neighbor_ids is not None # Return the piece's neighbor identification numbers return self._actual_neighbor_ids @property def width(self): """ Gets the size of the square puzzle piece. Since it is square, width its width equals its length. Returns (int): Width of the puzzle piece in pixels. """ return self._width @property def location(self): """ Gets the location of the puzzle piece on the board. Returns ([int]): Tuple of the (row, column) """ return self._assigned_loc @location.setter def location(self, new_loc): """ Updates the puzzle piece location. Args: new_loc ([int]): New puzzle piece location. """ if len(new_loc) != 2: raise ValueError("Location of a puzzle piece must be a two dimensional tuple") self._assigned_loc = new_loc @property def puzzle_id(self): """ Assigned Puzzle Identification Number Gets the location of the puzzle piece on the board. Returns (int): Assigned Puzzle ID number. """ return self._assigned_puzzle_id @property def actual_puzzle_id(self): """ Actual Puzzle Identification Number Gets the actual (i.e. correct) puzzle identification number of the puzzle. Returns (int): Actual (correct) puzzle identification number this piece originated from. """ return self._orig_puzzle_id @puzzle_id.setter def puzzle_id(self, new_puzzle_id): """ Updates the puzzle ID number for the puzzle piece. Returns (int): Board identification number """ self._assigned_puzzle_id = new_puzzle_id @property def original_piece_id(self): """ Original Piece ID Number Gets the original piece identification number Returns (int): Original identification number assigned to the piece at its creation. Should be globally unique. """ return self._orig_piece_id @property def id_number(self): """ Puzzle Piece ID Getter Gets the identification number for a puzzle piece. Returns (int): Puzzle piece identification number """ # Check whether the assigned piece ID is not none if PuzzlePiece._PERFORM_ASSERTION_CHECKS: assert self._assigned_piece_id is not None # Return the piece id number return self._assigned_piece_id @id_number.setter def id_number(self, new_piece_id): """ Piece ID Setter Sets the puzzle piece's identification number. Args: new_piece_id (int): Puzzle piece identification number """ self._assigned_piece_id = new_piece_id @property def lab_image(self): """ Get's a puzzle piece's image in the LAB colorspace. Returns (Numpy[int]): Numpy array of the piece's lab image. """ return self._img @property def rotation(self): """ Rotation Accessor Gets the puzzle piece's rotation. Returns (PuzzlePieceRotation): The puzzle piece's rotation """ return self._rotation @rotation.setter def rotation(self, new_rotation): """ Puzzle Piece Rotation Setter Updates a puzzle piece's rotation. Args: new_rotation (PuzzlePieceRotation): New rotation for the puzzle piece. """ self._rotation = new_rotation
[docs] def side_adjacent_to_location(self, location): """ Given an adjacent puzzle location, this function returns the side that is touching that adjacent location. Args: location (Tuple[int]): A puzzle piece location adjacent to this piece. Returns (PuzzlePieceSide): Side of this piece that is touching the adjacent location """ loc_and_side = self.get_neighbor_locations_and_sides() for (loc, side) in loc_and_side: if loc == location: return side raise ValueError("Specified Location: \"(%s,%s)\" is not adjacent this piece's location \"(%s, %s)\"" % (location[0], location[1], self.location[0], self.location[1]))
@property def results_image_coloring(self): """ Gets the results color image for the piece. Returns(List): Either a single BGR integer list when a solid color is used. If it is using polygon print, then the return is a List[(List[int], PuzzlePieceSide)]. """ return self._results_image_coloring @results_image_coloring.setter def results_image_coloring(self, color): """ Sets the image coloring when only a single color is needed. Args: color (List[int]): Color of the image in BGR format """ self._results_image_coloring = color
[docs] def reset_image_coloring_for_polygons(self): """ Sets up the results image coloring for """ self._results_image_coloring = []
[docs] def results_image_polygon_coloring(self, side, color): """ Sets the image coloring when only a single color is needed. Args: side (PuzzlePieceSide): Side of the piece that will be assigned a color. color (List[int]): Color of the image in BGR format """ self._results_image_coloring.append((side, color.value))
[docs] def get_neighbor_locations_and_sides(self): """ Neighbor Locations and Sides Given a puzzle piece, this function returns the four surrounding coordinates/location and the sides of THIS puzzle piece that corresponds to those locations so that it can be added to the open slot list. Returns ([([int], PuzzlePieceSide)]): Valid puzzle piece locations and the respective puzzle piece side. """ if PuzzlePiece._PERFORM_ASSERTION_CHECKS: assert self.location is not None assert self.rotation is not None # TODO this approach does not account for missing pieces. return PuzzlePiece._get_neighbor_locations_and_sides(self.location, self.rotation)
@staticmethod def _get_neighbor_locations_and_sides(piece_loc, piece_rotation): """ Neighbor Locations and Sides Static method that given a piece location and rotation, it returns the four surrounding coordinates/location and the puzzle piece side that aligns with it so that it can be added to the open slot list. Args: piece_loc ([int]): piece_rotation (PuzzlePieceRotation): Returns ([([int], PuzzlePieceSide)]): Valid puzzle piece locations and the respective puzzle piece side. """ # Get the top location and respective side top_loc = (piece_loc[0] - 1, piece_loc[1]) # noinspection PyTypeChecker location_piece_side_tuples = [(top_loc, PuzzlePiece._determine_unrotated_side(piece_rotation, PuzzlePieceSide.top))] # Get the right location and respective side right_loc = (piece_loc[0], piece_loc[1] + 1) # noinspection PyTypeChecker location_piece_side_tuples.append((right_loc, PuzzlePiece._determine_unrotated_side(piece_rotation, PuzzlePieceSide.right))) # Get the bottom location and its respective side bottom_loc = (piece_loc[0] + 1, piece_loc[1]) # noinspection PyTypeChecker location_piece_side_tuples.append((bottom_loc, PuzzlePiece._determine_unrotated_side(piece_rotation, PuzzlePieceSide.bottom))) # Get the right location and respective side left_loc = (piece_loc[0], piece_loc[1] - 1) # noinspection PyTypeChecker location_piece_side_tuples.append((left_loc, PuzzlePiece._determine_unrotated_side(piece_rotation, PuzzlePieceSide.left))) # Return the location/piece side tuples return location_piece_side_tuples
[docs] def bgr_image(self): """ Get's a puzzle piece's image in the BGR colorspace. Returns (Numpy[int]): Numpy array of the piece's BGR image. """ return cv2.cvtColor(self._img, cv2.COLOR_LAB2BGR)
[docs] def get_row_pixels(self, row_numb, reverse=False): """ Extracts a row of pixels from a puzzle piece. Args: row_numb (int): Pixel row in the image. Must be between 0 and the width of the piece - 1 (inclusive). reverse (Optional bool): Select whether to reverse the pixel information. Returns (Numpy[int]): A vector of 3-dimensional pixel values for a row in the image. """ if row_numb < 0: raise ValueError("Row number for a piece must be greater than or equal to zero.") if row_numb >= self._width: raise ValueError("Row number for a piece must be less than the puzzle's pieces width") if reverse: return self._img[row_numb, ::-1, :] else: return self._img[row_numb, :, :]
[docs] def get_column_pixels(self, col_numb, reverse=False): """ Extracts a row of pixels from a puzzle piece. Args: col_numb (int): Pixel column in the image. Must be between 0 and the width of the piece - 1 (inclusive). reverse (Optional bool): Select whether to reverse the pixel information. Returns (Numpy[int]): A vector of 3-dimensional pixel values for a column in the image. """ if col_numb < 0: raise ValueError("Column number for a piece must be greater than or equal to zero.") if col_numb >= self._width: raise ValueError("Column number for a piece must be less than the puzzle's pieces width") # If you reverse, change the order of the pixels. if reverse: return self._img[::-1, col_numb, :] else: return self._img[:, col_numb, :]
def _assign_to_original_location(self): """ Loopback Location Assigner Test Method Only. Correctly assigns a piece to its original location. """ self._assigned_loc = self._orig_loc def _set_id_number_to_original_id(self): """ Loopback ID Number Test Method Only. Sets the assigned and original piece id number to the same value. """ self._assigned_piece_id = self._orig_piece_id @staticmethod
[docs] def calculate_asymmetric_distance(piece_i, piece_i_side, piece_j, piece_j_side): """ Uses the Asymmetric Distance function to calculate the distance between two puzzle pieces. Args: piece_i (PuzzlePiece): piece_i_side (PuzzlePieceSide): piece_j (PuzzlePiece): piece_j_side (PuzzlePieceSide): Side of piece j that is adjacent to piece i. Returns (double): Distance between the sides of two puzzle pieces. """ # Get the border information for p_i if not pre-calculated i_border = None i_second_to_last = None if piece_i._predicted_border_values[piece_i_side.value] is None or not PuzzlePiece._USE_STORED_PREDICTED_VALUE_SPEED_UP: # Get the border and second to last ROW on the TOP side of piece i if piece_i_side == PuzzlePieceSide.top: i_border = piece_i.get_row_pixels(0) i_second_to_last = piece_i.get_row_pixels(1) # Get the border and second to last COLUMN on the RIGHT side of piece i elif piece_i_side == PuzzlePieceSide.right: i_border = piece_i.get_column_pixels(piece_i.width - 1) i_second_to_last = piece_i.get_column_pixels(piece_i.width - 2) # Get the border and second to last ROW on the BOTTOM side of piece i elif piece_i_side == PuzzlePieceSide.bottom: i_border = piece_i.get_row_pixels(piece_i.width - 1) i_second_to_last = piece_i.get_row_pixels(piece_i.width - 2) # Get the border and second to last COLUMN on the LEFT side of piece i elif piece_i_side == PuzzlePieceSide.left: i_border = piece_i.get_column_pixels(0) i_second_to_last = piece_i.get_column_pixels(1) else: raise ValueError("Invalid edge for piece i") # If rotation is allowed need to reverse pixel order in some cases. reverse = False # By default do not reverse # Always need to reverse when they are the same side if piece_i_side == piece_j_side: reverse = True # Get the pixels along the TOP of piece_j if piece_j_side == PuzzlePieceSide.top: if piece_i_side == PuzzlePieceSide.right: reverse = True j_border = piece_j.get_row_pixels(0, reverse) # Get the pixels along the RIGHT of piece_j elif piece_j_side == PuzzlePieceSide.right: if piece_i_side == PuzzlePieceSide.top: reverse = True j_border = piece_j.get_column_pixels(piece_i.width - 1, reverse) # Get the pixels along the BOTTOM of piece_j elif piece_j_side == PuzzlePieceSide.bottom: if piece_i_side == PuzzlePieceSide.left: reverse = True j_border = piece_j.get_row_pixels(piece_i.width - 1, reverse) # Get the pixels along the RIGHT of piece_j elif piece_j_side == PuzzlePieceSide.left: if piece_i_side == PuzzlePieceSide.bottom: reverse = True j_border = piece_j.get_column_pixels(0, reverse) else: raise ValueError("Invalid edge for piece i") # If needed, recalculate the side value. if piece_i._predicted_border_values[piece_i_side.value] is None or not PuzzlePiece._USE_STORED_PREDICTED_VALUE_SPEED_UP: # Calculate the value of pixels on piece j's edge. piece_i._predicted_border_values[piece_i_side.value] = (2 * (i_border.astype(numpy.int32)) - i_second_to_last.astype(numpy.int32)) # Get the predicated stored value predicted_j = piece_i._predicted_border_values[piece_i_side.value] # noinspection PyUnresolvedReferences pixel_diff = predicted_j.astype(numpy.int32) - j_border.astype(numpy.int32) # Return the sum of the absolute values. pixel_diff = numpy.absolute(pixel_diff) return numpy.sum(pixel_diff, dtype=numpy.int32)
[docs] def set_placed_piece_rotation(self, placed_side, neighbor_piece_side, neighbor_piece_rotation): """ Placed Piece Rotation Setter Given an already placed neighbor piece's adjacent side and rotation, this function sets the rotation of some newly placed piece that is put adjacent to that neighbor piece. Args: placed_side (PuzzlePieceSide): Side of the placed puzzle piece that is adjacent to the neighbor piece neighbor_piece_side (PuzzlePieceSide): Side of the neighbor piece that is adjacent to the newly placed piece. neighbor_piece_rotation (PuzzlePieceRotation): Rotation of the already placed neighbor piece """ # Calculate the placed piece's new rotation self.rotation = PuzzlePiece._calculate_placed_piece_rotation(placed_side, neighbor_piece_side, neighbor_piece_rotation)
@staticmethod def _calculate_placed_piece_rotation(placed_piece_side, neighbor_piece_side, neighbor_piece_rotation): """ Placed Piece Rotation Calculator Given an already placed neighbor piece, this function determines the correct rotation for a newly placed piece. Args: placed_piece_side (PuzzlePieceSide): Side of the placed puzzle piece adjacent to the existing piece neighbor_piece_side (PuzzlePieceSide): Side of the neighbor of the placed piece that is touching neighbor_piece_rotation (PuzzlePieceRotation): Rotation of the neighbor piece Returns (PuzzlePieceRotation): Rotation of the placed puzzle piece given the rotation and side of the neighbor piece. """ # Get the neighbor piece rotation unrotated_complement = neighbor_piece_side.complementary_side placed_rotation_val = int(neighbor_piece_rotation.value) # noinspection PyUnresolvedReferences placed_rotation_val += 90 * (PuzzlePieceRotation.degree_360.value + (unrotated_complement.value - placed_piece_side.value)) # Calculate the normalized rotation # noinspection PyUnresolvedReferences placed_rotation_val %= PuzzlePieceRotation.degree_360.value # Check if a valid rotation value. if PuzzlePiece._PERFORM_ASSERTION_CHECKS: assert placed_rotation_val % 90 == 0 # noinspection PyUnresolvedReferences return PuzzlePieceRotation(placed_rotation_val % PuzzlePieceRotation.degree_360.value) @staticmethod def _determine_unrotated_side(piece_rotation, rotated_side): """ Unrotated Side Determiner Given a piece's rotation and the side of the piece (from the reference of the puzzle), find its actual (i.e. unrotated) side. Args: piece_rotation (PuzzlePieceRotation): Specified rotation for a puzzle piece. rotated_side (PuzzlePieceSide): From a Puzzle perspective, this is the exposed side Returns(PuzzlePieceSide): Actual side of the puzzle piece """ rotated_side_val = rotated_side.value # Get the number of 90 degree rotations numb_90_degree_rotations = int(piece_rotation.value / 90) # Get the unrotated side unrotated_side = (rotated_side_val + (PuzzlePieceSide.get_numb_sides() - numb_90_degree_rotations)) unrotated_side %= PuzzlePieceSide.get_numb_sides() # Return the actual side return PuzzlePieceSide(unrotated_side) @staticmethod def _get_neighbor_piece_rotated_side(placed_piece_loc, neighbor_piece_loc): """ Args: placed_piece_loc ([int]): Location of the newly placed piece neighbor_piece_loc ([int): Location of the neighbor of the newly placed piece Returns (PuzzlePieceSide): Side of the newly placed piece where the placed piece is now location. ::Note:: This does not take into account any rotation of the neighbor piece. That is why this function is referred has "rotated side" in its name. """ # Calculate the row and column distances row_dist = placed_piece_loc[0] - neighbor_piece_loc[0] col_dist = placed_piece_loc[1] - neighbor_piece_loc[1] # Perform some checking on the pieces if PuzzlePiece._PERFORM_ASSERTION_CHECKS: # Verify the pieces are in the same puzzle assert abs(row_dist) + abs(col_dist) == 1 # Determine the relative side of the placed piece if row_dist == -1: return PuzzlePieceSide.top elif row_dist == 1: return PuzzlePieceSide.bottom elif col_dist == -1: return PuzzlePieceSide.left else: return PuzzlePieceSide.right @staticmethod
[docs] def create_solid_image(bgr_color, width, height=None): """ Create a solid image for displaying in output images. Args: bgr_color (Tuple[int]): Color in BLUE, GREEN, RED notation. Each element for blue, green, or red must be between 0 and 255 inclusive. width (int): Width of the image in pixels. height (Optional int): Height of the image in number of pixels. If it is not specified, then the image is a square. Returns (Numpy[int]): Image in the form of a NumPy matrix of size: (length by width by 3) """ # Handle the case when no height is specified. if height is None: height = width # Create a black image image = numpy.zeros((height, width, PuzzlePiece.NUMB_LAB_COLORSPACE_DIMENSIONS), numpy.uint8) # Fill with the bgr color image[:] = bgr_color.value # Optionally add a border around the pieces before returning if PuzzlePiece._ADD_RESULTS_IMAGE_BORDER: return PuzzlePiece.add_results_image_border(image) else: return image
@staticmethod
[docs] def add_results_image_border(image): """ Optionally add an image border around the piece image. This is primarily intended for use with the solid results images. Args: image (Numpy[int]): Piece image with no border Returns (Numpy[int]): Piece image with a border around the solid image. """ (height, width, _) = image.shape # noinspection PyUnresolvedReferences cv2.rectangle(image, (0, 0), (width, height), SolidColor.white.value, thickness=PuzzlePiece._WHITE_BORDER_THICKNESS) return image
@staticmethod
[docs] def create_side_polygon_image(bgr_color_by_side, width, height=None): """ Create a solid image for displaying in output images. Args: bgr_color_by_side (Tuple[(Tuple[int], PuzzlePieceSide)]): Color in BLUE, GREEN, RED notation for each specified puzzle piece side. Draws four triangles based off the width (int): Width of the image in pixels. height (Optional int): Height of the image in number of pixels. If it is not specified, then the image is a square. Returns (Numpy[int]): Image in the form of a NumPy matrix of size: (length by width by 3) """ # Handle the case when no height is specified. if height is None: height = width # Verify each side is accounted for. if PuzzlePiece._PERFORM_ASSERTION_CHECKS: assert len(bgr_color_by_side) == PuzzlePieceSide.get_numb_sides() # Define the center point of the image. center_point = [height / 2, width / 2] # Used for assertion checking. sides_drawn = [] # Define the other four coordinates for the polygon. top_left = [0, 0] top_right = [width - 1, 0] bottom_left = [0, height - 1] bottom_right = [height - 1, width - 1] # Create a black image image = numpy.zeros((width, height, PuzzlePiece.NUMB_LAB_COLORSPACE_DIMENSIONS), numpy.uint8) # For each side, fill with a polygon. for (side, color) in bgr_color_by_side: # Build the points in the polygon vector if side == PuzzlePieceSide.top: vector_points = [top_left, top_right] elif side == PuzzlePieceSide.right: vector_points = [top_right, bottom_right] elif side == PuzzlePieceSide.bottom: vector_points = [bottom_left, bottom_right] else: vector_points = [top_left, bottom_left] vector_points.append(center_point) # Add drawn sides to the assertion checks if PuzzlePiece._PERFORM_ASSERTION_CHECKS: # Ensure no side is drawn twice assert side not in sides_drawn # Add the side tot he list. sides_drawn.append(side) # Build a polygon polygon = numpy.array([vector_points], numpy.int32) cv2.fillConvexPoly(image, polygon, color) # Verify that all sides are drawn if PuzzlePiece._PERFORM_ASSERTION_CHECKS: assert len(sides_drawn) == PuzzlePieceSide.get_numb_sides() # Draw an "X" to clearly demarcate the triangles # noinspection PyUnresolvedReferences cv2.line(image, tuple(top_left), tuple(bottom_right), SolidColor.black.value, thickness=1) # noinspection PyUnresolvedReferences cv2.line(image, tuple(top_right), tuple(bottom_left), SolidColor.black.value, thickness=1) # Optionally add a border around the pieces before returning if PuzzlePiece._ADD_RESULTS_IMAGE_BORDER: return PuzzlePiece.add_results_image_border(image) else: return image
[docs] def is_correctly_placed(self, puzzle_offset_upper_left_location): """ Piece Placement Checker Checks whether the puzzle piece is correctly placed. Args: puzzle_offset_upper_left_location (Tuple[int]): Modified location for the origin of the puzzle Returns (bool): True if the puzzle piece is in the correct location and False otherwise. """ # Verify all dimensions for i in xrange(0, len(self._orig_loc)): # If for the current dimension if self._assigned_loc[i] - self._orig_loc[i] - puzzle_offset_upper_left_location[i] != 0: return False # Mark as correctly placed return True