Foydalanuvchi commited on
Commit
e1ff18c
·
1 Parent(s): 4b43fdd

Feature: Add professional video stabilization (2-pass Lucas-Kanade + Moving Average)

Browse files
Files changed (2) hide show
  1. filters.py +270 -0
  2. main.py +6 -4
filters.py CHANGED
@@ -959,6 +959,276 @@ def process_video_subtitle(video_path, output_path, progress_callback=None):
959
  try: os.remove(temp_audio_path)
960
  except: pass
961
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
962
 
963
  # ============================================================
964
  # PHASE 13: GLITCH / MIRROR / WATERMARK / BG REMOVE / STYLE
 
959
  try: os.remove(temp_audio_path)
960
  except: pass
961
 
962
+ # ============================================================
963
+ # VIDEO STABILIZATSIYA (Professional 2-Pass)
964
+ # ============================================================
965
+
966
+ def process_video_stabilize(video_path, output_path, progress_callback=None, smoothing_radius=30):
967
+ """Titrovchi videoni barqarorlashtiradi (2-pass: motion analysis + smooth trajectory).
968
+
969
+ Algoritm:
970
+ 1-pass: Kadrlar orasidagi harakatni aniqlash (Lucas-Kanade Optical Flow + Affine)
971
+ 2-pass: Harakatlar trajectoryasini moving-average bilan tekislash
972
+ Natija: Titrash yo'qoladi, video silliq ko'rinadi.
973
+ """
974
+ cap = None
975
+ writer = None
976
+ try:
977
+ video_path = os.path.abspath(video_path)
978
+ output_path = os.path.abspath(output_path)
979
+
980
+ cap = cv2.VideoCapture(video_path)
981
+ if not cap.isOpened():
982
+ logger.error(f"Stabilize: Video ochilmadi: {video_path}")
983
+ return None
984
+
985
+ fps = cap.get(cv2.CAP_PROP_FPS) or 24
986
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
987
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
988
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
989
+
990
+ if total_frames < 10:
991
+ logger.warning("Video juda qisqa — stabilizatsiya mumkin emas.")
992
+ cap.release()
993
+ return None
994
+
995
+ logger.info(f"Stabilize: {total_frames} kadr, {fps} FPS, {width}x{height}")
996
+
997
+ # ============= PASS 1: Motion Analysis =============
998
+ # Har bir kadr juftligi orasidagi siljish (dx, dy, da) ni hisoblash
999
+
1000
+ transforms = [] # [(dx, dy, da), ...]
1001
+
1002
+ ret, prev_frame = cap.read()
1003
+ if not ret:
1004
+ cap.release()
1005
+ return None
1006
+
1007
+ prev_gray = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
1008
+
1009
+ # Feature detection parametrlari (Shi-Tomasi corners)
1010
+ feature_params = dict(
1011
+ maxCorners=200,
1012
+ qualityLevel=0.01,
1013
+ minDistance=30,
1014
+ blockSize=3
1015
+ )
1016
+
1017
+ frame_idx = 0
1018
+ while True:
1019
+ ret, curr_frame = cap.read()
1020
+ if not ret:
1021
+ break
1022
+
1023
+ frame_idx += 1
1024
+ curr_gray = cv2.cvtColor(curr_frame, cv2.COLOR_BGR2GRAY)
1025
+
1026
+ # Feature'larni oldingi kadrda topish
1027
+ prev_pts = cv2.goodFeaturesToTrack(prev_gray, **feature_params)
1028
+
1029
+ if prev_pts is None or len(prev_pts) < 10:
1030
+ # Feature kam — oʻzgarishsiz deb belgilash
1031
+ transforms.append((0, 0, 0))
1032
+ prev_gray = curr_gray
1033
+ continue
1034
+
1035
+ # Lucas-Kanade optical flow
1036
+ curr_pts, status, _ = cv2.calcOpticalFlowPyrLK(
1037
+ prev_gray, curr_gray, prev_pts, None,
1038
+ winSize=(21, 21),
1039
+ maxLevel=3,
1040
+ criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 30, 0.01)
1041
+ )
1042
+
1043
+ # Faqat muvaffaqiyatli aniqlangan nuqtalar
1044
+ valid = status.flatten() == 1
1045
+ prev_valid = prev_pts[valid]
1046
+ curr_valid = curr_pts[valid]
1047
+
1048
+ if len(prev_valid) < 4:
1049
+ transforms.append((0, 0, 0))
1050
+ prev_gray = curr_gray
1051
+ continue
1052
+
1053
+ # Affine transformatsiya (siljish + aylanish)
1054
+ mat, inliers = cv2.estimateAffinePartial2D(prev_valid, curr_valid)
1055
+
1056
+ if mat is not None:
1057
+ dx = mat[0, 2] # X siljishi
1058
+ dy = mat[1, 2] # Y siljishi
1059
+ da = np.arctan2(mat[1, 0], mat[0, 0]) # Aylanish burchagi
1060
+ transforms.append((dx, dy, da))
1061
+ else:
1062
+ transforms.append((0, 0, 0))
1063
+
1064
+ prev_gray = curr_gray
1065
+
1066
+ if progress_callback and frame_idx % 10 == 0:
1067
+ progress_callback(min(40, int(frame_idx / total_frames * 40)))
1068
+
1069
+ cap.release()
1070
+
1071
+ if not transforms:
1072
+ logger.warning("Stabilize: Hech qanday harakat aniqlanmadi.")
1073
+ return None
1074
+
1075
+ if progress_callback:
1076
+ progress_callback(45)
1077
+
1078
+ # ============= Trajectory hisoblash va tekislash =============
1079
+
1080
+ # Kumulyativ trajectory (yig'indi)
1081
+ trajectory = []
1082
+ cum_x, cum_y, cum_a = 0.0, 0.0, 0.0
1083
+ for (dx, dy, da) in transforms:
1084
+ cum_x += dx
1085
+ cum_y += dy
1086
+ cum_a += da
1087
+ trajectory.append((cum_x, cum_y, cum_a))
1088
+
1089
+ # Moving Average bilan tekislash
1090
+ def moving_average(values, radius):
1091
+ """1D moving average filter."""
1092
+ n = len(values)
1093
+ smoothed = np.zeros(n)
1094
+ for i in range(n):
1095
+ start = max(0, i - radius)
1096
+ end = min(n, i + radius + 1)
1097
+ smoothed[i] = np.mean(values[start:end])
1098
+ return smoothed
1099
+
1100
+ traj_x = np.array([t[0] for t in trajectory])
1101
+ traj_y = np.array([t[1] for t in trajectory])
1102
+ traj_a = np.array([t[2] for t in trajectory])
1103
+
1104
+ # Trajectoryani tekislash (smoothing_radius=30 kadr)
1105
+ smooth_x = moving_average(traj_x, smoothing_radius)
1106
+ smooth_y = moving_average(traj_y, smoothing_radius)
1107
+ smooth_a = moving_average(traj_a, smoothing_radius)
1108
+
1109
+ # Tuzatish = tekislangan - asl trajectory
1110
+ diff_x = smooth_x - traj_x
1111
+ diff_y = smooth_y - traj_y
1112
+ diff_a = smooth_a - traj_a
1113
+
1114
+ # Yangilangan transformatsiyalar
1115
+ smooth_transforms = []
1116
+ for i, (dx, dy, da) in enumerate(transforms):
1117
+ smooth_transforms.append((
1118
+ dx + diff_x[i],
1119
+ dy + diff_y[i],
1120
+ da + diff_a[i]
1121
+ ))
1122
+
1123
+ if progress_callback:
1124
+ progress_callback(55)
1125
+
1126
+ # ============= PASS 2: Stabilizatsiya qo'llash =============
1127
+
1128
+ cap = cv2.VideoCapture(video_path)
1129
+
1130
+ # Border crop (qora chetlarni yo'qotish uchun 5% qirqish)
1131
+ crop_ratio = 0.05
1132
+ crop_x = int(width * crop_ratio)
1133
+ crop_y = int(height * crop_ratio)
1134
+ out_w = width - 2 * crop_x
1135
+ out_h = height - 2 * crop_y
1136
+
1137
+ temp_video_path = output_path + "_stab_temp.avi"
1138
+ fourcc = cv2.VideoWriter_fourcc(*'MJPG')
1139
+ writer = cv2.VideoWriter(temp_video_path, fourcc, fps, (out_w, out_h))
1140
+
1141
+ # Birinchi kadrni o'qib tashlash (prev_frame)
1142
+ ret, _ = cap.read()
1143
+ if not ret:
1144
+ cap.release()
1145
+ return None
1146
+
1147
+ # Birinchi kadrni asl holida yozish
1148
+ first_cropped = _[crop_y:crop_y+out_h, crop_x:crop_x+out_w]
1149
+ writer.write(first_cropped)
1150
+
1151
+ for i in range(len(smooth_transforms)):
1152
+ ret, frame = cap.read()
1153
+ if not ret:
1154
+ break
1155
+
1156
+ dx, dy, da = smooth_transforms[i]
1157
+
1158
+ # Affine matritsa yaratish
1159
+ cos_a = np.cos(da)
1160
+ sin_a = np.sin(da)
1161
+ transform_mat = np.array([
1162
+ [cos_a, -sin_a, dx],
1163
+ [sin_a, cos_a, dy]
1164
+ ], dtype=np.float64)
1165
+
1166
+ # Transformatsiyani qo'llash
1167
+ stabilized = cv2.warpAffine(
1168
+ frame, transform_mat, (width, height),
1169
+ borderMode=cv2.BORDER_REFLECT_101
1170
+ )
1171
+
1172
+ # Chetlarni qirqish (qora border yo'qotish)
1173
+ cropped = stabilized[crop_y:crop_y+out_h, crop_x:crop_x+out_w]
1174
+ writer.write(cropped)
1175
+
1176
+ if progress_callback and i % 5 == 0:
1177
+ progress_callback(min(95, 55 + int(i / len(smooth_transforms) * 40)))
1178
+
1179
+ cap.release()
1180
+ writer.release()
1181
+
1182
+ if progress_callback:
1183
+ progress_callback(96)
1184
+
1185
+ # ============= Audio'ni biriktirish =============
1186
+ try:
1187
+ original = VideoFileClip(video_path)
1188
+ processed = VideoFileClip(temp_video_path)
1189
+
1190
+ if original.audio is not None:
1191
+ final = processed.with_audio(original.audio)
1192
+ final.write_videofile(
1193
+ output_path, codec="libx264", audio_codec="aac",
1194
+ preset="medium", bitrate="6000k",
1195
+ threads=4, logger=None
1196
+ )
1197
+ final.close()
1198
+ else:
1199
+ processed.write_videofile(
1200
+ output_path, codec="libx264",
1201
+ preset="medium", bitrate="6000k",
1202
+ threads=4, logger=None
1203
+ )
1204
+
1205
+ original.close()
1206
+ processed.close()
1207
+ except Exception as audio_err:
1208
+ logger.warning(f"Stabilize audio biriktirish xatosi: {audio_err}")
1209
+ import shutil
1210
+ shutil.move(temp_video_path, output_path)
1211
+
1212
+ # Temp faylni tozalash
1213
+ if os.path.exists(temp_video_path):
1214
+ try: os.remove(temp_video_path)
1215
+ except: pass
1216
+
1217
+ if progress_callback:
1218
+ progress_callback(99)
1219
+
1220
+ logger.info(f"Video stabilizatsiya muvaffaqiyatli: {len(transforms)} kadr qayta ishlandi.")
1221
+ return output_path if os.path.exists(output_path) else None
1222
+ except Exception as e:
1223
+ logger.error(f"Video stabilizatsiya xatosi: {e}")
1224
+ return None
1225
+ finally:
1226
+ if cap is not None:
1227
+ try: cap.release()
1228
+ except: pass
1229
+ if writer is not None:
1230
+ try: writer.release()
1231
+ except: pass
1232
 
1233
  # ============================================================
1234
  # PHASE 13: GLITCH / MIRROR / WATERMARK / BG REMOVE / STYLE
main.py CHANGED
@@ -54,7 +54,7 @@ from filters import (
54
  process_video_bw, process_video_color_correct, process_video_remove_audio,
55
  process_video_trim, process_video_face_fix, process_video_auto_enhance,
56
  process_video_fps_boost, apply_nudenet_filter, process_video_nnsfw,
57
- process_video_subtitle,
58
  apply_glitch_filter, apply_mirror_filter, apply_watermark, apply_bg_remove,
59
  apply_style_transfer, apply_quality_boost,
60
  process_video_glitch, process_video_mirror, process_video_watermark,
@@ -361,7 +361,8 @@ async def handle_video(update: Update, context: ContextTypes.DEFAULT_TYPE):
361
  InlineKeyboardButton("🪞 Oyna Effekt", callback_data=f"mr|v|{short_id}")
362
  ],
363
  [
364
- InlineKeyboardButton("📱 Suv belgisi", callback_data=f"wm|v|{short_id}")
 
365
  ],
366
  [
367
  InlineKeyboardButton("🌐 Sub+O'zbek", callback_data=f"stuz|v|{short_id}"),
@@ -574,7 +575,8 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
574
  "t": process_video_trim, "vf": process_video_face_fix,
575
  "va": process_video_auto_enhance, "n": process_video_nnsfw,
576
  "sub": process_video_subtitle, "gl": process_video_glitch,
577
- "mr": process_video_mirror, "wm": process_video_watermark
 
578
  }
579
  func = video_funcs.get(action)
580
  if not func:
@@ -601,7 +603,7 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
601
  "st_anime": "AI anime", "st_sketch": "AI sketch",
602
  "st_oil": "AI oil paint", "st_cart": "AI cartoon",
603
  "stuz": "sub+o'zbek", "stru": "sub+rus", "sten": "sub+english",
604
- "qb": "sifat+ pro"
605
  }.get(action, f"filter_{action}")
606
  db.log_history(query.from_user.id, "photo" if m_type == "p" else "video", f_name, file_id)
607
 
 
54
  process_video_bw, process_video_color_correct, process_video_remove_audio,
55
  process_video_trim, process_video_face_fix, process_video_auto_enhance,
56
  process_video_fps_boost, apply_nudenet_filter, process_video_nnsfw,
57
+ process_video_subtitle, process_video_stabilize,
58
  apply_glitch_filter, apply_mirror_filter, apply_watermark, apply_bg_remove,
59
  apply_style_transfer, apply_quality_boost,
60
  process_video_glitch, process_video_mirror, process_video_watermark,
 
361
  InlineKeyboardButton("🪞 Oyna Effekt", callback_data=f"mr|v|{short_id}")
362
  ],
363
  [
364
+ InlineKeyboardButton("📱 Suv belgisi", callback_data=f"wm|v|{short_id}"),
365
+ InlineKeyboardButton("📹 Stabilizatsiya", callback_data=f"stb|v|{short_id}")
366
  ],
367
  [
368
  InlineKeyboardButton("🌐 Sub+O'zbek", callback_data=f"stuz|v|{short_id}"),
 
575
  "t": process_video_trim, "vf": process_video_face_fix,
576
  "va": process_video_auto_enhance, "n": process_video_nnsfw,
577
  "sub": process_video_subtitle, "gl": process_video_glitch,
578
+ "mr": process_video_mirror, "wm": process_video_watermark,
579
+ "stb": process_video_stabilize
580
  }
581
  func = video_funcs.get(action)
582
  if not func:
 
603
  "st_anime": "AI anime", "st_sketch": "AI sketch",
604
  "st_oil": "AI oil paint", "st_cart": "AI cartoon",
605
  "stuz": "sub+o'zbek", "stru": "sub+rus", "sten": "sub+english",
606
+ "qb": "sifat+ pro", "stb": "stabilizatsiya"
607
  }.get(action, f"filter_{action}")
608
  db.log_history(query.from_user.id, "photo" if m_type == "p" else "video", f_name, file_id)
609