openpilot is an open source driver assistance system. openpilot performs the functions of Automated Lane Centering and Adaptive Cruise Control for over 200 supported car makes and models.
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.
 
 
 
 
 
 

261 lines
9.7 KiB

#!/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__))
# big model is 864x288
VP_VALIDITY_CORNERS = np.array([[-150., -200.], [150., 200.]]) + VP_INIT
GRID_WEIGHT_INIT = 2e6
MAX_LINES = 500 # max lines to avoid over computation
HOOD_HEIGHT = H*3/4 # the part of image usually free from the car's hood
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 gaussian_kernel_1d(kernel):
#creates separable gaussian filter
u,s,v = np.linalg.svd(kernel)
x = u[:,0]*np.sqrt(s[0])
y = np.sqrt(s[0])*v[0,:]
return x, y
def blur_image(img, kernel_x, kernel_y):
return cv2.sepFilter2D(img.astype(np.uint16), -1, kernel_x, kernel_y)
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.kernel_x, self.kernel_y = gaussian_kernel_1d(self.kernel)
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))
# exclude tracks where:
# - pixel movement was less than 10 pixels
# - tracks are in the "hood region"
good_tracks = np.all([np.linalg.norm(uvs[:,1,:] - uvs[:,0,:], axis=1) > 10,
uvs[:,0,1] < HOOD_HEIGHT,
uvs[:,1,1] < HOOD_HEIGHT], axis = 0)
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_x, self.kernel_y)
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_x, self.kernel_y)
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()