noisy squigly lines

This commit is contained in:
Ruben van de Ven 2025-07-11 14:27:23 +02:00
parent 53e0c464c8
commit f2d7c65a18
3 changed files with 195 additions and 8 deletions

View file

@ -35,6 +35,7 @@ dependencies = [
"simplification>=0.7.12", "simplification>=0.7.12",
"supervisor>=4.2.5", "supervisor>=4.2.5",
"superfsmon>=1.2.3", "superfsmon>=1.2.3",
"noise>=1.2.2",
] ]
[project.scripts] [project.scripts]

View file

@ -29,12 +29,16 @@ from trap.node import Node
from trap.timer import Timer from trap.timer import Timer
from trap.utils import exponentialDecay, exponentialDecayRounded, relativePointToPolar, relativePolarToPoint from trap.utils import exponentialDecay, exponentialDecayRounded, relativePointToPolar, relativePolarToPoint
from noise import snoise2
logger = logging.getLogger('trap.stage') logger = logging.getLogger('trap.stage')
Coordinate = Tuple[float, float] Coordinate = Tuple[float, float]
DeltaT = float # delta_t in seconds DeltaT = float # delta_t in seconds
OPTION_GROW_ANOMALY_CIRCLE = False
OPTION_RENDER_DIFF_SEGMENT = False
class LineGenerator(ABC): class LineGenerator(ABC):
@abstractmethod @abstractmethod
def update_drawn_positions(self, dt: DeltaT): def update_drawn_positions(self, dt: DeltaT):
@ -252,6 +256,69 @@ class DiffSegment():
return RenderableLines([]) return RenderableLines([])
class DiffSegmentScan(DiffSegment):
"""
Provide alternative diffing, in the form of a sort of scan line
Should be faster with the laser
TODO: This is work in progress, does not work yet!
"""
def __init__(self, prediction: ProjectedTrack):
self.ptrack = prediction
self._target_track = prediction
self.finished = False
self._last_diff_frame_idx = 0
def finish(self):
self.finished = True
def prediction_offset(self):
"""Difference is starting moment between track and prediction"""
return self.ptrack.frame_index - self._target_track.frame_index
def nr_of_passed_points(self):
"""Number of points of the given ptrack that have passed"""
return len(self._target_track.projected_history) - 1 - self.prediction_offset()
# len(self.points) * self.POINT_INTERVAL
# run on each track update received
def update_track(self, track: ProjectedTrack):
self._target_track = track
if self.finished:
# don't add new points if finished
return
start_frame_idx = max(self.ptrack.frame_index, self._last_diff_frame_idx)
traj_diff_steps_back = track.frame_index - start_frame_idx # positive value
pred_diff_steps_forward = start_frame_idx - self.ptrack.frame_index # positive value
self._last_diff_frame_idx = track.frame_index
# run each render tick
def update_drawn_positions(self, dt: DeltaT, scenario: DrawnScenario):
# if not self.finished or not self.line.ready:
# self.line.update_drawn_positions(dt)
pass # TODO: use easing
def as_renderable(self) -> RenderableLines:
if self.finished:
return RenderableLines([])
color = SrgbaColor(0,0,1,1)
# steps_diff = self.nr_of_passed_points()
idx = self.nr_of_passed_points()
if len(self.ptrack.predictions[0]) < idx+1:
self.finish()
return RenderableLines([])
points = [self._target_track.projected_history[-1], self.ptrack.predictions[0][idx]]
points = [RenderablePoint(pos, color) for pos in points]
line = RenderableLine(points)
return RenderableLines([line])
class ScenarioScene(Enum): class ScenarioScene(Enum):
DETECTED = 1 DETECTED = 1
FIRST_PREDICTION = 2 FIRST_PREDICTION = 2
@ -368,12 +435,17 @@ class TrackScenario(StateMachine):
# just drop tracks if the predictions come to quick # just drop tracks if the predictions come to quick
return return
if track._track.predictions is None or not len(track._track.predictions):
# don't count to predictions if no prediction is set of given track (e.g. young tracks)
return
self.predictions.append(track) self.predictions.append(track)
if len(self.prediction_diffs): if len(self.prediction_diffs):
self.prediction_diffs[-1].finish() # existing diffing can end self.prediction_diffs[-1].finish() # existing diffing can end
# and create a new one # and create a new one
self.prediction_diffs.append(DiffSegment(track)) self.prediction_diffs.append(DiffSegment(track))
# self.prediction_diffs.append(DiffSegmentScan(track))
# check to change state # check to change state
try: try:
@ -457,6 +529,7 @@ class DrawnScenario(TrackScenario):
# self.track_id = track_id # self.track_id = track_id
self.last_update_t = time.perf_counter() self.last_update_t = time.perf_counter()
self.drawn_position: Optional[Coordinate] = None
self.drawn_positions: List[Coordinate] = [] self.drawn_positions: List[Coordinate] = []
self.drawn_pred_history: List[Coordinate] = [] self.drawn_pred_history: List[Coordinate] = []
self.drawn_predictions: List[List[Coordinate]] = [] self.drawn_predictions: List[List[Coordinate]] = []
@ -511,19 +584,25 @@ class DrawnScenario(TrackScenario):
# positions = self._track.get_projected_history(None, self.camera)[-MAX_HISTORY:] # positions = self._track.get_projected_history(None, self.camera)[-MAX_HISTORY:]
# self.drawn_positions = self.track.projected_history[-self.MAX_HISTORY:] # self.drawn_positions = self.track.projected_history[-self.MAX_HISTORY:]
self.drawn_positions = self.track.projected_history self.drawn_positions = self.track.projected_history
if self.drawn_position is None:
self.drawn_position = self.drawn_positions[-1]
else:
self.drawn_position[0] = exponentialDecay(self.drawn_position[0], self.drawn_positions[-1][0], 3, dt)
self.drawn_position[1] = exponentialDecay(self.drawn_position[1], self.drawn_positions[-1][1], 3, dt)
# 3. predictions # 3. predictions
if len(self.drawn_predictions) < len(self.predictions): if len(self.drawn_predictions) < len(self.predictions):
# first prediction # first prediction
if len(self.drawn_predictions) == 0: if len(self.drawn_predictions) == 0:
self.drawn_predictions.append(self.predictions[-1].predictions[0]) last_pred = self.predictions[-1]
self.drawn_predictions.append(last_pred.predictions[0])
else: else:
# if a new prediction has arised, transition from existing one. # if a new prediction has arised, transition from existing one.
# First, cut existing prediction # First, cut existing prediction
# CUT_GAP indicates that some is lost in the transition, to prevent glitches when velocity of person changes # CUT_GAP indicates that some is lost in the transition, to prevent glitches when velocity of person changes
end_step = self.predictions[-1].frame_index - self.predictions[-2].frame_index + self.CUT_GAP end_step = self.predictions[-1].frame_index - self.predictions[-2].frame_index + self.CUT_GAP
keep = self.drawn_predictions[-1][end_step:] keep = self.drawn_predictions[-1][end_step:]
last_item: Coordinate = keep[-1] last_item: Coordinate = (keep)[-1]
self.drawn_predictions[-1] = self.drawn_predictions[-1][:end_step] # cut the old part self.drawn_predictions[-1] = self.drawn_predictions[-1][:end_step] # cut the old part
# print(self.predictions[-1].frame_index, self.predictions[-2].frame_index, end_step, len(keep)) # print(self.predictions[-1].frame_index, self.predictions[-2].frame_index, end_step, len(keep))
# duplicate last item, so the new one has the same nr. of points as the incoming prediction (so it can actually transition) # duplicate last item, so the new one has the same nr. of points as the incoming prediction (so it can actually transition)
@ -623,10 +702,20 @@ class DrawnScenario(TrackScenario):
# 1. Trajectory history # 1. Trajectory history
# drawable_points, alphas = self.drawn_positions[:self.MAX_HISTORY], [1]*len(self.drawn_positions) # drawable_points, alphas = self.drawn_positions[:self.MAX_HISTORY], [1]*len(self.drawn_positions)
drawable_points, alphas = points_fade_out_alpha_mask(self.drawn_positions, track_age, TRACK_FADE_AFTER_DURATION, TRACK_END_FADE) # perlin/simplex noise
color = SrgbaColor(1.,0.,0.,1.-self.lost_factor()) # dt: change speed. Divide to make slower
# amp: amplitude of noise
# frequency: make smaller to make longer waves
noisy_points = apply_perlin_noise_to_line_normal(self.drawn_positions, t/3, .3, .05)
drawable_points, alphas = points_fade_out_alpha_mask(noisy_points, track_age, TRACK_FADE_AFTER_DURATION, TRACK_END_FADE)
color = SrgbaColor(1.,0.,1.,1.-self.lost_factor())
# TODO: effect configuration
points = [RenderablePoint(pos, color.as_faded(a)) for pos, a in zip(drawable_points, alphas)] points = [RenderablePoint(pos, color.as_faded(a)) for pos, a in zip(drawable_points, alphas)]
# points = [RenderablePoint(pos, color.as_faded(a)) for pos, a in zip(drawable_points, alphas)]
lines.append(RenderableLine(points)) lines.append(RenderableLine(points))
# 2. Position Marker / anomaly score # 2. Position Marker / anomaly score
@ -635,7 +724,8 @@ class DrawnScenario(TrackScenario):
# lines.append(circle_arc(self.drawn_positions[-1][0], self.drawn_positions[-1][1], 1, t, self.anomaly_score, anomaly_marker_color)) # lines.append(circle_arc(self.drawn_positions[-1][0], self.drawn_positions[-1][1], 1, t, self.anomaly_score, anomaly_marker_color))
# last point, (but this draws line in circle, requiring a 'jump back' for the laser) # last point, (but this draws line in circle, requiring a 'jump back' for the laser)
cx, cy = self.drawn_positions[-1][0], self.drawn_positions[-1][1], cx, cy = self.drawn_positions[-1][0], self.drawn_positions[-1][1],
radius = max(.1, self._drawn_anomaly_score * 1.)
radius = max(.1, self._drawn_anomaly_score * 1.) if OPTION_GROW_ANOMALY_CIRCLE else .1
steps=5 steps=5
if len(self.drawn_positions) >= steps: if len(self.drawn_positions) >= steps:
@ -690,8 +780,10 @@ class DrawnScenario(TrackScenario):
# colors = [color.as_faded(1) for a2 in range(len(drawn_diff))] # colors = [color.as_faded(1) for a2 in range(len(drawn_diff))]
# points = [RenderablePoint(pos, pos_color) for pos, pos_color in zip(drawn_diff, colors)] # points = [RenderablePoint(pos, pos_color) for pos, pos_color in zip(drawn_diff, colors)]
# lines.append(RenderableLine(points)) # lines.append(RenderableLine(points))
for diff in self.prediction_diffs:
lines.append_lines(diff.as_renderable()) if OPTION_RENDER_DIFF_SEGMENT:
for diff in self.prediction_diffs:
lines.append_lines(diff.as_renderable())
# # print(self.current_state) # # print(self.current_state)
@ -878,7 +970,7 @@ class Stage(Node):
# rl = RenderableLines(lines) # rl = RenderableLines(lines)
# with open('/tmp/lines.pcl', 'wb') as fp: # with open('/tmp/lines.pcl', 'wb') as fp:
# pickle.dump(rl, fp) # pickle.dump(rl, fp)
rl = lines.as_simplified(SimplifyMethod.RDP, .01) # or segmentise (see shapely) rl = lines.as_simplified(SimplifyMethod.RDP, .003) # or segmentise (see shapely)
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())
@ -905,3 +997,89 @@ class Stage(Node):
return argparser return argparser
# TODO place somewhere else:
# Gemma3:27b prompt: "python. Given a list of coordinates, that describes a line: `drawable_points: List[Tuple[float,float]]` apply perlin noise over the normal of the line, that changes over time `dt`."
def apply_perlin_noise_to_line_normal(drawable_points: np.ndarray, dt: float, amplitude: float = 1.0, frequency: float = 1.0) -> np.ndarray:
"""
Applies Perlin noise to the normals of a line described by a list of coordinates, changing over time.
Args:
drawable_points: A list of (x, y) tuples representing the points of the line.
dt: The time delta, used to animate the Perlin noise.
amplitude: The strength of the Perlin noise effect.
frequency: The frequency of the Perlin noise (how many waves per unit).
Returns:
A new list of (x, y) tuples representing the line with Perlin noise applied to the normals. If drawable_points
has fewer than 2 points, it returns the original list unchanged.
Raises:
TypeError: If drawable_points is not a list or dt is not a float.
ValueError: If the input points are not tuples of length 2.
"""
# if not isinstance(drawable_points, list):
# print(drawable_points, type(drawable_points))
# raise TypeError("drawable_points must be a list.")
if not isinstance(dt, float):
raise TypeError("dt must be a float.")
if len(drawable_points) < 2:
return drawable_points # Nothing to do with fewer than 2 points
# for point in drawable_points:
# if not isinstance(point, tuple) or len(point) != 2:
# raise ValueError("Each point in drawable_points must be a tuple of length 2.")
# noise = PerlinNoise(octaves=4) # You can adjust octaves for different noise patterns
new_points = []
for i in range(len(drawable_points)):
x, y = drawable_points[i]
# Calculate the normal vector. We'll approximate it using the previous and next points.
if i == 0:
# For the first point, use the next point to estimate the normal
next_x, next_y = drawable_points[i + 1]
normal_x = next_y - y
normal_y = -(next_x - x)
elif i == len(drawable_points) - 1:
# For the last point, use the previous point
prev_x, prev_y = drawable_points[i - 1]
normal_x = y - prev_y
normal_y = -(x - prev_x)
else:
prev_x, prev_y = drawable_points[i - 1]
next_x, next_y = drawable_points[i + 1]
normal_x = next_y - prev_y
normal_y = -(next_x - prev_x)
# Normalize the normal vector
norm = np.sqrt(normal_x**2 + normal_y**2)
if norm > 0:
normal_x /= norm
normal_y /= norm
# Apply Perlin noise to the normal
# noise_x = noise([x * frequency, (y + dt) * frequency]) * amplitude * normal_x
# noise_y = noise([x * frequency, (y + dt) * frequency]) * amplitude * normal_y
noise = snoise2(i * frequency, dt % 1000, octaves=4)
noise_x = noise * amplitude * normal_x
noise_y = noise * amplitude * normal_y
# print(noise_x, noise_y, dt, frequency, i, dt, snoise2(i * frequency, dt % 1000, octaves=4))
# Add the noise to the point's coordinates
new_x = x + noise_x
new_y = y + noise_y
new_points.append((new_x, new_y))
# print(drawable_points, new_points)
return np.array(new_points)

View file

@ -1300,6 +1300,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/5e/cb3dbdf3ae18e281b8b1b4691bb5d3465b383e04bde2c2a782c893f1ee21/nicegui-2.13.0-py3-none-any.whl", hash = "sha256:2343d37885df2c2e388a4f4c3f0ce9b308be02e16b0303108471a1a38fe3508f", size = 16482500 }, { url = "https://files.pythonhosted.org/packages/2e/5e/cb3dbdf3ae18e281b8b1b4691bb5d3465b383e04bde2c2a782c893f1ee21/nicegui-2.13.0-py3-none-any.whl", hash = "sha256:2343d37885df2c2e388a4f4c3f0ce9b308be02e16b0303108471a1a38fe3508f", size = 16482500 },
] ]
[[package]]
name = "noise"
version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/29/bb830ee6d934311e17a7a4fa1368faf3e73fbb09c0d80fc44e41828df177/noise-1.2.2.tar.gz", hash = "sha256:57a2797436574391ff63a111e852e53a4164ecd81ad23639641743cd1a209b65", size = 125615 }
[[package]] [[package]]
name = "notebook" name = "notebook"
version = "7.3.3" version = "7.3.3"
@ -2522,6 +2528,7 @@ dependencies = [
{ name = "gdown" }, { name = "gdown" },
{ name = "ipywidgets" }, { name = "ipywidgets" },
{ name = "jsonlines" }, { name = "jsonlines" },
{ name = "noise" },
{ name = "opencv-python" }, { name = "opencv-python" },
{ name = "pandas-helper-calc" }, { name = "pandas-helper-calc" },
{ name = "pyglet" }, { name = "pyglet" },
@ -2556,6 +2563,7 @@ requires-dist = [
{ name = "gdown", specifier = ">=4.7.1,<5" }, { name = "gdown", specifier = ">=4.7.1,<5" },
{ name = "ipywidgets", specifier = ">=8.1.5,<9" }, { name = "ipywidgets", specifier = ">=8.1.5,<9" },
{ name = "jsonlines", specifier = ">=4.0.0,<5" }, { name = "jsonlines", specifier = ">=4.0.0,<5" },
{ name = "noise", specifier = ">=1.2.2" },
{ name = "opencv-python", path = "opencv_python-4.10.0.84-cp310-cp310-linux_x86_64.whl" }, { name = "opencv-python", path = "opencv_python-4.10.0.84-cp310-cp310-linux_x86_64.whl" },
{ name = "pandas-helper-calc", git = "https://github.com/scls19fr/pandas-helper-calc" }, { name = "pandas-helper-calc", git = "https://github.com/scls19fr/pandas-helper-calc" },
{ name = "pyglet", specifier = ">=2.0.15,<3" }, { name = "pyglet", specifier = ">=2.0.15,<3" },