def _find_initial_grid_points_contours(preproc, transform, pattern_specs, det_params, vis=None): print('WARNING - FINDING INITIAL GRID POINTS BY CONTOURS IS DEPRECATED') pyutils.tic('initial grid estimate - contour') #TODO remove ctpl = pattern_specs.calibration_template # Alias coords_dst = points2numpy(ctpl.refpts_full_marker) coords_src = points2numpy(transform.marker_corners) H = cv2.getPerspectiveTransform(coords_src, coords_dst) if H is None: return None, vis h, w = ctpl.tpl_full.shape[:2] # OpenCV doc: finding contours is finding white objects from black background! warped_img = cv2.warpPerspective(preproc.wb, H, (w, h), cv2.INTER_CUBIC) warped_mask = cv2.warpPerspective( np.ones(preproc.wb.shape[:2], dtype=np.uint8), H, (w, h), cv2.INTER_NEAREST) cnts = cv2.findContours(warped_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) cnts = cnts[0] if len(cnts) == 2 else cnts[1] vis_alt = imutils.ensure_c3(warped_img.copy()) idx = 0 expected_circle_area = (pattern_specs.calibration_template.dia_circle_px / 2)**2 * np.pi exp_circ_area_lower = 0.5 * expected_circle_area exp_circ_area_upper = 2 * expected_circle_area for shape in cnts: area = cv2.contourArea(shape) if area < exp_circ_area_lower or area > exp_circ_area_upper: color = (255, 0, 0) else: color = (0, 0, 255) # continue # Centroid M = cv2.moments(shape) try: cx = np.round(M['m10'] / M['m00']) cy = np.round(M['m01'] / M['m00']) except ZeroDivisionError: continue idx += 1 if det_params.debug: cv2.drawContours(vis_alt, [shape], 0, color, -1) cv2.circle(vis_alt, (int(cx), int(cy)), 1, (255, 255, 0), -1) if idx % 10 == 0: imvis.imshow(vis_alt, 'Points by contour', wait_ms=10) if det_params.debug: imvis.imshow(vis_alt, 'Points by contour', wait_ms=10) initial_estimates = list() #TODO match the points #TODO draw debug on vis pyutils.toc('initial grid estimate - contour') #TODO remove return initial_estimates, vis
def test_ensure_c3(): # Invalid inputs assert ensure_c3(None) is None for invalid in [np.zeros(17), np.ones((4, 3, 2)), np.zeros((2, 2, 5))]: with pytest.raises(ValueError): ensure_c3(invalid) # Grayscale image (2-dim) x = np.random.randint(0, 255, (20, 30)) c3 = ensure_c3(x) assert c3.ndim == 3 and c3.shape[2] == 3 for c in range(3): assert np.array_equal(x, c3[:, :, c]) # Grayscale image (3-dim, 1-channel) x = np.random.randint(0, 255, (10, 5, 1)) c3 = ensure_c3(x) assert c3.ndim == 3 and c3.shape[2] == 3 for c in range(3): assert np.array_equal(x[:, :, 0], c3[:, :, c]) # RGB(A) inputs for x in [ np.random.randint(0, 255, (12, 23, 3)), np.random.randint(0, 255, (12, 23, 4)) ]: c3 = ensure_c3(x) assert c3.ndim == 3 and c3.shape[2] == 3 assert np.array_equal(x[:, :, :3], c3)
def contour(img): g = imutils.grayscale(img) _, g = cv2.threshold(g, 0.0, 255.0, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # g = imutils.gaussian_blur(g, 1) # g = cv2.blur(g, (3,3)) vis = imutils.ensure_c3(g) vis = img.copy() edges = cv2.Canny(g, 50, 200, apertureSize=3) kernel = np.ones((3, 3), np.uint8) edges = cv2.dilate(edges, kernel, iterations=1) cnts = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) #cv2.CHAIN_APPROX_SIMPLE) cnts = cnts[0] if len(cnts) == 2 else cnts[1] #https://docs.opencv.org/3.4/d4/d73/tutorial_py_contours_begin.html # White on black! approximated_polygons = list() for cnt in cnts: # get simplified convex hull epsilon = 0.05 * cv2.arcLength(cnt, True) approx = cv2.approxPolyDP(cnt, epsilon, True) hull = cv2.convexHull(approx) # cv2.drawContours(vis, [approx], 0, (0,255,0), 3) approximated_polygons.append({ 'hull': hull, 'approx': approx, 'hull_area': cv2.contourArea(hull), 'corners': len(hull) }) def _key(a): return a['hull_area'] approximated_polygons.sort(key=_key, reverse=True) i = 0 for approx in approximated_polygons: #cv2.drawContours(vis, [approx['cnt']], 0, (0,255,0) if i < 10 else (255,0,0), 3) cv2.drawContours(vis, [approx['hull']], 0, (0, 255, 0) if 3 < approx['corners'] < 6 else (255, 0, 0), 3) i += 1 # if i < 15: # print('Largest', i, approx['approx'], approx['area']) # imvis.imshow(vis, title='contours', wait_ms=-1) imvis.imshow(vis, title='contours', wait_ms=-1)
def find_target(img, pattern_specs, det_params=ContourDetectionParams()): # pyutils.tic('img-preprocessing')#TODO remove preprocessed = _md_preprocess_img(img, det_params) if det_params.debug: ### The pattern specification (+ rendered templates) takes ~1MB # # https://stackoverflow.com/questions/449560/how-do-i-determine-the-size-of-an-object-in-python # print(f"""Object sizes: # pattern_spec: {sizeof_fmt(sys.getsizeof(pattern_specs))} # det_params: {sizeof_fmt(sys.getsizeof(det_params))} # preprocessed: {sizeof_fmt(sys.getsizeof(preprocessed))} # """) # print('REQUIRES pympler!!') # from pympler import asizeof # print(f"""Sizes with pympler: # pattern_spec: {sizeof_fmt(asizeof.asizeof(pattern_specs))} # det_params: {sizeof_fmt(asizeof.asizeof(det_params))} # preprocessed: {sizeof_fmt(asizeof.asizeof(preprocessed))} # """) from vito import imvis vis = imutils.ensure_c3(preprocessed.gray) else: vis = None # pyutils.toc('img-preprocessing')#TODO remove - 20-30ms # pyutils.tic('center-candidates-contours')#TODO remove candidate_shapes, vis = _md_find_center_marker_candidates( det_params, preprocessed, vis) # pyutils.toc('center-candidates-contours')#TODO remove 1-2ms # pyutils.tic('center-verification-projective')#TODO remove 1-2ms # Find best fitting candidate (if any) transforms = list() for shape in candidate_shapes: # Compute homography between marker template and detected candidate transform = _find_center_marker_transform( preprocessed, shape, det_params, pattern_specs.calibration_template) if transform is not None: transforms.append(transform) transforms.sort(key=lambda t: t.similarity, reverse=True) # pyutils.toc('center-verification-projective')#TODO remove if len(transforms) > 0: _find_grid(preprocessed, transforms[0], pattern_specs, det_params, vis=vis)
def _find_initial_grid_points_correlation(preproc, transform, pattern_specs, det_params, vis=None): pyutils.tic('initial grid estimate - correlation') #TODO remove ctpl = pattern_specs.calibration_template # Alias coords_dst = points2numpy(ctpl.refpts_full_marker) coords_src = points2numpy(transform.marker_corners) H = cv2.getPerspectiveTransform(coords_src, coords_dst) if H is None: return None, vis h, w = ctpl.tpl_full.shape[:2] warped_img = cv2.warpPerspective(preproc.bw, H, (w, h), cv2.INTER_CUBIC) warped_mask = cv2.warpPerspective( np.ones(preproc.bw.shape[:2], dtype=np.uint8), H, (w, h), cv2.INTER_NEAREST) ncc = cv2.matchTemplate( warped_img, ctpl.tpl_cropped_circle, cv2.TM_CCOEFF_NORMED) # mask must be template size?? ncc[ncc < det_params.grid_ccoeff_thresh_initial] = 0 if det_params.debug: overlay = imutils.ensure_c3( imvis.overlay(ctpl.tpl_full, 0.3, warped_img, warped_mask)) warped_img_corners = pru.apply_projection( H, points2numpy(image_corners(preproc.bw), Nx2=False)) for i in range(4): pt1 = numpy2cvpt(warped_img_corners[:, i]) pt2 = numpy2cvpt(warped_img_corners[:, (i + 1) % 4]) cv2.line(overlay, pt1, pt2, color=(0, 0, 255), thickness=3) #FIXME FIXME FIXME # Idea: detect blobs in thresholded NCC # this could replace the greedy nms below # barycenter/centroid of each blob gives the top-left corner (then compute the relative offset to get the initial corner guess) initial_estimates = list() tpl = ctpl.tpl_cropped_circle while True: y, x = np.unravel_index(ncc.argmax(), ncc.shape) # print('Next', y, x, ncc[y, x], det_params.grid_ccoeff_thresh_initial, ncc.shape) if ncc[y, x] < det_params.grid_ccoeff_thresh_initial: break initial_estimates.append( CalibrationGridPoint(x=x, y=y, score=ncc[y, x])) # Clear the NCC peak around the detected template left = x - tpl.shape[1] // 2 top = y - tpl.shape[0] // 2 left, top, nms_w, nms_h = imutils.clip_rect_to_image( (left, top, tpl.shape[1], tpl.shape[0]), ncc.shape[1], ncc.shape[0]) right = left + nms_w bottom = top + nms_h ncc[top:bottom, left:right] = 0 if det_params.debug: cv2.rectangle(overlay, (x, y), (x + ctpl.tpl_cropped_circle.shape[1], y + ctpl.tpl_cropped_circle.shape[0]), (255, 0, 255)) if len(initial_estimates) % 20 == 0: # imvis.imshow(imvis.pseudocolor(ncc, [-1, 1]), 'NCC Result', wait_ms=10) imvis.imshow(overlay, 'Points by correlation', wait_ms=10) if vis is not None: cv2.drawContours(vis, [transform.shape['hull']], 0, (200, 0, 200), 3) if det_params.debug: print('Check "Points by correlation". Press key to continue') imvis.imshow(overlay, 'Points by correlation', wait_ms=-1) pyutils.toc('initial grid estimate - correlation') #TODO remove return initial_estimates, vis
def _md_find_center_marker_candidates(det_params, preprocessed, vis_img=None): """Locate candidate regions which could contain the marker.""" debug_shape_extraction = det_params.debug and True # We don't want hierarchies of contours here, just the largest (i.e. the # root) contour of each hierarchy is fine: cnts = cv2.findContours(preprocessed.wb, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts = cnts[0] if len(cnts) == 2 else cnts[1] # Collect the convex hulls of all detected contours shapes = list() if debug_shape_extraction: tmp_vis = imutils.ensure_c3(preprocessed.wb) tmp_drawn = 0 for cnt in cnts: # Compute a simplified convex hull epsilon = det_params.simplification_factor * cv2.arcLength(cnt, True) approx = cv2.approxPolyDP(cnt, epsilon, True) # Important: # Simplification with too large epsilons could lead to intersecting # polygons. These would cause incorrect area computation, and more # "fun" behavior. Thus, we work with the shape's convex hull from # now on. hull = cv2.convexHull(approx) area = cv2.contourArea(hull) if debug_shape_extraction: cv2.drawContours(tmp_vis, [cnt], 0, (255, 0, 0), 7) cv2.drawContours(tmp_vis, [hull], 0, (255, 0, 255), 7) tmp_drawn += 1 if tmp_drawn % 10 == 0: imvis.imshow(tmp_vis, 'Shape candidates', wait_ms=10) if det_params.min_marker_area_px is None or\ area >= det_params.min_marker_area_px: shapes.append({ 'hull': hull, 'approx': approx, 'cnt': cnt, 'hull_area': area, 'num_corners': len(hull) }) if debug_shape_extraction: print('Check "shape candidates". Press key to continue') imvis.imshow(tmp_vis, 'Shape candidates', wait_ms=-1) # Sort candidate shapes by area (descending) shapes.sort(key=lambda s: s['hull_area'], reverse=True) # Collect valid convex hulls, i.e. having 4-6 corners which could # correspond to a rectangular region. candidate_shapes = list() for shape in shapes: is_candidate = False if 3 < shape['num_corners'] <= 8: candidate = _ensure_quadrilateral( shape ) #TODO pass image for debug visualizations, preprocessed.original) if candidate is not None: is_candidate = True candidate_shapes.append(candidate) if vis_img is not None: cv2.drawContours(vis_img, [shape['hull']], 0, (0, 255, 0) if is_candidate else (255, 0, 0), 7) if det_params.max_candidates_per_image > 0 and det_params.max_candidates_per_image <= len( candidate_shapes): logging.info( f'Reached maximum amount of {det_params.max_candidates_per_image} candidate shapes.' ) break return candidate_shapes, vis_img
def _compute_reference_grid(self): #TODO doc debug = True num_refpts_per_row = self.circles_per_row - 1 num_refpts_per_col = self.circles_per_col - 1 if debug: from vito import imvis, imutils import cv2 vis = imutils.ensure_c3(self.calibration_template.tpl_full.copy()) visited = np.zeros((num_refpts_per_col, num_refpts_per_row), dtype=np.bool) nodes_to_visit = deque() nodes_to_visit.append(self._make_reference_point(0, 0)) nnr = 0 visible_count = 0 reference_points = list() while nodes_to_visit: n = nodes_to_visit.popleft() vidx = self._refpt2posgrid(n.col, n.row) if vidx is None or visited[vidx.row, vidx.col]: continue nnr += 1 visited[vidx.row, vidx.col] = True # # #circle test 31.03. bad idea # # if n.neighbor_dir is None or n.neighbor_dir == 0: # # nodes_to_visit.append(self._make_reference_point(col=n.col, row=n.row-1, neighbor_dir=0)) # # nodes_to_visit.append(self._make_reference_point(col=n.col-1, row=n.row-1, neighbor_dir=0)) # # if n.neighbor_dir is None or n.neighbor_dir == 1: # # nodes_to_visit.append(self._make_reference_point(col=n.col-1, row=n.row, neighbor_dir=1)) # # nodes_to_visit.append(self._make_reference_point(col=n.col-1, row=n.row+1, neighbor_dir=1)) # # if n.neighbor_dir is None or n.neighbor_dir == 2: # # nodes_to_visit.append(self._make_reference_point(col=n.col, row=n.row+1, neighbor_dir=2)) # # nodes_to_visit.append(self._make_reference_point(col=n.col+1, row=n.row+1, neighbor_dir=2)) # # if n.neighbor_dir is None or n.neighbor_dir == 3: # # nodes_to_visit.append(self._make_reference_point(col=n.col+1, row=n.row, neighbor_dir=3)) # # nodes_to_visit.append(self._make_reference_point(col=n.col+1, row=n.row-1, neighbor_dir=3)) ## 8-neighborhood (works okayish 31.03) nodes_to_visit.append( self._make_reference_point(col=n.col, row=n.row - 1)) nodes_to_visit.append( self._make_reference_point(col=n.col - 1, row=n.row - 1)) nodes_to_visit.append( self._make_reference_point(col=n.col - 1, row=n.row)) nodes_to_visit.append( self._make_reference_point(col=n.col - 1, row=n.row + 1)) nodes_to_visit.append( self._make_reference_point(col=n.col, row=n.row + 1)) nodes_to_visit.append( self._make_reference_point(col=n.col + 1, row=n.row + 1)) nodes_to_visit.append( self._make_reference_point(col=n.col + 1, row=n.row)) nodes_to_visit.append( self._make_reference_point(col=n.col + 1, row=n.row - 1)) # ## 4-neighborhood # # nodes_to_visit.append(self._make_reference_point(col=n.col, row=n.row-1)) # # nodes_to_visit.append(self._make_reference_point(col=n.col-1, row=n.row)) # # nodes_to_visit.append(self._make_reference_point(col=n.col, row=n.row+1)) # # nodes_to_visit.append(self._make_reference_point(col=n.col+1, row=n.row)) if n.surrounding_circles is not None: reference_points.append(n) if debug: pt = self._mm2px(n.pos_mm_tl) # cv2.putText(vis, f'{nnr:d}', pt.int_repr(), cv2.FONT_HERSHEY_PLAIN, 1, (0, 0, 255), 1) cv2.putText(vis, f'{len(reference_points)-1:d}', pt.int_repr(), cv2.FONT_HERSHEY_PLAIN, 1, (0, 0, 255), 1) cv2.circle(vis, pt.int_repr(), 3, (255, 0, 0), -1) imvis.imshow(vis, "Reference Grid", wait_ms=1) object.__setattr__(self, 'reference_points', reference_points) if debug: print('Check "reference grid". Press key to continue.') imvis.imshow(vis, "Reference Grid", wait_ms=-1)