gnosticdev commited on
Commit
b59250f
·
verified ·
1 Parent(s): e17dfba

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +212 -112
app.py CHANGED
@@ -3,10 +3,10 @@ import cv2
3
  import numpy as np
4
  from pathlib import Path
5
  import tempfile
6
- import shutil
7
  import threading
8
  import time
9
  from datetime import datetime, timedelta
 
10
  import os
11
 
12
  class WatermarkRemover:
@@ -19,8 +19,8 @@ class WatermarkRemover:
19
  """Inicia thread para limpiar archivos antiguos cada hora"""
20
  def cleanup_worker():
21
  while True:
 
22
  self.cleanup_old_files()
23
- time.sleep(3600) # Revisar cada hora
24
 
25
  thread = threading.Thread(target=cleanup_worker, daemon=True)
26
  thread.start()
@@ -34,211 +34,311 @@ class WatermarkRemover:
34
  file_time = datetime.fromtimestamp(file_path.stat().st_mtime)
35
  if file_time < cutoff_time:
36
  file_path.unlink()
37
- print(f"Archivo eliminado: {file_path.name}")
38
  except Exception as e:
39
- print(f"Error en limpieza: {e}")
40
 
41
- def detect_watermark_region(self, frame):
42
  """
43
- Detecta regiones candidatas de marca de agua usando análisis de opacidad
44
- y detección de bordes
45
  """
46
- # Convertir a escala de grises
47
- gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
48
 
49
- # Detectar bordes para encontrar contornos de marca de agua
50
- edges = cv2.Canny(gray, 50, 150)
 
51
 
52
- # Encontrar regiones con alta densidad de bordes (posibles watermarks)
53
- kernel = np.ones((15, 15), np.uint8)
54
- dilated = cv2.dilate(edges, kernel, iterations=2)
55
 
56
- # Encontrar contornos
57
- contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
58
 
59
- # Filtrar contornos por tamaño (marcas de agua suelen ser pequeñas/medianas)
60
- watermark_regions = []
61
- h, w = frame.shape[:2]
62
 
63
- for contour in contours:
64
- area = cv2.contourArea(contour)
65
- # Marca de agua típicamente entre 0.1% y 10% del área total
66
- if 0.001 * (h * w) < area < 0.1 * (h * w):
67
- x, y, w_box, h_box = cv2.boundingRect(contour)
68
- watermark_regions.append((x, y, w_box, h_box))
69
 
70
- return watermark_regions
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- def inpaint_watermark(self, frame, regions):
73
  """
74
- Elimina marcas de agua usando inpainting
75
  """
76
- # Crear máscara para las regiones detectadas
77
- mask = np.zeros(frame.shape[:2], dtype=np.uint8)
78
 
79
- for (x, y, w, h) in regions:
80
- # Expandir ligeramente la región para asegurar eliminación completa
81
- padding = 5
82
- x1 = max(0, x - padding)
83
- y1 = max(0, y - padding)
84
- x2 = min(frame.shape[1], x + w + padding)
85
- y2 = min(frame.shape[0], y + h + padding)
86
-
87
- mask[y1:y2, x1:x2] = 255
 
 
88
 
89
- # Aplicar inpainting si hay regiones detectadas
90
- if np.any(mask):
91
- result = cv2.inpaint(frame, mask, 3, cv2.INPAINT_TELEA)
92
- return result, True
93
 
94
- return frame, False
 
 
 
 
 
 
95
 
96
- def remove_static_patterns(self, frame, reference_frames):
97
  """
98
- Elimina patrones estáticos comparando con frames de referencia
99
  """
100
- if len(reference_frames) < 3:
101
- return frame
102
 
103
- # Calcular mediana de frames de referencia
104
- median_frame = np.median(reference_frames, axis=0).astype(np.uint8)
 
 
 
105
 
106
- # Diferencia entre frame actual y mediana
107
- diff = cv2.absdiff(frame, median_frame)
108
- gray_diff = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)
109
 
110
- # Umbralizar para encontrar píxeles estáticos diferentes
111
- _, mask = cv2.threshold(gray_diff, 30, 255, cv2.THRESH_BINARY_INV)
112
 
113
- # Dilatar máscara para incluir bordes
114
- kernel = np.ones((5, 5), np.uint8)
115
- mask = cv2.dilate(mask, kernel, iterations=1)
 
 
 
 
 
 
 
 
116
 
117
- # Aplicar inpainting en áreas estáticas diferentes
118
- result = cv2.inpaint(frame, mask, 3, cv2.INPAINT_TELEA)
119
 
120
  return result
121
 
122
- def process_video(self, video_path, progress=gr.Progress()):
123
  """
124
- Procesa el video completo para eliminar marcas de agua móviles
125
  """
126
  try:
127
- progress(0, desc="Iniciando procesamiento...")
128
 
129
- # Abrir video
130
  cap = cv2.VideoCapture(video_path)
131
-
132
  if not cap.isOpened():
133
  return None, "Error: No se pudo abrir el video"
134
 
135
- # Obtener propiedades del video
136
  fps = int(cap.get(cv2.CAP_PROP_FPS))
137
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
138
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
139
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
140
 
141
- # Crear archivo de salida
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
143
  output_path = self.temp_dir / f"cleaned_{timestamp}.mp4"
144
 
145
- # Configurar writer
146
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
147
  out = cv2.VideoWriter(str(output_path), fourcc, fps, (width, height))
148
 
149
- reference_frames = []
 
150
  frame_count = 0
151
 
152
- progress(0.1, desc="Procesando frames...")
153
-
154
  while True:
155
  ret, frame = cap.read()
156
  if not ret:
157
  break
158
 
159
- # Mantener buffer de frames de referencia (últimos 10 frames)
160
- if len(reference_frames) >= 10:
161
- reference_frames.pop(0)
162
- reference_frames.append(frame.copy())
163
 
164
- # Detectar y eliminar marcas de agua
165
- processed_frame = frame.copy()
 
 
 
166
 
167
- # Método 1: Detección por contornos
168
- regions = self.detect_watermark_region(processed_frame)
169
- if regions:
170
- processed_frame, _ = self.inpaint_watermark(processed_frame, regions)
171
 
172
- # Método 2: Eliminación de patrones estáticos
173
- if len(reference_frames) >= 5:
174
- processed_frame = self.remove_static_patterns(processed_frame, reference_frames)
175
 
176
- # Escribir frame procesado
177
- out.write(processed_frame)
178
 
 
179
  frame_count += 1
 
 
 
 
 
 
 
 
 
 
180
  if frame_count % 10 == 0:
181
- progress_value = 0.1 + (0.8 * frame_count / total_frames)
182
- progress(progress_value, desc=f"Procesando: {frame_count}/{total_frames} frames")
183
 
184
- # Liberar recursos
185
  cap.release()
186
  out.release()
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  progress(1.0, desc="¡Completado!")
189
 
190
- return str(output_path), f"Video procesado exitosamente: {frame_count} frames"
191
 
192
  except Exception as e:
193
- return None, f"Error durante el procesamiento: {str(e)}"
194
 
195
  # Crear instancia del removedor
196
  remover = WatermarkRemover()
197
 
198
- def process_video_interface(video_file):
199
  """Interfaz para Gradio"""
200
  if video_file is None:
201
- return None, "Por favor, carga un video"
202
 
203
- output_path, message = remover.process_video(video_file)
204
 
205
  return output_path, message
206
 
207
  # Crear interfaz de Gradio
208
- with gr.Blocks(title="Eliminador de Marcas de Agua") as demo:
209
  gr.Markdown("""
210
- # 🎬 Eliminador de Marcas de Agua de Videos
211
 
212
- Sube tu video y esta herramienta eliminará automáticamente las marcas de agua móviles.
 
 
 
 
213
 
214
- **Características:**
215
- - Detecta y elimina marcas de agua móviles
216
- - Procesa videos de cualquier tamaño
217
- - Limpieza automática de archivos temporales (cada 2 horas)
218
- - Soporta múltiples formatos de video
219
  """)
220
 
221
  with gr.Row():
222
- with gr.Column():
223
- video_input = gr.Video(label="Video Original")
224
- process_btn = gr.Button("🚀 Eliminar Marcas de Agua", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
- with gr.Column():
 
227
  video_output = gr.Video(label="Video Limpio")
228
- status_output = gr.Textbox(label="Estado", lines=3)
229
 
230
  process_btn.click(
231
  fn=process_video_interface,
232
- inputs=[video_input],
233
  outputs=[video_output, status_output]
234
  )
235
 
236
  gr.Markdown("""
237
  ---
238
- ### 📝 Notas:
239
- - Los archivos temporales se eliminan automáticamente después de 2 horas
240
- - El procesamiento puede tardar según el tamaño del video
241
- - Para mejores resultados, usa videos con buena calidad
 
 
 
 
 
 
 
242
  """)
243
 
244
  if __name__ == "__main__":
 
3
  import numpy as np
4
  from pathlib import Path
5
  import tempfile
 
6
  import threading
7
  import time
8
  from datetime import datetime, timedelta
9
+ import subprocess
10
  import os
11
 
12
  class WatermarkRemover:
 
19
  """Inicia thread para limpiar archivos antiguos cada hora"""
20
  def cleanup_worker():
21
  while True:
22
+ time.sleep(3600)
23
  self.cleanup_old_files()
 
24
 
25
  thread = threading.Thread(target=cleanup_worker, daemon=True)
26
  thread.start()
 
34
  file_time = datetime.fromtimestamp(file_path.stat().st_mtime)
35
  if file_time < cutoff_time:
36
  file_path.unlink()
37
+ print(f"Eliminado: {file_path.name}")
38
  except Exception as e:
39
+ print(f"Error limpieza: {e}")
40
 
41
+ def detect_moving_watermark(self, frames_sample):
42
  """
43
+ Detecta marca de agua analizando varianza temporal en regiones
 
44
  """
45
+ # Convertir a float para cálculos precisos
46
+ frames_float = [frame.astype(np.float32) for frame in frames_sample]
47
 
48
+ # Calcular varianza temporal por píxel
49
+ frames_stack = np.stack(frames_float, axis=0)
50
+ temporal_variance = np.var(frames_stack, axis=0)
51
 
52
+ # Promedio de varianza en canales
53
+ variance_gray = np.mean(temporal_variance, axis=2)
 
54
 
55
+ # Normalizar
56
+ variance_normalized = cv2.normalize(variance_gray, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
57
 
58
+ # Encontrar regiones con BAJA varianza (marca de agua se mueve pero mantiene características)
59
+ # Y alta presencia (aparece en todos los frames)
60
+ _, low_variance_mask = cv2.threshold(variance_normalized, 50, 255, cv2.THRESH_BINARY_INV)
61
 
62
+ # Calcular máscara de presencia constante
63
+ frames_gray = [cv2.cvtColor(f, cv2.COLOR_BGR2GRAY) for f in frames_sample]
64
+ median_intensity = np.median(frames_gray, axis=0).astype(np.uint8)
 
 
 
65
 
66
+ # Detectar regiones semi-transparentes (marca de agua típica)
67
+ presence_mask = cv2.inRange(median_intensity, 30, 220)
68
+
69
+ # Combinar máscaras
70
+ watermark_mask = cv2.bitwise_and(low_variance_mask, presence_mask)
71
+
72
+ # Limpiar ruido
73
+ kernel = np.ones((3, 3), np.uint8)
74
+ watermark_mask = cv2.morphologyEx(watermark_mask, cv2.MORPH_OPEN, kernel, iterations=2)
75
+ watermark_mask = cv2.morphologyEx(watermark_mask, cv2.MORPH_CLOSE, kernel, iterations=2)
76
+
77
+ return watermark_mask
78
 
79
+ def track_watermark_position(self, prev_gray, curr_gray, prev_points):
80
  """
81
+ Rastrea la posición de la marca de agua entre frames usando optical flow
82
  """
83
+ if prev_points is None or len(prev_points) == 0:
84
+ return None
85
 
86
+ # Parámetros para Lucas-Kanade optical flow
87
+ lk_params = dict(
88
+ winSize=(21, 21),
89
+ maxLevel=3,
90
+ criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 30, 0.01)
91
+ )
92
+
93
+ # Calcular optical flow
94
+ next_points, status, _ = cv2.calcOpticalFlowPyrLK(
95
+ prev_gray, curr_gray, prev_points, None, **lk_params
96
+ )
97
 
98
+ if next_points is None:
99
+ return None
 
 
100
 
101
+ # Filtrar puntos válidos
102
+ good_new = next_points[status == 1]
103
+
104
+ if len(good_new) < 3:
105
+ return None
106
+
107
+ return good_new.reshape(-1, 1, 2).astype(np.float32)
108
 
109
+ def create_adaptive_mask(self, frame, base_mask, tracked_points=None):
110
  """
111
+ Crea máscara adaptativa para la marca de agua
112
  """
113
+ mask = base_mask.copy()
 
114
 
115
+ if tracked_points is not None and len(tracked_points) > 0:
116
+ # Expandir máscara alrededor de puntos rastreados
117
+ for point in tracked_points:
118
+ x, y = point.ravel()
119
+ cv2.circle(mask, (int(x), int(y)), 15, 255, -1)
120
 
121
+ # Dilatar para cubrir completamente la marca de agua
122
+ kernel = np.ones((7, 7), np.uint8)
123
+ mask = cv2.dilate(mask, kernel, iterations=2)
124
 
125
+ # Suavizar bordes de la máscara
126
+ mask = cv2.GaussianBlur(mask, (21, 21), 0)
127
 
128
+ return mask
129
+
130
+ def advanced_inpainting(self, frame, mask):
131
+ """
132
+ Inpainting mejorado usando múltiples técnicas
133
+ """
134
+ # Convertir máscara a binaria
135
+ _, binary_mask = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY)
136
+
137
+ # Aplicar inpainting con algoritmo Navier-Stokes (mejor para texturas)
138
+ result = cv2.inpaint(frame, binary_mask, 5, cv2.INPAINT_NS)
139
 
140
+ # Segunda pasada con Telea para refinar
141
+ result = cv2.inpaint(result, binary_mask, 3, cv2.INPAINT_TELEA)
142
 
143
  return result
144
 
145
+ def process_video(self, video_path, sensitivity=50, progress=gr.Progress()):
146
  """
147
+ Procesa video completo eliminando marcas de agua móviles
148
  """
149
  try:
150
+ progress(0, desc="Abriendo video...")
151
 
 
152
  cap = cv2.VideoCapture(video_path)
 
153
  if not cap.isOpened():
154
  return None, "Error: No se pudo abrir el video"
155
 
156
+ # Propiedades del video
157
  fps = int(cap.get(cv2.CAP_PROP_FPS))
158
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
159
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
160
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
161
 
162
+ progress(0.05, desc="Analizando marca de agua...")
163
+
164
+ # Leer muestra de frames para detectar marca de agua
165
+ sample_frames = []
166
+ sample_size = min(30, total_frames)
167
+ for i in range(sample_size):
168
+ frame_idx = int(i * total_frames / sample_size)
169
+ cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
170
+ ret, frame = cap.read()
171
+ if ret:
172
+ sample_frames.append(frame)
173
+
174
+ # Detectar máscara base de marca de agua
175
+ base_watermark_mask = self.detect_moving_watermark(sample_frames)
176
+
177
+ # Encontrar puntos característicos en la marca de agua para tracking
178
+ mask_points = cv2.goodFeaturesToTrack(
179
+ base_watermark_mask,
180
+ maxCorners=50,
181
+ qualityLevel=0.01,
182
+ minDistance=10
183
+ )
184
+
185
+ progress(0.1, desc="Procesando frames...")
186
+
187
+ # Reiniciar video
188
+ cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
189
+
190
+ # Archivo de salida
191
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
192
  output_path = self.temp_dir / f"cleaned_{timestamp}.mp4"
193
 
194
+ # Usar x264 para mejor compresión
195
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
196
  out = cv2.VideoWriter(str(output_path), fourcc, fps, (width, height))
197
 
198
+ prev_gray = None
199
+ tracked_points = mask_points
200
  frame_count = 0
201
 
 
 
202
  while True:
203
  ret, frame = cap.read()
204
  if not ret:
205
  break
206
 
207
+ curr_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
 
 
 
208
 
209
+ # Actualizar tracking de la marca de agua
210
+ if prev_gray is not None and tracked_points is not None:
211
+ tracked_points = self.track_watermark_position(
212
+ prev_gray, curr_gray, tracked_points
213
+ )
214
 
215
+ # Crear máscara adaptativa
216
+ adaptive_mask = self.create_adaptive_mask(
217
+ frame, base_watermark_mask, tracked_points
218
+ )
219
 
220
+ # Aplicar inpainting
221
+ cleaned_frame = self.advanced_inpainting(frame, adaptive_mask)
 
222
 
223
+ # Escribir frame
224
+ out.write(cleaned_frame)
225
 
226
+ prev_gray = curr_gray
227
  frame_count += 1
228
+
229
+ # Re-detectar puntos cada 30 frames para mantener tracking
230
+ if frame_count % 30 == 0:
231
+ tracked_points = cv2.goodFeaturesToTrack(
232
+ base_watermark_mask,
233
+ maxCorners=50,
234
+ qualityLevel=0.01,
235
+ minDistance=10
236
+ )
237
+
238
  if frame_count % 10 == 0:
239
+ progress_val = 0.1 + (0.85 * frame_count / total_frames)
240
+ progress(progress_val, desc=f"Frame {frame_count}/{total_frames}")
241
 
 
242
  cap.release()
243
  out.release()
244
 
245
+ progress(0.95, desc="Optimizando video...")
246
+
247
+ # Recodificar con ffmpeg si está disponible
248
+ final_output = self.temp_dir / f"final_{timestamp}.mp4"
249
+ try:
250
+ subprocess.run([
251
+ 'ffmpeg', '-i', str(output_path),
252
+ '-c:v', 'libx264', '-crf', '23',
253
+ '-preset', 'medium', '-c:a', 'copy',
254
+ '-y', str(final_output)
255
+ ], check=True, capture_output=True, timeout=300)
256
+
257
+ output_path.unlink() # Eliminar temporal
258
+ output_path = final_output
259
+ except:
260
+ pass # Si ffmpeg falla, usar el video original
261
+
262
  progress(1.0, desc="¡Completado!")
263
 
264
+ return str(output_path), f"Video procesado: {frame_count} frames\n🎯 Marca de agua eliminada"
265
 
266
  except Exception as e:
267
+ return None, f"Error: {str(e)}"
268
 
269
  # Crear instancia del removedor
270
  remover = WatermarkRemover()
271
 
272
+ def process_video_interface(video_file, sensitivity):
273
  """Interfaz para Gradio"""
274
  if video_file is None:
275
+ return None, "⚠️ Por favor, carga un video"
276
 
277
+ output_path, message = remover.process_video(video_file, sensitivity)
278
 
279
  return output_path, message
280
 
281
  # Crear interfaz de Gradio
282
+ with gr.Blocks(title="Eliminador de Marcas de Agua - REAL", theme=gr.themes.Soft()) as demo:
283
  gr.Markdown("""
284
+ # 🎬 Eliminador de Marcas de Agua REAL
285
 
286
+ **Sistema avanzado de eliminación de marcas de agua móviles usando:**
287
+ - 🔍 Análisis de varianza temporal
288
+ - 🎯 Optical Flow tracking
289
+ - 🖌️ Inpainting multi-capa (Navier-Stokes + Telea)
290
+ - 🧹 Limpieza automática de archivos (cada 2 horas)
291
 
292
+ ---
 
 
 
 
293
  """)
294
 
295
  with gr.Row():
296
+ with gr.Column(scale=1):
297
+ gr.Markdown("### 📥 Entrada")
298
+ video_input = gr.Video(label="Video con Marca de Agua")
299
+
300
+ sensitivity = gr.Slider(
301
+ minimum=10,
302
+ maximum=100,
303
+ value=50,
304
+ step=10,
305
+ label="Sensibilidad de Detección",
306
+ info="Mayor valor = detección más agresiva"
307
+ )
308
+
309
+ process_btn = gr.Button("🚀 ELIMINAR MARCA DE AGUA", variant="primary", size="lg")
310
+
311
+ gr.Markdown("""
312
+ ### 💡 Tips:
313
+ - Sube videos claros
314
+ - Si no detecta la marca, aumenta sensibilidad
315
+ - El proceso toma tiempo según duración del video
316
+ """)
317
 
318
+ with gr.Column(scale=1):
319
+ gr.Markdown("### 📤 Resultado")
320
  video_output = gr.Video(label="Video Limpio")
321
+ status_output = gr.Textbox(label="Estado del Proceso", lines=4)
322
 
323
  process_btn.click(
324
  fn=process_video_interface,
325
+ inputs=[video_input, sensitivity],
326
  outputs=[video_output, status_output]
327
  )
328
 
329
  gr.Markdown("""
330
  ---
331
+ ### 🔧 Cómo Funciona:
332
+
333
+ 1. **Análisis Temporal**: Examina múltiples frames para identificar patrones de marca de agua
334
+ 2. **Tracking Adaptativo**: Sigue el movimiento de la marca usando optical flow
335
+ 3. **Inpainting Inteligente**: Rellena las áreas detectadas con contenido del fondo
336
+ 4. **Limpieza Automática**: Borra archivos temporales mayores a 2 horas
337
+
338
+ ### ⚠️ Limitaciones:
339
+ - Funciona mejor con marcas de agua semi-transparentes
340
+ - Videos muy comprimidos pueden tener resultados variables
341
+ - Marcas de agua muy grandes pueden dejar artefactos
342
  """)
343
 
344
  if __name__ == "__main__":