Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 23 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,38 +110,40 @@ previews, and chain in either order without extra dependencies.

## content-aware elastic collapse

`collapse` lives under **SHAPE & MOTION**. The default `elastic` mode evolves
seeded, spatially nonuniform displacement fields. Gradient-rich regions resist
deformation; motion can amplify it elsewhere. Fields are analyzed at no more
than 192 pixels on the longest side, smoothed in time, then resized into
full-frame maps for CPU `cv2.remap`. This is not a uniform zoom. Reflective
sampling keeps edges filled and output dimensions never change.
`collapse` lives under **SHAPE & MOTION**. The default `seam` mode behaves like
Photoshop content-aware scaling: each frame starts at its original composition,
then its content-aware shrink grows linearly from the first frame to the clip's
end. Low-energy seams are removed from a reduced analysis frame, and the carved
coordinate map is stretched back over the original canvas, so output dimensions
never change. High-gradient regions resist shrinking. Analysis is capped at 192
pixels on the longest side and updates at most five times per second; the
displacement field is smoothed between updates. `mode=elastic` keeps the earlier
seeded, spatially nonuniform elastic warp available as an alternative.

| option | range / values | default |
| --- | --- | --- |
| `intensity` | 0-100%, displacement strength | 65% |
| `horizontal` | 0-100%, horizontal deformation scale | 65% |
| `vertical` | 0-100%, vertical deformation scale | 45% |
| `intensity` | 0-100%, overall scale strength (elastic warp strength in elastic mode) | 65% |
| `horizontal` | 0-100%, share of the maximum width shrink / horizontal warp | 65% |
| `vertical` | 0-100%, share of the maximum height shrink / vertical warp | 45% |
| `preserve` | 0-100%, content/edge protection | 75% |
| `mode` | elastic, seam | elastic |
| `seam_strength` | 0-100%, offline seam removal amount | 60% |
| `mode` | seam, elastic | seam |
| `seam_strength` | 0-100%, maximum content-aware shrink by clip end | 60% |
| `motion` | 0-100%, elastic motion influence | 50% |
| `smoothness` | 0-100%, spatial field smoothness | 70% |
| `speed` | 0-4 elastic cycles per second | 0.35 |
| `coherence` | 0-100%, elapsed-time field smoothing | 80% |
| `seed` | 0-2147483647; 0 derives phases from the source | 0 |
| `seed` | 0-2147483647; elastic mode only, 0 derives phases from the source | 0 |

The optional `seam` mode is an offline content-aware carve, not a full-resolution
seam search on every frame. A gradient-energy dynamic program removes at most
four seams per axis from the reduced frame, at most once every 0.2 seconds of
video. Source coordinate maps are carved alongside the image, resized to the
original dimensions, and temporally smoothed before sampling the current frame.
This smooths seam decisions rather than crossfading old pixels. Seam mode uses
`seam_strength`, the two axis scales, preservation, smoothness, coherence and
intensity. Elastic motion, speed and random phases do not drive seam selection.
In seam mode, shrink is capped at 50% per axis. The target reduction is
`50% × intensity × seam_strength × horizontal/vertical control`; `preserve`
steers seams away from strong edges. The final displacement is multiplied by
elapsed clip progress, producing a linear ramp while each output frame remains
at source dimensions. `smoothness` and `coherence` steady the reduced-resolution
seam map; motion, speed, and random phases apply only to elastic mode.

Zero intensity is an exact passthrough; two zero axis scales disable both modes.
Zero seam strength disables seam mode. The first frame is deformed immediately.
Zero seam strength disables seam mode. Seam mode leaves the first frame
unchanged and reaches its configured scale at the end of the clip.
State resets on a new input/shape, explicit reset, rewind, frame discontinuity,
explicit scene cut, or a large RGB scene change. Persistent analysis state is
bounded independently of clip length; full-frame remap buffers are transient.
Expand Down
38 changes: 16 additions & 22 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,28 +373,22 @@ Shared rules for the stateful effects:
- On a motion event, queue a bounded frame or patch snapshot with a due timestamp. Release delayed frames or short repayment bursts without blocking the render loop. Handle clip end when queued debt remains.
- Options: motion threshold/sensitivity, min/max delay, debt gain, release rate, burst size, freeze chance, patch mode, queue capacity, seed.
- Test that static input creates little debt, a sudden moving shape triggers delayed output, sensitivity and release controls change behavior, fixed seeds repeat, and queue memory stays bounded.
- [x] E2.46 - content-aware elastic collapse (prompt effect 05): deform the image nonuniformly while keeping the output dimensions fixed.
- Make real-time elastic remapping the default: evolve a smooth displacement/scale field, then sample with a CPU remap such as `cv2.remap` if OpenCV is justified. Do not reduce this to uniform zoom.
- Keep seam carving as an optional offline mode: use a gradient/energy map, work at reduced resolution, limit seams per update, smooth decisions over time, then resize to the required frame size. Do not carve every full-resolution frame without measuring flicker and cost.
- Options: intensity, horizontal/vertical scale, content preservation, seam strength/mode, motion influence, field smoothness/speed, temporal coherence, seed.
- Test fixed output size, stronger deformation at higher intensity, repeatability for a fixed seed, and less incidental flicker with temporal smoothing. Benchmark real-time and seam modes separately.
_Done: `collapse` defaults to seeded, smooth nonuniform elastic fields with
gradient-based content protection and motion weighting, sampled by CPU
`cv2.remap`. Offline `mode=seam` uses gradient-energy minimum-cost seams and
carved source-coordinate maps at <=192px, <=4 seams per axis, <=5 updates/s;
displacement decisions are smoothed over elapsed time. Exposes intensity,
axis scales, preservation, mode/strength, motion, smoothness/speed, coherence
and seed through the registry and menu. Reuses the stateful adapter, preserving
dimensions/audio and supporting sediment in either adjacent chain order.
Optional headless OpenCV is isolated in requirements-collapse.txt. Tests cover
deformation strength, content protection, repeatability, resets/cuts/rewinds,
tiny/short/static inputs, no mutation, passthrough, bounds, bounded state,
flicker reduction in both modes and real CLI/menu renders. All nine suites pass.
Separate 120-frame CPU benchmarks at 640x360 and 1280x720 are saved in
benchmarks/collapse.json, including frame time, update cost and peak process
RSS; README records the measured results without an end-to-end real-time claim.
Existing stateful VFR normalization, adjacent-processor and preview limits
remain explicit._
- [x] E2.46 - content-aware scaling and elastic warp (prompt effect 05): change image composition while preserving output dimensions.
- Default to progressive content-aware scale: linearly ramp seam-carved shrink from identity at clip start to the configured target at clip end, then remap each frame to its original canvas.
- Analyze at reduced resolution, preserve high-energy regions, cap shrink to half per axis, and smooth cached decisions. Keep the earlier seeded elastic warp as an alternate mode.
- Options: intensity, horizontal/vertical shrink, content preservation, seam strength/mode, elastic motion influence, field smoothness/speed, temporal coherence, seed.
- Test linear progress against clip duration, fixed output size, content preservation, repeatability, bounded state and real CLI/menu renders.
_Done: `collapse` defaults to a content-aware seam scale that begins at the
source composition and grows linearly to its target over probed clip duration.
The low-resolution energy map removes seams from low-detail regions, then the
source-coordinate map is stretched back to the original frame dimensions.
Analysis is capped at 192px; shrink is limited to 50% per axis and refreshed
at <=5Hz with temporal smoothing. The seeded elastic warp remains selectable.
Duration flows through the shared stateful adapter. Tests cover exact start,
linear midpoint field, end-frame deformation, output shape/audio, resets,
preservation, optional dependencies and TUI/CLI integration. Optional headless
OpenCV remains isolated in requirements-collapse.txt. Existing stateful VFR
normalization, adjacent-processor and preview limits remain explicit._
- [ ] E2.47 - pixel archaeology (prompt effect 09): transplant patches from earlier frames using content matching.
- Keep a bounded reservoir of old frames or patches. Cache inexpensive descriptors such as color mean/variance, gradient histograms, or downsampled pixels; sample candidates instead of searching every patch in every frame.
- Match current regions to historical patches by color, texture, or edge compatibility. Include a controlled mismatch mode. Support hard copy and soft/feathered boundaries; every replacement must come from actual video history, not random rectangles.
Expand Down
25 changes: 14 additions & 11 deletions effex.py
Original file line number Diff line number Diff line change
Expand Up @@ -1925,12 +1925,13 @@ def build_temporal_sediment(ctx: EffectContext) -> Plan:
"percent", 45, minimum=0, maximum=100, step=5),
Param("preserve", "content preservation", "protect strong edges from collapse",
"percent", 75, minimum=0, maximum=100, step=5),
Param("mode", "mode", "fast elastic field or bounded offline seam carving",
"choice", "elastic", choices=(
("elastic", "elastic", "smooth content-weighted CPU remapping"),
("seam", "seam (offline)", "reduced-resolution energy-based seams"),
Param("mode", "mode", "progressive content-aware scaling or elastic warping",
"choice", "seam", choices=(
("seam", "content-aware scale",
"linearly shrink by seams, then resize to the original frame"),
("elastic", "elastic warp", "smooth content-weighted CPU remapping"),
)),
Param("seam_strength", "seam strength", "seams removed per offline update",
Param("seam_strength", "shrink amount", "maximum content-aware shrink by the end of the clip",
"percent", 60, minimum=0, maximum=100, step=5),
Param("motion", "motion influence", "amplify elastic deformation in moving regions",
"percent", 50, minimum=0, maximum=100, step=5),
Expand All @@ -1947,14 +1948,14 @@ def build_temporal_sediment(ctx: EffectContext) -> Plan:

def build_collapse(ctx: EffectContext) -> Plan:
width, height = _source_dims(ctx.clip, ctx.frame)
mode = str(ctx.opts.get("mode", "elastic"))
mode = str(ctx.opts.get("mode", "seam"))
return Plan(
vf=f"stateful elastic collapse ({mode}) frame pass",
out_suffix=ctx.suffix or "_COLLAPSED", width=width, height=height,
notes=[
"nonuniform content-aware displacement with fixed output dimensions",
"elastic mode uses seeded smooth fields and CPU OpenCV remapping",
"offline seam mode analyzes at most 192px, up to 4 seams per axis at 5Hz",
"content-aware mode linearly shrinks over the clip and remaps to original dimensions",
"seam analysis is capped at 192px; target scales remove at most half of either axis",
"elastic mode remains available for seeded smooth fields and CPU OpenCV remapping",
"needs requirements-collapse.txt; no stateful preview or original VFR PTS",
],
)
Expand Down Expand Up @@ -2111,8 +2112,8 @@ def build_inversion(ctx: EffectContext) -> Plan:
"pass 2: ffmpeg -f concat -i list.txt -c copy <output>.mp4",
)),
Effect(key="collapse", name="elastic collapse",
blurb="content-aware elastic deformation or offline seam collapse",
tag="stateful CPU displacement and remap",
blurb="progressive content-aware scaling or elastic warp",
tag="stateful seam carving and CPU remap",
build=build_collapse, params=COLLAPSE_PARAMS,
frame_processor=ElasticCollapse,
stages=(
Expand Down Expand Up @@ -2601,6 +2602,8 @@ def run_stateful_chain(chain: Chain, ctx: EffectContext, clip: Clip, tools: Tool
factory = effect.frame_processor
processor = factory()
opts = opts_of(ctx.opts, effect)
if factory is ElasticCollapse:
opts["_clip_duration"] = clip.duration
seed = int(opts.get("seed", 0) or 0)
if not seed:
seed = 20261023 + (clip.size % 1000003)
Expand Down
39 changes: 24 additions & 15 deletions seam_carving.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
numpy alone.
"""
import math
from typing import List
from typing import List, Optional

_MAX_SEAMS = 4
_MAX_SEAMS = 96


def _coerce_scalar(value: float) -> float:
Expand All @@ -26,8 +26,10 @@ def _coerce_scalar(value: float) -> float:
return min(1.0, max(0.0, number))


def _coerce_seams(value) -> int:
"""Clamp max_seams to an integer in [0, 4]."""
def _coerce_seams(value) -> Optional[int]:
"""Clamp max_seams to [0, 96], or keep None as an uncapped request."""
if value is None:
return None
try:
number = int(value)
except (TypeError, ValueError, OverflowError):
Expand Down Expand Up @@ -126,17 +128,17 @@ def _remove_horizontal_seam(array, seam: List[int]):


def build_seam_field(frame, horizontal, vertical, strength, preservation,
max_seams: int = 4):
max_seams: Optional[int] = 4, max_shrink: float = 1.0):
"""Return a normalized HxWx2 displacement field from seam carving.

``frame`` is a reduced-resolution uint8 RGB array (H, W, 3). The scalar
controls are clamped to [0, 1] (non-finite becomes 0). ``max_seams`` is
clamped to [0, 4]. Up to ``strength * horizontal * max_seams`` vertical
seams (one pixel per row) are removed, producing x displacement, and up to
``strength * vertical * max_seams`` horizontal seams (one pixel per
column) are removed, producing y displacement. Each seam is the min-cost
path through the preservation-weighted gradient energy, found by dynamic
programming.
``max_seams`` caps removals at [0, 96]; None derives the count from the
axis length and ``max_shrink``. ``max_shrink`` is clamped to [0, 1]. With
an explicit cap, up to ``strength * horizontal * max_seams`` vertical
seams and ``strength * vertical * max_seams`` horizontal seams are removed,
subject to each axis's ``max_shrink`` limit. Each seam is the min-cost path
through preservation-weighted gradient energy, found by dynamic programming.

Source coordinate maps are carved alongside the energy, then resized back
to the original HxW via cv2.resize. The returned field is the carved
Expand All @@ -152,6 +154,7 @@ def build_seam_field(frame, horizontal, vertical, strength, preservation,
strength = _coerce_scalar(strength)
preservation = _coerce_scalar(preservation)
max_seams = _coerce_seams(max_seams)
max_shrink = _coerce_scalar(max_shrink)

frame = np.asarray(frame)
if frame.ndim != 3 or frame.shape[2] != 3:
Expand All @@ -165,10 +168,16 @@ def build_seam_field(frame, horizontal, vertical, strength, preservation,
or (horizontal == 0.0 and vertical == 0.0)):
return zero

n_vertical = min(int(round(strength * horizontal * max_seams)),
width - 1)
n_horizontal = min(int(round(strength * vertical * max_seams)),
height - 1)
vertical_limit = min(width - 1, int(round((width - 1) * max_shrink)))
horizontal_limit = min(height - 1, int(round((height - 1) * max_shrink)))
if max_seams is None:
n_vertical = int(round(strength * horizontal * vertical_limit))
n_horizontal = int(round(strength * vertical * horizontal_limit))
else:
n_vertical = int(round(strength * horizontal * max_seams))
n_horizontal = int(round(strength * vertical * max_seams))
n_vertical = min(n_vertical, vertical_limit)
n_horizontal = min(n_horizontal, horizontal_limit)
if n_vertical == 0 and n_horizontal == 0:
return zero

Expand Down
Loading
Loading