Skip to content

Commit 1d9a738

Browse files
author
arch
committed
add code for equirectangular tracking
1 parent 452a5b7 commit 1d9a738

File tree

8 files changed

+359
-13
lines changed

8 files changed

+359
-13
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ A Python program that use Computer Vision to predict the funscript actions. Most
77
## Documentation
88

99
The application documentation is located in [`./docs/app/docs`](https://github.com/michael-mueller-git/Python-Funscript-Editor/blob/main/docs/app/docs)
10+
11+
## License
12+
13+
Distributed under the MIT License. See `LICENSE` for more information.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Config Files:
2828
#### `settings.yaml`
2929

3030
- `use_zoom` (bool): Enable or disable an additional step to zoom in the Video before selecting a tracking feature for the Woman or Men.
31+
- `use_equirectangular` (bool): Convert video in normal perspective view before apply tracking. This should improve the tracking at the border of videos, because there is the distortion very high.
3132
- `tracking_direction` (str): Specify the tracking direction. Allowed values are `'x'` and `'y'`.
3233
- `max_playback_fps` (int): Limit the max player speed in the tracking preview window (0 = disable limit)
3334

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
""" Methods to split equirectangular panorama into normal perspective view. """
2+
3+
import os
4+
import sys
5+
import cv2
6+
import time
7+
import logging
8+
9+
from threading import Thread
10+
from queue import Queue
11+
12+
import numpy as np
13+
14+
from funscript_editor.data.filevideostream import FileVideoStream
15+
16+
17+
class Equirectangular:
18+
""" Python Class to split equirectangular panorama into normal perspective view.
19+
20+
NOTE:
21+
We use the same api as the FileVideoStream to allow MITM
22+
23+
Args:
24+
video_stream (FileVideoStream): the file video stream instance
25+
FOV (int): perspective FOV
26+
THETA (int): left/right angle in degree (right direction is positive, left direction is negative)
27+
PHI (int) up/down angle in degree (up direction positive, down direction negative)
28+
height (int): output image height
29+
width (int): output image width
30+
RADIUS (int, optional): sphere radius
31+
"""
32+
33+
def __init__(self,
34+
video_stream :FileVideoStream,
35+
FOV: int,
36+
THETA: int,
37+
PHI :int,
38+
height :int,
39+
width :int,
40+
queue_size :int = 256):
41+
self.video_stream = video_stream
42+
self.FOV = FOV
43+
self.THETA = THETA
44+
self.PHI = PHI
45+
self.height = height
46+
self.width = width
47+
self.stopped = False
48+
self.sleep_time = 0.001
49+
50+
self.Q = Queue(maxsize=queue_size)
51+
self.thread = Thread(target=self.run, args=())
52+
self.thread.daemon = True
53+
self.thread.start()
54+
55+
56+
@staticmethod
57+
def get_perspective(
58+
img :np.ndarray,
59+
FOV :int,
60+
THETA :int,
61+
PHI :int,
62+
height :int,
63+
width :int,
64+
RADIUS :int = 128) -> np.ndarray:
65+
"""
66+
Get a normal perspective view from a panorama view.
67+
68+
Args:
69+
img (str, or opencv image object): path to image or opencv image data
70+
FOV (int): perspective FOV
71+
THETA (int): left/right angle in degree (right direction is positive, left direction is negative)
72+
PHI (int) up/down angle in degree (up direction positive, down direction negative)
73+
height (int): output image height
74+
width (int): output image width
75+
RADIUS (int, optional): sphere radius
76+
77+
Returns:
78+
array: opencv image data
79+
"""
80+
input_image = cv2.imread(img, cv2.IMREAD_COLOR) if isinstance(img, str) else img
81+
[input_height, input_width, _] = input_image.shape
82+
83+
equ_h = input_height
84+
equ_w = input_width
85+
equ_cx = (equ_w - 1) / 2.0
86+
equ_cy = (equ_h - 1) / 2.0
87+
88+
wFOV = FOV if FOV < 180 else 179
89+
hFOV = float(height) / width * wFOV
90+
91+
c_x = (width - 1) / 2.0
92+
c_y = (height - 1) / 2.0
93+
94+
wangle = (180 - wFOV) / 2.0
95+
w_len = 2 * RADIUS * np.sin(np.radians(wFOV / 2.0)) / np.sin(np.radians(wangle))
96+
w_interval = w_len / (width - 1)
97+
98+
hangle = (180 - hFOV) / 2.0
99+
h_len = 2 * RADIUS * np.sin(np.radians(hFOV / 2.0)) / np.sin(np.radians(hangle))
100+
h_interval = h_len / (height - 1)
101+
x_map = np.zeros([height, width], np.float32) + RADIUS
102+
y_map = np.tile((np.arange(0, width) - c_x) * w_interval, [height, 1])
103+
z_map = -np.tile((np.arange(0, height) - c_y) * h_interval, [width, 1]).T
104+
D = np.sqrt(x_map**2 + y_map**2 + z_map**2)
105+
xyz = np.zeros([height, width, 3], np.float)
106+
xyz[:, :, 0] = (RADIUS / D * x_map)[:, :]
107+
xyz[:, :, 1] = (RADIUS / D * y_map)[:, :]
108+
xyz[:, :, 2] = (RADIUS / D * z_map)[:, :]
109+
110+
y_axis = np.array([0.0, 1.0, 0.0], np.float32)
111+
z_axis = np.array([0.0, 0.0, 1.0], np.float32)
112+
[R1, _] = cv2.Rodrigues(z_axis * np.radians(THETA))
113+
[R2, _] = cv2.Rodrigues(np.dot(R1, y_axis) * np.radians(-PHI))
114+
115+
xyz = xyz.reshape([height * width, 3]).T
116+
xyz = np.dot(R1, xyz)
117+
xyz = np.dot(R2, xyz).T
118+
lat = np.arcsin(xyz[:, 2] / RADIUS)
119+
lon = np.zeros([height * width], np.float)
120+
theta = np.arctan(xyz[:, 1] / xyz[:, 0])
121+
idx1 = xyz[:, 0] > 0
122+
idx2 = xyz[:, 1] > 0
123+
124+
idx3 = ((1 - idx1) * idx2).astype(np.bool)
125+
idx4 = ((1 - idx1) * (1 - idx2)).astype(np.bool)
126+
127+
lon[idx1] = theta[idx1]
128+
lon[idx3] = theta[idx3] + np.pi
129+
lon[idx4] = theta[idx4] - np.pi
130+
131+
lon = lon.reshape([height, width]) / np.pi * 180
132+
lat = -lat.reshape([height, width]) / np.pi * 180
133+
lon = lon / 180 * equ_cx + equ_cx
134+
lat = lat / 90 * equ_cy + equ_cy
135+
136+
return cv2.remap(input_image,
137+
lon.astype(np.float32),
138+
lat.astype(np.float32),
139+
cv2.INTER_CUBIC,
140+
borderMode=cv2.BORDER_WRAP)
141+
142+
143+
def stop(self) -> None:
144+
""" Stop equirectangular stream """
145+
self.video_stream.stop()
146+
self.stopped = True
147+
# wait until stream resources are released
148+
self.thread.join()
149+
150+
151+
def read(self) -> np.ndarray:
152+
""" Get next frame from equirectangular stream
153+
154+
Returns:
155+
np.ndarray: opencv image data
156+
"""
157+
return self.Q.get()
158+
159+
160+
def isOpen(self) -> bool:
161+
""" Check if equirectangular stream is open or a frame is still available in the buffer
162+
163+
Returns:
164+
bool: True if equirectangular strem is open or a frame is still available in the buffer else False
165+
"""
166+
return self.__more() or not self.stopped
167+
168+
169+
def __more(self) -> bool:
170+
""" Check if frames in the queue are available
171+
172+
Returns:
173+
bool: True if a frame is available else False
174+
"""
175+
tries = 0
176+
while self.Q.qsize() == 0 and not self.stopped and tries < 5:
177+
time.sleep(self.sleep_time)
178+
tries += 1
179+
180+
return self.Q.qsize() > 0
181+
182+
183+
def run(self) -> None:
184+
""" Function to transform the frames from the file video stream into a queue """
185+
while not self.stopped and self.video_stream.isOpen():
186+
if self.Q.full():
187+
time.sleep(self.sleep_time)
188+
else:
189+
frame = self.video_stream.read()
190+
frame = Equirectangular.get_perspective(
191+
frame,
192+
self.FOV,
193+
self.THETA,
194+
self.PHI,
195+
self.height,
196+
self.width
197+
)
198+
self.Q.put(frame)
199+
200+
self.stopped = True
201+
202+
203+
@property
204+
def current_frame_pos(self) -> int:
205+
""" Get current frame position
206+
207+
Returns:
208+
int: current frame
209+
"""
210+
return self.video_stream.current_frame_pos
211+
212+
213+
@property
214+
def number_of_frames(self) -> int:
215+
""" Get number of frames in video
216+
217+
Returns:
218+
int: number of frames in video
219+
"""
220+
return self.video_stream.number_of_frames
221+
222+
223+
@property
224+
def fps(self) -> float:
225+
""" Get Video FPS
226+
227+
Returns:
228+
float: Video FPS
229+
"""
230+
return self.video_stream.fps
231+
232+
233+
@property
234+
def frame_width(self) -> int:
235+
""" Get Video Frame Width
236+
237+
Returns:
238+
int: video frame width
239+
"""
240+
return self.video_stream.frame_width
241+
242+
243+
@property
244+
def frame_height(self) -> int:
245+
""" Get Video Frame Height
246+
247+
Returns:
248+
int: video frame height
249+
"""
250+
return self.video_stream.frame_heigt

0 commit comments

Comments
 (0)