Skip to content

Commit c0f07ef

Browse files
committed
feat: auto saving 360
1 parent f046ddb commit c0f07ef

1 file changed

Lines changed: 98 additions & 8 deletions

File tree

kalman_arch/tools/panorama360/main_generator.py

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,29 @@ class CylindricalStitcher:
3838
def __init__(self, focal_length=700):
3939
self.focal_length = focal_length
4040
self.finder = cv2.SIFT_create()
41-
self.bf = cv2.BFMatcher(cv2.NORM_L2, crossCheck=False)
41+
# Używamy FLANN dla szybszego i dokładniejszego dopasowywania przy dużych zestawach cech
42+
index_params = dict(algorithm=1, trees=5)
43+
search_params = dict(checks=50)
44+
self.flann = cv2.FlannBasedMatcher(index_params, search_params)
45+
4246
self.panorama = None
4347
self.added_frames_count = 0
48+
self.is_saved_360 = False
49+
50+
# Teoretyczna szerokość pełnego obrotu 360 stopni
51+
self.target_width_360 = int(2 * np.pi * self.focal_length)
52+
53+
# Przechowywanie punktów kluczowych pierwszej klatki
54+
self.first_frame_kp = None
55+
self.first_frame_des = None
4456

4557
def reset(self):
4658
self.panorama = None
4759
self.added_frames_count = 0
60+
self.is_saved_360 = False
61+
self.first_frame_kp = None
62+
self.first_frame_des = None
63+
print("[System] Resetowanie panoramy...")
4864

4965
def rotate_frame(self, img, angle):
5066
if angle == 90:
@@ -77,25 +93,88 @@ def warp_cylinder(self, img):
7793

7894
return cylindrical_img, (mask * 255).astype(np.uint8)
7995

96+
def check_loop_closure(self, frame_cyl):
97+
"""Sprawdza powrót do startu po przekroczeniu 50% szerokości przy użyciu Homografii."""
98+
if self.panorama is None or self.first_frame_des is None:
99+
return False
100+
101+
current_width = self.panorama.shape[1]
102+
half_width_threshold = self.target_width_360 * 0.5
103+
104+
# WARUNEK 1: Sprawdzamy dopiero, gdy jesteśmy przynajmniej w połowie oczekiwanej szerokości
105+
if current_width < half_width_threshold:
106+
return False
107+
108+
kp_frame, des_frame = self.finder.detectAndCompute(frame_cyl, None)
109+
if des_frame is None:
110+
return False
111+
112+
# Dopasowanie przy użyciu FLANN (stabilniejsze niż podstawowy BFMatcher)
113+
matches = self.flann.knnMatch(des_frame, self.first_frame_des, k=2)
114+
good_matches = []
115+
for match in matches:
116+
if len(match) == 2:
117+
m, n = match
118+
# Wybór najbardziej unikalnych punktów (test Ratio Lowe'a)
119+
if m.distance < 0.7 * n.distance:
120+
good_matches.append(m)
121+
122+
# WARUNEK 2: Jeśli mamy potencjalne punkty wspólne, weryfikujemy je Homografią
123+
if len(good_matches) >= 15:
124+
src_pts = np.float32(
125+
[kp_frame[m.queryIdx].pt for m in good_matches]
126+
).reshape(-1, 1, 2)
127+
dst_pts = np.float32(
128+
[self.first_frame_kp[m.trainIdx].pt for m in good_matches]
129+
).reshape(-1, 1, 2)
130+
131+
# Homografia (findHomography + RANSAC) wybacza drobne różnice w kącie patrzenia kamery
132+
H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
133+
134+
if H is not None:
135+
num_inliers = np.sum(mask)
136+
# Jeżeli poprawnie przetłumaczono geometrię dla min. 12 punktów - mamy to!
137+
if num_inliers >= 12:
138+
print(
139+
f"\n[Loop Closure] Znaleziono dopasowanie perspektywiczne! Poprawne punkty (inliers): {num_inliers}"
140+
)
141+
return True
142+
return False
143+
80144
def try_stitch(self, frame_cyl, frame_mask):
145+
# Inicjalizacja pierwszej klatki
81146
if self.panorama is None:
82147
self.panorama = frame_cyl.copy()
83148
self.added_frames_count = 1
149+
# Konwertujemy deskryptory na float32 (wymóg dla FLANN Matchera)
150+
kp, des = self.finder.detectAndCompute(frame_cyl, None)
151+
if des is not None:
152+
self.first_frame_kp = kp
153+
self.first_frame_des = des.astype(np.float32)
84154
return True
85155

156+
# Sprawdzamy pętlę przed modyfikacją panoramy
157+
if self.is_saved_360 is False and self.check_loop_closure(frame_cyl):
158+
self.is_saved_360 = True
159+
160+
# Konwersja deskryptorów panoramy i klatki na float32 do dopasowania standardowego
86161
kp1, des1 = self.finder.detectAndCompute(self.panorama, None)
87162
kp2, des2 = self.finder.detectAndCompute(frame_cyl, None)
88163

89164
if des1 is None or des2 is None:
90165
return False
91166

92-
matches = self.bf.knnMatch(des2, des1, k=2)
167+
des1_f = des1.astype(np.float32)
168+
des2_f = des2.astype(np.float32)
169+
170+
matches = self.flann.knnMatch(des2_f, des1_f, k=2)
93171
good_matches = []
94172
for match in matches:
95173
if len(match) == 2:
96174
m, n = match
97175
if m.distance < 0.7 * n.distance:
98176
good_matches.append(m)
177+
99178
if len(good_matches) <= 10:
100179
print("Skipped frame - too few common points.")
101180
return False
@@ -143,7 +222,18 @@ def try_stitch(self, frame_cyl, frame_mask):
143222

144223
self.panorama = large_canvas
145224
self.added_frames_count += 1
146-
print(f"Added frame. New width: {self.panorama.shape[1]}px")
225+
226+
# Informacja o postępie szerokości w konsoli
227+
progress_pct = (self.panorama.shape[1] / self.target_width_360) * 100
228+
print(
229+
f"Added frame. Width: {self.panorama.shape[1]}px ({progress_pct:.1f}% of 360° target)"
230+
)
231+
232+
# Zapis automatyczny po wykryciu pętli
233+
if self.is_saved_360 is True:
234+
self.save_to_file(prefix="panorama_360_hybrid")
235+
self.is_saved_360 = "done"
236+
147237
return True
148238

149239
def get_preview(self, max_width=1366):
@@ -156,11 +246,11 @@ def get_preview(self, max_width=1366):
156246
return cv2.resize(self.panorama, (preview_w, preview_h))
157247
return self.panorama.copy()
158248

159-
def save_to_file(self):
249+
def save_to_file(self, prefix="panorama"):
160250
if self.panorama is not None:
161-
filename = f"panorama_{datetime.now().strftime('%H%M%S')}.png"
251+
filename = f"{prefix}_{datetime.now().strftime('%H%M%S')}.png"
162252
cv2.imwrite(filename, self.panorama)
163-
print(f"Saved: {filename}")
253+
print(f"====== AUTO-SAVE SUCCESFUL: {filename} ======")
164254

165255

166256
def rotate_frame(img, angle):
@@ -287,9 +377,9 @@ def main(config):
287377

288378
if __name__ == "__main__":
289379
config = {
290-
"video_port": 1, # video port
380+
"video_port": 2, # video port
291381
"capture_interval": 15, # capture interval
292382
"focal_length": 700, # focal length typical [600-800]
293-
"camera_rotation": 270, # camera rotation
383+
"camera_rotation": 90, # camera rotation
294384
}
295385
main(config)

0 commit comments

Comments
 (0)