fix(camera-ai): smooth generated camera curves

This commit is contained in:
KINDNICK
2026-08-09 16:33:02 +09:00
parent 5a2e95485e
commit b78c6319b6
18 changed files with 4078 additions and 418 deletions
Binary file not shown.
@@ -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",
@@ -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"
],
@@ -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:
@@ -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,