11"""Canonical frame-selection algorithms shared by the CLI and TUI."""
22
33from collections import OrderedDict
4- from math import log1p
4+ from math import expm1 , log1p
55from statistics import median
66from typing import Any , Dict , List , Optional , Sequence , Set , Tuple
77
1414OUTLIER_DEFAULT_WINDOW_SIZE = 15
1515OUTLIER_DEFAULT_SENSITIVITY = 60
1616OUTLIER_MIN_WINDOW_SIZE = 5
17- OUTLIER_MIN_RELATIVE_DROP = 0.05
18- OUTLIER_MAX_RELATIVE_DROP = 0.30
17+ OUTLIER_MIN_RELATIVE_DROP = 0.005
18+ OUTLIER_MAX_RELATIVE_DROP = 0.08
1919OUTLIER_MIN_ROBUST_THRESHOLD = 1.5
2020
2121
@@ -204,6 +204,20 @@ def _outlier_positions(
204204 for _ , frame in group
205205 ]
206206 scores = [log1p (score ) for score in raw_scores ]
207+ dip_magnitudes = [0.0 ] * len (scores )
208+ relative_drops = [0.0 ] * len (scores )
209+ for position in range (1 , len (scores ) - 1 ):
210+ # A local dip must sit below both adjacent frames. Using the lower
211+ # neighbor as the baseline prevents one unusually sharp neighbor from
212+ # making an otherwise ordinary frame look like an outlier.
213+ expected_score = min (scores [position - 1 ], scores [position + 1 ])
214+ dip_magnitudes [position ] = max (0.0 , expected_score - scores [position ])
215+ expected_raw_score = expm1 (expected_score )
216+ if expected_raw_score > 0 :
217+ relative_drops [position ] = max (
218+ 0.0 ,
219+ (expected_raw_score - raw_scores [position ]) / expected_raw_score ,
220+ )
207221
208222 actual_window_size = max (OUTLIER_MIN_WINDOW_SIZE , window_size )
209223 if actual_window_size % 2 == 0 :
@@ -221,38 +235,33 @@ def _outlier_positions(
221235 )
222236 outliers : Set [int ] = set ()
223237
224- for position , current_score in enumerate (scores ):
238+ for position , dip_magnitude in enumerate (dip_magnitudes ):
239+ if position == 0 or position == len (scores ) - 1 :
240+ continue
225241 window_start = max (0 , position - half_window )
226242 window_end = min (len (group ), position + half_window + 1 )
227- neighbor_scores = scores [window_start :position ] + scores [position + 1 : window_end ]
243+ neighbor_dips = (
244+ dip_magnitudes [window_start :position ]
245+ + dip_magnitudes [position + 1 : window_end ]
246+ )
228247 required_neighbors = max (2 , min_neighbors )
229- if len (neighbor_scores ) < required_neighbors :
248+ if len (neighbor_dips ) < required_neighbors :
230249 continue
231- neighbor_median = median (neighbor_scores )
232- raw_neighbor_scores = (
233- raw_scores [window_start :position ]
234- + raw_scores [position + 1 : window_end ]
235- )
236- raw_neighbor_median = median (raw_neighbor_scores )
250+ neighbor_median = median (neighbor_dips )
237251 absolute_deviations = [
238- abs (score - neighbor_median ) for score in neighbor_scores
252+ abs (neighbor_dip - neighbor_median ) for neighbor_dip in neighbor_dips
239253 ]
240254 median_absolute_deviation = median (absolute_deviations )
241255 robust_scale = median_absolute_deviation * 1.4826
242256 if robust_scale == 0 :
243257 robust_scale = max (abs (neighbor_median ) * 0.01 , 1e-9 )
244258
245- deficit = neighbor_median - current_score
246- robust_deficit = deficit / robust_scale
247- relative_drop = (
248- (raw_neighbor_median - raw_scores [position ]) / raw_neighbor_median
249- if raw_neighbor_median > 0
250- else 0.0
251- )
259+ excess_dip = dip_magnitude - neighbor_median
260+ robust_excess = excess_dip / robust_scale
252261 if (
253- deficit > 0
254- and relative_drop >= minimum_relative_drop
255- and robust_deficit > robust_threshold
262+ excess_dip > 0
263+ and relative_drops [ position ] >= minimum_relative_drop
264+ and robust_excess > robust_threshold
256265 ):
257266 outliers .add (position )
258267 return outliers
0 commit comments