Compare commits

...

2 commits

Author SHA1 Message Date
Ruben van de Ven
e24e270a42 debug lines as param 2025-10-17 17:05:39 +02:00
Ruben van de Ven
1f07974466 Render debug lines from a map 2025-10-17 16:58:18 +02:00
6 changed files with 182 additions and 52 deletions

View file

@ -36,6 +36,7 @@ dependencies = [
"supervisor>=4.2.5", "supervisor>=4.2.5",
"superfsmon>=1.2.3", "superfsmon>=1.2.3",
"noise>=1.2.2", "noise>=1.2.2",
"svgpathtools>=1.7.1",
] ]
[project.scripts] [project.scripts]

View file

@ -157,12 +157,6 @@ class DistortedCamera(ABC):
with calibration_path.open('r') as fp: with calibration_path.open('r') as fp:
data = json.load(fp) data = json.load(fp)
camera = cls.from_calibdata(data, H, fps) camera = cls.from_calibdata(data, H, fps)
points_file = calibration_path.with_name('irl_points.json')
if points_file.with_name('irl_points.json').exists():
with points_file.open('r') as fp:
debug_points = json.load(fp)
camera.init_debug_data(debug_points)
return camera return camera
@ -177,11 +171,6 @@ class DistortedCamera(ABC):
else: else:
camera = Camera.from_calibdata(calibdata, H, fps) camera = Camera.from_calibdata(calibdata, H, fps)
points_file = calibration_path.with_name('irl_points.json')
if points_file.with_name('irl_points.json').exists():
with points_file.open('r') as fp:
debug_points = json.load(fp)
camera.init_debug_data(debug_points)
return camera return camera
# return cls.from_calibfile(calibration_path, H, fps) # return cls.from_calibfile(calibration_path, H, fps)
@ -193,15 +182,6 @@ class DistortedCamera(ABC):
coords = self.project_points(coords, scale) coords = self.project_points(coords, scale)
return coords return coords
def init_debug_data(self, points: List[List[float, float]]):
self.debug_points = points
self.debug_lines = [
[[11, 6.2], [4.046,6.2] ],
[self.debug_points[9], self.debug_points[2]],
[self.debug_points[4], self.debug_points[3]],
[self.debug_points[6], self.debug_points[7]],
[self.debug_points[7], self.debug_points[5]],
]
class FisheyeCamera(DistortedCamera): class FisheyeCamera(DistortedCamera):

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import datetime import datetime
import json import json
import logging import logging
from pathlib import Path
import time import time
from argparse import ArgumentParser, Namespace from argparse import ArgumentParser, Namespace
from multiprocessing.synchronize import Event as BaseEvent from multiprocessing.synchronize import Event as BaseEvent
@ -20,6 +21,7 @@ from pyglet import shapes
from trap.base import Detection from trap.base import Detection
from trap.counter import CounterListerner from trap.counter import CounterListerner
from trap.frame_emitter import Frame, Track from trap.frame_emitter import Frame, Track
from trap.lines import load_lines_from_svg
from trap.node import Node from trap.node import Node
from trap.preview_renderer import FrameWriter from trap.preview_renderer import FrameWriter
from trap.tools import draw_track_predictions, draw_track_projected, to_point from trap.tools import draw_track_predictions, draw_track_projected, to_point
@ -55,6 +57,9 @@ class CvRenderer(Node):
self.tracks: Dict[str, Track] = {} self.tracks: Dict[str, Track] = {}
self.predictions: Dict[str, Track] = {} self.predictions: Dict[str, Track] = {}
self.scale = 100
self.debug_lines = debug_lines = load_lines_from_svg(self.config.debug_map, self.scale, '') if self.config.debug_map else []
def refresh_labels(self, dt: float): def refresh_labels(self, dt: float):
"""Every frame""" """Every frame"""
@ -178,7 +183,12 @@ class CvRenderer(Node):
first_time = frame.time first_time = frame.time
# img = frame.img # img = frame.img
img = decorate_frame(frame, tracker_frame, prediction_frame, first_time, self.config, self.tracks, self.predictions, self.detections, self.config.render_clusters) # save_file = Path("videos/snap.png")
# if not save_file.exists():
# img = frame.camera.img_to_world(frame.img, 100)
# cv2.imwrite(save_file, img)
img = decorate_frame(frame, tracker_frame, prediction_frame, first_time, self.config, self.tracks, self.predictions, self.detections, self.config.render_clusters, self.debug_lines, self.scale)
logger.debug(f"write frame {frame.time - first_time:.3f}s") logger.debug(f"write frame {frame.time - first_time:.3f}s")
if self.out_writer: if self.out_writer:
@ -259,7 +269,10 @@ class CvRenderer(Node):
""", """,
type=str, type=str,
default=None) default=None)
render_parser.add_argument('--debug-map',
help='specify a map (svg-file) from which to load lines which will be overlayed',
type=str,
default="../DATASETS/hof3/map_hof.svg")
return render_parser return render_parser
# colorset = itertools.product([0,255], repeat=3) # but remove white # colorset = itertools.product([0,255], repeat=3) # but remove white
@ -291,8 +304,8 @@ def get_animation_position(track: Track, current_frame: Frame):
def decorate_frame(frame: Frame, tracker_frame: Frame, prediction_frame: Frame, first_time: float, config: Namespace, tracks: Dict[str, Track], predictions: Dict[str, Track], detections: Optional[List[Detection]], as_clusters = True) -> np.array: def decorate_frame(frame: Frame, tracker_frame: Frame, prediction_frame: Frame, first_time: float, config: Namespace, tracks: Dict[str, Track], predictions: Dict[str, Track], detections: Optional[List[Detection]], as_clusters = True, debug_lines = [], scale: float = 100) -> np.array:
scale = 100
# TODO: replace opencv with QPainter to support alpha? https://doc.qt.io/qtforpython-5/PySide2/QtGui/QPainter.html#PySide2.QtGui.PySide2.QtGui.QPainter.drawImage # TODO: replace opencv with QPainter to support alpha? https://doc.qt.io/qtforpython-5/PySide2/QtGui/QPainter.html#PySide2.QtGui.PySide2.QtGui.QPainter.drawImage
# or https://github.com/pygobject/pycairo?tab=readme-ov-file # or https://github.com/pygobject/pycairo?tab=readme-ov-file
# or https://pyglet.readthedocs.io/en/latest/programming_guide/shapes.html # or https://pyglet.readthedocs.io/en/latest/programming_guide/shapes.html
@ -351,15 +364,29 @@ def decorate_frame(frame: Frame, tracker_frame: Frame, prediction_frame: Frame,
for track_id, track in tracks.items(): for track_id, track in tracks.items():
inv_H = np.linalg.pinv(tracker_frame.H) inv_H = np.linalg.pinv(tracker_frame.H)
draw_track_projected(img, track, int(track_id), frame.camera, conversion) draw_track_projected(img, track, int(track_id), frame.camera, conversion)
for line in debug_lines:
for rp1, rp2 in zip(line.points, line.points[1:]):
p1 = (
int(rp1.position[0]*scale),
int(rp1.position[1]*scale),
)
p2 = (
int(rp2.position[0]*scale),
int(rp2.position[1]*scale),
)
cv2.line(img, p1, p2, (255,0,0), 2)
# points = [(int(point[0]*scale), int(point[1]*scale)) for point in points]
if hasattr(frame.camera, 'debug_points'): # for num, points in enumerate(frame.camera.debug_lines):
for num, point in enumerate(frame.camera.debug_points): # cv2.line(img, points[0], points[1], (255,0,0), 2)
cv2.circle(img, (int(point[0]*scale), int(point[1]*scale)), 5, (255,0,0), 2)
cv2.putText(img, f"{num}", (int(point[0]*scale)+20, int(point[1]*scale)), cv2.FONT_HERSHEY_PLAIN, 1, (255,0,0), 1)
for num, points in enumerate(frame.camera.debug_lines):
points = [(int(point[0]*scale), int(point[1]*scale)) for point in points] # if hasattr(frame.camera, 'debug_points'):
cv2.line(img, points[0], points[1], (255,0,0), 2) # for num, point in enumerate(frame.camera.debug_points):
# cv2.circle(img, (int(point[0]*scale), int(point[1]*scale)), 5, (255,0,0), 2)
# cv2.putText(img, f"{num}", (int(point[0]*scale)+20, int(point[1]*scale)), cv2.FONT_HERSHEY_PLAIN, 1, (255,0,0), 1)
if not prediction_frame: if not prediction_frame:
cv2.putText(img, f"Waiting for prediction...", (500,17), cv2.FONT_HERSHEY_PLAIN, 1, (255,255,0), 1) cv2.putText(img, f"Waiting for prediction...", (500,17), cv2.FONT_HERSHEY_PLAIN, 1, (255,255,0), 1)

View file

@ -3,10 +3,12 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, IntEnum from enum import Enum, IntEnum
import math import math
from typing import List, Tuple from pathlib import Path
from typing import Dict, List, Tuple
import numpy as np import numpy as np
from simplification.cutil import simplify_coords_idx, simplify_coords_vw_idx from simplification.cutil import simplify_coords_idx, simplify_coords_vw_idx
import svgpathtools
""" """
See [notebook](../test_path_transforms.ipynb) for examples See [notebook](../test_path_transforms.ipynb) for examples
@ -93,7 +95,7 @@ class RenderableLines():
# def merge(self, rl: RenderableLines): # def merge(self, rl: RenderableLines):
RenderableLayers = Dict[int, RenderableLines]
def circle_arc(cx, cy, r, t, l, c: SrgbaColor): def circle_arc(cx, cy, r, t, l, c: SrgbaColor):
""" """
@ -132,4 +134,59 @@ def cross_points(cx, cy, r, c: SrgbaColor):
pointlist.append(RenderablePoint(pos, c)) pointlist.append(RenderablePoint(pos, c))
path2 = RenderableLine(pointlist) path2 = RenderableLine(pointlist)
return [path, path2] return [path, path2]
def load_lines_from_svg(svg_path: Path, scale: float, c: SrgbaColor) -> List[RenderableLine]:
lines = []
paths, attributes = svgpathtools.svg2paths(svg_path)
for path in paths:
try:
# segments = path.segments
coordinates = []
for i, segment in enumerate(path):
if isinstance(segment, svgpathtools.Line):
if i == 0:
# avoid duplicate coords
coordinates.append((segment.start.real, segment.start.imag))
coordinates.append((segment.end.real, segment.end.imag))
elif isinstance(segment, svgpathtools.Arc):
#Approximating arcs with line segments (adjust steps for precision)
steps = 10
for i in range(steps + 1):
t = i / steps
x = segment.point(t).real
y = segment.point(t).imag
coordinates.append((x, y))
elif isinstance(segment, svgpathtools.CubicBezier):
steps = 10
for i in range(steps + 1):
t = i / steps
x = segment.point(t).real
y = segment.point(t).imag
coordinates.append((x, y))
elif isinstance(segment, svgpathtools.QuadraticBezier):
steps = 10
for i in range(steps + 1):
t = i / steps
x = segment.point(t).real
y = segment.point(t).imag
coordinates.append((x, y))
else:
print(f"Unsupported segment type: {type(segment)}")
# Create LineString from coordinates
if len(coordinates) > 1:
coordinates = (np.array(coordinates) / scale).tolist()
points = [RenderablePoint(pos, c) for pos in coordinates]
line = RenderableLine(points)
lines.append(line)
# linestring = shapely.geometry.LineString(coordinates)
# linestrings.append(linestring)
except Exception as e:
print(f"Error processing path: {e}")
return lines

View file

@ -15,6 +15,7 @@ from matplotlib.pyplot import isinteractive
import numpy as np import numpy as np
from shapely import LineString, MultiLineString, line_locate_point, linestrings from shapely import LineString, MultiLineString, line_locate_point, linestrings
import shapely
from shapely.ops import substring from shapely.ops import substring
from statemachine import Event, State, StateMachine from statemachine import Event, State, StateMachine
from statemachine.exceptions import TransitionNotAllowed from statemachine.exceptions import TransitionNotAllowed
@ -26,7 +27,7 @@ from trap import shapes
from trap.base import Camera, DataclassJSONEncoder, DistortedCamera, Frame, ProjectedTrack, Track from trap.base import Camera, DataclassJSONEncoder, DistortedCamera, Frame, ProjectedTrack, Track
from trap.counter import CounterSender from trap.counter import CounterSender
from trap.laser_renderer import circle_points, rotateMatrix from trap.laser_renderer import circle_points, rotateMatrix
from trap.lines import RenderableLine, RenderableLines, RenderablePoint, RenderablePosition, SimplifyMethod, SrgbaColor, circle_arc from trap.lines import RenderableLayers, RenderableLine, RenderableLines, RenderablePoint, RenderablePosition, SimplifyMethod, SrgbaColor, circle_arc, load_lines_from_svg
from trap.node import Node from trap.node import Node
from trap.timer import Timer from trap.timer import Timer
from trap.utils import exponentialDecay, exponentialDecayRounded, lerp, relativePointToPolar, relativePolarToPoint from trap.utils import exponentialDecay, exponentialDecayRounded, lerp, relativePointToPolar, relativePolarToPoint
@ -795,11 +796,33 @@ class DrawnScenario(TrackScenario):
ls = substring(ls, 0, t_factor*ls.length, ls.length) ls = substring(ls, 0, t_factor*ls.length, ls.length)
# print(prediction_track_age) # print(prediction_track_age)
dashed = dashed_line(ls, 1, .5, prediction_track_age, False)
# print(dashed) # Option 1 : dashes
dashed = dashed_line(ls, .8, 1., prediction_track_age, False)
for line in dashed.geoms: for line in dashed.geoms:
dash_points = [RenderablePoint(point, color) for point in line.coords] dash_points = [RenderablePoint(point, color) for point in line.coords]
lines.append(RenderableLine(dash_points)) lines.append(RenderableLine(dash_points))
# Option 2 : flash
flash_distance = prediction_track_age * 5
# flashes = []
# for i in range(10):
# flashes.append(substring(ls, flash_distance*i, flash_distance + .5))
# flash_multiline = shapely.union_all(flashes)
# flashes = flash_multiline.geoms if isinstance(flash_multiline, MultiLineString) else [flash_multiline]
# print(flashes)
# for flash_ls in flashes:
# flash_points = [RenderablePoint(point, color) for point in flash_ls.coords]
# if len(flash_points) > 1:
# lines.append(RenderableLine(flash_points))
# flash_points = [RenderablePoint(point, color) for point in flash_ls.coords]
# if len(flash_points) > 1:
# lines.append(RenderableLine(flash_points))
# lines.append(RenderableLine(points)) # lines.append(RenderableLine(points))
@ -937,6 +960,11 @@ class Stage(Node):
self.counter = CounterSender() self.counter = CounterSender()
self.frame: Optional[Frame] = None self.frame: Optional[Frame] = None
if self.config.debug_map:
debug_color = SrgbaColor(0.,0.,1.,1.)
self.debug_lines = RenderableLines(load_lines_from_svg(self.config.debug_map, 100, debug_color))
def run(self): def run(self):
@ -1003,19 +1031,7 @@ class Stage(Node):
# 0. DEBUG lines: # 0. DEBUG lines:
if OPTION_RENDER_DEBUG:
if self.frame and hasattr(self.frame.camera, 'debug_lines'):
debug_color = SrgbaColor(0.,0.,1.,1.)
for points in self.frame.camera.debug_lines:
line_points = []
# interpolate, so the laser can correct the lines
for i in range(20):
t = i / 19
x = lerp(points[0][0], points[1][0], t)
y = lerp(points[0][1], points[1][1], t)
line_points.append(RenderablePoint((x, y), debug_color))
lines.append(RenderableLine(line_points))
# 1. Draw each scenario: # 1. Draw each scenario:
@ -1033,9 +1049,29 @@ class Stage(Node):
self.counter.set("stage.lines", len(lines.lines)) self.counter.set("stage.lines", len(lines.lines))
self.counter.set("stage.points_orig", lines.point_count()) self.counter.set("stage.points_orig", lines.point_count())
self.counter.set("stage.points", rl.point_count()) self.counter.set("stage.points", rl.point_count())
# debug_lines = RenderableLines([])
# if self.frame and hasattr(self.frame.camera, 'debug_lines'):
# for points in self.frame.camera.debug_lines:
# line_points = []
# # interpolate, so the laser can correct the lines
# for i in range(20):
# t = i / 19
# x = lerp(points[0][0], points[1][0], t)
# y = lerp(points[0][1], points[1][1], t)
# line_points.append(RenderablePoint((x, y), debug_color))
# debug_lines.append(RenderableLine(line_points))
layers: RenderableLayers = {
1: rl,
2: self.debug_lines,
}
# print(rl.__dict__) # print(rl.__dict__)
self.stage_sock.send_json(obj=rl, cls=DataclassJSONEncoder) self.stage_sock.send_json(obj=layers, cls=DataclassJSONEncoder)
# print(json.dumps(rl, cls=DataclassJSONEncoder)) # print(json.dumps(rl, cls=DataclassJSONEncoder))
@ -1058,6 +1094,10 @@ class Stage(Node):
help='Manually specity communication addr for the stage messages (the rendered lines)', help='Manually specity communication addr for the stage messages (the rendered lines)',
type=str, type=str,
default="tcp://0.0.0.0:99174") default="tcp://0.0.0.0:99174")
argparser.add_argument('--debug-map',
help='specify a map (svg-file) from which to load lines which will be overlayed',
type=str,
default="../DATASETS/hof3/map_hof.svg")
return argparser return argparser

25
uv.lock
View file

@ -2234,6 +2234,29 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/69/4d/3a493f15f5b80608857ef157f382ace494f51d9031e6bee6082437dd1403/supervisor_win-4.7.0-py2.py3-none-any.whl", hash = "sha256:bd98554c2a0878704c3f3fd95e38965d9986eae6a2ad29f34d73d0aee138a481", size = 303996 }, { url = "https://files.pythonhosted.org/packages/69/4d/3a493f15f5b80608857ef157f382ace494f51d9031e6bee6082437dd1403/supervisor_win-4.7.0-py2.py3-none-any.whl", hash = "sha256:bd98554c2a0878704c3f3fd95e38965d9986eae6a2ad29f34d73d0aee138a481", size = 303996 },
] ]
[[package]]
name = "svgpathtools"
version = "1.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "scipy" },
{ name = "svgwrite" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/5c/27c896f25e794d8eb1e75a1ab04fad3fcc272b5251d20f634a669e858da0/svgpathtools-1.7.1.tar.gz", hash = "sha256:beaef20fd78164aa5f0a7d4fd164ef20cb0d3d015cdec50c8c168e9d6547f041", size = 2135227 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/00/c23f53a9e91092239ff6f1fcc39463626e293f6b24898739996fe2a6eebd/svgpathtools-1.7.1-py2.py3-none-any.whl", hash = "sha256:3cbb8ba0e8d200f9639034608d9c55b68efbc1bef99ea99559a3e7cb024fb738", size = 68280 },
]
[[package]]
name = "svgwrite"
version = "1.4.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/16/c1/263d4e93b543390d86d8eb4fc23d9ce8a8d6efd146f9427364109004fa9b/svgwrite-1.4.3.zip", hash = "sha256:a8fbdfd4443302a6619a7f76bc937fc683daf2628d9b737c891ec08b8ce524c3", size = 189516 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/15/640e399579024a6875918839454025bb1d5f850bb70d96a11eabb644d11c/svgwrite-1.4.3-py3-none-any.whl", hash = "sha256:bb6b2b5450f1edbfa597d924f9ac2dd099e625562e492021d7dd614f65f8a22d", size = 67122 },
]
[[package]] [[package]]
name = "tensorboard" name = "tensorboard"
version = "2.19.0" version = "2.19.0"
@ -2541,6 +2564,7 @@ dependencies = [
{ name = "simplification" }, { name = "simplification" },
{ name = "superfsmon" }, { name = "superfsmon" },
{ name = "supervisor" }, { name = "supervisor" },
{ name = "svgpathtools" },
{ name = "tensorboardx" }, { name = "tensorboardx" },
{ name = "torch", version = "1.12.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, { name = "torch", version = "1.12.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" },
{ name = "torch", version = "1.12.1+cu113", source = { url = "https://download.pytorch.org/whl/cu113/torch-1.12.1%2Bcu113-cp310-cp310-linux_x86_64.whl" }, marker = "sys_platform == 'linux'" }, { name = "torch", version = "1.12.1+cu113", source = { url = "https://download.pytorch.org/whl/cu113/torch-1.12.1%2Bcu113-cp310-cp310-linux_x86_64.whl" }, marker = "sys_platform == 'linux'" },
@ -2576,6 +2600,7 @@ requires-dist = [
{ name = "simplification", specifier = ">=0.7.12" }, { name = "simplification", specifier = ">=0.7.12" },
{ name = "superfsmon", specifier = ">=1.2.3" }, { name = "superfsmon", specifier = ">=1.2.3" },
{ name = "supervisor", specifier = ">=4.2.5" }, { name = "supervisor", specifier = ">=4.2.5" },
{ name = "svgpathtools", specifier = ">=1.7.1" },
{ name = "tensorboardx", specifier = ">=2.6.2.2,<3" }, { name = "tensorboardx", specifier = ">=2.6.2.2,<3" },
{ name = "torch", marker = "python_full_version < '3.10' or python_full_version >= '4' or sys_platform != 'linux'", specifier = "==1.12.1" }, { name = "torch", marker = "python_full_version < '3.10' or python_full_version >= '4' or sys_platform != 'linux'", specifier = "==1.12.1" },
{ name = "torch", marker = "python_full_version >= '3.10' and python_full_version < '4' and sys_platform == 'linux'", url = "https://download.pytorch.org/whl/cu113/torch-1.12.1%2Bcu113-cp310-cp310-linux_x86_64.whl" }, { name = "torch", marker = "python_full_version >= '3.10' and python_full_version < '4' and sys_platform == 'linux'", url = "https://download.pytorch.org/whl/cu113/torch-1.12.1%2Bcu113-cp310-cp310-linux_x86_64.whl" },