Skip to content

Commit 766845e

Browse files
author
arch
committed
improve change point detection (#10)
1 parent 42bf35b commit 766845e

File tree

6 files changed

+123
-7
lines changed

6 files changed

+123
-7
lines changed

docs/app/docs/user-guide/build.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Build from Source
1+
# Build Application from Source
22

33
For Windows user i recommend to use the release version from [github release page](https://github.com/michael-mueller-git/Python-Funscript-Editor/releases)
44

@@ -55,3 +55,13 @@ python3 funscript-editor.py
5555
By default the latest code is used which may contain bugs. So you maybe want to switch to the latest release version with `` git checkout $(git describe --tags `git rev-list --tags --max-count=1`)``.
5656

5757
You can always update the application to the latest version with `git checkout main && git pull` inside the repository directory.
58+
59+
## Arch Linux and Arch-Based Distributions
60+
61+
My development platform is Arch Linux. The code should run therefore without problems. When installing the dependencies, make sure you install the following packages from the repositories, as the wrapper will have problems if the versions differ from the pip packages and the local C, C++ libraries.
62+
63+
```bash
64+
sudo pacman -Sy python-opencv python-pyqt5
65+
```
66+
67+
All other required python packages can be installed from pip.

docs/app/docs/user-guide/config.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Config Files:
2727
- `notification_sound` (str) Specify the wav file to play when tracking finished (write 'off' to disable the sound notification).
2828
- `tracking_lost_time` (int) Time in milliseconds at which the tracking is stopped if the selected feature is not found.
2929
- `scene_detector` (str): Specify the scene detector algorithm. Available options: `'CSV'`, `'CONTENT'`, `'THRESHOLD'`
30+
- `raw_output` (bool): Output an position for each frame. This option disable post processing!
31+
- `additional_changepoints` (bool): Insert change points at high second derivative
3032

3133
#### `hyperparameter.yaml`
3234

@@ -35,12 +37,14 @@ Config Files:
3537
- `local_max_delta_in_percent` (float): Specify the maximum deviation for a max point in percent (recommend range: `0.0 - 10.0`)
3638
- `local_min_delta_in_percent` (float): Specify the maximum deviation for a min point in percent (recommend range: `0.0 - 10.0`)
3739
- `min_frames` (int): Specify the minimum required frames for the tracking. Wee need this parameter to ensure there is at leas two strokes in the tracking result.
40+
- `changepoint_detection_threshold` (float): threshold value tor get additional change points by comparing second derivative with the rolling standard deviation and given threshold
41+
- `additional_changepoints_merge_threshold_in_ms` (int): threshold value in milliseconds to merge additional change points
3842
- `shift_top_points` (int): Shift predicted top points by given frame number. Positive values delay the position and negative values result in an earlier position.
3943
- `shift_bottom_points` (int): Shift predicted bottom points by given frame number. Positive values delay the position and negative values result in an earlier position.
4044
- `top_points_offset` (float): An fix offset to the top points (positive values move the point up and negative values move the point down). The offset respect the user defined upper and lower limit.
4145
- `bottom_points_offset` (float): An fix offset to the bottom points (positive values move the point up and negative values move the point down). The offset respect the user defined upper and lower limit.
4246
- `top_threshold` (float): Define the top threshold. All top points greater than `(max - threshold)` will be set to the specified max value. Set 0.0 to disable this function.
4347
- `bottom_threshold` (float): Define the bottom threshold. All bottom points lower than `(min + threshold)` will be set to the specified min value. Set 0.0 to disable this function.
44-
- `min_scene_len` (int): Specify the miniimum scene length in seconds for the scene detector
45-
- `scene_content_detector_threshold` (float): Thresold value for the content detector to detect an scene change
46-
- `scene_threshold_detector_threshold` (int): Thresold value for the threshold detector to detect an scene change
48+
- `min_scene_len` (int): Specify the minimum scene length in seconds for the scene detector
49+
- `scene_content_detector_threshold` (float): Threshold value for the content detector to detect an scene change
50+
- `scene_threshold_detector_threshold` (int): Threshold value for the threshold detector to detect an scene change

funscript-editor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
if __name__ == '__main__':
77
try:
88
main()
9+
except SystemExit as e:
10+
pass
911
except:
1012
traceback.print_exc()
1113
input()

funscript_editor/algorithms/signalprocessing.py

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
""" Signal Processing Algorithms """
22

33
import numpy as np
4-
from funscript_editor.utils.config import HYPERPARAMETER
4+
import logging
5+
from funscript_editor.utils.config import HYPERPARAMETER, SETTINGS
56

67
def scale_signal(signal :list, lower: float = 0, upper: float = 99) -> list:
78
""" Scale an signal (list of float or int) between given lower and upper value
@@ -66,6 +67,69 @@ def moving_average(x :list, w: int) -> list:
6667
+[avg[-1] for _ in range(len(avg)+w,len(x))]
6768

6869

70+
def moving_standard_deviation(x: list, w: int) -> list:
71+
""" Get 2 seconds moving standard deviation of given signal
72+
73+
Args:
74+
x (list): input signal
75+
w (int): window size
76+
77+
Returns:
78+
list: moving standard deviation
79+
"""
80+
w = round(w)
81+
std = [np.std(x[ii-w:ii+w+1]) for ii in range(w,len(x)-w)]
82+
# TODO use extrapolation function
83+
return [std[0] for _ in range(w)]\
84+
+list(std)\
85+
+[std[-1] for _ in range(w)]
86+
87+
88+
def second_derivative(x: list, w: int = 1) -> list:
89+
""" Get second derivative of given signal
90+
91+
Args:
92+
x (list): input signal
93+
w (int): window size for signal smothing
94+
95+
Returns:
96+
list: second derivative
97+
"""
98+
if w < 0: w = 1
99+
if w % 2 == 0: w += 1
100+
return moving_average(np.diff(moving_average(np.diff(x, 1).tolist(), w), 1).tolist(), w)
101+
102+
103+
def get_changepoints(score: list, fps: int, alpha: float) -> list:
104+
""" Get change points by comparing second derivative with the rolling standard deviation
105+
106+
Args:
107+
score (list): list with float or int signal values
108+
fps (int): rounded fps of the video
109+
alpha (float): threshold value for standard deviation comparing
110+
111+
Returns:
112+
list: idx list with changepoints
113+
"""
114+
dx2 = second_derivative(score)
115+
std = moving_standard_deviation(dx2, round(fps * HYPERPARAMETER['avg_sec_for_local_min_max_extraction']))
116+
dx2_abs = abs(np.array(dx2))
117+
tmp_max_idx = -1
118+
changepoints = []
119+
for pos in range(len(dx2_abs)):
120+
if abs(dx2_abs[pos]) > alpha*std[pos]:
121+
if tmp_max_idx < 0: tmp_max_idx = pos
122+
elif dx2_abs[tmp_max_idx] <= dx2_abs[pos]: tmp_max_idx = pos
123+
elif tmp_max_idx >= 0:
124+
changepoints.append(tmp_max_idx)
125+
tmp_max_idx = -1
126+
127+
#if tmp_max_idx > 0:
128+
# changepoints.append((tmp_max_idx))
129+
130+
return changepoints
131+
132+
69133
def get_local_max_and_min_idx(score :list, fps: int, shift_min :int = 0, shift_max :int = 0) -> dict:
70134
""" Get the local max and min positions in given signal
71135
@@ -78,6 +142,7 @@ def get_local_max_and_min_idx(score :list, fps: int, shift_min :int = 0, shift_m
78142
Returns:
79143
dict: dict with 2 lists with all local max and min indexes ({'min':[], 'max':[]})
80144
"""
145+
logger = logging.getLogger("changepoints")
81146
avg = moving_average(score, w=round(fps * HYPERPARAMETER['avg_sec_for_local_min_max_extraction']))
82147
changepoints = {'min': [], 'max': []}
83148
tmp_min_idx, tmp_max_idx = -1, -1
@@ -96,6 +161,12 @@ def get_local_max_and_min_idx(score :list, fps: int, shift_min :int = 0, shift_m
96161
changepoints['max'].append(tmp_max_idx)
97162
tmp_max_idx = -1
98163

164+
#if tmp_min_idx > 0:
165+
# changepoints['min'].append((tmp_min_idx))
166+
167+
#if tmp_max_idx > 0:
168+
# changepoints['max'].append((tmp_max_idx))
169+
99170
delta_max = (max(score) - min(score)) * 0.01 * min((10.0, float(HYPERPARAMETER['local_max_delta_in_percent'])))
100171
delta_min = (max(score) - min(score)) * 0.01 * min((10.0, float(HYPERPARAMETER['local_min_delta_in_percent'])))
101172

@@ -112,6 +183,26 @@ def get_local_max_and_min_idx(score :list, fps: int, shift_min :int = 0, shift_m
112183
new_pos += 1
113184
changepoints['max'][k] = new_pos
114185

186+
187+
if SETTINGS['additional_changepoints']:
188+
logger.info("add additional change points")
189+
merge_threshold = max(1, round(fps * float(HYPERPARAMETER['additional_changepoints_merge_threshold_in_ms']) / 1000.0))
190+
additional_changepoints = get_changepoints(score, fps, float(HYPERPARAMETER['changepoint_detection_threshold']))
191+
for cp_idx in additional_changepoints:
192+
if len(list(filter(lambda x: abs(cp_idx - x) <= merge_threshold, changepoints['min']))) > 0:
193+
continue
194+
195+
if len(list(filter(lambda x: abs(cp_idx - x) <= merge_threshold, changepoints['max']))) > 0:
196+
continue
197+
198+
if score[cp_idx] < avg[cp_idx]:
199+
logger.debug("add additional min changepoint at idx %d", cp_idx)
200+
changepoints['min'].append(cp_idx)
201+
else:
202+
logger.debug("add additional max changepoint at idx %d", cp_idx)
203+
changepoints['max'].append(cp_idx)
204+
205+
115206
# apply manual shift
116207
if shift_min != 0:
117208
for k, idx in enumerate(changepoints['min']):

funscript_editor/config/hyperparameter.yaml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ local_min_delta_in_percent: 0.1
2323
# ensure there is at leas two strokes in the tracking result.
2424
min_frames: 90
2525

26+
# threshold value tor get additional change points by comparing second derivative with the rolling standard deviation and given threshold
27+
changepoint_detection_threshold: 1.2
28+
29+
# threshold value in milliseconds to merge additional change points
30+
additional_changepoints_merge_threshold_in_ms: 100
31+
2632

2733
##################
2834
# Scene Detector #
@@ -31,10 +37,10 @@ min_frames: 90
3137
# Specify the miniimum scene length in seconds
3238
min_scene_len: 3
3339

34-
# Thresold value for the content detector to detect an scene change
40+
# Threshold value for the content detector to detect an scene change
3541
scene_content_detector_threshold: 30.0
3642

37-
# Thresold value for the threshold detector to detect an scene change
43+
# Threshold value for the threshold detector to detect an scene change
3844
scene_threshold_detector_threshold: 12
3945

4046

funscript_editor/config/settings.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ scene_detector: 'CSV'
3131

3232
# Output an position for each frame. This option disable post processing!
3333
raw_output: False
34+
35+
# Insert change points at high second derivative
36+
additional_changepoints: True

0 commit comments

Comments
 (0)