1
+ from skimage import io
2
+ from skimage import morphology ,feature ,transform ,measure
3
+ from pathlib import Path
4
+ from scipy import stats
5
+ from scipy import ndimage
6
+ from shapely import geometry
7
+ import numpy as np
8
+
9
+ from .utils import collide2d ,point_box_relation ,door_room_relation
10
+
11
+ class Floorplan ():
12
+
13
+ @property
14
+ def boundary (self ): return self .image [...,0 ]
15
+
16
+ @property
17
+ def category (self ): return self .image [...,1 ]
18
+
19
+ @property
20
+ def instance (self ): return self .image [...,2 ]
21
+
22
+ @property
23
+ def inside (self ): return self .image [...,3 ]
24
+
25
+ def __init__ (self ,file_path ):
26
+ self .path = file_path
27
+ self .name = Path (self .path ).stem
28
+ self .image = io .imread (self .path )
29
+ self .h ,self .w ,self .c = self .image .shape
30
+
31
+ self .front_door = None
32
+ self .exterior_boundary = None
33
+ self .rooms = None
34
+ self .edges = None
35
+
36
+ self .archs = None
37
+ self .graph = None
38
+
39
+ self ._get_front_door ()
40
+ self ._get_exterior_boundary ()
41
+ self ._get_rooms ()
42
+ self ._get_edges ()
43
+
44
+ def __repr__ (self ):
45
+ return f'{ self .name } ,({ self .h } ,{ self .w } ,{ self .c } )'
46
+
47
+ def _get_front_door (self ):
48
+ front_door_mask = self .boundary == 255
49
+ # fast bbox
50
+ # min_h,max_h = np.where(np.any(front_door_mask,axis=1))[0][[0,-1]]
51
+ # min_w,max_w = np.where(np.any(front_door_mask,axis=0))[0][[0,-1]]
52
+ # self.front_door = np.array([min_h,min_w,max_h,max_w],dtype=int)
53
+ region = measure .regionprops (front_door_mask .astype (int ))[0 ]
54
+ self .front_door = np .array (region .bbox ,dtype = int )
55
+
56
+ def _get_exterior_boundary (self ):
57
+ if self .front_door is None : self ._get_front_door ()
58
+ self .exterior_boundary = []
59
+
60
+ min_h ,max_h = np .where (np .any (self .boundary ,axis = 1 ))[0 ][[0 ,- 1 ]]
61
+ min_w ,max_w = np .where (np .any (self .boundary ,axis = 0 ))[0 ][[0 ,- 1 ]]
62
+ min_h = max (min_h - 10 ,0 )
63
+ min_w = max (min_w - 10 ,0 )
64
+ max_h = min (max_h + 10 ,self .h )
65
+ max_w = min (max_w + 10 ,self .w )
66
+
67
+ # src: http://staff.ustc.edu.cn/~fuxm/projects/DeepLayout/index.html
68
+ # search direction:0(right)/1(down)/2(left)/3(up)
69
+ # find the left-top point
70
+ flag = False
71
+ for h in range (min_h , max_h ):
72
+ for w in range (min_w , max_w ):
73
+ if self .inside [h , w ] == 255 :
74
+ self .exterior_boundary .append ((h , w , 0 ))
75
+ flag = True
76
+ break
77
+ if flag :
78
+ break
79
+
80
+ # left/top edge: inside
81
+ # right/bottom edge: outside
82
+ while (flag ):
83
+ if self .exterior_boundary [- 1 ][2 ] == 0 :
84
+ for w in range (self .exterior_boundary [- 1 ][1 ]+ 1 , max_w ):
85
+ corner_sum = 0
86
+ if self .inside [self .exterior_boundary [- 1 ][0 ], w ] == 255 :
87
+ corner_sum += 1
88
+ if self .inside [self .exterior_boundary [- 1 ][0 ]- 1 , w ] == 255 :
89
+ corner_sum += 1
90
+ if self .inside [self .exterior_boundary [- 1 ][0 ], w - 1 ] == 255 :
91
+ corner_sum += 1
92
+ if self .inside [self .exterior_boundary [- 1 ][0 ]- 1 , w - 1 ] == 255 :
93
+ corner_sum += 1
94
+ if corner_sum == 1 :
95
+ new_point = (self .exterior_boundary [- 1 ][0 ], w , 1 )
96
+ break
97
+ if corner_sum == 3 :
98
+ new_point = (self .exterior_boundary [- 1 ][0 ], w , 3 )
99
+ break
100
+
101
+ if self .exterior_boundary [- 1 ][2 ] == 1 :
102
+ for h in range (self .exterior_boundary [- 1 ][0 ]+ 1 , max_h ):
103
+ corner_sum = 0
104
+ if self .inside [h , self .exterior_boundary [- 1 ][1 ]] == 255 :
105
+ corner_sum += 1
106
+ if self .inside [h - 1 , self .exterior_boundary [- 1 ][1 ]] == 255 :
107
+ corner_sum += 1
108
+ if self .inside [h , self .exterior_boundary [- 1 ][1 ]- 1 ] == 255 :
109
+ corner_sum += 1
110
+ if self .inside [h - 1 , self .exterior_boundary [- 1 ][1 ]- 1 ] == 255 :
111
+ corner_sum += 1
112
+ if corner_sum == 1 :
113
+ new_point = (h , self .exterior_boundary [- 1 ][1 ], 2 )
114
+ break
115
+ if corner_sum == 3 :
116
+ new_point = (h , self .exterior_boundary [- 1 ][1 ], 0 )
117
+ break
118
+
119
+ if self .exterior_boundary [- 1 ][2 ] == 2 :
120
+ for w in range (self .exterior_boundary [- 1 ][1 ]- 1 , min_w , - 1 ):
121
+ corner_sum = 0
122
+ if self .inside [self .exterior_boundary [- 1 ][0 ], w ] == 255 :
123
+ corner_sum += 1
124
+ if self .inside [self .exterior_boundary [- 1 ][0 ]- 1 , w ] == 255 :
125
+ corner_sum += 1
126
+ if self .inside [self .exterior_boundary [- 1 ][0 ], w - 1 ] == 255 :
127
+ corner_sum += 1
128
+ if self .inside [self .exterior_boundary [- 1 ][0 ]- 1 , w - 1 ] == 255 :
129
+ corner_sum += 1
130
+ if corner_sum == 1 :
131
+ new_point = (self .exterior_boundary [- 1 ][0 ], w , 3 )
132
+ break
133
+ if corner_sum == 3 :
134
+ new_point = (self .exterior_boundary [- 1 ][0 ], w , 1 )
135
+ break
136
+
137
+ if self .exterior_boundary [- 1 ][2 ] == 3 :
138
+ for h in range (self .exterior_boundary [- 1 ][0 ]- 1 , min_h , - 1 ):
139
+ corner_sum = 0
140
+ if self .inside [h , self .exterior_boundary [- 1 ][1 ]] == 255 :
141
+ corner_sum += 1
142
+ if self .inside [h - 1 , self .exterior_boundary [- 1 ][1 ]] == 255 :
143
+ corner_sum += 1
144
+ if self .inside [h , self .exterior_boundary [- 1 ][1 ]- 1 ] == 255 :
145
+ corner_sum += 1
146
+ if self .inside [h - 1 , self .exterior_boundary [- 1 ][1 ]- 1 ] == 255 :
147
+ corner_sum += 1
148
+ if corner_sum == 1 :
149
+ new_point = (h , self .exterior_boundary [- 1 ][1 ], 0 )
150
+ break
151
+ if corner_sum == 3 :
152
+ new_point = (h , self .exterior_boundary [- 1 ][1 ], 2 )
153
+ break
154
+
155
+ if new_point != self .exterior_boundary [0 ]:
156
+ self .exterior_boundary .append (new_point )
157
+ else :
158
+ flag = False
159
+ self .exterior_boundary = [[r ,c ,d ,0 ] for r ,c ,d in self .exterior_boundary ]
160
+
161
+ door_y1 ,door_x1 ,door_y2 ,door_x2 = self .front_door
162
+ door_h ,door_w = door_y2 - door_y1 ,door_x2 - door_x1
163
+ is_vertical = door_h > door_w or door_h == 1 #
164
+
165
+ insert_index = None
166
+ door_index = None
167
+ new_p = []
168
+ th = 3
169
+ for i in range (len (self .exterior_boundary )):
170
+ y1 ,x1 ,d ,_ = self .exterior_boundary [i ]
171
+ y2 ,x2 ,_ ,_ = self .exterior_boundary [(i + 1 )% len (self .exterior_boundary )]
172
+ if is_vertical != d % 2 : continue
173
+ if is_vertical and (x1 - th < door_x1 < x1 + th or x1 - th < door_x2 < x1 + th ): # 1:down 3:up
174
+ l1 = geometry .LineString ([[y1 ,x1 ],[y2 ,x2 ]])
175
+ l2 = geometry .LineString ([[door_y1 ,x1 ],[door_y2 ,x1 ]])
176
+ l12 = l1 .intersection (l2 )
177
+ if l12 .length > 0 :
178
+ dy1 ,dy2 = l12 .xy [0 ] # (y1>y2)==(dy1>dy2)
179
+ insert_index = i
180
+ door_index = i + (y1 != dy1 )
181
+ if y1 != dy1 : new_p .append ([dy1 ,x1 ,d ,1 ])
182
+ if y2 != dy2 : new_p .append ([dy2 ,x1 ,d ,1 ])
183
+ elif not is_vertical and (y1 - th < door_y1 < y1 + th or y1 - th < door_y2 < y1 + th ):
184
+ l1 = geometry .LineString ([[y1 ,x1 ],[y2 ,x2 ]])
185
+ l2 = geometry .LineString ([[y1 ,door_x1 ],[y1 ,door_x2 ]])
186
+ l12 = l1 .intersection (l2 )
187
+ if l12 .length > 0 :
188
+ dx1 ,dx2 = l12 .xy [1 ] # (x1>x2)==(dx1>dx2)
189
+ insert_index = i
190
+ door_index = i + (x1 != dx1 )
191
+ if x1 != dx1 : new_p .append ([y1 ,dx1 ,d ,1 ])
192
+ if x2 != dx2 : new_p .append ([y1 ,dx2 ,d ,1 ])
193
+
194
+ if len (new_p )> 0 :
195
+ self .exterior_boundary = self .exterior_boundary [:insert_index + 1 ]+ new_p + self .exterior_boundary [insert_index + 1 :]
196
+ self .exterior_boundary = self .exterior_boundary [door_index :]+ self .exterior_boundary [:door_index ]
197
+
198
+ self .exterior_boundary = np .array (self .exterior_boundary ,dtype = int )
199
+
200
+ def _get_rooms (self ):
201
+ rooms = []
202
+ regions = measure .regionprops (self .instance )
203
+ for region in regions :
204
+ c = stats .mode (self .category [region .coords [:,0 ],region .coords [:,1 ]])[0 ][0 ]
205
+ y0 ,x0 ,y1 ,x1 = np .array (region .bbox )
206
+ rooms .append ([y0 ,x0 ,y1 ,x1 ,c ])
207
+ self .rooms = np .array (rooms ,dtype = int )
208
+
209
+ def _get_edges (self ,th = 9 ):
210
+ if self .rooms is None : self ._get_rooms ()
211
+ edges = []
212
+ for u in range (len (self .rooms )):
213
+ for v in range (u + 1 ,len (self .rooms )):
214
+ if not collide2d (self .rooms [u ,:4 ],self .rooms [v ,:4 ],th = th ): continue
215
+ uy0 , ux0 , uy1 , ux1 , c1 = self .rooms [u ]
216
+ vy0 , vx0 , vy1 , vx1 , c2 = self .rooms [v ]
217
+ uc = (uy0 + uy1 )/ 2 ,(ux0 + ux1 )/ 2
218
+ vc = (vy0 + vy1 )/ 2 ,(vx0 + vx1 )/ 2
219
+ if ux0 < vx0 and ux1 > vx1 and uy0 < vy0 and uy1 > vy1 :
220
+ relation = 5 #'surrounding'
221
+ elif ux0 >= vx0 and ux1 <= vx1 and uy0 >= vy0 and uy1 <= vy1 :
222
+ relation = 4 #'inside'
223
+ else :
224
+ relation = point_box_relation (uc ,self .rooms [v ,:4 ])
225
+ edges .append ([u ,v ,relation ])
226
+
227
+ self .edges = np .array (edges ,dtype = int )
228
+
229
+ def _get_archs (self ):
230
+ '''
231
+ Interior doors
232
+ '''
233
+ archs = []
234
+
235
+ # treat archs as instances
236
+ # index = len(self.rooms)+1
237
+
238
+ # for category in range(num_category,len(room_label)):
239
+ for category in [17 ]: # only get doors for building graphs
240
+ mask = (self .category == category ).astype (np .uint8 )
241
+
242
+ # distance transform -> threshold -> corner detection -> remove corner -> watershed -> label region
243
+ #distance = cv2.distanceTransform(mask,cv2.DIST_C,3)
244
+ distance = ndimage .morphology .distance_transform_cdt (mask )
245
+
246
+ # local_maxi = feature.peak_local_max(distance, indices=False) # line with one pixel
247
+ local_maxi = (distance > 1 ).astype (np .uint8 )
248
+
249
+ # corner_measurement = feature.corner_shi_tomasi(local_maxi) # short lines will be removed
250
+ corner_measurement = feature .corner_harris (local_maxi )
251
+
252
+ local_maxi [corner_measurement > 0 ] = 0
253
+
254
+ markers = measure .label (local_maxi )
255
+
256
+ labels = morphology .watershed (- distance , markers , mask = mask , connectivity = 8 )
257
+ regions = measure .regionprops (labels )
258
+
259
+ for region in regions :
260
+ y0 ,x0 ,y1 ,x1 = np .array (region .bbox )
261
+ archs .append ([y0 ,x0 ,y1 ,x1 ,category ])
262
+
263
+ self .archs = np .array (archs ,dtype = int )
264
+
265
+ def _get_graph (self ,th = 9 ):
266
+ '''
267
+ More detail graph
268
+ '''
269
+ if self .rooms is None : self ._get_rooms ()
270
+ if self .archs is None : self ._get_archs ()
271
+ graph = []
272
+ door_pos = [[None ,0 ] for i in range (len (self .rooms ))]
273
+ edge_set = set ()
274
+
275
+ # add accessible edges
276
+ doors = self .archs [self .archs [:,- 1 ]== 17 ]
277
+ for i in range (len (doors )):
278
+ bbox = doors [i ,:4 ]
279
+
280
+ # left <-> right
281
+ for row in range (bbox [0 ],bbox [2 ]):
282
+
283
+ u = self .instance [row ,bbox [1 ]- 1 ]- 1
284
+ v = self .instance [row ,bbox [3 ]+ 1 ]- 1
285
+ if (u ,v ) in edge_set or (v ,u ) in edge_set :continue
286
+ if u >= 0 and v >= 0 and (u ,v ):
287
+ edge_set .add ((u ,v ))
288
+ graph .append ([u ,v ,None ,1 ,i ])
289
+
290
+ # up <-> down
291
+ for col in range (bbox [1 ],bbox [3 ]):
292
+ u = self .instance [bbox [0 ]- 1 ,col ]- 1
293
+ v = self .instance [bbox [2 ]+ 1 ,col ]- 1
294
+ if (u ,v ) in edge_set or (v ,u ) in edge_set :continue
295
+ if u >= 0 and v >= 0 and (u ,v ):
296
+ edge_set .add ((u ,v ))
297
+ graph .append ([u ,v ,None ,1 ,i ])
298
+
299
+ # add adjacent edges
300
+ for u in range (len (self .rooms )):
301
+ for v in range (u + 1 ,len (self .rooms )):
302
+ if (u ,v ) in edge_set or (v ,u ) in edge_set : continue
303
+
304
+ # collision detection
305
+ if collide2d (self .rooms [u ,:4 ],self .rooms [v ,:4 ],th = th ):
306
+ edge_set .add ((u ,v ))
307
+ graph .append ([u ,v ,None ,0 ,None ])
308
+
309
+ # add edge relation
310
+ for i in range (len (graph )):
311
+ u ,v ,e ,t ,d = graph [i ]
312
+ uy0 , ux0 , uy1 , ux1 = self .rooms [u ,:4 ]
313
+ vy0 , vx0 , vy1 , vx1 = self .rooms [v ,:4 ]
314
+ uc = (uy0 + uy1 )/ 2 ,(ux0 + ux1 )/ 2
315
+ vc = (vy0 + vy1 )/ 2 ,(vx0 + vx1 )/ 2
316
+
317
+ if ux0 < vx0 and ux1 > vx1 and uy0 < vy0 and uy1 > vy1 :
318
+ relation = 5 #'surrounding'
319
+ elif ux0 >= vx0 and ux1 <= vx1 and uy0 >= vy0 and uy1 <= vy1 :
320
+ relation = 4 #'inside'
321
+ else :
322
+ relation = point_box_relation (uc ,self .rooms [v ,:4 ])
323
+
324
+ graph [i ][2 ] = relation
325
+
326
+
327
+ if d is not None :
328
+ c_u = self .rooms [u ,- 1 ]
329
+ c_v = self .rooms [v ,- 1 ]
330
+
331
+ if c_u > c_v and door_pos [u ][0 ] is None :
332
+ room = u
333
+ else :
334
+ room = v
335
+ door_pos [room ][0 ]= d
336
+
337
+
338
+ d_center = self .archs [d ,:4 ]
339
+ d_center = (d_center [:2 ]+ d_center [2 :])/ 2.0
340
+
341
+ dpos = door_room_relation (d_center ,self .rooms [room ,:4 ])
342
+ if dpos != 0 : door_pos [room ][1 ] = dpos
343
+
344
+ self .graph = graph
345
+ self .door_pos = door_pos
346
+
347
+ def to_dict (self ,xyxy = True ,dtype = int ):
348
+ '''
349
+ Compress data, notice:
350
+ !!! int->uint8: a(uint8)+b(uint8) may overflow !!!
351
+ '''
352
+ return {
353
+ 'name' :self .name ,
354
+ 'types' :self .rooms [:,- 1 ].astype (dtype ),
355
+ 'boxes' :(self .rooms [:,[1 ,0 ,3 ,2 ]]).astype (dtype )
356
+ if xyxy else self .rooms [:,:4 ].astype (dtype ),
357
+ 'boundary' :self .exterior_boundary [:,[1 ,0 ,2 ,3 ]].astype (dtype )
358
+ if xyxy else self .exterior_boundary .astype (dtype ),
359
+ 'edges' :self .edges .astype (dtype )
360
+ }
361
+
362
+ if __name__ == "__main__" :
363
+ pass
0 commit comments