|
| 1 | +import cv2 |
| 2 | +import numpy as np |
| 3 | +import time |
| 4 | +from OminiBot_HV import ominibothv |
| 5 | + |
| 6 | +def region_of_interest(img, vertices): |
| 7 | + mask = np.zeros_like(img) |
| 8 | + cv2.fillPoly(mask, vertices, (255, 255, 255)) |
| 9 | + return cv2.bitwise_and(img, mask) |
| 10 | + |
| 11 | +# hsv color |
| 12 | +low_yellow = np.array([26, 77, 100]) |
| 13 | +high_yellow = np.array([34, 255, 255]) |
| 14 | +low_white = np.array([0, 0, 221]) |
| 15 | +high_white = np.array([180, 15, 255]) |
| 16 | + |
| 17 | +cap = cv2.VideoCapture(0) |
| 18 | +cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) |
| 19 | +cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320) |
| 20 | +cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240) |
| 21 | + |
| 22 | +robot_control = ominibothv('/dev/ominibot', 115200) |
| 23 | +time.sleep(3) |
| 24 | + |
| 25 | +CENTER_X = 160 |
| 26 | +BASE_SPEED = 0.15 |
| 27 | +TURN_GAIN = BASE_SPEED / 80.0 |
| 28 | +SMOOTH = 0.5 |
| 29 | +HALF_LANE = 70 # px offset when following a single wall |
| 30 | +MARGIN = 20 # dead zone around center for left/right classing |
| 31 | +Y0, Y1 = 145, 205 # lookahead scan band |
| 32 | +CORR_MAX = 0.16 |
| 33 | +MAX_STEP = 35 # max target move per frame (anti-runaway / anti-jump) |
| 34 | +LOST_STOP = 40 |
| 35 | +MIN_AREA = 25 # ignore tiny noise blobs |
| 36 | + |
| 37 | +interest_vertices = [np.array([ |
| 38 | + [0, 240], [0, 200], [80, 120], [240, 120], [320, 200], [320, 240] |
| 39 | +])] |
| 40 | + |
| 41 | +def blob_centroids(band): |
| 42 | + num, _, stats, cents = cv2.connectedComponentsWithStats(band, connectivity=8) |
| 43 | + return [cents[i][0] for i in range(1, num) if stats[i, cv2.CC_STAT_AREA] >= MIN_AREA] |
| 44 | + |
| 45 | +show_ok = True |
| 46 | +prev_correction = 0.0 |
| 47 | +prev_target = CENTER_X |
| 48 | +lost = 0 |
| 49 | + |
| 50 | +try: |
| 51 | + while True: |
| 52 | + ret, frame = cap.read() |
| 53 | + if not ret or frame is None: |
| 54 | + robot_control.motor_speed(0.0, 0.0, 0.0, 0.0) |
| 55 | + continue |
| 56 | + |
| 57 | + cropped = region_of_interest(frame.copy(), interest_vertices) |
| 58 | + hsv = cv2.cvtColor(cropped, cv2.COLOR_BGR2HSV) |
| 59 | + yellow_mask = cv2.inRange(hsv, low_yellow, high_yellow) |
| 60 | + white_mask = cv2.inRange(hsv, low_white, high_white) |
| 61 | + |
| 62 | + # ---- yellow walls: classify blobs by side, take the inner one each side |
| 63 | + yblobs = blob_centroids(yellow_mask[Y0:Y1, :]) |
| 64 | + left_side = [x for x in yblobs if x < CENTER_X - MARGIN] |
| 65 | + right_side = [x for x in yblobs if x > CENTER_X + MARGIN] |
| 66 | + left_wall = max(left_side) if left_side else None # rightmost left blob |
| 67 | + right_wall = min(right_side) if right_side else None # leftmost right blob |
| 68 | + |
| 69 | + # ---- white center line: the white blob nearest to image center |
| 70 | + wblobs = blob_centroids(white_mask[Y0:Y1, :]) |
| 71 | + white_x = min(wblobs, key=lambda x: abs(x - CENTER_X)) if wblobs else None |
| 72 | + |
| 73 | + # ---- choose target ---- |
| 74 | + if left_wall is not None and right_wall is not None: |
| 75 | + target_x = int((left_wall + right_wall) / 2) |
| 76 | + src = "walls" |
| 77 | + elif white_x is not None: |
| 78 | + target_x = int(white_x) |
| 79 | + src = "white" |
| 80 | + elif left_wall is not None: |
| 81 | + target_x = int(left_wall + HALF_LANE) |
| 82 | + src = "Lwall" |
| 83 | + elif right_wall is not None: |
| 84 | + target_x = int(right_wall - HALF_LANE) |
| 85 | + src = "Rwall" |
| 86 | + else: |
| 87 | + target_x = prev_target |
| 88 | + src = "hold" |
| 89 | + |
| 90 | + have_signal = (left_wall is not None) or (right_wall is not None) or (white_x is not None) |
| 91 | + lost = 0 if have_signal else lost + 1 |
| 92 | + |
| 93 | + # rate limit: target can only move so far per frame |
| 94 | + target_x = max(prev_target - MAX_STEP, min(prev_target + MAX_STEP, target_x)) |
| 95 | + target_x = max(0, min(319, target_x)) |
| 96 | + prev_target = target_x |
| 97 | + target_y = (Y0 + Y1) // 2 |
| 98 | + |
| 99 | + # ---- steering ---- |
| 100 | + error = target_x - CENTER_X |
| 101 | + correction = error * TURN_GAIN |
| 102 | + correction = SMOOTH * correction + (1 - SMOOTH) * prev_correction |
| 103 | + correction = max(min(correction, CORR_MAX), -CORR_MAX) |
| 104 | + prev_correction = correction |
| 105 | + |
| 106 | + forward = max(0.07, BASE_SPEED - 0.6 * abs(correction)) |
| 107 | + if lost > LOST_STOP: |
| 108 | + forward = 0.0 |
| 109 | + correction = 0.0 |
| 110 | + src = "STOP" |
| 111 | + |
| 112 | + robot_speed_l = max(min(forward + correction, 0.30), -0.12) |
| 113 | + robot_speed_r = max(min(forward - correction, 0.30), -0.12) |
| 114 | + |
| 115 | + print("{:>5} L:{} R:{} w:{} tgt:{:>3} corr:{:+.3f} l:{:.2f} r:{:.2f}".format( |
| 116 | + src, None if left_wall is None else int(left_wall), |
| 117 | + None if right_wall is None else int(right_wall), |
| 118 | + white_x if white_x is None else int(white_x), |
| 119 | + target_x, correction, robot_speed_l, robot_speed_r)) |
| 120 | + |
| 121 | + robot_control.motor_speed(robot_speed_l * -1, robot_speed_r, 0.0, 0.0) |
| 122 | + |
| 123 | + # ---- visualization ---- |
| 124 | + cv2.rectangle(frame, (0, Y0), (319, Y1), (80, 80, 80), 1) |
| 125 | + if left_wall is not None: |
| 126 | + cv2.circle(frame, (int(left_wall), target_y), 6, (255, 0, 0), -1) |
| 127 | + if right_wall is not None: |
| 128 | + cv2.circle(frame, (int(right_wall), target_y), 6, (255, 0, 0), -1) |
| 129 | + if white_x is not None: |
| 130 | + cv2.circle(frame, (int(white_x), target_y), 6, (0, 255, 0), -1) |
| 131 | + cv2.line(frame, (CENTER_X, 0), (CENTER_X, 240), (0, 0, 255), 1) |
| 132 | + cv2.arrowedLine(frame, (CENTER_X, 240), (target_x, target_y), (0, 255, 255), 3, tipLength=0.3) |
| 133 | + cv2.circle(frame, (target_x, target_y), 6, (255, 0, 255), -1) |
| 134 | + cv2.imwrite('debug_live.jpg', frame) |
| 135 | + if show_ok: |
| 136 | + try: |
| 137 | + cv2.imshow('frame', frame) |
| 138 | + if (cv2.waitKey(1) & 0xFF) == ord('q'): |
| 139 | + break |
| 140 | + except cv2.error: |
| 141 | + show_ok = False |
| 142 | + |
| 143 | +except KeyboardInterrupt: |
| 144 | + pass |
| 145 | +finally: |
| 146 | + robot_control.motor_speed(0.0, 0.0, 0.0, 0.0) |
| 147 | + cap.release() |
| 148 | + cv2.destroyAllWindows() |
0 commit comments