Skip to content

Commit 3b9672f

Browse files
author
arch
committed
refactoring
1 parent 7019bea commit 3b9672f

File tree

3 files changed

+181
-56
lines changed

3 files changed

+181
-56
lines changed

docs/app/docs/user-guide/ofs-integration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Currently we use a hacky lua script to communicate between the Python Funscript
99
3. Download the **latest** `funscript_generator.lua` script from [github release page](https://github.com/michael-mueller-git/Python-Funscript-Editor/releases).
1010
4. Move the downloaded `funscript_generator.lua` script to `data/lua` in your OFS directory.
1111
5. Open the `funscript_generator.lua` file and adjust the `Settings.FunscriptGenerator` and `Settings.TmpFile` variable.
12-
- **NOTE:** You have to use use `/` or `\\` for the `\` symbols in your path!
12+
- **NOTE:** If you copy the path from Windows explorer to the variable in the lua script you have to escape the `\` symbol. This mean you have to put another `\` in front of each `\` so that there are always `\\` pairs. As an alternative, you can use a simple `/` instead of the single `\` Symbole that also work.
1313
- `Settings.FunscriptGenerator`: Point to the extracted Python Funscript Editor program (better double check the complete path string to avoid errors later on).
1414
- `Settings.TmpFile`: Specifies a temporary file location where to store the result (must be a file not a directory!). The file does not have to exist at the moment. The specified file will be generated from the Python Funscript Editor and will be overwritten automatically at the next time the generator is started!
1515
6. Now launch OFS.

funscript_editor/algorithms/funscriptgenerator.py

Lines changed: 142 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,35 @@ class FunscriptGeneratorParameter:
3232
start_frame: int = 0 # default is video start (input: set current video position)
3333
end_frame: int = -1 # default is video end (-1)
3434
track_men: bool = True # set by userinput at start (message box)
35-
skip_frames: int = max((0, int(HYPERPARAMETER['skip_frames'])))
35+
36+
# Settings
3637
max_playback_fps: int = max((0, int(SETTINGS['max_playback_fps'])))
3738
direction: str = SETTINGS['tracking_direction']
3839
use_zoom: bool = SETTINGS['use_zoom']
39-
shift_bottom_points: int = int(HYPERPARAMETER['shift_bottom_points'])
40-
shift_top_points: int = int(HYPERPARAMETER['shift_top_points'])
41-
top_points_offset: float = float(HYPERPARAMETER['top_points_offset'])
42-
bottom_points_offset: float = float(HYPERPARAMETER['bottom_points_offset'])
4340
zoom_factor: float = max((1.0, float(SETTINGS['zoom_factor'])))
44-
top_threshold: float = float(HYPERPARAMETER['top_threshold'])
45-
bottom_threshold: float = float(HYPERPARAMETER['bottom_threshold'])
4641
preview_scaling: float = float(SETTINGS['preview_scaling'])
4742
projection: str = str(SETTINGS['projection']).lower()
4843
use_kalman_filter: bool = SETTINGS['use_kalman_filter']
4944

45+
# General
46+
skip_frames: int = max((0, int(HYPERPARAMETER['skip_frames'])))
47+
48+
# y-Movement
49+
shift_bottom_points: int = int(HYPERPARAMETER['shift_bottom_points'])
50+
shift_top_points: int = int(HYPERPARAMETER['shift_top_points'])
51+
bottom_points_offset: float = float(HYPERPARAMETER['bottom_points_offset'])
52+
top_points_offset: float = float(HYPERPARAMETER['top_points_offset'])
53+
bottom_threshold: float = float(HYPERPARAMETER['bottom_threshold'])
54+
top_threshold: float = float(HYPERPARAMETER['top_threshold'])
55+
56+
# x-Movement
57+
shift_left_points: int = int(HYPERPARAMETER['shift_left_points'])
58+
shift_right_points: int = int(HYPERPARAMETER['shift_right_points'])
59+
left_points_offset: float = float(HYPERPARAMETER['left_points_offset'])
60+
right_points_offset: float = float(HYPERPARAMETER['right_points_offset'])
61+
left_threshold: float = float(HYPERPARAMETER['left_threshold'])
62+
right_threshold: float = float(HYPERPARAMETER['right_threshold'])
63+
5064

5165
class FunscriptGenerator(QtCore.QThread):
5266
""" Funscript Generator Thread
@@ -74,7 +88,8 @@ def __init__(self,
7488
self.tracking_fps = []
7589
self.score = {
7690
'x': [],
77-
'y': []
91+
'y': [],
92+
'd': []
7893
}
7994
self.bboxes = {
8095
'Men': [],
@@ -283,12 +298,15 @@ def calculate_score(self) -> None:
283298
if self.params.track_men:
284299
self.score['x'] = [m[0] - w[0] for w, m in zip(self.bboxes['Woman'], self.bboxes['Men'])]
285300
self.score['y'] = [m[1] - w[1] for w, m in zip(self.bboxes['Woman'], self.bboxes['Men'])]
301+
self.score['d'] = [np.sqrt(np.sum((np.array(m[:2]) - np.array(w[:2])) ** 2, axis=0)) for w, m in zip(bboxes['Woman'], bboxes['Men'])]
286302
else:
287303
self.score['x'] = [max([x[0] for x in self.bboxes['Woman']]) - w[0] for w in self.bboxes['Woman']]
288304
self.score['y'] = [max([x[1] for x in self.bboxes['Woman']]) - w[1] for w in self.bboxes['Woman']]
305+
# TODO: how to calc d?
289306

290307
self.score['x'] = sp.scale_signal(self.score['x'], 0, 100)
291308
self.score['y'] = sp.scale_signal(self.score['y'], 0, 100)
309+
self.score['d'] = sp.scale_signal(self.score['d'], 0, 100)
292310

293311

294312
def scale_score(self, status: str, direction : str = 'y') -> None:
@@ -307,7 +325,10 @@ def scale_score(self, status: str, direction : str = 'y') -> None:
307325
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
308326
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
309327

310-
if direction == 'x':
328+
if direction == 'd':
329+
min_frame = np.argmin(np.array(self.score['d'])) + self.params.start_frame
330+
max_frame = np.argmax(np.array(self.score['d'])) + self.params.start_frame
331+
elif direction == 'x':
311332
min_frame = np.argmin(np.array(self.score['x'])) + self.params.start_frame
312333
max_frame = np.argmax(np.array(self.score['x'])) + self.params.start_frame
313334
else:
@@ -337,19 +358,35 @@ def scale_score(self, status: str, direction : str = 'y') -> None:
337358
imgMin = cv2.resize(imgMin, None, fx=scale, fy=scale)
338359
imgMax = cv2.resize(imgMax, None, fx=scale, fy=scale)
339360

361+
if direction == 'y':
362+
title_min = "Bottom"
363+
elif direction == 'x':
364+
title_min = "Left"
365+
else:
366+
title_min = "Minimum"
367+
368+
if direction == 'y':
369+
title_max = "Top"
370+
elif direction == 'x':
371+
title_max = "Right"
372+
else:
373+
title_max = "Maxmimum"
374+
340375
(desired_min, desired_max) = self.min_max_selector(
341376
image_min = imgMin,
342377
image_max = imgMax,
343378
info = status,
344-
title_min = str("Bottom" if direction != "x" else "Left"),
345-
title_max = ("Top" if direction != "x" else "Right")
379+
title_min = title_min,
380+
title_max = title_max
346381
)
347382
else:
348383
self.logger.warning("Determine min and max failed")
349384
desired_min = 0
350385
desired_max = 99
351386

352-
if direction == 'x':
387+
if direction == 'd':
388+
self.score['d'] = sp.scale_signal(self.score['d'], desired_min, desired_max)
389+
elif direction == 'x':
353390
self.score['x'] = sp.scale_signal(self.score['x'], desired_min, desired_max)
354391
else:
355392
self.score['y'] = sp.scale_signal(self.score['y'], desired_min, desired_max)
@@ -729,15 +766,29 @@ def apply_shift(self, frame_number, position: str) -> int:
729766
Args:
730767
position (str): is max or min
731768
"""
732-
if position in ['max', 'top'] and self.params.direction != 'x':
733-
if frame_number >= -1*self.params.shift_top_points \
734-
and frame_number + self.params.shift_top_points < len(self.score['y']): \
735-
return self.params.start_frame + frame_number + self.params.shift_top_points
769+
if self.params.direction == 'd':
770+
shift_a = 0
771+
elif self.params.direction == 'x':
772+
shift_a = self.params.shift_right_points
773+
else:
774+
shift_a = self.params.shift_top_points
775+
776+
if self.params.direction == 'd':
777+
shift_b = 0
778+
elif self.params.direction == 'x':
779+
shift_b = self.params.shift_left_points
780+
else:
781+
shift_b = self.params.shift_bottom_points
736782

737-
if position in ['min', 'bottom'] and self.params.direction != 'x':
738-
if frame_number >= -1*self.params.shift_bottom_points \
739-
and frame_number + self.params.shift_bottom_points < len(self.score['y']): \
740-
return self.params.start_frame + frame_number + self.params.shift_bottom_points
783+
if position in ['max', 'top', 'right'] :
784+
if frame_number >= -1*shift_a \
785+
and frame_number + shift_a < len(self.score['y']): \
786+
return self.params.start_frame + frame_number + shift_a
787+
788+
if position in ['min', 'bottom', 'left']:
789+
if frame_number >= -1*shift_b \
790+
and frame_number + shift_b < len(self.score['y']): \
791+
return self.params.start_frame + frame_number + shift_b
741792

742793
return self.params.start_frame + frame_number
743794

@@ -751,16 +802,28 @@ def get_score_with_offset(self, idx_dict) -> list:
751802
Returns:
752803
list: score with offset
753804
"""
754-
if self.params.direction == 'x':
755-
return self.score['x']
805+
if self.params.direction == 'd':
806+
offset_a = 0
807+
elif self.params.direction == 'x':
808+
offset_a = self.params.right_points_offset
809+
else:
810+
offset_a = self.params.top_points_offset
811+
812+
if self.params.direction == 'd':
813+
offset_b = 0
814+
elif self.params.direction == 'x':
815+
offset_b = self.params.left_points_offset
816+
else:
817+
offset_b = self.params.bottom_points_offset
756818

757819
score = copy.deepcopy(self.score['y'])
758820
score_min, score_max = min(score), max(score)
759-
for idx in idx_dict['min']:
760-
score[idx] = max(( score_min, min((score_max, score[idx] + self.params.bottom_points_offset)) ))
761821

762822
for idx in idx_dict['max']:
763-
score[idx] = max(( score_min, min((score_max, score[idx] + self.params.top_points_offset)) ))
823+
score[idx] = max(( score_min, min((score_max, score[idx] + offset_a)) ))
824+
825+
for idx in idx_dict['min']:
826+
score[idx] = max(( score_min, min((score_max, score[idx] + offset_b)) ))
764827

765828
return score
766829

@@ -786,55 +849,79 @@ def apply_kalman_filter(self) -> None:
786849
self.bboxes['Men'][idx] = (prediction[0], prediction[1], item[2], item[3])
787850

788851

789-
def run(self) -> None:
790-
""" The Funscript Generator Thread Function """
791-
# NOTE: score['y'] and score['x'] should have the same number size so it should be enouth to check one score length
792-
with Listener(on_press=self.on_key_press) as listener:
793-
status = self.tracking()
794-
795-
if self.params.use_kalman_filter:
796-
self.apply_kalman_filter()
797-
798-
if len(self.score['y']) >= HYPERPARAMETER['min_frames']:
799-
self.logger.info("Scale score")
800-
if self.params.direction != 'x':
801-
self.scale_score(status, direction='y')
802-
else:
803-
self.scale_score(status, direction='x')
804-
805-
if len(self.score['y']) < HYPERPARAMETER['min_frames']:
806-
self.finished(status + ' -> Tracking time insufficient', False)
807-
return
852+
def determin_change_points(self) -> dict:
853+
""" Determine all change points
808854
855+
Returns:
856+
dict: all local max and min points in score {'min':[idx1, idx2, ...], 'max':[idx1, idx2, ...]}
857+
"""
809858
self.logger.info("Determine local max and min")
810-
if self.params.direction != 'x':
811-
idx_dict = sp.get_local_max_and_min_idx(self.score['y'], self.video_info.fps)
812-
else:
859+
if self.params.direction == 'd':
860+
idx_dict = sp.get_local_max_and_min_idx(self.score['d'], self.video_info.fps)
861+
elif self.params.direction == 'x':
813862
idx_dict = sp.get_local_max_and_min_idx(self.score['x'], self.video_info.fps)
863+
else:
864+
idx_dict = sp.get_local_max_and_min_idx(self.score['y'], self.video_info.fps)
865+
return idx_dict
814866

815-
idx_list = [x for k in ['min', 'max'] for x in idx_dict[k]]
816-
idx_list.sort()
817867

818-
if False:
819-
self.plot_scores('debug_001.png')
820-
if self.params.direction != 'x':
821-
self.plot_y_score('debug_002.png', idx_list)
868+
def create_funscript(self, idx_dict: dict) -> None:
869+
""" Generate the Funscript
822870
871+
Args:
872+
idx_dict (dict): dictionary with all local max and min points in score
873+
{'min':[idx1, idx2, ...], 'max':[idx1, idx2, ...]}
874+
"""
823875
output_score = self.get_score_with_offset(idx_dict)
876+
877+
if self.params.direction == 'd':
878+
threshold_a = 0
879+
elif self.params.direction == 'x':
880+
threshold_a = self.params.left_threshold
881+
else:
882+
threshold_a = self.params.bottom_threshold
883+
884+
if self.params.direction == 'd':
885+
threshold_b = 0
886+
elif self.params.direction == 'x':
887+
threshold_b = self.params.right_threshold
888+
else:
889+
threshold_b = self.params.top_threshold
890+
824891
for idx in idx_dict['min']:
825892
self.funscript.add_action(
826893
min(output_score) \
827-
if output_score[idx] < min(output_score) + self.params.bottom_threshold \
894+
if output_score[idx] < min(output_score) + threshold_a \
828895
else round(output_score[idx]),
829896
FFmpegStream.frame_to_millisec(self.apply_shift(idx, 'min'), self.video_info.fps)
830897
)
831898

832899
for idx in idx_dict['max']:
833900
self.funscript.add_action(
834901
max(output_score) \
835-
if output_score[idx] > max(output_score) - self.params.top_threshold \
902+
if output_score[idx] > max(output_score) - threshold_b \
836903
else round(output_score[idx]),
837904
FFmpegStream.frame_to_millisec(self.apply_shift(idx, 'max'), self.video_info.fps)
838905
)
839906

907+
908+
def run(self) -> None:
909+
""" The Funscript Generator Thread Function """
910+
# NOTE: score['y'] and score['x'] should have the same number size so it should be enouth to check one score length
911+
with Listener(on_press=self.on_key_press) as listener:
912+
status = self.tracking()
913+
914+
if self.params.use_kalman_filter:
915+
self.apply_kalman_filter()
916+
917+
if len(self.score['y']) >= HYPERPARAMETER['min_frames']:
918+
self.logger.info("Scale score")
919+
self.scale_score(status, direction=self.params.direction)
920+
921+
if len(self.score['y']) < HYPERPARAMETER['min_frames']:
922+
self.finished(status + ' -> Tracking time insufficient', False)
923+
return
924+
925+
idx_dict = self.determin_change_points()
926+
self.create_funscript(idx_dict)
840927
self.finished(status, True)

funscript_editor/config/hyperparameter.yaml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Python Funscript Editor hyperparameter config
22

3+
###########
4+
# General #
5+
###########
6+
37
# This parameter specifies how many frames are skipped and interpolated during tracking.
48
# Increase this parameter to improve the processing speed on slow hardware. But higher
59
# values result in poorer predictions!
@@ -13,6 +17,11 @@ avg_sec_for_local_min_max_extraction: 1.9
1317
# ensure there is at leas two strokes in the tracking result.
1418
min_frames: 100
1519

20+
21+
##############
22+
# y-Movement #
23+
##############
24+
1625
# Shift predicted top points by given frame number. Positive values delay the position
1726
# and negative values result in an earlier position.
1827
shift_top_points: 0
@@ -36,3 +45,32 @@ top_threshold: 2.5
3645
# Define the bottom threshold. All bottom points lower than (min + threshold) will be set to
3746
# the specified min value. Set 0.0 to disable this function.
3847
bottom_threshold: 2.5
48+
49+
50+
##############
51+
# x-Movement #
52+
##############
53+
54+
# Shift predicted right points by given frame number. Positive values delay the position
55+
# and negative values result in an earlier position.
56+
shift_right_points: 0
57+
58+
# Shift predicted left points by given frame number. Positive values delay the position
59+
# and negative values result in an earlier position.
60+
shift_left_points: 0
61+
62+
# An fix offset to the left points (positive values move the point right and negative values
63+
# move the point left). The offset respect the user defined upper and lower limit.
64+
left_points_offset: 0.0
65+
66+
# An fix offset to the right points (positive values move the point right and negative values
67+
# move the point left). The offset respect the user defined upper and lower limit.
68+
right_points_offset: 0.0
69+
70+
# Define the right threshold. All right points greater than (max - threshold) will be set to
71+
# the specified max value. Set 0.0 to disable this function.
72+
right_threshold: 0.0
73+
74+
# Define the left threshold. All left points lower than (min + threshold) will be set to
75+
# the specified min value. Set 0.0 to disable this function.
76+
left_threshold: 0.0

0 commit comments

Comments
 (0)