|
| 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