MogensR commited on
Commit
3c8897a
·
1 Parent(s): c6741c9

Update utils/utils.py

Browse files
Files changed (1) hide show
  1. utils/utils.py +377 -1
utils/utils.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
  Unified Utilities Module for BackgroundFX Pro
3
- Combines FileManager, VideoUtils, ImageUtils, and CV utilities
4
  """
5
 
6
  # Set OMP_NUM_THREADS at the very beginning to prevent libgomp errors
@@ -17,6 +17,7 @@
17
  from datetime import datetime
18
  import subprocess
19
  import time
 
20
 
21
  import cv2
22
  import numpy as np
@@ -109,6 +110,381 @@ class BackgroundReplacementError(Exception):
109
  """Custom exception for background replacement failures"""
110
  pass
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  # ============================================================================
113
  # FILE MANAGER CLASS
114
  # ============================================================================
 
1
  """
2
  Unified Utilities Module for BackgroundFX Pro
3
+ Combines FileManager, VideoUtils, ImageUtils, ValidationUtils, and CV utilities
4
  """
5
 
6
  # Set OMP_NUM_THREADS at the very beginning to prevent libgomp errors
 
17
  from datetime import datetime
18
  import subprocess
19
  import time
20
+ import re
21
 
22
  import cv2
23
  import numpy as np
 
110
  """Custom exception for background replacement failures"""
111
  pass
112
 
113
+ # ============================================================================
114
+ # VALIDATION UTILS CLASS
115
+ # ============================================================================
116
+
117
+ class ValidationUtils:
118
+ """Validation utilities for BackgroundFX Pro application."""
119
+
120
+ # Supported formats
121
+ SUPPORTED_VIDEO_FORMATS = {'.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv', '.m4v'}
122
+ SUPPORTED_IMAGE_FORMATS = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.webp'}
123
+
124
+ # Size limits (in bytes)
125
+ MAX_VIDEO_SIZE = 500 * 1024 * 1024 # 500MB
126
+ MAX_IMAGE_SIZE = 50 * 1024 * 1024 # 50MB
127
+ MIN_VIDEO_SIZE = 1024 # 1KB (to avoid empty files)
128
+
129
+ # Video constraints
130
+ MAX_VIDEO_DURATION = 300 # 5 minutes in seconds
131
+ MIN_VIDEO_DURATION = 1 # 1 second minimum
132
+ MAX_RESOLUTION = (3840, 2160) # 4K
133
+ MIN_RESOLUTION = (320, 240) # Minimum reasonable resolution
134
+ MAX_FPS = 120
135
+ MIN_FPS = 10
136
+
137
+ @staticmethod
138
+ def validate_video_file(file_path, check_content=False):
139
+ """
140
+ Validate video file for processing.
141
+
142
+ Args:
143
+ file_path: Path to the video file
144
+ check_content: Whether to perform deep content validation
145
+
146
+ Returns:
147
+ tuple: (is_valid, error_message)
148
+ """
149
+ from pathlib import Path
150
+
151
+ if not file_path:
152
+ return False, "No file path provided"
153
+
154
+ path = Path(file_path)
155
+
156
+ # Check if file exists
157
+ if not path.exists():
158
+ return False, f"File not found: {file_path}"
159
+
160
+ # Check file extension
161
+ if path.suffix.lower() not in ValidationUtils.SUPPORTED_VIDEO_FORMATS:
162
+ return False, f"Unsupported video format: {path.suffix}. Supported formats: {', '.join(ValidationUtils.SUPPORTED_VIDEO_FORMATS)}"
163
+
164
+ # Check file size
165
+ file_size = path.stat().st_size
166
+ if file_size > ValidationUtils.MAX_VIDEO_SIZE:
167
+ size_mb = file_size / (1024 * 1024)
168
+ return False, f"Video file too large: {size_mb:.1f}MB (max: {ValidationUtils.MAX_VIDEO_SIZE / (1024 * 1024):.0f}MB)"
169
+
170
+ if file_size < ValidationUtils.MIN_VIDEO_SIZE:
171
+ return False, "Video file appears to be empty or corrupted"
172
+
173
+ # Deep content validation if requested
174
+ if check_content:
175
+ try:
176
+ cap = cv2.VideoCapture(str(file_path))
177
+
178
+ if not cap.isOpened():
179
+ return False, "Unable to open video file - may be corrupted"
180
+
181
+ # Get video properties
182
+ fps = cap.get(cv2.CAP_PROP_FPS)
183
+ frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT)
184
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
185
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
186
+
187
+ # Calculate duration
188
+ duration = frame_count / fps if fps > 0 else 0
189
+
190
+ cap.release()
191
+
192
+ # Validate properties
193
+ if duration > ValidationUtils.MAX_VIDEO_DURATION:
194
+ return False, f"Video too long: {duration:.1f}s (max: {ValidationUtils.MAX_VIDEO_DURATION}s)"
195
+
196
+ if duration < ValidationUtils.MIN_VIDEO_DURATION:
197
+ return False, f"Video too short: {duration:.1f}s (min: {ValidationUtils.MIN_VIDEO_DURATION}s)"
198
+
199
+ if width > ValidationUtils.MAX_RESOLUTION[0] or height > ValidationUtils.MAX_RESOLUTION[1]:
200
+ return False, f"Video resolution too high: {width}x{height} (max: {ValidationUtils.MAX_RESOLUTION[0]}x{ValidationUtils.MAX_RESOLUTION[1]})"
201
+
202
+ if width < ValidationUtils.MIN_RESOLUTION[0] or height < ValidationUtils.MIN_RESOLUTION[1]:
203
+ return False, f"Video resolution too low: {width}x{height} (min: {ValidationUtils.MIN_RESOLUTION[0]}x{ValidationUtils.MIN_RESOLUTION[1]})"
204
+
205
+ if fps > ValidationUtils.MAX_FPS:
206
+ return False, f"Frame rate too high: {fps:.1f} fps (max: {ValidationUtils.MAX_FPS} fps)"
207
+
208
+ if fps < ValidationUtils.MIN_FPS:
209
+ return False, f"Frame rate too low: {fps:.1f} fps (min: {ValidationUtils.MIN_FPS} fps)"
210
+
211
+ except Exception as e:
212
+ return False, f"Error validating video content: {str(e)}"
213
+
214
+ return True, "Video file is valid"
215
+
216
+ @staticmethod
217
+ def validate_image_file(file_path, check_content=False):
218
+ """
219
+ Validate image file for background replacement.
220
+
221
+ Args:
222
+ file_path: Path to the image file
223
+ check_content: Whether to perform deep content validation
224
+
225
+ Returns:
226
+ tuple: (is_valid, error_message)
227
+ """
228
+ from pathlib import Path
229
+
230
+ if not file_path:
231
+ return False, "No file path provided"
232
+
233
+ path = Path(file_path)
234
+
235
+ # Check if file exists
236
+ if not path.exists():
237
+ return False, f"File not found: {file_path}"
238
+
239
+ # Check file extension
240
+ if path.suffix.lower() not in ValidationUtils.SUPPORTED_IMAGE_FORMATS:
241
+ return False, f"Unsupported image format: {path.suffix}. Supported formats: {', '.join(ValidationUtils.SUPPORTED_IMAGE_FORMATS)}"
242
+
243
+ # Check file size
244
+ file_size = path.stat().st_size
245
+ if file_size > ValidationUtils.MAX_IMAGE_SIZE:
246
+ size_mb = file_size / (1024 * 1024)
247
+ return False, f"Image file too large: {size_mb:.1f}MB (max: {ValidationUtils.MAX_IMAGE_SIZE / (1024 * 1024):.0f}MB)"
248
+
249
+ # Deep content validation if requested
250
+ if check_content:
251
+ try:
252
+ img = cv2.imread(str(file_path))
253
+ if img is None:
254
+ return False, "Unable to read image file - may be corrupted"
255
+
256
+ height, width = img.shape[:2]
257
+
258
+ # Check dimensions
259
+ if width > ValidationUtils.MAX_RESOLUTION[0] or height > ValidationUtils.MAX_RESOLUTION[1]:
260
+ return False, f"Image resolution too high: {width}x{height} (max: {ValidationUtils.MAX_RESOLUTION[0]}x{ValidationUtils.MAX_RESOLUTION[1]})"
261
+
262
+ if width < ValidationUtils.MIN_RESOLUTION[0] or height < ValidationUtils.MIN_RESOLUTION[1]:
263
+ return False, f"Image resolution too low: {width}x{height} (min: {ValidationUtils.MIN_RESOLUTION[0]}x{ValidationUtils.MIN_RESOLUTION[1]})"
264
+
265
+ except Exception as e:
266
+ return False, f"Error validating image content: {str(e)}"
267
+
268
+ return True, "Image file is valid"
269
+
270
+ @staticmethod
271
+ def validate_processing_params(params):
272
+ """
273
+ Validate processing parameters.
274
+
275
+ Args:
276
+ params: Dictionary of processing parameters
277
+
278
+ Returns:
279
+ tuple: (is_valid, error_message)
280
+ """
281
+ if not params:
282
+ return False, "No parameters provided"
283
+
284
+ # Validate confidence threshold
285
+ if 'confidence_threshold' in params:
286
+ conf = params['confidence_threshold']
287
+ if not isinstance(conf, (int, float)):
288
+ return False, "Confidence threshold must be a number"
289
+ if conf < 0 or conf > 1:
290
+ return False, "Confidence threshold must be between 0 and 1"
291
+
292
+ # Validate mask dilation
293
+ if 'mask_dilation' in params:
294
+ dilation = params['mask_dilation']
295
+ if not isinstance(dilation, int):
296
+ return False, "Mask dilation must be an integer"
297
+ if dilation < 0 or dilation > 50:
298
+ return False, "Mask dilation must be between 0 and 50"
299
+
300
+ # Validate edge smoothing
301
+ if 'edge_smoothing' in params:
302
+ smooth = params['edge_smoothing']
303
+ if not isinstance(smooth, int):
304
+ return False, "Edge smoothing must be an integer"
305
+ if smooth < 0 or smooth > 100:
306
+ return False, "Edge smoothing must be between 0 and 100"
307
+
308
+ # Validate color adjustment
309
+ if 'color_adjustment' in params:
310
+ color_adj = params['color_adjustment']
311
+ if not isinstance(color_adj, bool):
312
+ return False, "Color adjustment must be a boolean"
313
+
314
+ # Validate output quality
315
+ if 'output_quality' in params:
316
+ quality = params['output_quality']
317
+ if not isinstance(quality, int):
318
+ return False, "Output quality must be an integer"
319
+ if quality < 1 or quality > 100:
320
+ return False, "Output quality must be between 1 and 100"
321
+
322
+ # Validate processing method
323
+ if 'processing_method' in params:
324
+ method = params['processing_method']
325
+ valid_methods = {'sam2', 'matanyone', 'cv_fallback', 'auto'}
326
+ if method not in valid_methods:
327
+ return False, f"Invalid processing method. Must be one of: {', '.join(valid_methods)}"
328
+
329
+ return True, "Parameters are valid"
330
+
331
+ @staticmethod
332
+ def validate_output_path(output_path, create_dirs=False):
333
+ """
334
+ Validate output path for saving results.
335
+
336
+ Args:
337
+ output_path: Path where output will be saved
338
+ create_dirs: Whether to create directories if they don't exist
339
+
340
+ Returns:
341
+ tuple: (is_valid, error_message)
342
+ """
343
+ from pathlib import Path
344
+
345
+ if not output_path:
346
+ return False, "No output path provided"
347
+
348
+ path = Path(output_path)
349
+ parent_dir = path.parent
350
+
351
+ # Check if parent directory exists
352
+ if not parent_dir.exists():
353
+ if create_dirs:
354
+ try:
355
+ parent_dir.mkdir(parents=True, exist_ok=True)
356
+ except Exception as e:
357
+ return False, f"Failed to create output directory: {str(e)}"
358
+ else:
359
+ return False, f"Output directory does not exist: {parent_dir}"
360
+
361
+ # Check write permissions
362
+ if not os.access(parent_dir, os.W_OK):
363
+ return False, f"No write permission for directory: {parent_dir}"
364
+
365
+ # Check if file already exists
366
+ if path.exists():
367
+ if not os.access(path, os.W_OK):
368
+ return False, f"Cannot overwrite existing file: {output_path}"
369
+
370
+ return True, "Output path is valid"
371
+
372
+ @staticmethod
373
+ def sanitize_filename(filename):
374
+ """
375
+ Sanitize filename to be safe for filesystem.
376
+
377
+ Args:
378
+ filename: Original filename
379
+
380
+ Returns:
381
+ str: Sanitized filename
382
+ """
383
+ from pathlib import Path
384
+
385
+ # Get the stem and suffix separately
386
+ path = Path(filename)
387
+ stem = path.stem
388
+ suffix = path.suffix
389
+
390
+ # Remove or replace invalid characters
391
+ # Keep only alphanumeric, dash, underscore, and dot
392
+ stem = re.sub(r'[^\w\-_.]', '_', stem)
393
+
394
+ # Remove multiple underscores
395
+ stem = re.sub(r'_+', '_', stem)
396
+
397
+ # Remove leading/trailing underscores
398
+ stem = stem.strip('_')
399
+
400
+ # Ensure filename is not empty
401
+ if not stem:
402
+ stem = 'output'
403
+
404
+ # Limit length (keep it reasonable for most filesystems)
405
+ max_length = 200
406
+ if len(stem) > max_length:
407
+ stem = stem[:max_length]
408
+
409
+ return f"{stem}{suffix}"
410
+
411
+ @staticmethod
412
+ def validate_memory_available(required_mb=1000):
413
+ """
414
+ Check if sufficient memory is available.
415
+
416
+ Args:
417
+ required_mb: Required memory in megabytes
418
+
419
+ Returns:
420
+ tuple: (is_sufficient, available_mb, error_message)
421
+ """
422
+ try:
423
+ import psutil
424
+
425
+ mem = psutil.virtual_memory()
426
+ available_mb = mem.available / (1024 * 1024)
427
+
428
+ if available_mb < required_mb:
429
+ return False, available_mb, f"Insufficient memory: {available_mb:.0f}MB available, {required_mb:.0f}MB required"
430
+
431
+ return True, available_mb, f"Sufficient memory available: {available_mb:.0f}MB"
432
+
433
+ except ImportError:
434
+ # If psutil not available, assume sufficient memory
435
+ return True, -1, "Memory check skipped (psutil not available)"
436
+ except Exception as e:
437
+ return True, -1, f"Memory check failed: {str(e)}"
438
+
439
+ @staticmethod
440
+ def validate_gpu_available():
441
+ """
442
+ Check if GPU is available for processing.
443
+
444
+ Returns:
445
+ tuple: (is_available, device_info)
446
+ """
447
+ try:
448
+ if torch.cuda.is_available():
449
+ device_name = torch.cuda.get_device_name(0)
450
+ memory_gb = torch.cuda.get_device_properties(0).total_memory / (1024**3)
451
+ return True, f"GPU available: {device_name} ({memory_gb:.1f}GB)"
452
+ else:
453
+ return False, "No GPU available - will use CPU"
454
+
455
+ except ImportError:
456
+ return False, "PyTorch not available for GPU check"
457
+ except Exception as e:
458
+ return False, f"GPU check failed: {str(e)}"
459
+
460
+ @staticmethod
461
+ def validate_url(url):
462
+ """
463
+ Validate URL format.
464
+
465
+ Args:
466
+ url: URL string to validate
467
+
468
+ Returns:
469
+ tuple: (is_valid, error_message)
470
+ """
471
+ if not url:
472
+ return False, "No URL provided"
473
+
474
+ # Basic URL pattern
475
+ url_pattern = re.compile(
476
+ r'^https?://' # http:// or https://
477
+ r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' # domain...
478
+ r'localhost|' # localhost...
479
+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
480
+ r'(?::\d+)?' # optional port
481
+ r'(?:/?|[/?]\S+)$', re.IGNORECASE)
482
+
483
+ if url_pattern.match(url):
484
+ return True, "Valid URL"
485
+ else:
486
+ return False, "Invalid URL format"
487
+
488
  # ============================================================================
489
  # FILE MANAGER CLASS
490
  # ============================================================================