You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
			
				
					247 lines
				
				9.1 KiB
			
		
		
			
		
	
	
					247 lines
				
				9.1 KiB
			| 
								 
											7 years ago
										 
									 | 
							
								#!/usr/bin/env python
							 | 
						||
| 
								 | 
							
								import os
							 | 
						||
| 
								 | 
							
								import zmq
							 | 
						||
| 
								 | 
							
								import cv2
							 | 
						||
| 
								 | 
							
								import copy
							 | 
						||
| 
								 | 
							
								import math
							 | 
						||
| 
								 | 
							
								import json
							 | 
						||
| 
								 | 
							
								import numpy as np
							 | 
						||
| 
								 | 
							
								import selfdrive.messaging as messaging
							 | 
						||
| 
								 | 
							
								from selfdrive.swaglog import cloudlog
							 | 
						||
| 
								 | 
							
								from selfdrive.services import service_list
							 | 
						||
| 
								 | 
							
								from common.params import Params
							 | 
						||
| 
								 | 
							
								from common.ffi_wrapper import ffi_wrap
							 | 
						||
| 
								 | 
							
								import common.transformations.orientation as orient
							 | 
						||
| 
								 | 
							
								from common.transformations.model import model_height, get_camera_frame_from_model_frame
							 | 
						||
| 
								 | 
							
								from common.transformations.camera import view_frame_from_device_frame, get_view_frame_from_road_frame, \
							 | 
						||
| 
								 | 
							
								                                          eon_intrinsics, get_calib_from_vp, normalize, denormalize, H, W
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								MIN_SPEED_FILTER = 7 # m/s  (~15.5mph)
							 | 
						||
| 
								 | 
							
								MAX_YAW_RATE_FILTER = math.radians(3) # per second
							 | 
						||
| 
								 | 
							
								FRAMES_NEEDED = 120  # allow to update VP every so many frames
							 | 
						||
| 
								 | 
							
								VP_CYCLES_NEEDED = 2
							 | 
						||
| 
								 | 
							
								CALIBRATION_CYCLES_NEEDED = FRAMES_NEEDED * VP_CYCLES_NEEDED
							 | 
						||
| 
								 | 
							
								DT = 0.1      # nominal time step of 10Hz (orbd_live runs slower on pc)
							 | 
						||
| 
								 | 
							
								VP_RATE_LIM = 2. * DT    # 2 px/s
							 | 
						||
| 
								 | 
							
								VP_INIT = np.array([W/2., H/2.])
							 | 
						||
| 
								 | 
							
								EXTERNAL_PATH = os.path.dirname(os.path.abspath(__file__))
							 | 
						||
| 
								 | 
							
								VP_VALIDITY_CORNERS = np.array([[-200., -200.], [200., 200.]])  + VP_INIT
							 | 
						||
| 
								 | 
							
								GRID_WEIGHT_INIT = 2e6
							 | 
						||
| 
								 | 
							
								MAX_LINES = 500    # max lines to avoid over computation
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								DEBUG = os.getenv("DEBUG") is not None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Wrap c code for slow grid incrementation
							 | 
						||
| 
								 | 
							
								c_header = "\nvoid increment_grid(double *grid, double *lines, long long n);"
							 | 
						||
| 
								 | 
							
								c_code = "#define H %d\n" % H
							 | 
						||
| 
								 | 
							
								c_code += "#define W %d\n" % W
							 | 
						||
| 
								 | 
							
								c_code += "\n" + open(os.path.join(EXTERNAL_PATH, "get_vp.c")).read()
							 | 
						||
| 
								 | 
							
								ffi, lib = ffi_wrap('get_vp', c_code, c_header)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class Calibration:
							 | 
						||
| 
								 | 
							
								  UNCALIBRATED = 0
							 | 
						||
| 
								 | 
							
								  CALIBRATED = 1
							 | 
						||
| 
								 | 
							
								  INVALID = 2
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def increment_grid_c(grid, lines, n):
							 | 
						||
| 
								 | 
							
								  lib.increment_grid(ffi.cast("double *", grid.ctypes.data),
							 | 
						||
| 
								 | 
							
								                ffi.cast("double *", lines.ctypes.data),
							 | 
						||
| 
								 | 
							
								                ffi.cast("long long", n))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_lines(p):
							 | 
						||
| 
								 | 
							
								  A = (p[:,0,1] - p[:,1,1])
							 | 
						||
| 
								 | 
							
								  B = (p[:,1,0] - p[:,0,0])
							 | 
						||
| 
								 | 
							
								  C = (p[:,0,0]*p[:,1,1] - p[:,1,0]*p[:,0,1])
							 | 
						||
| 
								 | 
							
								  return np.column_stack((A, B, -C))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def correct_pts(pts, rot_speeds, dt):
							 | 
						||
| 
								 | 
							
								  pts = np.hstack((pts, np.ones((pts.shape[0],1))))
							 | 
						||
| 
								 | 
							
								  cam_rot = dt * view_frame_from_device_frame.dot(rot_speeds)
							 | 
						||
| 
								 | 
							
								  rot = orient.rot_matrix(*cam_rot.T).T
							 | 
						||
| 
								 | 
							
								  pts_corrected = rot.dot(pts.T).T
							 | 
						||
| 
								 | 
							
								  pts_corrected[:,0] /= pts_corrected[:,2]
							 | 
						||
| 
								 | 
							
								  pts_corrected[:,1] /= pts_corrected[:,2]
							 | 
						||
| 
								 | 
							
								  return pts_corrected[:,:2]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def gaussian_kernel(sizex, sizey, stdx, stdy, dx, dy):
							 | 
						||
| 
								 | 
							
								  y, x = np.mgrid[-sizey:sizey+1, -sizex:sizex+1]
							 | 
						||
| 
								 | 
							
								  g = np.exp(-((x - dx)**2 / (2. * stdx**2) + (y - dy)**2 / (2. * stdy**2)))
							 | 
						||
| 
								 | 
							
								  return g / g.sum()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def blur_image(img, kernel):
							 | 
						||
| 
								 | 
							
								  return cv2.filter2D(img.astype(np.uint16), -1, kernel)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def is_calibration_valid(vp):
							 | 
						||
| 
								 | 
							
								  return vp[0] > VP_VALIDITY_CORNERS[0,0] and vp[0] < VP_VALIDITY_CORNERS[1,0] and \
							 | 
						||
| 
								 | 
							
								         vp[1] > VP_VALIDITY_CORNERS[0,1] and vp[1] < VP_VALIDITY_CORNERS[1,1]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class Calibrator(object):
							 | 
						||
| 
								 | 
							
								  def __init__(self, param_put=False):
							 | 
						||
| 
								 | 
							
								    self.param_put = param_put
							 | 
						||
| 
								 | 
							
								    self.speed = 0
							 | 
						||
| 
								 | 
							
								    self.vp_cycles = 0
							 | 
						||
| 
								 | 
							
								    self.angle_offset = 0.
							 | 
						||
| 
								 | 
							
								    self.yaw_rate = 0.
							 | 
						||
| 
								 | 
							
								    self.l100_last_updated = 0
							 | 
						||
| 
								 | 
							
								    self.prev_orbs = None
							 | 
						||
| 
								 | 
							
								    self.kernel = gaussian_kernel(11, 11, 2.35, 2.35, 0, 0)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    self.vp = copy.copy(VP_INIT)
							 | 
						||
| 
								 | 
							
								    self.cal_status = Calibration.UNCALIBRATED
							 | 
						||
| 
								 | 
							
								    self.frame_counter = 0
							 | 
						||
| 
								 | 
							
								    self.params = Params()
							 | 
						||
| 
								 | 
							
								    calibration_params = self.params.get("CalibrationParams")
							 | 
						||
| 
								 | 
							
								    if calibration_params:
							 | 
						||
| 
								 | 
							
								      try:
							 | 
						||
| 
								 | 
							
								        calibration_params = json.loads(calibration_params)
							 | 
						||
| 
								 | 
							
								        self.vp = np.array(calibration_params["vanishing_point"])
							 | 
						||
| 
								 | 
							
								        self.cal_status = Calibration.CALIBRATED if is_calibration_valid(self.vp) else Calibration.INVALID
							 | 
						||
| 
								 | 
							
								        self.vp_cycles = VP_CYCLES_NEEDED
							 | 
						||
| 
								 | 
							
								        self.frame_counter = CALIBRATION_CYCLES_NEEDED
							 | 
						||
| 
								 | 
							
								      except Exception:
							 | 
						||
| 
								 | 
							
								        cloudlog.exception("CalibrationParams file found but error encountered")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    self.vp_unfilt = self.vp
							 | 
						||
| 
								 | 
							
								    self.orb_last_updated = 0.
							 | 
						||
| 
								 | 
							
								    self.reset_grid()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  def reset_grid(self):
							 | 
						||
| 
								 | 
							
								    if self.cal_status == Calibration.UNCALIBRATED:
							 | 
						||
| 
								 | 
							
								      # empty grid
							 | 
						||
| 
								 | 
							
								      self.grid = np.zeros((H+1, W+1), dtype=np.float)
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								      # gaussian grid, centered at vp
							 | 
						||
| 
								 | 
							
								      self.grid = gaussian_kernel(W/2., H/2., 16, 16, self.vp[0] - W/2., self.vp[1] - H/2.) * GRID_WEIGHT_INIT
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  def rescale_grid(self):
							 | 
						||
| 
								 | 
							
								    self.grid *= 0.9
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  def update(self, uvs, yaw_rate, speed):
							 | 
						||
| 
								 | 
							
								    if len(uvs) < 10 or \
							 | 
						||
| 
								 | 
							
								       abs(yaw_rate) > MAX_YAW_RATE_FILTER or \
							 | 
						||
| 
								 | 
							
								       speed < MIN_SPEED_FILTER:
							 | 
						||
| 
								 | 
							
								      return
							 | 
						||
| 
								 | 
							
								    rot_speeds = np.array([0.,0.,-yaw_rate])
							 | 
						||
| 
								 | 
							
								    uvs[:,1,:] = denormalize(correct_pts(normalize(uvs[:,1,:]), rot_speeds, self.dt))
							 | 
						||
| 
								 | 
							
								    good_tracks = np.linalg.norm(uvs[:,1,:] - uvs[:,0,:], axis=1) > 10
							 | 
						||
| 
								 | 
							
								    uvs = uvs[good_tracks]
							 | 
						||
| 
								 | 
							
								    if uvs.shape[0] > MAX_LINES:
							 | 
						||
| 
								 | 
							
								      uvs = uvs[np.random.choice(uvs.shape[0], MAX_LINES, replace=False), :]
							 | 
						||
| 
								 | 
							
								    lines = get_lines(uvs)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    increment_grid_c(self.grid, lines, len(lines))
							 | 
						||
| 
								 | 
							
								    self.frame_counter += 1
							 | 
						||
| 
								 | 
							
								    if (self.frame_counter % FRAMES_NEEDED) == 0:
							 | 
						||
| 
								 | 
							
								      grid = blur_image(self.grid, self.kernel)
							 | 
						||
| 
								 | 
							
								      argmax_vp = np.unravel_index(np.argmax(grid), grid.shape)[::-1]
							 | 
						||
| 
								 | 
							
								      self.rescale_grid()
							 | 
						||
| 
								 | 
							
								      self.vp_unfilt = np.array(argmax_vp)
							 | 
						||
| 
								 | 
							
								      self.vp_cycles += 1
							 | 
						||
| 
								 | 
							
								      if (self.vp_cycles - VP_CYCLES_NEEDED) % 10 == 0:    # update file every 10
							 | 
						||
| 
								 | 
							
								        # skip rate_lim before writing the file to avoid writing a lagged vp
							 | 
						||
| 
								 | 
							
								        if self.cal_status != Calibration.CALIBRATED:
							 | 
						||
| 
								 | 
							
								          self.vp = self.vp_unfilt
							 | 
						||
| 
								 | 
							
								        if self.param_put:
							 | 
						||
| 
								 | 
							
								          cal_params = {"vanishing_point": list(self.vp), "angle_offset2": self.angle_offset}
							 | 
						||
| 
								 | 
							
								          self.params.put("CalibrationParams", json.dumps(cal_params))
							 | 
						||
| 
								 | 
							
								      return self.vp_unfilt
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  def parse_orb_features(self, log):
							 | 
						||
| 
								 | 
							
								    matches = np.array(log.orbFeatures.matches)
							 | 
						||
| 
								 | 
							
								    n = len(log.orbFeatures.matches)
							 | 
						||
| 
								 | 
							
								    t = float(log.orbFeatures.timestampLastEof)*1e-9
							 | 
						||
| 
								 | 
							
								    if t == 0 or n == 0:
							 | 
						||
| 
								 | 
							
								      return t, np.zeros((0,2,2))
							 | 
						||
| 
								 | 
							
								    orbs = denormalize(np.column_stack((log.orbFeatures.xs,
							 | 
						||
| 
								 | 
							
								                                        log.orbFeatures.ys)))
							 | 
						||
| 
								 | 
							
								    if self.prev_orbs is not None:
							 | 
						||
| 
								 | 
							
								      valid_matches = (matches > -1) & (matches < len(self.prev_orbs))
							 | 
						||
| 
								 | 
							
								      tracks = np.hstack((orbs[valid_matches], self.prev_orbs[matches[valid_matches]])).reshape((-1,2,2))
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								      tracks = np.zeros((0,2,2))
							 | 
						||
| 
								 | 
							
								    self.prev_orbs = orbs
							 | 
						||
| 
								 | 
							
								    return t, tracks
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  def update_vp(self):
							 | 
						||
| 
								 | 
							
								    # rate limit to not move VP too fast
							 | 
						||
| 
								 | 
							
								    self.vp = np.clip(self.vp_unfilt, self.vp - VP_RATE_LIM, self.vp + VP_RATE_LIM)
							 | 
						||
| 
								 | 
							
								    if self.vp_cycles < VP_CYCLES_NEEDED:
							 | 
						||
| 
								 | 
							
								      self.cal_status = Calibration.UNCALIBRATED
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								      self.cal_status = Calibration.CALIBRATED if is_calibration_valid(self.vp) else Calibration.INVALID
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  def handle_orb_features(self, log):
							 | 
						||
| 
								 | 
							
								    self.update_vp()
							 | 
						||
| 
								 | 
							
								    self.time_orb, frame_raw = self.parse_orb_features(log)
							 | 
						||
| 
								 | 
							
								    self.dt = min(self.time_orb - self.orb_last_updated, 1.)
							 | 
						||
| 
								 | 
							
								    self.orb_last_updated = self.time_orb
							 | 
						||
| 
								 | 
							
								    if self.time_orb - self.l100_last_updated < 1:
							 | 
						||
| 
								 | 
							
								      return self.update(frame_raw, self.yaw_rate, self.speed)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  def handle_live100(self, log):
							 | 
						||
| 
								 | 
							
								    self.l100_last_updated = self.time_orb
							 | 
						||
| 
								 | 
							
								    self.speed = log.live100.vEgo
							 | 
						||
| 
								 | 
							
								    self.angle_offset = log.live100.angleOffset
							 | 
						||
| 
								 | 
							
								    self.yaw_rate = log.live100.curvature * self.speed
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  def handle_debug(self):
							 | 
						||
| 
								 | 
							
								    grid_blurred = blur_image(self.grid, self.kernel)
							 | 
						||
| 
								 | 
							
								    grid_grey = np.clip(grid_blurred/(0.1 + np.max(grid_blurred))*255, 0, 255)
							 | 
						||
| 
								 | 
							
								    grid_color = np.repeat(grid_grey[:,:,np.newaxis], 3, axis=2)
							 | 
						||
| 
								 | 
							
								    grid_color[:,:,0] = 0
							 | 
						||
| 
								 | 
							
								    cv2.circle(grid_color, tuple(self.vp.astype(np.int64)), 2, (255, 255, 0), -1)
							 | 
						||
| 
								 | 
							
								    cv2.imshow("debug", grid_color.astype(np.uint8))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    cv2.waitKey(1)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  def send_data(self, livecalibration):
							 | 
						||
| 
								 | 
							
								    calib = get_calib_from_vp(self.vp)
							 | 
						||
| 
								 | 
							
								    extrinsic_matrix = get_view_frame_from_road_frame(0, calib[1], calib[2], model_height)
							 | 
						||
| 
								 | 
							
								    ke = eon_intrinsics.dot(extrinsic_matrix)
							 | 
						||
| 
								 | 
							
								    warp_matrix = get_camera_frame_from_model_frame(ke, model_height)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    cal_send = messaging.new_message()
							 | 
						||
| 
								 | 
							
								    cal_send.init('liveCalibration')
							 | 
						||
| 
								 | 
							
								    cal_send.liveCalibration.calStatus = self.cal_status
							 | 
						||
| 
								 | 
							
								    cal_send.liveCalibration.calPerc = min(self.frame_counter * 100 / CALIBRATION_CYCLES_NEEDED, 100)
							 | 
						||
| 
								 | 
							
								    cal_send.liveCalibration.warpMatrix2 = map(float, warp_matrix.flatten())
							 | 
						||
| 
								 | 
							
								    cal_send.liveCalibration.extrinsicMatrix = map(float, extrinsic_matrix.flatten())
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    livecalibration.send(cal_send.to_bytes())
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def calibrationd_thread(gctx=None, addr="127.0.0.1"):
							 | 
						||
| 
								 | 
							
								  context = zmq.Context()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  live100 = messaging.sub_sock(context, service_list['live100'].port, addr=addr, conflate=True)
							 | 
						||
| 
								 | 
							
								  orbfeatures = messaging.sub_sock(context, service_list['orbFeatures'].port, addr=addr, conflate=True)
							 | 
						||
| 
								 | 
							
								  livecalibration = messaging.pub_sock(context, service_list['liveCalibration'].port)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  calibrator = Calibrator(param_put = True)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  # buffer with all the messages that still need to be input into the kalman
							 | 
						||
| 
								 | 
							
								  while 1:
							 | 
						||
| 
								 | 
							
								    of = messaging.recv_one(orbfeatures)
							 | 
						||
| 
								 | 
							
								    l100 = messaging.recv_one_or_none(live100)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    new_vp = calibrator.handle_orb_features(of)
							 | 
						||
| 
								 | 
							
								    if DEBUG and new_vp is not None:
							 | 
						||
| 
								 | 
							
								      print 'got new vp', new_vp
							 | 
						||
| 
								 | 
							
								    if l100 is not None:
							 | 
						||
| 
								 | 
							
								      calibrator.handle_live100(l100)
							 | 
						||
| 
								 | 
							
								    if DEBUG:
							 | 
						||
| 
								 | 
							
								      calibrator.handle_debug()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    calibrator.send_data(livecalibration)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def main(gctx=None, addr="127.0.0.1"):
							 | 
						||
| 
								 | 
							
								  calibrationd_thread(gctx, addr)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								if __name__ == "__main__":
							 | 
						||
| 
								 | 
							
								  main()
							 | 
						||
| 
								 | 
							
								
							 |