tools/Rerun: Add video logging features (#32810)
	
		
	
				
					
				
			* working * multiprocessing * fix that * print services * all services + fix * less verbose * start readme * segment range * cleanup * update readme + fix bug in 'all' * cleanup + update readme * update readme * cleanup * cleanup * rm frame_iter * cleanup * staticmethod * proc kill * split files * fix range with hevc vids * update reamde + add prompt * readme * readme * readmex-params-test2
							parent
							
								
									1da71f5c3c
								
							
						
					
					
						commit
						5e0aff92ae
					
				
				 3 changed files with 299 additions and 64 deletions
			
			
		@ -0,0 +1,57 @@ | 
				
			||||
# Rerun | 
				
			||||
Rerun is a tool to quickly visualize time series data. It supports all openpilot logs , both the `logMessages` and video logs. | 
				
			||||
 | 
				
			||||
[Instructions](https://rerun.io/docs/reference/viewer/overview) for navigation within the Rerun Viewer. | 
				
			||||
 | 
				
			||||
## Usage | 
				
			||||
``` | 
				
			||||
usage: run.py [-h] [--demo] [--qcam] [--fcam] [--ecam] [--dcam] [--print_services] [--services [SERVICES ...]] [route_or_segment_name] | 
				
			||||
 | 
				
			||||
A helper to run rerun on openpilot routes | 
				
			||||
 | 
				
			||||
options: | 
				
			||||
  -h, --help                  show this help message and exit | 
				
			||||
  --demo                      Use the demo route instead of providing one (default: False) | 
				
			||||
  --qcam                      Log decimated driving camera (default: False) | 
				
			||||
  --fcam                      Log driving camera (default: False) | 
				
			||||
  --ecam                      Log wide camera (default: False) | 
				
			||||
  --dcam                      Log driver monitoring camera (default: False) | 
				
			||||
  --print_services            List out openpilot services (default: False) | 
				
			||||
  --services [SERVICES ...]   Specify openpilot services that will be logged. No service will be logged if not specified. | 
				
			||||
                              To log all services include 'all' as one of your services (default: []) | 
				
			||||
  --route [ROUTE]             The route or segment name to plot (default: None) | 
				
			||||
``` | 
				
			||||
 | 
				
			||||
Examples using route name to observe accelerometer and qcamera: | 
				
			||||
 | 
				
			||||
`./run.py --services accelerometer --qcam --route "a2a0ccea32023010/2023-07-27--13-01-19"` | 
				
			||||
 | 
				
			||||
Examples using segment range (more on [SegmentRange](https://github.com/commaai/openpilot/tree/master/tools/lib)): | 
				
			||||
 | 
				
			||||
`./run.py --qcam --route "a2a0ccea32023010/2023-07-27--13-01-19/2:4"` | 
				
			||||
 | 
				
			||||
## Cautions: | 
				
			||||
- You can specify `--services all` to visualize all `logMessage`, but it will draw a lot of memory usage and take a long time to log all messages. Rerun isn't ready for logging big number of data. | 
				
			||||
 | 
				
			||||
- Logging hevc videos (`--fcam`, `--ecam`, and `--dcam`)  are expensive, and it's recommended to use `--qcam` for optimized performance. If possible, limiting your route to a few segments using `SegmentRange` will speed up logging and reduce memory usage | 
				
			||||
 | 
				
			||||
This example draws 13GB of memory: | 
				
			||||
 | 
				
			||||
`./run.py --services accelerometer --qcam --route "a2a0ccea32023010/2023-07-27--13-01-19"` | 
				
			||||
 | 
				
			||||
 | 
				
			||||
## Openpilot services | 
				
			||||
To list all openpilot services: | 
				
			||||
 | 
				
			||||
`./run.py --print_services` | 
				
			||||
 | 
				
			||||
Examples including openpilot services: | 
				
			||||
 | 
				
			||||
`./run.py --services accelerometer cameraodometry --route "a2a0ccea32023010/2023-07-27--13-01-19/0/q"` | 
				
			||||
 | 
				
			||||
Examples including all services: | 
				
			||||
 | 
				
			||||
`./run.py --services all --route "a2a0ccea32023010/2023-07-27--13-01-19/0/q"` | 
				
			||||
 | 
				
			||||
## Demo | 
				
			||||
`./run.py --services accelerometer carcontrol caroutput --qcam --demo` | 
				
			||||
@ -0,0 +1,92 @@ | 
				
			||||
import tqdm | 
				
			||||
import subprocess | 
				
			||||
import multiprocessing | 
				
			||||
from enum import StrEnum | 
				
			||||
from functools import partial | 
				
			||||
from collections import namedtuple | 
				
			||||
 | 
				
			||||
from openpilot.tools.lib.framereader import ffprobe | 
				
			||||
 | 
				
			||||
CameraConfig = namedtuple("CameraConfig", ["qcam", "fcam", "ecam", "dcam"]) | 
				
			||||
 | 
				
			||||
class CameraType(StrEnum): | 
				
			||||
  qcam = "qcamera" | 
				
			||||
  fcam = "fcamera" | 
				
			||||
  ecam = "ecamera" | 
				
			||||
  dcam = "dcamera" | 
				
			||||
 | 
				
			||||
 | 
				
			||||
def probe_packet_info(camera_path): | 
				
			||||
  args = ["ffprobe", "-v", "quiet", "-show_packets", "-probesize", "10M", camera_path] | 
				
			||||
  dat = subprocess.check_output(args) | 
				
			||||
  dat = dat.decode().split() | 
				
			||||
  return dat | 
				
			||||
 | 
				
			||||
 | 
				
			||||
class _FrameReader: | 
				
			||||
  def __init__(self, camera_path, segment, h, w, start_time): | 
				
			||||
    self.camera_path = camera_path | 
				
			||||
    self.segment = segment | 
				
			||||
    self.h = h | 
				
			||||
    self.w = w | 
				
			||||
    self.start_time = start_time | 
				
			||||
 | 
				
			||||
    self.ts = self._get_ts() | 
				
			||||
 | 
				
			||||
  def _read_stream_nv12(self): | 
				
			||||
    frame_sz = self.w * self.h * 3 // 2 | 
				
			||||
    proc = subprocess.Popen( | 
				
			||||
             ["ffmpeg", "-v", "quiet", "-i", self.camera_path, "-f", "rawvideo", "-pix_fmt", "nv12", "-"], | 
				
			||||
             stdout=subprocess.PIPE, | 
				
			||||
             stderr=subprocess.DEVNULL | 
				
			||||
           ) | 
				
			||||
    try: | 
				
			||||
      while True: | 
				
			||||
        dat = proc.stdout.read(frame_sz) | 
				
			||||
        if len(dat) == 0: | 
				
			||||
          break | 
				
			||||
        yield dat | 
				
			||||
    finally: | 
				
			||||
      proc.kill() | 
				
			||||
 | 
				
			||||
  def _get_ts(self): | 
				
			||||
    dat = probe_packet_info(self.camera_path) | 
				
			||||
    try: | 
				
			||||
      ret = [float(d.split('=')[1]) for d in dat if d.startswith("pts_time=")] | 
				
			||||
    except ValueError: | 
				
			||||
      # pts_times aren't available. Infer timestamps from duration_times | 
				
			||||
      ret = [d for d in dat if d.startswith("duration_time")] | 
				
			||||
      ret = [float(d.split('=')[1])*(i+1)+(self.segment*60)+self.start_time for i, d in enumerate(ret)] | 
				
			||||
    return ret | 
				
			||||
 | 
				
			||||
  def __iter__(self): | 
				
			||||
    for i, frame in enumerate(self._read_stream_nv12()): | 
				
			||||
      yield self.ts[i], frame | 
				
			||||
 | 
				
			||||
 | 
				
			||||
class CameraReader: | 
				
			||||
  def __init__(self, camera_paths, start_time, seg_idxs): | 
				
			||||
    self.seg_idxs = seg_idxs | 
				
			||||
    self.camera_paths = camera_paths | 
				
			||||
    self.start_time = start_time | 
				
			||||
 | 
				
			||||
    probe = ffprobe(camera_paths[0])["streams"][0] | 
				
			||||
    self.h = probe["height"] | 
				
			||||
    self.w = probe["width"] | 
				
			||||
 | 
				
			||||
    self.__frs = {} | 
				
			||||
 | 
				
			||||
  def _get_fr(self, i): | 
				
			||||
    if i not in self.__frs: | 
				
			||||
      self.__frs[i] = _FrameReader(self.camera_paths[i], segment=i, h=self.h, w=self.w, start_time=self.start_time) | 
				
			||||
    return self.__frs[i] | 
				
			||||
 | 
				
			||||
  def _run_on_segment(self, func, i): | 
				
			||||
    return func(self._get_fr(i)) | 
				
			||||
 | 
				
			||||
  def run_across_segments(self, num_processes, func): | 
				
			||||
    with multiprocessing.Pool(num_processes) as pool: | 
				
			||||
      num_segs = len(self.seg_idxs) | 
				
			||||
      for _ in tqdm.tqdm(pool.imap_unordered(partial(self._run_on_segment, func), self.seg_idxs), total=num_segs): | 
				
			||||
        continue | 
				
			||||
 | 
				
			||||
					Loading…
					
					
				
		Reference in new issue