fix(camera-ai): smooth generated camera curves
This commit is contained in:
Binary file not shown.
Binary file not shown.
+7
-7
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"schemaVersion": "cw-camera-worker-build-identity-v1",
|
||||
"workerVersion": "0.1.5",
|
||||
"createdUtc": "2026-08-08T20:03:04.908909+00:00",
|
||||
"workerVersion": "0.1.6",
|
||||
"createdUtc": "2026-08-09T07:15:01.579387+00:00",
|
||||
"python": "3.12.13",
|
||||
"sourceRootRelative": "cwai_sources/repository",
|
||||
"sourceSha256": {
|
||||
"MachineLearning/CameraDirector/adjacent_transition.py": "6c1b62c2996960af23d62e9c3acd3114600effda03273b006f7076c467c4bdfa",
|
||||
"MachineLearning/CameraDirector/build_hybrid_preparation_cache.py": "f948b1a68352893b078ac1f6ef458df2107565378eed32890276776ca0dfdad6",
|
||||
"MachineLearning/CameraDirector/camera_kinematics.py": "75816a55ac1dc655731432f061c2f4356cc3b583f3ce33797578431adcd9ba39",
|
||||
"MachineLearning/CameraDirector/camera_kinematics.py": "e94eefcb56c4ebdccbc57ae8d3650109b2c912e3642892ac13add9409db21a7f",
|
||||
"MachineLearning/CameraDirector/camera_runtime_data.py": "29fd30a1c97d384f7bb22d024a4f88178f6245858925c302b059a7a1afdeaac1",
|
||||
"MachineLearning/CameraDirector/cw_camera_cli.py": "943870a4cdec7e4b830b9c945d690e700902cf2a37d1e49987bba85bf0b0ea87",
|
||||
"MachineLearning/CameraDirector/cw_camera_runtime.py": "c4cfbc61173cb541932632e3b0a0751ae75c9d6ca3b56680722caa78f74e797d",
|
||||
"MachineLearning/CameraDirector/cw_camera_runtime.py": "62421300af470389b5e356267d91e4dcb31626762705cd1c1d20c3fa11d271f3",
|
||||
"MachineLearning/CameraDirector/data_driven_cut_planner.py": "36cf86a80577278d2e966cb0ebf7c3110b5b1dda5abd1d8a6b0a76e31d10e32b",
|
||||
"MachineLearning/CameraDirector/generate_hybrid.py": "4bf19ffdd8e244a82b4ea0656bad030a30470a9e2065bdccde8bfb9d99922fea",
|
||||
"MachineLearning/CameraDirector/generate_hybrid.py": "b6b1e7d02d96794e57c0ae9867ad18de7c6698d7bac05da1834490e2c0f3b4e3",
|
||||
"MachineLearning/CameraDirector/hybrid_candidate_cache.py": "a0a5b6a8f612f18cb2875394f17ed89950e1e62e38c1226457d9e35848f6e380",
|
||||
"MachineLearning/CameraDirector/hybrid_cut_reference.py": "53521d9e31d315c011892961887baf0cde6451020bf39c9d080a5072d4707fcb",
|
||||
"MachineLearning/CameraDirector/hybrid_preparation_cache.py": "1b0df53124109dee2d744777979a0553999cb276cb6f538a17ddce6516e3da8b",
|
||||
@@ -23,8 +23,8 @@
|
||||
"MachineLearning/CameraDirector/trajectory_quality.py": "20cd2ddc1b2349f8c33f6d1460580f37964a521eaa86ceee5b8ae457512ba49d"
|
||||
},
|
||||
"preparationLogicIdentifier": "acba2a46d9d979b013b7154bada6b6a8a7adfd7acd1721876ce6699ed4e03e84",
|
||||
"candidateLogicIdentifier": "7c771be40d4c6ebc6b15a96c64ed05062f6109d20605670be9b0e07587f2b25a",
|
||||
"generationCodeIdentifier": "470c77fe5d69046aa7bcadfbbb1337d6f0025fe0be374cb2302ea41ae00cdc47",
|
||||
"candidateLogicIdentifier": "bb9e883085df6a95de09f2181191c83c13cf1b3adff5b64e41b3d60d99350522",
|
||||
"generationCodeIdentifier": "8973f4642cd18f5645d33871c544e5394bb538ec5db86d4abc0536f50a00a9ed",
|
||||
"generationCodeFiles": [
|
||||
"adjacent_transition.py",
|
||||
"camera_kinematics.py",
|
||||
|
||||
+171
-29
@@ -19,7 +19,7 @@ import numpy as np
|
||||
|
||||
|
||||
TRANSLATION_KINEMATIC_POLICY_VERSION = (
|
||||
"yamo-angle-aware-turn-v6-natural-easing-impulse-c2"
|
||||
"yamo-angle-aware-turn-v7-local-impulse-authored-dynamics-guard"
|
||||
)
|
||||
DEFAULT_REVERSAL_ANGLE_DEGREES = 40.0
|
||||
DEFAULT_STOP_SPEED_RATIO = 0.12
|
||||
@@ -38,25 +38,33 @@ DEFAULT_SPEED_RATIO_TOLERANCE = 0.025
|
||||
DEFAULT_ANGLE_COMPARISON_TOLERANCE_DEGREES = 1e-3
|
||||
DEFAULT_MINIMUM_ORBIT_ANGULAR_SPEED_RADIANS_PER_SECOND = math.radians(2.0)
|
||||
DEFAULT_MAXIMUM_ENDPOINT_ADJUSTMENT_METERS = 0.75
|
||||
DEFAULT_MAXIMUM_ACCELERATION_REGRESSION_RATIO = 1.25
|
||||
DEFAULT_MAXIMUM_ACCELERATION_REGRESSION_DELTA_MPS2 = 8.0
|
||||
DEFAULT_ACCELERATION_GUARD_FLOOR_MPS2 = 25.0
|
||||
DEFAULT_MAXIMUM_JERK_REGRESSION_RATIO = 1.35
|
||||
DEFAULT_MAXIMUM_JERK_REGRESSION_DELTA_MPS3 = 350.0
|
||||
DEFAULT_JERK_GUARD_FLOOR_MPS3 = 350.0
|
||||
# A regularizer is a repair pass, not another motion authoring stage. The old
|
||||
# 25 m/s2 and 350 m/s3 absolute floors let a quiet authored curve become several
|
||||
# times rougher while still reporting a successful repair (the reviewed
|
||||
# Shot_066 class rose from roughly 255 to 548 m/s3). Keep only small numerical
|
||||
# headroom: a rewrite may trade at most five percent of either peak, and the
|
||||
# absolute allowance is deliberately tiny for already-quiet paths.
|
||||
DEFAULT_MAXIMUM_ACCELERATION_REGRESSION_RATIO = 1.05
|
||||
DEFAULT_MAXIMUM_ACCELERATION_REGRESSION_DELTA_MPS2 = 1.0
|
||||
DEFAULT_ACCELERATION_GUARD_FLOOR_MPS2 = 0.5
|
||||
DEFAULT_MAXIMUM_JERK_REGRESSION_RATIO = 1.05
|
||||
DEFAULT_MAXIMUM_JERK_REGRESSION_DELTA_MPS3 = 15.0
|
||||
DEFAULT_JERK_GUARD_FLOOR_MPS3 = 10.0
|
||||
# A second, deliberately narrow gate catches a concentrated turn that evades
|
||||
# the 40-degree rule yet still carries an edit-visible sideways impulse. The
|
||||
# thresholds were calibrated against 2,479 authored turn events: the combined
|
||||
# gate adds only 10 events (0.4%) beyond the existing abrupt-turn policy while
|
||||
# covering the reviewed Shot_066 profile (high stop ratio plus ~22 m/s^2 and
|
||||
# ~200 m/s^3 local lateral dynamics).
|
||||
# ~200 m/s^3 local lateral dynamics). The zero-net-direction extension was
|
||||
# separately scanned across 3,054 authored shots: it admitted one cut-boundary
|
||||
# discontinuity, which the authored-dynamics guard correctly rejected.
|
||||
DEFAULT_IMPULSIVE_TURN_ANGLE_DEGREES = 15.0
|
||||
DEFAULT_IMPULSIVE_STOP_SPEED_RATIO = 0.78
|
||||
DEFAULT_IMPULSIVE_CORNER_CONCENTRATION = 0.40
|
||||
DEFAULT_IMPULSIVE_LATERAL_ACCELERATION_MPS2 = 22.0
|
||||
DEFAULT_IMPULSIVE_LATERAL_JERK_MPS3 = 200.0
|
||||
DEFAULT_IMPULSIVE_TARGET_SPEED_RATIO = 0.72
|
||||
DEFAULT_IMPULSIVE_REVERSAL_WINDOW_SECONDS = 0.50
|
||||
DEFAULT_IMPULSIVE_REVERSAL_WINDOW_SECONDS = 0.55
|
||||
# A generated curve can satisfy the broad monotonic-speed definition of
|
||||
# ``natural_easing`` while its turn is still concentrated enough to read as a
|
||||
# lateral kick. Keep this as a separate, narrower contradiction gate rather
|
||||
@@ -492,10 +500,16 @@ def analyze_translational_reversals(
|
||||
lateral_jerk_peaks[result_index] = float(
|
||||
np.max(np.linalg.norm(lateral, axis=1))
|
||||
)
|
||||
# A one- or two-frame sideways kick can return to the original travel
|
||||
# direction, leaving the persistent flank angle close to zero. Use the
|
||||
# absolute adjacent-frame turn as a second angle witness. The existing
|
||||
# coherence, travel, concentration, acceleration and jerk gates keep a
|
||||
# deliberate distributed dolly/truck/arc from being classified as an
|
||||
# impulse; continuous orbit motion remains explicitly excluded.
|
||||
impulsive_valid = (
|
||||
(flank_speeds >= minimum_flank_speed_mps)
|
||||
& (
|
||||
angles
|
||||
np.maximum(angles, peak_corner_angles)
|
||||
>= DEFAULT_IMPULSIVE_TURN_ANGLE_DEGREES
|
||||
- DEFAULT_ANGLE_COMPARISON_TOLERANCE_DEGREES
|
||||
)
|
||||
@@ -793,6 +807,26 @@ def _dynamics_regression_exceeds_guard(
|
||||
)
|
||||
|
||||
|
||||
def _local_translation_dynamics_metrics(
|
||||
positions_meters: np.ndarray,
|
||||
left: int,
|
||||
right: int,
|
||||
sample_rate: float,
|
||||
) -> dict[str, float]:
|
||||
"""Measure a rewrite window including every finite-difference seam.
|
||||
|
||||
A shot-wide maximum can hide a new local kick when some unrelated frame is
|
||||
already rougher. Three samples on either side cover the velocity,
|
||||
acceleration and jerk stencils, so this local measurement makes the guard
|
||||
sensitive to the exact neighbourhood changed by ``_ease_through_stop``.
|
||||
"""
|
||||
|
||||
positions = _validated_positions(positions_meters)
|
||||
start = max(0, int(left) - 3)
|
||||
stop = min(len(positions), int(right) + 4)
|
||||
return translation_dynamics_metrics(positions[start:stop], sample_rate)
|
||||
|
||||
|
||||
def _event_metadata(events: list[TranslationReversal]) -> dict[str, object]:
|
||||
abrupt = [event for event in events if event.abrupt]
|
||||
impulsive = [event for event in events if event.impulsive]
|
||||
@@ -859,6 +893,12 @@ def regularize_translational_reversals(
|
||||
applied_angles: list[float] = []
|
||||
applied_target_speed_ratios: list[float] = []
|
||||
applied_impulsive_frames: list[int] = []
|
||||
applied_window_radii_frames: list[int] = []
|
||||
attempted_regularized_frames: list[int] = []
|
||||
dynamics_guard_rejected_frames: list[int] = []
|
||||
adaptive_radius_frames: list[int] = []
|
||||
dynamics_before = translation_dynamics_metrics(positions, sample_rate)
|
||||
attempted_dynamics_after = dict(dynamics_before)
|
||||
|
||||
pass_count = 0
|
||||
pending_events = abrupt_before if apply_policy else []
|
||||
@@ -884,11 +924,12 @@ def regularize_translational_reversals(
|
||||
# The high-impulse fallback is intentionally rare, so give its
|
||||
# braking envelope a slightly longer C2 runway instead of forcing
|
||||
# a stronger speed change into the normal 0.35-second window.
|
||||
neighbor_limit = (
|
||||
preferred_radius = (
|
||||
impulsive_requested_radius
|
||||
if event.impulsive
|
||||
else requested_radius
|
||||
)
|
||||
neighbor_limit = min(event.frame, len(output) - 1 - event.frame)
|
||||
if event_index > 0:
|
||||
neighbor_limit = min(
|
||||
neighbor_limit,
|
||||
@@ -899,23 +940,104 @@ def regularize_translational_reversals(
|
||||
neighbor_limit,
|
||||
(event_frames[event_index + 1] - event.frame) // 2,
|
||||
)
|
||||
radius = min(
|
||||
neighbor_limit,
|
||||
event.frame,
|
||||
len(output) - 1 - event.frame,
|
||||
)
|
||||
if radius < 4:
|
||||
preferred_radius = min(preferred_radius, neighbor_limit)
|
||||
if preferred_radius < 4:
|
||||
continue
|
||||
output = _ease_through_stop(
|
||||
output,
|
||||
event.frame,
|
||||
radius,
|
||||
target_speed_ratio=event.target_speed_ratio,
|
||||
turn_angle_degrees=event.angle_degrees,
|
||||
|
||||
attempted_regularized_frames.append(event.frame)
|
||||
# First retain the established window. If its C2 rewrite is still
|
||||
# locally rougher than the authored input, spend at most another
|
||||
# quarter second on the runway before giving up. This adaptive
|
||||
# path fixes boundary-limited cases such as Shot_066 without
|
||||
# weakening the dynamics contract or flattening every motion.
|
||||
extension_step = max(2, int(round(0.05 * sample_rate)))
|
||||
maximum_adaptive_radius = min(
|
||||
neighbor_limit,
|
||||
preferred_radius + max(4, int(round(0.25 * sample_rate))),
|
||||
)
|
||||
candidate_radii = list(
|
||||
range(
|
||||
preferred_radius,
|
||||
maximum_adaptive_radius + 1,
|
||||
extension_step,
|
||||
)
|
||||
)
|
||||
if candidate_radii[-1] != maximum_adaptive_radius:
|
||||
candidate_radii.append(maximum_adaptive_radius)
|
||||
|
||||
# Keep the authored comparison region fixed while trying wider
|
||||
# runways. If this ROI grew with ``radius``, an unrelated rough
|
||||
# frame newly entering (for example at r=27 after r=21 failed)
|
||||
# could raise the baseline maximum and hide a fresh center kick.
|
||||
fixed_local_before = _local_translation_dynamics_metrics(
|
||||
output,
|
||||
event.frame - preferred_radius,
|
||||
event.frame + preferred_radius,
|
||||
sample_rate,
|
||||
)
|
||||
accepted_output: np.ndarray | None = None
|
||||
accepted_radius = preferred_radius
|
||||
for radius in candidate_radii:
|
||||
candidate = _ease_through_stop(
|
||||
output,
|
||||
event.frame,
|
||||
radius,
|
||||
target_speed_ratio=event.target_speed_ratio,
|
||||
turn_angle_degrees=event.angle_degrees,
|
||||
)
|
||||
local_before = _local_translation_dynamics_metrics(
|
||||
output,
|
||||
event.frame - radius,
|
||||
event.frame + radius,
|
||||
sample_rate,
|
||||
)
|
||||
local_after = _local_translation_dynamics_metrics(
|
||||
candidate,
|
||||
event.frame - radius,
|
||||
event.frame + radius,
|
||||
sample_rate,
|
||||
)
|
||||
fixed_local_after = _local_translation_dynamics_metrics(
|
||||
candidate,
|
||||
event.frame - preferred_radius,
|
||||
event.frame + preferred_radius,
|
||||
sample_rate,
|
||||
)
|
||||
candidate_dynamics = translation_dynamics_metrics(
|
||||
candidate,
|
||||
sample_rate,
|
||||
)
|
||||
for key in attempted_dynamics_after:
|
||||
attempted_dynamics_after[key] = max(
|
||||
attempted_dynamics_after[key],
|
||||
candidate_dynamics[key],
|
||||
)
|
||||
if _dynamics_regression_exceeds_guard(local_before, local_after):
|
||||
continue
|
||||
if _dynamics_regression_exceeds_guard(
|
||||
fixed_local_before,
|
||||
fixed_local_after,
|
||||
):
|
||||
continue
|
||||
if _dynamics_regression_exceeds_guard(
|
||||
dynamics_before,
|
||||
candidate_dynamics,
|
||||
):
|
||||
continue
|
||||
accepted_output = candidate
|
||||
accepted_radius = radius
|
||||
break
|
||||
|
||||
if accepted_output is None:
|
||||
dynamics_guard_rejected_frames.append(event.frame)
|
||||
continue
|
||||
output = accepted_output
|
||||
if accepted_radius != preferred_radius:
|
||||
adaptive_radius_frames.append(event.frame)
|
||||
applied_frames.append(event.frame)
|
||||
applied_angles.append(event.angle_degrees)
|
||||
applied_target_speed_ratios.append(event.target_speed_ratio)
|
||||
applied_window_radii_frames.append(accepted_radius)
|
||||
if event.impulsive:
|
||||
applied_impulsive_frames.append(event.frame)
|
||||
pass_count = int(bool(applied_frames))
|
||||
@@ -934,25 +1056,38 @@ def regularize_translational_reversals(
|
||||
# bounded spatially and remain eligible for the unresolved-turn penalty.
|
||||
output = positions + (output - positions) * correction_scale
|
||||
|
||||
dynamics_before = translation_dynamics_metrics(positions, sample_rate)
|
||||
attempted_dynamics_after = translation_dynamics_metrics(output, sample_rate)
|
||||
attempted_regularized_frames = list(applied_frames)
|
||||
dynamics_guard_triggered = bool(applied_frames) and (
|
||||
final_attempted_dynamics = translation_dynamics_metrics(output, sample_rate)
|
||||
for key in attempted_dynamics_after:
|
||||
attempted_dynamics_after[key] = max(
|
||||
attempted_dynamics_after[key],
|
||||
final_attempted_dynamics[key],
|
||||
)
|
||||
final_dynamics_guard_triggered = bool(applied_frames) and (
|
||||
_dynamics_regression_exceeds_guard(
|
||||
dynamics_before,
|
||||
attempted_dynamics_after,
|
||||
final_attempted_dynamics,
|
||||
)
|
||||
)
|
||||
if dynamics_guard_triggered:
|
||||
dynamics_guard_triggered = bool(
|
||||
dynamics_guard_rejected_frames or final_dynamics_guard_triggered
|
||||
)
|
||||
if final_dynamics_guard_triggered:
|
||||
# A rejected rewrite remains an abrupt candidate and is therefore
|
||||
# demoted by the normal selection penalty. Keeping the untouched curve
|
||||
# is safer than publishing a numerically "resolved" turn with an
|
||||
# edit-visible acceleration or jerk seam.
|
||||
dynamics_guard_rejected_frames.extend(
|
||||
frame
|
||||
for frame in applied_frames
|
||||
if frame not in dynamics_guard_rejected_frames
|
||||
)
|
||||
output = positions.copy()
|
||||
applied_frames.clear()
|
||||
applied_angles.clear()
|
||||
applied_target_speed_ratios.clear()
|
||||
applied_impulsive_frames.clear()
|
||||
applied_window_radii_frames.clear()
|
||||
adaptive_radius_frames.clear()
|
||||
pass_count = 0
|
||||
endpoint_adjustment_clamped = False
|
||||
|
||||
@@ -981,6 +1116,10 @@ def regularize_translational_reversals(
|
||||
"translationKinematicAttemptedReversalFrames": (
|
||||
attempted_regularized_frames
|
||||
),
|
||||
"translationKinematicDynamicsGuardRejectedFrames": (
|
||||
dynamics_guard_rejected_frames
|
||||
),
|
||||
"translationKinematicAdaptiveRadiusFrames": adaptive_radius_frames,
|
||||
"translationDirectionReversalCountBefore": before_metadata["count"],
|
||||
"translationAbruptReversalCountBefore": before_metadata["abruptCount"],
|
||||
"translationDirectionReversalFramesBefore": before_metadata["frames"],
|
||||
@@ -1012,6 +1151,9 @@ def regularize_translational_reversals(
|
||||
"translationRegularizedReversalFrames": applied_frames,
|
||||
"translationRegularizedTurnAnglesDegrees": applied_angles,
|
||||
"translationRegularizedTargetSpeedRatios": (applied_target_speed_ratios),
|
||||
"translationRegularizedWindowRadiiFrames": (
|
||||
applied_window_radii_frames
|
||||
),
|
||||
"translationImpulsiveTurnCountBefore": before_metadata[
|
||||
"impulsiveCount"
|
||||
],
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ from typing import Any, Iterable
|
||||
|
||||
BUILD_IDENTITY_SCHEMA_VERSION = "cw-camera-worker-build-identity-v1"
|
||||
BUILD_IDENTITY_FILE = "cw_camera_worker_build_identity.json"
|
||||
WORKER_VERSION = "0.1.5"
|
||||
WORKER_VERSION = "0.1.6"
|
||||
|
||||
|
||||
def is_frozen_runtime() -> bool:
|
||||
|
||||
+978
-52
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
../../Scripts/wheel.exe,sha256=A69vg7y22iGuDMaqOWKEJzU_3jcjugdke2okLg6B5Cg,108448
|
||||
../../Scripts/wheel.exe,sha256=A40JvpbJRmAo327Gh4g5jNGRWEobaBMdktVtpIqzJAk,108448
|
||||
wheel-0.45.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
wheel-0.45.1.dist-info/LICENSE.txt,sha256=MMI2GGeRCPPo6h0qZYx8pBe9_IkcmO8aifpP8MmChlQ,1107
|
||||
wheel-0.45.1.dist-info/METADATA,sha256=mKz84H7m7jsxJyzeIcTVORiTb0NPMV39KvOIYhGgmjA,2313
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schemaVersion": "cw-camera-worker-distribution-build-v1",
|
||||
"createdUtc": "2026-08-08T20:03:52.1316389Z",
|
||||
"workerVersion": "0.1.5",
|
||||
"createdUtc": "2026-08-09T07:15:49.8790622Z",
|
||||
"workerVersion": "0.1.6",
|
||||
"protocolVersion": "1",
|
||||
"buildIdentitySchemaVersion": "cw-camera-worker-build-identity-v1",
|
||||
"buildEnvironment": {
|
||||
@@ -65,9 +65,9 @@
|
||||
],
|
||||
"payload": {
|
||||
"fileCount": 646,
|
||||
"bytes": 267603627,
|
||||
"executableSha256": "055fb16b60454b0d131ed2201eccad8d11977bd99ef011468d44638b33e4a1ef",
|
||||
"buildIdentitySha256": "84428e90a523ec40aa54d45c7b795e586979fce6f4a0e139e0bd83fc3c0ce906",
|
||||
"bytes": 267657984,
|
||||
"executableSha256": "09901a07746f98e94d3a4307df83b37044732e15b5001cdc9c7a67133d520847",
|
||||
"buildIdentitySha256": "34120be099cf1355cafb9af96cdb355ca9cd1b69651f20a050a1e30db1f3948e",
|
||||
"sanityRange": {
|
||||
"minimumFileCount": 500,
|
||||
"maximumFileCount": 1000,
|
||||
|
||||
Reference in New Issue
Block a user