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.
246 lines
9.1 KiB
246 lines
9.1 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__))
|
|
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()
|
|
|
|
|