Parsing ASCII floor plan image in python?

Question:

I am trying to identify the number of rooms and furniture(S,C,W,P) in an ASCII floorplan. A typical floorplan looks like this with different rooms and layouts. What would be the best way to tackle this?

+---------------+-------------------+           +----------+
|               |                   |           |          |
|  (office)     |            C      |           |   C      |
|               |                   |           |          |
|           W   |                   +-----------+          |
|               |                   |           |          |
|   S           |   (bathroom)     S|      S    |          |
|           +---+--------+----------+           |          |
|          /P           S|                      |          |
|         /              |                      |          |
|        /   (kitchen)   |      (bedroom)       |  P       |
+-------+                |                      |          |
|                       |                      |          |
|            SSWP       |   W              W   |          |
|          +-------------+----------------------+          |
|                                                          |
|             (hallway)                                    |
|    W                                                     |
+--------------+-------------+-------------+               |
               |             |                            |
               |             |                       C    |
               | P           |                            |
               |             |                            |
        +------+           P |                  +----------+
        |S                   |                              
        |    (balcony)   C   |                              
        +--------------------+      
Asked By: RakeshS

||

Answers:

My approach would be:

  • consider the plan as a grid
  • split each row in segments of "space" (non-walls)
  • join spaces that share at least one column
  • look for a name, if found save the joint space as a roon
  • count the furnitures in the room

That results in the following code (I defined a couple of classes for readability):

from dataclasses import dataclass, field
from re import finditer, search
from typing import Dict, List

@dataclass
class RowSegment():
    row_no : int
    col_start : int
    col_end : int

    def __repr__(self):
        return f'{self.row_no}:{self.col_start}-{self.col_end}'

@dataclass
class Space():
    name : str = 'noname'
    furn_count : Dict[str, int] = field(default_factory = dict)
    segs : List[RowSegment] = field(default_factory = list)
    complete : bool = False

    def __repr__(self):
        return f'{self.name}: {[i for i in self.furn_count.items()]}n{[s for s in self.segs]}'

plan = """
+---------------+-------------------+           +----------+
|               |                   |           |          |
|  (office)     |            C      |           |   C      |
|               |                   |           |          |
|           W   |                   +-----------+          |
|               |                   |           |          |
|   S           |   (bathroom)     S|      S    |          |
|           +---+--------+----------+           |          |
|          /P           S|                      |          |
|         /              |                      |          |
|        /   (kitchen)   |      (bedroom)       |  P       |
+-------+                |                      |          |
|                       |                      |          |
|            SSWP       |   W              W   |          |
|          +-------------+----------------------+          |
|                                                          |
|             (hallway)                                    |
|    W                                                     |
+--------------+-------------+-------------+               |
               |             |                            |
               |             |                       C    |
               | P           |                            |
               |             |                            |
        +------+           P |                  +----------+
        |S                   |                              
        |    (balcony)   C   |                              
        +--------------------+                              """

furn_types = 'CPSW'
workspaces = []
rooms = []
rows = plan.split('n')
rows.pop(0)

for no, row in enumerate(rows):
    found = list(finditer(r'[^/\|+-]+', row))
    if found:
        rsegs = [RowSegment(no, rs.start(), rs.end()) for rs in found]
        for ws in workspaces:
            for rs in rsegs:
                if max(ws.segs[-1].col_start, rs.col_start) < min(ws.segs[-1].col_end, rs.col_end): #add rs to ws
                    ws.segs.append(rs)
                    rsegs.remove(rs)
                    break
            else: #no rs added => complete
                text = ''.join([rows[s.row_no][s.col_start:s.col_end] for s in ws.segs])
                name = search(r'(w+)', text)
                if name:
                    ws.name = name[0][1:-1]
                    ws.furn_count = {f: text.count(f) for f in furn_types}
                    rooms.append(ws)
                ws.complete = True
        #reset ws list to only not complete
        workspaces = [ws for ws in workspaces if ws.complete == False]
        #create new wss with remaining rss
        for rs in rsegs:
            newws = Space()
            newws.segs.append(rs)
            workspaces.append(newws)
for r in rooms:
    print(r)

The output (I’m also printing the "spaces" that compose each room) is:

bathroom: [('C', 1), ('P', 0), ('S', 1), ('W', 0)]
[1:17-36, 2:17-36, 3:17-36, 4:17-36, 5:17-36, 6:17-36]
office: [('C', 0), ('P', 0), ('S', 1), ('W', 1)]
[1:1-16, 2:1-16, 3:1-16, 4:1-16, 5:1-16, 6:1-16, 7:1-12, 8:1-11, 9:1-10, 10:1-9]
bedroom: [('C', 0), ('P', 0), ('S', 1), ('W', 2)]
[5:37-48, 6:37-48, 7:37-48, 8:26-48, 9:26-48, 10:26-48, 11:26-48, 12:26-48, 13:26-48]
kitchen: [('C', 0), ('P', 2), ('S', 3), ('W', 1)]
[8:12-25, 9:11-25, 10:10-25, 11:9-25, 12:10-25, 13:11-25]
hallway: [('C', 2), ('P', 1), ('S', 0), ('W', 1)]
[1:49-59, 2:49-59, 3:49-59, 4:49-59, 5:49-59, 6:49-59, 7:49-59, 8:49-59, 9:49-59, 10:49-59, 11:49-59, 12:49-59, 13:49-59, 14:49-59, 15:1-59, 16:1-59, 17:1-59, 18:44-59, 19:45-59, 20:46-59, 21:47-59, 22:48-59]
balcony: [('C', 1), ('P', 2), ('S', 1), ('W', 0)]
[19:16-29, 20:16-29, 21:16-29, 22:16-29, 23:16-29, 24:9-29, 25:9-29]
Answered By: gimix

I would suggest something like:

  • consider the plan as a grid

  • replace all alphabetic letters (and parenthesis) with spaces

  • set as wall every cell with a non-space char

  • set the 1st non-wall cell as room 1

    • loop over cells that are "air" near this one and set them as same room and loop over neightboring cells again and again untill you find walls
    • when done, take a remaining cell and set it as room X

do again and again until all cells are attributed to different rooms

  • compare the original plan (that have letters) with the given rooms you found
Answered By: Ren

Assuming the below as I am inferring it from the (very short and unclear) problem statement (please inform if these are true). Answer is directional at best.

  1. ASCII plan is stored in txt file in local machine and needs to be read from software interface
  2. The plan will always be rectangular as the solution does not incorporate any other shape
  3. the ASCII chanrecters that represent the wall (| and / and and -) corners (+)
  4. Rooms will always be named in round brackets and will not spill outside room and will not be written overlapping a wall or outside the room

Heuristic

The search of a room and the room name is based on BFS algorithm which one can understand using this arcticle https://medium.com/geekculture/breadth-first-search-bfs-algorithm-with-python-952aea707e93

Python Solution

importing numpy and reading the file from the location on local machine. Maxlength is the length of the rectagular plan

floor_plan = []; 
maxlength = 0;

Now we need to read the whole plan so as to have an object on which we will run the BFS


with open('txtfilewithASCIIplan.txt',encoding='utf-8-sig') as file:
    for line in file.read().splitlines():
        line_list = list(line)
        if(len(line_list) > maxlength):
            maxlength = len(line_list)
        floor_plan.append(line_list)

Create the object now


for _,row in enumerate(floor_plan):
    row.extend(list(' '*(maxlength - len(row))))

create the variables to be used in the BFS as described below. Comments are self explainatory

# seperator characters
seperator = {'+', '-', '|', '/', '\'}

#Number of Rows
number_of_rows = len(floor_plan)

#Number of Columns
number_of_columns = len(floor_plan[0])

#Matrix to keep track of points visited
visited = np.zeros(shape=(number_of_rows, number_of_columns), dtype=bool)

# contains a list of tuples; where the first element in tuple is room name and the second element is list of chairs in that room
room_with_chairs = []

# we store all the chairs present in the whole floor plan
list_of_all_letters = []

Do not visit a visited cell or a seperator

def shouldvisit(cell):
    x = cell[0]
    y = cell[1]
    return not(visited[x][y] or (floor_plan[x][y] in seperator))

A sense check for seeing if we are in a diagonal corner which is essentially a Checks if both given cells contain separators

def are_both_separators(cell1, cell2):
    return (floor_plan[cell1[0]][cell1[1]] in seperator) and (floor_plan[cell2[0]][cell2[1]] in seperator)

BFS needs to know its neighbours and we collect them here

# gets all the valid adjacent cells which can be at most 8
def get_neighbours(cell):
    x = cell[0]
    y = cell[1]
    neighbours = []

    # check for floor plan boundary
    y_0 = y > 0 #check if current position is not along left-most verticle edge of floor plan
    y_max = y < number_of_columns - 1 #check if current position is not along right-most verticle edge of floor plan
    x_0 = x > 0 #check if current position is not along top-most horizontal edge of floor plan
    x_max = x < number_of_rows - 1 #check if current position is not along bottom-most horizontal edge of floor plan

    # Below is the check for valid neighbours
    # The second condition is necessary because while checking a diagonal neighbour, we must make sure that
    # the cells along the other diagonal do not contain separators as in that case the diagonal neighbour would be on the other side of the boundary
    
    #check validity of i-1,j-1 & i-1,j+1
    if(x_0):

        if(y_0 and not are_both_separators((x-1, y), (x, y-1))):
            neighbours.append((x-1, y-1))
        neighbours.append((x-1, y))
        if(y_max) and not are_both_separators((x-1, y), (x, y+1)):
            neighbours.append((x-1, y+1))

    #check validity of i,j-1
    if(y_0):
        neighbours.append((x, y-1))

    #check validity of i,j+1
    if(y_max):
        neighbours.append((x, y+1))

    ##check validity of i+1,j-1 & i+1,j & i+1,j+1
    if(x_max):
        if(y_0 and not are_both_separators((x+1, y), (x, y-1))):
            neighbours.append((x+1, y-1))
        neighbours.append((x+1, y))
        
        if(y_max and not are_both_separators((x+1, y), (x, y+1))):
            neighbours.append((x+1, y+1))
    
    return neighbours

Now we search for the name of the room

def get_room_name(cell):
    x = cell[0]
    y = cell[1]
    row = ''.join(floor_plan[x]) #for string conversion
    l=y;
    r=y;
    while(l>0 and row[l]!='('):
        l-=1;
    while(r<len(row)-1 and row[r]!=')'):
        r+=1;
    return row[l+1:r]

BFS

def bfs(cell):
    queue = [cell]
    chairs = []
    area_name = None
    visited[cell[0]][cell[1]] = True    
    
    while(queue != []):
        curr_cell = queue.pop()
        x = curr_cell[0]
        y = curr_cell[1]

        curr_char = floor_plan[x][y]
        if(curr_char.isupper()):
            chairs.append(curr_char)

        elif((curr_char.islower() or curr_char == '(' or curr_char == ')') and area_name == None):
            area_name = get_room_name(curr_cell)

        for neighbour in get_neighbours(curr_cell):
            if(shouldvisit(neighbour)):
                visited[neighbour[0]][neighbour[1]] = True
                queue.append(neighbour)
    return (area_name, chairs)

Now running the BFS on the plan

# Doing BFS over every unvisited non-separator cell
for i, row in enumerate(floor_plan):
    for j, _ in enumerate(row):
        if(shouldvisit((i, j))):
            local_outcome = bfs((i, j))
            if(local_outcome[0] != None):
                room_with_chairs.append(local_outcome)
                list_of_all_letters.extend(local_outcome[1])

This completes the identification

Answered By: Navesh Kumar
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.