|
| 1 | +import servos |
| 2 | +from collections import deque |
| 3 | +from maix import time, app, camera, image, display, touchscreen, nn, sys |
| 4 | +import cv2 |
| 5 | +import numpy as np |
| 6 | + |
| 7 | +# init uart register |
| 8 | +SERVO_PORT_NAME = '/dev/ttyS4' # 舵机串口号 |
| 9 | +SERVO_BAUDRATE = 115200 # 舵机的波特率 |
| 10 | +SERVO_ID = [1, 2] # 舵机ID,依次是pitch,roll,yaw |
| 11 | +SERVO_MIN_POSITION = [1650, 0] # 舵机位置最小值 |
| 12 | +SERVO_MAX_POSITION = [2446, 4096] # 舵机位置最大值 |
| 13 | +value_per_angle = 4096 / 270 |
| 14 | +SERVO_ANGLE = [0, 0] # 舵机旋转的角度范围 |
| 15 | +for i, _ in enumerate(SERVO_ANGLE): |
| 16 | + SERVO_ANGLE[i] = (SERVO_MAX_POSITION[i] - SERVO_MIN_POSITION[i]) / value_per_angle |
| 17 | +test_get_position_enable = False # 测试读取舵机的位置, 时能后舵机关闭扭矩, 并且持续读出舵机的位置 |
| 18 | +test_set_pitch_position_enable = False # 测试设置pitch方向的位置 |
| 19 | +test_set_roll_position_enable = False # 测试设置roll方向的位置 |
| 20 | + |
| 21 | +# OS04D10 fov h=90, v=81 |
| 22 | +# SC850SL fov h=86.7, v=48.8 |
| 23 | +CAMERA_FOV_H = 86.7 # 水平视场角, 查询镜头手册获取。 |
| 24 | +CAMERA_FOV_V = 48.8 # 垂直视场角, 查询镜头手册获取。 |
| 25 | + |
| 26 | +# blobs_thresholds = [[0, 100, -31, -10, 83, 103]] |
| 27 | +# blobs_thresholds = [[0, 100, -31, 12, 77, 103]] # 小黄鸭 os04d10 |
| 28 | +blobs_thresholds = [[0, 100, -13, 12, 50, 75]] # 小黄鸭 sc850sl |
| 29 | +# blobs_thresholds = [[25, 80, -39, -19, 21, 41]] # 绿色 |
| 30 | + |
| 31 | +blobs_area_threshold = 500 # blob最小面积 |
| 32 | +blobs_pixels_threshold = 500 # blob最小像素点 |
| 33 | + |
| 34 | +pitch_pid = [0.45, 0.00001, 0.000, 0] # [P I D I_max] |
| 35 | +roll_pid = [0.6, 0.01, 0.000, 0] # [P I D I_max] |
| 36 | + |
| 37 | +manual_wb_enable = True # 使能手动白平衡,保证找色块时颜色不会随色温变化 |
| 38 | +wb_gain_list = [0.0682, 0, 0, 0.04897] # 手动白平衡增益参数 |
| 39 | + |
| 40 | +pitch_reverse = True # reverse out value direction |
| 41 | +roll_reverse = True # reverse out value direction |
| 42 | + |
| 43 | +class RunningAverage: |
| 44 | + def __init__(self, n=5): |
| 45 | + self.n = n |
| 46 | + self.buf = deque(maxlen=n) |
| 47 | + def add(self, v): |
| 48 | + self.buf.append(v) |
| 49 | + def value(self): |
| 50 | + return sum(self.buf)/len(self.buf) if self.buf else 0.0 |
| 51 | + |
| 52 | +class Status: |
| 53 | + GET_RETANGLE = 0, |
| 54 | + DETECT = 1, |
| 55 | + |
| 56 | +def rgb_list_to_lab_list(rgb_list): |
| 57 | + """ |
| 58 | + 将RGB列表转换为Lab列表 |
| 59 | + rgb_list: [[r, g, b], [r, g, b], ...],值范围0-255 |
| 60 | + 返回: [[L, a, b], [L, a, b], ...] |
| 61 | + """ |
| 62 | + # 转换为numpy数组 |
| 63 | + rgb_array = np.array(rgb_list, dtype=np.uint8).reshape(-1, 1, 3) |
| 64 | + |
| 65 | + # 转换为float并归一化 |
| 66 | + rgb_float = rgb_array.astype(np.float32) / 255.0 |
| 67 | + |
| 68 | + # 转换到Lab颜色空间 |
| 69 | + lab_array = cv2.cvtColor(rgb_float, cv2.COLOR_RGB2LAB) |
| 70 | + |
| 71 | + # 转换回列表格式 |
| 72 | + lab_list = lab_array.reshape(-1, 3).astype(int).tolist() |
| 73 | + |
| 74 | + return lab_list |
| 75 | + |
| 76 | +class App: |
| 77 | + def __init__(self, gimbal: servos.Gimbal): |
| 78 | + self.cam = camera.Camera(640, 360) |
| 79 | + self.disp = display.Display() |
| 80 | + self.ts = touchscreen.TouchScreen() |
| 81 | + self.img_exit = image.load("./assets/exit.jpg").resize(40, 40) |
| 82 | + self.img_exit_touch = image.load("./assets/exit_touch.jpg").resize(40, 40) |
| 83 | + self.exit_box = [0, 0, self.img_exit.width(), self.img_exit.height()] |
| 84 | + self.need_exit = False |
| 85 | + |
| 86 | + if manual_wb_enable: |
| 87 | + self.cam.awb_mode(camera.AwbMode.Manual) |
| 88 | + self.cam.set_wb_gain(wb_gain_list) |
| 89 | + self.gimbal = gimbal |
| 90 | + self.smooth_pitch = RunningAverage() |
| 91 | + self.smooth_roll = RunningAverage() |
| 92 | + self.status = Status.GET_RETANGLE |
| 93 | + |
| 94 | + def check_touch_box(self, t, box, oft = 0): |
| 95 | + """This method is used for exiting and you normally do not need to modify or call it. |
| 96 | + You usually don't need to modify it. |
| 97 | + """ |
| 98 | + if t[2] and t[0] + oft > box[0] and t[0] < box[0] + box[2] + oft and t[1] + oft > box[1] and t[1] < box[1] + box[3] + oft: |
| 99 | + return True |
| 100 | + else: |
| 101 | + return False |
| 102 | + |
| 103 | + def run(self): |
| 104 | + pressing = False |
| 105 | + target = nn.Object() |
| 106 | + btn_str = "Select" |
| 107 | + font_size = image.string_size(btn_str) |
| 108 | + |
| 109 | + while not app.need_exit(): |
| 110 | + if self.status == Status.GET_RETANGLE: |
| 111 | + img = self.cam.read() |
| 112 | + touch_status = self.ts.read() |
| 113 | + if touch_status[2]: # Finger press detected |
| 114 | + if not pressing: |
| 115 | + target.x = touch_status[0] |
| 116 | + target.y = touch_status[1] |
| 117 | + print("Start select") |
| 118 | + pressing = True |
| 119 | + else: |
| 120 | + if pressing: # Finger released, finalize selection |
| 121 | + target.w = touch_status[0] - target.x |
| 122 | + target.h = touch_status[1] - target.y |
| 123 | + if target.w > 0 and target.h > 0: |
| 124 | + print(f"Init with rectangle x: {target.x}, y: {target.y}, w: {target.w}, h: {target.h}") |
| 125 | + rgb_list = img.get_pixel(target.x + target.w // 2, target.y + target.h // 2, True) |
| 126 | + print('rgb list', rgb_list) |
| 127 | + lab_list = rgb_list_to_lab_list(rgb_list)[0] |
| 128 | + print('lab list', lab_list) |
| 129 | + blobs_thresholds = [[lab_list[0] - 15, lab_list[0] + 15, lab_list[1] - 15, lab_list[1] + 15, lab_list[2] - 15, lab_list[2] + 15]] |
| 130 | + print('blobs_thresholds', blobs_thresholds) |
| 131 | + self.status = Status.DETECT |
| 132 | + else: |
| 133 | + print(f"Rectangle invalid, x: {target.x}, y: {target.y}, w: {target.w}, h: {target.h}") |
| 134 | + pressing = False |
| 135 | + if pressing: |
| 136 | + img.draw_string(2, img.height() - font_size[1] * 2, "Select and release to complete", image.Color.from_rgb(255, 0, 0), 1.5) |
| 137 | + img.draw_rect(target.x, target.y, touch_status[0] - target.x, touch_status[1] - target.y, image.Color.from_rgb(255, 0, 0), 3) |
| 138 | + else: |
| 139 | + img.draw_string(2, img.height() - font_size[1] * 2, "Select target on screen", image.Color.from_rgb(255, 0, 0), 1.5) |
| 140 | + self.disp.show(img) |
| 141 | + elif self.status == Status.DETECT: |
| 142 | + pitch_error = 0 |
| 143 | + roll_error = 0 |
| 144 | + img = self.cam.read() # Reads an image frame. |
| 145 | + touch_status = self.ts.read() |
| 146 | + if touch_status[2]: |
| 147 | + self.status = Status.GET_RETANGLE |
| 148 | + img_w = img.width() |
| 149 | + img_h = img.height() |
| 150 | + obj_x = -1 |
| 151 | + obj_y = -1 |
| 152 | + blobs = img.find_blobs(blobs_thresholds, area_threshold = blobs_area_threshold, pixels_threshold = blobs_pixels_threshold) |
| 153 | + if len(blobs) > 0: |
| 154 | + b = blobs[0] |
| 155 | + corners = b.mini_corners() |
| 156 | + obj_x = b.cx() |
| 157 | + obj_y = b.cy() |
| 158 | + |
| 159 | + print(b.cx(), b.cy()) |
| 160 | + for i in range(4): |
| 161 | + img.draw_line(corners[i][0], corners[i][1], corners[(i + 1) % 4][0], corners[(i + 1) % 4][1], image.COLOR_RED) |
| 162 | + |
| 163 | + if obj_x != -1 and obj_y != -1: |
| 164 | + center_x = img_w / 2 |
| 165 | + center_y = img_h / 2 |
| 166 | + angle_x = (obj_x - center_x) / img_w * CAMERA_FOV_H |
| 167 | + angle_y = (obj_y - center_y) / img_h * CAMERA_FOV_V |
| 168 | + print(f'angle_x:{angle_x:.2f} fov_h:{CAMERA_FOV_H:.2f}', f'angle_y:{angle_y:.2f} fov_v:{CAMERA_FOV_V:.2f}') |
| 169 | + |
| 170 | + # # 偏移的角度映射到舵机偏移的百分比 |
| 171 | + # pitch_error = (angle_y + SERVO_ANGLE[0] / 2) / SERVO_ANGLE[0] * 100 |
| 172 | + # roll_error = (angle_x + SERVO_ANGLE[1] / 2) / SERVO_ANGLE[1] * 100 |
| 173 | + |
| 174 | + # 偏移的角度转舵机旋转值 |
| 175 | + pitch_error = angle_y * value_per_angle |
| 176 | + roll_error = angle_x * value_per_angle |
| 177 | + |
| 178 | + print(f'pitch_error:{pitch_error:.2f} pitch_angle_range:{SERVO_ANGLE[0]:.2f} roll_error:{roll_error:.2f} roll_angle_range:{SERVO_ANGLE[1]:.2f}') |
| 179 | + gimbal.run(pitch_error, roll_error, pitch_reverse = pitch_reverse, roll_reverse=roll_reverse) |
| 180 | + |
| 181 | + if self.need_exit: |
| 182 | + app.set_exit_flag(True) |
| 183 | + break |
| 184 | + |
| 185 | + t = self.ts.read() |
| 186 | + img = img.resize(self.disp.width(), self.disp.height(), image.Fit.FIT_CONTAIN) |
| 187 | + if self.check_touch_box(t, self.exit_box, 20): |
| 188 | + img.draw_image(self.exit_box[0], self.exit_box[1], self.img_exit_touch) |
| 189 | + self.need_exit = True |
| 190 | + else: |
| 191 | + img.draw_image(self.exit_box[0], self.exit_box[1], self.img_exit) |
| 192 | + |
| 193 | + self.disp.show(img) |
| 194 | +if __name__ == '__main__': |
| 195 | + s = servos.Servos(SERVO_ID, SERVO_PORT_NAME, SERVO_BAUDRATE, SERVO_MIN_POSITION, SERVO_MAX_POSITION) |
| 196 | + pid_pitch = servos.PID(p=pitch_pid[0], i=pitch_pid[1], d=pitch_pid[2], imax=pitch_pid[3]) |
| 197 | + pid_roll = servos.PID(p=roll_pid[0], i=roll_pid[1], d=roll_pid[2], imax=roll_pid[3]) |
| 198 | + gimbal = servos.Gimbal(s, pid_pitch, pid_roll) |
| 199 | + |
| 200 | + if test_get_position_enable: |
| 201 | + s.test_position() |
| 202 | + elif test_set_pitch_position_enable: |
| 203 | + s.uservo.set_position(1, 2048) |
| 204 | + while True: |
| 205 | + pos = s.uservo.get_position(1) |
| 206 | + if pos: |
| 207 | + if pos > 2000 and pos < 3100: |
| 208 | + break |
| 209 | + time.sleep_ms(1) |
| 210 | + |
| 211 | + t = time.ticks_us() |
| 212 | + s.uservo.set_position(1, 2548) |
| 213 | + while True: |
| 214 | + pos = s.uservo.get_position(1) |
| 215 | + if pos: |
| 216 | + if pos > 2548-50 and pos < 2548+50: |
| 217 | + break |
| 218 | + time.sleep_ms(1) |
| 219 | + print('cost', time.ticks_us() - t) |
| 220 | + elif test_set_roll_position_enable: |
| 221 | + id = 2 |
| 222 | + angle = 120 |
| 223 | + start_position = 1024 |
| 224 | + dst_position = start_position + angle / 270 * 4096 |
| 225 | + s.uservo.set_position(id, start_position) |
| 226 | + |
| 227 | + while True: |
| 228 | + pos = s.uservo.get_position(id) |
| 229 | + if pos: |
| 230 | + if pos > start_position - 25 and pos < start_position + 25: |
| 231 | + break |
| 232 | + print('wait start position, dst position', dst_position,'curr position', pos) |
| 233 | + time.sleep_ms(100) |
| 234 | + |
| 235 | + t = time.ticks_us() |
| 236 | + s.uservo.set_position(id, dst_position) |
| 237 | + print('run set_position cost', time.ticks_us() - t) |
| 238 | + |
| 239 | + t = time.ticks_us() |
| 240 | + while True: |
| 241 | + pos = s.uservo.get_position(id) |
| 242 | + if pos: |
| 243 | + if pos > dst_position-25 and pos < dst_position+100: |
| 244 | + break |
| 245 | + print('wait dst position, dst position', dst_position,'curr position', pos) |
| 246 | + # time.sleep_ms(1) |
| 247 | + print('wait cost', time.ticks_us() - t) |
| 248 | + |
| 249 | + while not app.need_exit(): |
| 250 | + pos = s.uservo.get_position(id) |
| 251 | + print(pos) |
| 252 | + time.sleep_ms(1000) |
| 253 | + else: |
| 254 | + a = App(gimbal) |
| 255 | + a.run() |
0 commit comments