Skip to content

Commit f6381d5

Browse files
committed
3D from drawing (mostly) working
Ends are not correct No intrinsic camera correction
1 parent 2ae8eff commit f6381d5

8 files changed

+605
-49
lines changed

Image_based/bezier_cyl_2d.py

+9
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ def radius(self, t):
5454
@param t - t between 0 and 1"""
5555
return (1 - t) * self.start_radius + t * self.end_radius
5656

57+
def curve_length(self, t_step=0.1):
58+
""" Approximate length of curve
59+
@param t_step - t values to sample at
60+
@return approximate length of curve"""
61+
pts = self.pt_axis(np.linspace(0, 1, int(1.0 / t_step)))
62+
pts_diff_sq = (pts[1:, :] - pts[0:-1, :]) ** 2
63+
norm_sq = np.sum(pts_diff_sq, axis=1)
64+
return np.sum(np.sqrt(norm_sq))
65+
5766
@staticmethod
5867
def _orientation(start_pt, end_pt):
5968
"""Set the orientation and ensure left-right or down-up

Image_based/fit_bezier_cyl_2d.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def extract_least_squares(self, a_constraints, b_rhs):
6969
pts_diffs = np.sum(np.abs(new_pts - b_rhs[0:3, :]))
7070

7171
# Don't let the end points contract
72-
if self.orientation is "vertical":
72+
if self.orientation == "vertical":
7373
new_pts[0, 1] = self.p0[1]
7474
new_pts[2, 1] = self.p2[1]
7575
else:

Image_based/fit_bezier_cyl_2d_sketch.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,10 @@ def _sketch_curve_to_bezier(sketch_curves):
123123
radii[i] = 0.5 * np.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
124124
radius = np.mean(np.array(radii))
125125
sketch_crv = BezierCyl2D(start_pt=start_pt, mid_pt=mid_pt, end_pt=end_pt, radius=radius)
126-
if len(sketch_crv.cross_bars) > 1:
127-
half_way = len(sketch_crv.cross_baars) // 2
128-
sketch_crv.start_radius = np.radius = np.mean(np.array(radii[0:half_way]))
129-
sketch_crv.end_radius = np.radius = np.mean(np.array(radii[half_way:]))
126+
if len(sketch_curves.cross_bars) > 1:
127+
half_way = len(sketch_curves.cross_bars) // 2
128+
sketch_curves.start_radius = np.radius = np.mean(np.array(radii[0:half_way]))
129+
sketch_curves.end_radius = np.radius = np.mean(np.array(radii[half_way:]))
130130

131131
fit_crv = FitBezierCyl2D(sketch_crv)
132132
ts = np.linspace(0, 1, len(sketch_curves.backbone_pts))
+273
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
#!/usr/bin/env python3
2+
3+
# Read in the depth image and the fitted bezier curve.
4+
# Assumes one fitted 2d curve
5+
# Extract depth values
6+
# - Average values along spline cross section
7+
# - Returns 3D curve
8+
9+
import numpy as np
10+
import cv2
11+
import json
12+
from os.path import exists
13+
from line_seg_2d import LineSeg2D
14+
from HandleFileNames import HandleFileNames
15+
from bezier_cyl_2d import BezierCyl2D
16+
from bezier_cyl_3d import BezierCyl3D
17+
from fit_bezier_cyl_2d_edge import FitBezierCyl2DEdge
18+
from split_masks import convert_jet_to_grey
19+
20+
21+
class FitBezierCyl3dDepth:
22+
def __init__(self, fname_depth_image, crv_2d, params=None, fname_calculated=None, fname_debug=None, b_recalc=False):
23+
""" Read in the mask image, use the stats to start the quad fit, then fit the quad
24+
@param fname_depth_image: Depth image name
25+
@param crv_2d: 2d bezier curve
26+
@param params: Parameters for filtering the depth image - how finely to sample along the edge and how much to believe edge
27+
perc_width_depth - percent of width to use, should be 0.1 to 0.85
28+
perc_along_depth - take median of pixels from a perc of curve, should be 0.1 to 0.3
29+
camera_width_angle - angle in degrees, 45 for intel d45, etc
30+
@param fname_calculated: the file name for the saved .json file; should be image name w/o _stats.json
31+
@param fname_debug: the file name for a debug image showing the bounding box, etc. Set to None if no debug
32+
@param b_recalc: Force recalculate the result, y/n"""
33+
34+
# First do the stats - this also reads the image in
35+
self.crv_2d = crv_2d
36+
# Keep extracted depth values
37+
self.depth_values = []
38+
39+
# Get depth image
40+
mask_image_depth = cv2.imread(fname_depth_image)
41+
if len(mask_image_depth.shape) == 3:
42+
# data * alpha + beta, beta = 0 convert to unsigned int
43+
# most maxed out 65535
44+
#depth_colormap = cv2.applyColorMap(cv2.convertScaleAbs(depth_image, alpha=0.03), cv2.COLORMAP_JET)
45+
self.depth_image = convert_jet_to_grey(mask_image_depth) / 255.0
46+
self.depth_image = self.depth_image / 0.03
47+
#self.depth_image = mask_image_depth
48+
#self.depth_image = cv2.cvtColor(mask_image_depth, cv2.COLOR_BGR2GRAY)
49+
else:
50+
self.depth_image = mask_image_depth
51+
52+
# Create the file names for the calculated data that we'll store (initial curve, curve fit to mask, parameters)
53+
if fname_calculated:
54+
self.fname_depth_stats = fname_calculated + "_depth_stats.json"
55+
self.fname_params = fname_calculated + "_depth_params.json"
56+
self.fname_crv_3d = fname_calculated + "_crv_3d.json"
57+
58+
# Copy params used mask and add the new ones
59+
self.params = {}
60+
if params is not None:
61+
for k in params.keys():
62+
self.params[k] = params[k]
63+
if "perc_width_depth" not in self.params:
64+
self.params["perc_width_depth"] = 0.65
65+
if "perc_along_depth" not in self.params:
66+
self.params["perc_along_depth"] = 0.1
67+
if "camera_width_angle" not in self.params:
68+
self.params["camera_width_angle"] = 50
69+
70+
# Get the raw edge data
71+
if b_recalc or not fname_calculated or not exists(self.fname_depth_stats):
72+
# Recalculate and write
73+
self.depth_stats = FitBezierCyl3dDepth.full_depth_stats(self.depth_image,
74+
self.crv_2d,
75+
self.params)
76+
77+
# Write out the 3d bezier curve
78+
if fname_calculated:
79+
with open(self.fname_depth_stats, 'w') as f:
80+
json.dump(self.depth_stats, f, indent=" ")
81+
with open(self.fname_params, 'w') as f:
82+
json.dump(self.params, f, indent=" ")
83+
else:
84+
# Read in the stored data
85+
with open(self.fname_depth_stats, 'r') as f:
86+
self.depth_stats = json.load(f)
87+
with open(self.fname_params, 'r') as f:
88+
self.params = json.load(f)
89+
90+
# Now use the params to filter the raw edge location data - produces the left, right edge curves
91+
if b_recalc or not fname_calculated or not exists(self.fname_crv_3d):
92+
# Recalculate and write
93+
self.crv_3d = FitBezierCyl3dDepth.curve_from_stats(self.depth_stats, self.crv_2d, self.params)
94+
if fname_calculated:
95+
with open(self.fname_crv_3d, 'w') as f:
96+
self.crv_3d.write_json(self.fname_crv_3d)
97+
else:
98+
# Read in the reconstructed curve
99+
self.crv_3d = BezierCyl3D.read_json(self.fname_crv_3d, None, True)
100+
101+
if fname_debug:
102+
# Draw the mask with the initial and fitted curve
103+
self.crv_3d.make_mesh()
104+
self.crv_3d.write_mesh(fname_debug + ".obj")
105+
print("To do")
106+
107+
@staticmethod
108+
def full_depth_stats(image_depth, crv_2d, params):
109+
""" Get the best pixel offset (if any) for each point/pixel along the edge
110+
@param image_depth - the depth image
111+
@param crv_2d - the 2d curve
112+
@param params - parameters for conversion
113+
@return t, stats for depth, spaced n apart"""
114+
115+
# Fuzzy rectangles along the boundary
116+
n_pixs = int(crv_2d.curve_length() * params["perc_along_depth"])
117+
rects, _ = crv_2d.interior_rects(step_size=n_pixs, perc_width=params["perc_width_depth"])
118+
119+
ts = np.linspace(0, 1, len(rects) + 1)
120+
121+
# Size of the rectangle(s) to cutout is based on the step size and the radius
122+
height = int(crv_2d.radius(0.5))
123+
width = n_pixs
124+
125+
ret_stats = {"n_segs": len(ts) - 1,
126+
"image_size": (image_depth.shape[1], image_depth.shape[0]),
127+
"ts":[],
128+
"z_at_center":[],
129+
"radius_3d":[],
130+
"divs": [],
131+
"depth_divs":[],
132+
"depth_values":[],
133+
"r_at_depth":[],
134+
"t_at_depth":[]}
135+
im_cutouts = []
136+
ts_image = []
137+
rs_image = []
138+
trans_back = []
139+
140+
n_total_pixs = width * height
141+
divs = (0, n_total_pixs // 4, n_total_pixs // 2, 3 * n_total_pixs // 4, n_total_pixs-1)
142+
ret_stats["divs"] = divs
143+
144+
rs_seg = np.linspace(-params["perc_width_depth"], params["perc_width_depth"], height)
145+
for i_rect, r in enumerate(rects):
146+
# Cutout the image for the boundary rectangle
147+
# Note this will be a height x width numpy array
148+
im_warp, tform3_back = crv_2d.image_cutout(image_depth, r, step_size=width, height=height)
149+
im_cutouts.append(im_warp)
150+
trans_back.append(trans_back)
151+
152+
ret_stats["ts"].append((ts[i_rect], ts[i_rect+1]))
153+
154+
depth_unsorted = np.reshape(im_warp[:, :], (n_total_pixs))
155+
depth_sort = np.sort(depth_unsorted)
156+
157+
ret_stats["depth_divs"].append([depth_sort[d] for d in divs])
158+
ts_seg = np.linspace(ts[i_rect], ts[i_rect + 1], width)
159+
t_image = np.ones((height, width))
160+
for c in range(0, height):
161+
t_image[c, :] = ts_seg
162+
r_image = np.ones((height, width))
163+
for r in range(0, width):
164+
r_image[:, r] = rs_seg * crv_2d.radius(ts_seg[r])
165+
166+
# if radius value is correct, and curve centered, this would be the z value and radius
167+
pix_max = int(n_total_pixs * .95)
168+
depth_at_center = depth_sort[pix_max]
169+
rad_2d = crv_2d.radius(ts_seg[width // 2])
170+
ang_subtend_degrees = params["camera_width_angle"] * (2 * rad_2d) / image_depth.shape[1]
171+
ang_subtend_radians = np.pi * ang_subtend_degrees / 180.0
172+
radius_3d = 0.5 * depth_at_center * np.tan(ang_subtend_radians)
173+
z_at_center = depth_at_center - radius_3d
174+
175+
rad_clip_min = z_at_center
176+
ret_stats["z_at_center"].append(z_at_center)
177+
ret_stats["radius_3d"].append(radius_3d)
178+
179+
for r in range(0, width):
180+
for c in range(1, height):
181+
if im_warp[c, r] > rad_clip_min:
182+
ret_stats["depth_values"].append(im_warp[c, r])
183+
ret_stats["r_at_depth"].append(t_image[c, r])
184+
ret_stats["t_at_depth"].append(r_image[c, r])
185+
186+
"""
187+
for r in range(0, width):
188+
for c in range(1, height):
189+
p1_in = np.transpose(np.array([r, c, 1.0]))
190+
p1_back = tform3_back @ p1_in
191+
pt_spine = crv_2d.pt_axis(ts_seg[r])
192+
vec_norm = crv_2d.norm_axis(ts_seg[r], "left")
193+
perc_along = np.dot(p1_back[:2] - pt_spine, vec_norm)
194+
h_perc = perc_along / crv_2d.radius(ts_seg[r])
195+
"""
196+
return ret_stats
197+
198+
@staticmethod
199+
def curve_from_stats(stats_depth, crv_2d, params):
200+
"""
201+
From the raw stats, create a set of evenly-spaced t values
202+
@param stats_depth: The stats from full_depth_stats
203+
@param crv_2d - the 2d curve
204+
@param params: max edge pixel value, step_size, perc to search, and n pts to reconstruct
205+
@return: 3d curve
206+
"""
207+
208+
pts = []
209+
image_width = stats_depth["image_size"][0]
210+
image_height = stats_depth["image_size"][1]
211+
cam_width_ang_half = 0.5 * params['camera_width_angle']
212+
cam_height_ang_half = 0.5 * params['camera_width_angle'] * stats_depth['image_size'][1] / stats_depth['image_size'][0]
213+
print(f"cam x ang {cam_width_ang_half * 2} cam y ang {cam_height_ang_half * 2} {image_width}, {image_height}")
214+
for i in range(0, stats_depth["n_segs"]):
215+
z_at_center = stats_depth["z_at_center"][i]
216+
t = 0.5 * (stats_depth["ts"][i][0] + stats_depth["ts"][i][1])
217+
radius_3d = stats_depth["radius_3d"][i]
218+
pt2d = crv_2d.pt_axis(t)
219+
# -1 to 1
220+
ang_x = 2.0 * (pt2d[0] - image_width / 2) / image_width
221+
# - ang/2 to ang/2
222+
ang_x_degrees = cam_width_ang_half * ang_x
223+
# radians
224+
ang_x_radians = np.pi * ang_x_degrees / 180.0
225+
226+
# -1 to 1
227+
ang_y = -2.0 * (pt2d[1] - image_height / 2) / image_height
228+
# - ang/2 to ang/2
229+
ang_y_degrees = cam_height_ang_half * ang_y
230+
# radians
231+
ang_y_radians = np.pi * ang_y_degrees / 180.0
232+
233+
x = np.tan(ang_x_radians) * z_at_center
234+
y = np.tan(ang_y_radians) * z_at_center
235+
print(f"x {pt2d[0]} {ang_x} {x} y {pt2d[1]} {ang_y} {y}")
236+
pts.append([x, y, -z_at_center])
237+
238+
i_mid = len(stats_depth["z_at_center"]) // 2
239+
p1 = []
240+
crv_3d = BezierCyl3D(pts[0], pts[i_mid], pts[-1], stats_depth["radius_3d"][0], stats_depth["radius_3d"][-1])
241+
return crv_3d
242+
243+
244+
if __name__ == '__main__':
245+
# path_bpd = "./data/trunk_segmentation_names.json"
246+
path_bpd = "./data/forcindy_fnames.json"
247+
all_files = HandleFileNames.read_filenames(path_bpd)
248+
249+
b_do_debug = True
250+
b_do_recalc = True
251+
for ind in all_files.loop_masks():
252+
rgb_fname = all_files.get_image_name(path=all_files.path, index=ind, b_add_tag=True)
253+
edge_fname = all_files.get_edge_image_name(path=all_files.path_calculated, index=ind, b_add_tag=True)
254+
depth_fname = all_files.get_depth_image_name(path=all_files.path, index=ind, b_add_tag=True)
255+
mask_fname = all_files.get_mask_name(path=all_files.path, index=ind, b_add_tag=True)
256+
depth_fname_debug = all_files.get_mask_name(path=all_files.path_debug, index=ind, b_add_tag=False)
257+
if not b_do_debug:
258+
depth_fname_debug = None
259+
260+
edge_fname_calculate = all_files.get_mask_name(path=all_files.path_calculated, index=ind, b_add_tag=False)
261+
depth_fname_calculate = all_files.get_mask_name(path=all_files.path_calculated, index=ind, b_add_tag=False)
262+
263+
if not exists(mask_fname):
264+
raise ValueError(f"Error, file {mask_fname} does not exist")
265+
if not exists(rgb_fname):
266+
raise ValueError(f"Error, file {rgb_fname} does not exist")
267+
268+
edge_crv = FitBezierCyl2DEdge(rgb_fname, edge_fname, mask_fname, edge_fname_calculate, None, b_recalc=False)
269+
270+
crv_3d = FitBezierCyl3dDepth(depth_fname, edge_crv.bezier_crv_fit_to_edge,
271+
params=None,
272+
fname_calculated=depth_fname_calculate,
273+
fname_debug=depth_fname_debug, b_recalc=b_do_recalc)

0 commit comments

Comments
 (0)