16
16
from BaseStatsImage import BaseStatsImage
17
17
from HandleFileNames import HandleFileNames
18
18
19
- class FitBezierCyl2D ( BaseStatsImage ) :
19
+ class FitBezierCyl2DEdge :
20
20
def __init__ (self , fname_mask_image , fname_calculated = None , fname_debug = None , b_recalc = False ):
21
- """ Read in the mask image, use the stats to start the quad fit, then fit the quad
22
- @param fname_mask_image: name of mask file, rgb or gray scale image with white where the mask is
23
- @param fname_calculated: the file name for the saved .json file; should be image name w/o _stats.json
24
- @param fname_debug: the file name for a debug image showing the bounding box, etc
25
- @param b_recalc: Force recalculate the result, y/n"""
26
-
27
- # Initializes the base stats image class, which does the image read and calculates the stats
28
- self .BaseStatsImage .__init__ (fname_mask_image , fname_calculated , fname_debug , b_recalc )
29
-
30
- # Fit a quad to the mask, using the end points of the base image as a starting point
31
- print ("Fitting quads" )
32
- self .fname_quad = None # The actual quadratic bezier
33
- self .fname_params = None # Parameters used to do the fit
34
- if fname_calculated :
35
- self .fname_quad = fname_calculated + "quad.json"
36
- self .fname_params = fname_calculated + "quad_params.json"
37
-
38
- self .quad = None
39
-
40
- # Current parameters for the vertical leader - will need to make this a parameter
41
- self .params = {"step_size" : int (0.5 * self .stats_dict ['width' ] * 1.5 ), "width_mask" : 1.4 , "width" : 0.25 }
42
-
43
- if b_recalc or not fname_calculated or not exists (fname_calculated ):
44
- if exists (self .fname_quad ) and not b_recalc :
45
- self .quad = BezierCyl2D .read_json (self .fname_quad )
46
- with open (self .fname_params , 'r' ) as f :
47
- self .params = json .load (f )
48
- else :
49
- self .quad = self .fit_quad_to_mask (self .mask_image , stats = self .stats_dict , params = self .params )
50
- self .quad .write_json (self .fname_quad )
51
- with open (self .fname_params , 'w' ) as f :
52
- json .dump (self .params , f )
53
-
54
- if fname_debug :
55
- # Draw the edge and original image with the fitted quad and rects
56
- im_covert_back = cv2 .cvtColor (self .mask_image , cv2 .COLOR_GRAY2RGB )
57
- self .debug_image (im_covert_back ) # The eigen vec
58
- self .debug_image_quad_fit (im_covert_back )
59
- cv2 .imwrite (fname_debug , im_covert_back )
60
-
61
- self .score = self .score_quad (self .quad )
62
-
63
- def _setup_least_squares (self , ts , ):
64
- """Setup the least squares approximation
65
- @param ts - t values to use
66
- @returns A, B for Ax = b """
67
- # Set up the matrix - include the 3 current points plus the centers of the mask
68
- a_constraints = np .zeros ((len (ts ) + 3 , 3 ))
69
- ts_constraints = np .zeros ((len (ts ) + 3 ))
70
- b_rhs = np .zeros ((len (ts ) + 3 , 2 ))
71
- ts_constraints [- 3 ] = 0.0
72
- ts_constraints [- 2 ] = 0.5
73
- ts_constraints [- 1 ] = 1.0
74
- ts_constraints [:- 3 ] = np .transpose (ts )
75
- a_constraints [:, - 3 ] = (1 - ts_constraints ) * (1 - ts_constraints )
76
- a_constraints [:, - 2 ] = 2 * (1 - ts_constraints ) * ts_constraints
77
- a_constraints [:, - 1 ] = ts_constraints * ts_constraints
78
- for i , t in enumerate (ts_constraints ):
79
- b_rhs [i , :] = self .pt_axis (ts_constraints [i ])
80
- return a_constraints , b_rhs
81
-
82
- def _extract_least_squares (self , a_constraints , b_rhs ):
83
- """ Do the actual Ax = b and keep horizontal/vertical end points
84
- @param a_constraints the A of Ax = b
85
- @param b_rhs the b of Ax = b
86
- @returns fit error L0 norm"""
87
- if a_constraints .shape [0 ] < 3 :
88
- return 0.0
89
-
90
- # a_at = a_constraints @ a_constraints.transpose()
91
- # rank = np.rank(a_at)
92
- # if rank < 3:
93
- # return 0.0
94
-
95
- new_pts , residuals , rank , _ = np .linalg .lstsq (a_constraints , b_rhs , rcond = None )
96
-
97
- print (f"Residuals { residuals } , rank { rank } " )
98
- b_rhs [1 , :] = self .p1
99
- pts_diffs = np .sum (np .abs (new_pts - b_rhs [0 :3 , :]))
100
-
101
- if self .orientation is "vertical" :
102
- new_pts [0 , 1 ] = self .p0 [1 ]
103
- new_pts [2 , 1 ] = self .p2 [1 ]
104
- else :
105
- new_pts [0 , 0 ] = self .p0 [0 ]
106
- new_pts [2 , 0 ] = self .p2 [0 ]
107
- self .p0 = new_pts [0 , :]
108
- self .p1 = new_pts [1 , :]
109
- self .p2 = new_pts [2 , :]
110
- return pts_diffs
111
-
112
- def adjust_quad_by_mask (self , im_mask , step_size = 40 , perc_width = 1.2 , axs = None ):
113
- """Replace the linear approximation with one based on following the mask
114
- @param im_mask - mask image
115
- @param step_size - how many pixels to step along
116
- @param perc_width - how much wider than the radius to look in the mask
117
- @param axs - optional axes to draw the cutout in
118
- @returns how much the points moved"""
119
- height = int (self .radius_2d )
120
- rects , ts = self .interior_rects (step_size = step_size , perc_width = perc_width )
121
-
122
- # Set up the matrix - include the 3 current points plus the centers of the mask
123
- a_constraints , b_rhs = self ._setup_least_squares (ts )
21
+ # TODO: Make this look like fit_bezier_cyl_2d_mask, creating a FitBezierCyl2DMask then using the
22
+ # output of that to do the fit to the edge process
124
23
125
- x_grid , y_grid = np .meshgrid (range (0 , step_size ), range (0 , height ))
126
- if axs is not None :
127
- axs .imshow (im_mask , origin = 'lower' )
128
- for i , r in enumerate (rects ):
129
- b_rect_inside = BezierCyl2D ._rect_in_image (im_mask , r , pad = 2 )
130
-
131
- im_warp , tform_inv = self ._image_cutout (im_mask , r , step_size = step_size , height = height )
132
- if b_rect_inside and np .sum (im_warp > 0 ) > 0 :
133
- x_mean = np .mean (x_grid [im_warp > 0 ])
134
- y_mean = np .mean (y_grid [im_warp > 0 ])
135
- pt_warp_back = tform_inv @ np .transpose (np .array ([x_mean , y_mean , 1 ]))
136
- print (f"{ self .pt_axis (ts [i ])} ({ x_mean } , { y_mean } ), { pt_warp_back } " )
137
- b_rhs [i , :] = pt_warp_back [0 :2 ]
138
- else :
139
- print (f"Empty slice { r } " )
140
-
141
- if axs is not None :
142
- axs .clear ()
143
- axs .imshow (im_warp , origin = 'lower' )
144
-
145
- return self ._extract_least_squares (a_constraints , b_rhs )
146
24
147
25
def _hough_edge_to_middle (self , p1 , p2 ):
148
26
""" Convert the two end points to an estimate of the mid-point and the pt on the spine
@@ -157,6 +35,10 @@ def _hough_edge_to_middle(self, p1, p2):
157
35
return mid_pt , pt_middle
158
36
159
37
def adjust_quad_by_hough_edges (self , im_edge , step_size = 40 , perc_width = 0.3 , axs = None ):
38
+ # TODO Make this look like fit_bezier_crv_to_mask in FitBezierCyl2DMask
39
+ # TODO rename quad to Beier
40
+ # TODO Like adjust, create a FitBezierCrv2D from an input Bezier crv (the setup_least_squares etc will work)
41
+ # TODO change self.ls etc to fit_bezier_crv
160
42
"""Replace the linear approximation with one based on following the mask
161
43
@param im_mask - mask image
162
44
@param step_size - how many pixels to step along
@@ -241,7 +123,8 @@ def adjust_quad_by_hough_edges(self, im_edge, step_size=40, perc_width=0.3, axs=
241
123
return self ._extract_least_squares (a_constraints , b_rhs )
242
124
243
125
def set_end_pts (self , pt0 , pt2 ):
244
- """ Set the end point to the new end point while trying to keep the curve the same
126
+ """ TODO: pass in the curve and set the crv's end points
127
+ Set the end point to the new end point while trying to keep the curve the same
245
128
@param pt0 new p0
246
129
@param pt2 new p2"""
247
130
l0 = LineSeg2D (self .p0 , self .p1 )
@@ -261,39 +144,6 @@ def set_end_pts(self, pt0, pt2):
261
144
262
145
return self ._extract_least_squares (a_constraints , b_rhs )
263
146
264
- @staticmethod
265
- def fit_quad_to_mask (self , im_mask , stats , params ):
266
- """ Fit a quad to the mask, edge image
267
- @param im_mask - the image mask
268
- @param stats - the stats from BaseStatsImage
269
- @param params - the parameters to use in the fit
270
- @returns fitted quad and stats used in the fit"""
271
-
272
- # Fit a quad to the trunk
273
- pt_lower_left = stats ['center' ]
274
- vec_len = stats ["Length" ] * 0.4
275
- while pt_lower_left [0 ] > 2 + stats ['x_min' ] and pt_lower_left [1 ] > 2 + stats ['y_min' ]:
276
- pt_lower_left = stats ["center" ] - stats ["EigenVector" ] * vec_len
277
- vec_len = vec_len * 1.1
278
-
279
- pt_upper_right = stats ['center' ]
280
- vec_len = stats ["Length" ] * 0.4
281
- while pt_upper_right [0 ] < - 2 + stats ['x_max' ] and pt_upper_right [1 ] < - 2 + stats ['y_max' ]:
282
- pt_upper_right = stats ["center" ] + stats ["EigenVector" ] * vec_len
283
- vec_len = vec_len * 1.1
284
-
285
- quad = BezierCyl2D (pt_lower_left , pt_upper_right , 0.5 * stats ['width' ])
286
-
287
- # Iteratively move the quad to the center of the mask
288
- print (f"Res: " , end = "" )
289
- for i in range (0 , 5 ):
290
- res = quad .adjust_quad_by_mask (im_mask ,
291
- step_size = params ["step_size" ], perc_width = params ["width_mask" ],
292
- axs = None )
293
- print (f"{ res } " , end = "" )
294
- print ("" )
295
- return quad , params
296
-
297
147
def find_edges_hough_transform (self , im_edge , step_size = 40 , perc_width = 0.3 , axs = None ):
298
148
"""Find the hough transform of the images in the boxes; save the line orientations
299
149
@param im_edge - edge image
@@ -424,71 +274,7 @@ def adjust_quad_by_edge_image(im_edge, quad, params):
424
274
425
275
return quad
426
276
427
- def check_interior_depth (self , im_depth , step_size = 40 , perc_width = 0.3 ):
428
- """ Find which pixels are valid depth and fit average depth
429
- @param im_depth - depth image
430
- @param step_size how many pixels to move along the boundary
431
- @param perc_width How much of the radius to move in/out of the edge
432
- """
433
- height = int (self .radius_2d )
434
- rects , ts = self .interior_rects (step_size = step_size , perc_width = perc_width )
435
-
436
- stats = []
437
- perc_consistant = 0.0
438
- for i , r in enumerate (rects ):
439
- b_rect_inside = BezierCyl2D ._rect_in_image (im_depth , r , pad = 2 )
440
-
441
- im_warp , tform_inv = self ._image_cutout (im_depth , r , step_size = step_size , height = height )
442
-
443
- stats_slice = {"Min" : np .min (im_warp ),
444
- "Max" : np .max (im_warp ),
445
- "Median" : np .median (im_warp )}
446
- stats_slice ["Perc_in_range" ] = np .count_nonzero (np .abs (im_warp - stats_slice ["Median" ]) < 10 ) / (im_warp .size )
447
- perc_consistant += stats_slice ["Perc_in_range" ]
448
- stats .append (stats_slice )
449
- perc_consistant /= len (rects )
450
- return perc_consistant , stats
451
-
452
- @staticmethod
453
- def adjust_quad_by_flow_image (im_flow , quad , params ):
454
- """ Not really useful now - fixes the mask by calculating the average depth in the flow mask under the bezier
455
- the trimming off mask pixels that don't belong
456
- @param im_flow: Flow or depth image (gray scale)
457
- @param quad: The quad to adjust
458
- @param params: Parameters to use for adjust
459
- @return the adjusted quad"""
460
-
461
- print ("Quad adjust res: " , end = "" )
462
- for i in range (0 , 5 ):
463
- res = quad .adjust_quad_by_mask (im_flow ,
464
- step_size = params ["step_size" ], perc_width = params ["width_mask" ],
465
- axs = None )
466
- print (f"{ res } " )
467
- print (" done" )
468
-
469
- return quad
470
-
471
- def score_quad (self , im_flow , quad ):
472
- """ See if the quad makes sense over the optical flow image
473
- @quad - the quad
474
- """
475
-
476
- # Two checks: one, are the depth/optical fow values largely consistent under the quad center
477
- # Are there boundaries in the optical flow image where the edge of the quad is?
478
- im_flow_mask = cv2 .cvtColor (im_flow , cv2 .COLOR_BGR2GRAY )
479
- perc_consistant , stats_slice = quad .check_interior_depth (im_flow_mask )
480
-
481
- diff = 0
482
- for i in range (1 , len (stats_slice )):
483
- diff_slices = np .abs (stats_slice [i ]["Median" ] - stats_slice [i - 1 ]["Median" ])
484
- if diff_slices > 20 :
485
- print (f"Warning: Depth values not consistant from slice { self .fname_quad } { i } { stats_slice } " )
486
- diff += diff_slices
487
- if perc_consistant < 0.9 :
488
- print (f"Warning: not consistant { self .fname_quad } { stats_slice } " )
489
- return perc_consistant , diff / (len (stats_slice ) - 1 )
490
-
491
- def debug_image_quad_fit (self , image_debug ):
277
+ def debug_image_edge_fit (self , image_debug ):
492
278
""" Draw the fitted quad on the image
493
279
@param image_debug - rgb image"""
494
280
# Draw the original, the edges, and the depth mask with the fitted quad
0 commit comments