pythonlistrecursionconnected-components

How to sublist in a list the same value pixels of an image (Four Pixel Connectivity)


I'm looking to output a list of lists in python, in which each list contains the pixel coordinates of each shape of a single colour.

I.e. looking at this image: a 128x128 png

Which I have converted into a 2D grid of RGB values.

I am looking to create a list of lists that each contain the pixel coordinates of the purple "shapes" (squares, rectangles, etc). The criteria for a "shape" is simply at least two same colour pixels adjacent to each other (only up, down, left, right).

In my attempt:

# returns a list of instances of a tile which are all adjacent to each other
# i.e. finds a window's individual tiles and returns them as a list

def coagulateTiles(self, tileType, startX, startY, existingComponents = [], foundTiles = None):
    
    # if the pixel is not the right tile OR if it has already been part of another search
    if not self.getTileType(startX, startY) == tileType or not (startX, startY) in foundTiles:
        return existingComponents
    else:
        existingComponents.append((startX, startY))
    
    # a dictionary of the adjacent tiles {"north" = (x, y), etc...}
    adjacentCoordDict = self.getAdjacentCoords(startX, startY)
    
    for adjacent in adjacentCoordDict:
        if adjacent == tileType:
            x, y = adjacentCoordDict[adjacent][0], adjacentCoordDict[adjacent][1]
            existingComponents.append(self.coagulateTiles(tileType, x, y, existingComponents))
        
    return existingComponents

def tileSearch(self, tile):
    items = []
    foundTiles = []
    for x in range(self.getSizeX()):
        for y in range(self.getSizeY()):
            if self.getTileType(x, y) == tile:
                if tile in self.getAdjacentTiles(x, y).values():
                    foundTiles.append((x, y))
                    listOfTileComponents = self.coagulateTiles(tile, x, y, foundTiles = foundTiles)
                    items.append(listOfTileComponents)
    return items

I end up finding all those pixels, but the outputted list puts all the pixels together into a single list rather than separating the "shapes" into individual sublists.

It returns something like [[(1, 1), (2, 1), (3, 1), (1, 2), (1, 3)]] rather than [[(1, 1), (2, 1)], [(3, 1), (1, 2), (1, 3)]]

My gut feeling is that of course there is a simple logical error in my attempt, but I can't seem to find where I can specify the sublist creation for it to work.

Any ideas?

Here's the code needed to convert the image above into the grid I'm working with:

import numpy as np
from PIL import Image

# (x=0,y=0) of the grid is in the top left corner
class Grid():
    def __init__(self, sizeX, sizeY):
         self.sizeX = sizeX
         self.sizeY = sizeY
         global rgbMap
         rgbMap = {
            "wall":(  0,  0,  0),
            "background": (255,255,255),
            "closet": (192,192,224), 
            "bathroom": (192,255,255), # /washroom
            "dining room": (224,255,192), # livingroom/kitchen/dining room
            "bedroom": (255,224,128), 
            "hall": (255,160, 96), 
            "balcony": (255,224,224), 
            "opening": (255, 60,128) # door & window 
            }
         global tileMap
         tileMap = {v: k for k, v in rgbMap.items()}
    
        #  create a 2d numpy array of the given size
         self.grid = self.createGrid(sizeX, sizeY)
         
    def createGrid(self, sizeX, sizeY):
         return np.empty((sizeX, sizeY), dtype=tuple)
         
    # function to input a colour into the 2d grid  
    def populate(self, locationX, locationY, rgbTuple):
        self.grid[locationX,locationY] = rgbTuple
        
    def get(self):
        return self.grid
    
    def getSizeX(self):
        return self.sizeX
    
    def getSizeY(self):
        return self.sizeY

class MapGenerator:
    def __init__(self, finalSize = 128):
        # resize the image to be axa size where a = 128
        self.workingGridSize = 512
        self.finalSize = finalSize

    def createFromSaveFile(self):
        self.createGrid(fromMemory=True)
        print("populating the grid with data")
        self.populateGrid(self.finalSize, self.grid, self.floorplan)
        self.floorplan.close()
    def createGrid(self, fromMemory = False):
        if fromMemory == True:
            self.floorplan = Image.open('saved.png')
            self.grid = Grid(self.finalSize, self.finalSize)
        else:
            self.floorplan = Image.open('result.png')
            self.grid = Grid(self.workingGridSize, self.workingGridSize)

    def populateGrid(self, gridsize, givenGrid, image):
        for x in range(0, gridsize):
            for y in range(0, gridsize):
            # print(map.getpixel((x,y)))
                givenGrid.populate(x, y, image.getpixel((x,y)))

if __name__ == "__main__":
    newMap = MapGenerator()
    # newMap.create()
    
    
    newMap.createFromSaveFile()
    
    openings = newMap.grid.tileSearch("opening")
    print(openings)

Solution

  • i've come up with a DFS recursive solution that works:

    def tileSearch(self, tile):
            """returns a 2d list containing all the pixels 
            of each shape of a searched tile type as a sublist
            
            a shape is defined as at least two pixels 
            of the same colour touching each other with Four-Pixel Connectivity
            (up, down, left, right)
    
            Args:
                tile (string): the tile to search as a string (see rgbMap dict for tile list)
    
            Returns:
                list: a 2D list containing each shape as a sublist of its pixels
            """        
            searchGrid = copy.deepcopy(self)
            resultList = list()
            for x in range(searchGrid.getSizeX()):
                for y in range(searchGrid.getSizeY()):
                    if searchGrid.getTileType(x, y) == tile:
                        adjacentTiles = list(searchGrid.getAdjacentTiles(x, y).values())
                        if tile in adjacentTiles:
                            shapeList = list()
                            searchGrid.coagulateShape(tile, x, y, shapeList)
                            resultList.append(shapeList)
            return resultList
                        
        # returns a list of instances of a tile which are all adjacent to each other
        # i.e. finds a window's individual tiles and returns them as a list
        def coagulateShape(self, tile, x, y, shapeList = []):
            """ Performs a recursive Depth First Search
            
            returns a list of coordinates of a tile which are all adjacent to each other 
            through Four Pixel connectivity (up, down, left, right)
            
            i.e. finds a shape's individual tiles and returns them as a list
            
            Args:
                tile (string): the tile to search for (see rgbMap dict for tile list)
                x (int): the x coordinate of the starting pixel
                y (int): the y coordinate of the starting pixel
                shapeList (list): the list that the shape's coordinates will be appended to recursively
            """        
            # base case
            if not self.getTileType(x, y) == tile:
                return
            
            # recursive case
            self.populate(x, y, (0, 0, 0, 0))
            shapeList.append((x, y))
            adjacentPixels = list(self.getAdjacentCoords(x, y).values())
            for pixel in adjacentPixels:
                self.coagulateShape(tile, pixel[0], pixel[1], shapeList)