|
""" |
|
Quality analysis and metrics for BackgroundFX Pro. |
|
Provides REAL metrics instead of fake 100% values. |
|
""" |
|
|
|
import numpy as np |
|
import cv2 |
|
import torch |
|
from typing import Dict, List, Optional, Tuple, Any |
|
from dataclasses import dataclass, field |
|
from collections import deque |
|
import logging |
|
from scipy import signal, ndimage |
|
|
|
import json |
|
from pathlib import Path |
|
from datetime import datetime |
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
@dataclass |
|
class QualityMetrics: |
|
"""Real quality metrics container.""" |
|
|
|
edge_accuracy: float = 0.0 |
|
edge_smoothness: float = 0.0 |
|
edge_completeness: float = 0.0 |
|
|
|
|
|
temporal_stability: float = 0.0 |
|
temporal_consistency: float = 0.0 |
|
flicker_score: float = 0.0 |
|
|
|
|
|
mask_coverage: float = 0.0 |
|
mask_accuracy: float = 0.0 |
|
mask_confidence: float = 0.0 |
|
hole_ratio: float = 0.0 |
|
|
|
|
|
detail_preservation: float = 0.0 |
|
hair_detail_score: float = 0.0 |
|
texture_quality: float = 0.0 |
|
|
|
|
|
overall_quality: float = 0.0 |
|
processing_confidence: float = 0.0 |
|
|
|
|
|
breakdown: Dict[str, float] = field(default_factory=dict) |
|
warnings: List[str] = field(default_factory=list) |
|
|
|
def to_dict(self) -> Dict[str, Any]: |
|
"""Convert to dictionary.""" |
|
return { |
|
'edge_accuracy': round(self.edge_accuracy, 3), |
|
'edge_smoothness': round(self.edge_smoothness, 3), |
|
'edge_completeness': round(self.edge_completeness, 3), |
|
'temporal_stability': round(self.temporal_stability, 3), |
|
'temporal_consistency': round(self.temporal_consistency, 3), |
|
'flicker_score': round(self.flicker_score, 3), |
|
'mask_coverage': round(self.mask_coverage, 3), |
|
'mask_accuracy': round(self.mask_accuracy, 3), |
|
'mask_confidence': round(self.mask_confidence, 3), |
|
'hole_ratio': round(self.hole_ratio, 3), |
|
'detail_preservation': round(self.detail_preservation, 3), |
|
'hair_detail_score': round(self.hair_detail_score, 3), |
|
'texture_quality': round(self.texture_quality, 3), |
|
'overall_quality': round(self.overall_quality, 3), |
|
'processing_confidence': round(self.processing_confidence, 3), |
|
'breakdown': self.breakdown, |
|
'warnings': self.warnings |
|
} |
|
|
|
def get_summary(self) -> str: |
|
"""Get human-readable summary.""" |
|
status = "Excellent" if self.overall_quality > 0.9 else \ |
|
"Good" if self.overall_quality > 0.75 else \ |
|
"Fair" if self.overall_quality > 0.6 else "Poor" |
|
|
|
return (f"Quality: {status} ({self.overall_quality:.1%})\n" |
|
f"Edge: {self.edge_accuracy:.1%} | " |
|
f"Temporal: {self.temporal_stability:.1%} | " |
|
f"Detail: {self.detail_preservation:.1%}") |
|
|
|
|
|
@dataclass |
|
class QualityConfig: |
|
"""Configuration for quality analysis.""" |
|
enable_deep_analysis: bool = True |
|
temporal_window: int = 5 |
|
edge_threshold: float = 0.1 |
|
min_confidence: float = 0.6 |
|
detect_artifacts: bool = True |
|
compute_ssim: bool = True |
|
compute_psnr: bool = True |
|
save_reports: bool = True |
|
report_dir: str = "LOGS/quality_reports" |
|
warning_thresholds: Dict[str, float] = field(default_factory=lambda: { |
|
'edge_accuracy': 0.7, |
|
'temporal_stability': 0.75, |
|
'mask_accuracy': 0.8, |
|
'detail_preservation': 0.7 |
|
}) |
|
|
|
|
|
class QualityAnalyzer: |
|
"""Comprehensive quality analysis system.""" |
|
|
|
def __init__(self, config: Optional[QualityConfig] = None): |
|
self.config = config or QualityConfig() |
|
self.frame_buffer = deque(maxlen=self.config.temporal_window) |
|
self.mask_buffer = deque(maxlen=self.config.temporal_window) |
|
self.metrics_history = deque(maxlen=100) |
|
self.frame_count = 0 |
|
|
|
|
|
self.edge_analyzer = EdgeQualityAnalyzer() |
|
self.temporal_analyzer = TemporalQualityAnalyzer() |
|
self.detail_analyzer = DetailPreservationAnalyzer() |
|
self.artifact_detector = ArtifactDetector() |
|
|
|
|
|
if self.config.save_reports: |
|
Path(self.config.report_dir).mkdir(parents=True, exist_ok=True) |
|
|
|
def analyze_frame(self, |
|
original_frame: np.ndarray, |
|
processed_frame: np.ndarray, |
|
mask: np.ndarray, |
|
alpha: Optional[np.ndarray] = None) -> QualityMetrics: |
|
"""Analyze frame quality with REAL metrics.""" |
|
self.frame_count += 1 |
|
metrics = QualityMetrics() |
|
|
|
|
|
self.frame_buffer.append(processed_frame) |
|
self.mask_buffer.append(mask) |
|
|
|
|
|
edge_metrics = self.edge_analyzer.analyze(original_frame, mask, alpha) |
|
metrics.edge_accuracy = edge_metrics['accuracy'] |
|
metrics.edge_smoothness = edge_metrics['smoothness'] |
|
metrics.edge_completeness = edge_metrics['completeness'] |
|
|
|
|
|
if len(self.mask_buffer) >= 2: |
|
temporal_metrics = self.temporal_analyzer.analyze( |
|
self.mask_buffer, self.frame_buffer |
|
) |
|
metrics.temporal_stability = temporal_metrics['stability'] |
|
metrics.temporal_consistency = temporal_metrics['consistency'] |
|
metrics.flicker_score = temporal_metrics['flicker'] |
|
else: |
|
|
|
metrics.temporal_stability = 1.0 |
|
metrics.temporal_consistency = 1.0 |
|
metrics.flicker_score = 0.0 |
|
|
|
|
|
mask_metrics = self._analyze_mask_quality(mask, alpha) |
|
metrics.mask_coverage = mask_metrics['coverage'] |
|
metrics.mask_accuracy = mask_metrics['accuracy'] |
|
metrics.mask_confidence = mask_metrics['confidence'] |
|
metrics.hole_ratio = mask_metrics['hole_ratio'] |
|
|
|
|
|
detail_metrics = self.detail_analyzer.analyze( |
|
original_frame, processed_frame, mask |
|
) |
|
metrics.detail_preservation = detail_metrics['overall'] |
|
metrics.hair_detail_score = detail_metrics['hair_detail'] |
|
metrics.texture_quality = detail_metrics['texture'] |
|
|
|
|
|
if self.config.detect_artifacts: |
|
artifacts = self.artifact_detector.detect(processed_frame, mask) |
|
if artifacts['found']: |
|
for artifact in artifacts['types']: |
|
metrics.warnings.append(f"Artifact detected: {artifact}") |
|
|
|
|
|
metrics.overall_quality = self._compute_overall_quality(metrics) |
|
metrics.processing_confidence = self._compute_confidence(metrics) |
|
|
|
|
|
self._generate_warnings(metrics) |
|
|
|
|
|
self.metrics_history.append(metrics) |
|
|
|
|
|
if self.config.save_reports and self.frame_count % 30 == 0: |
|
self._save_report(metrics) |
|
|
|
return metrics |
|
|
|
def _analyze_mask_quality(self, mask: np.ndarray, |
|
alpha: Optional[np.ndarray] = None) -> Dict[str, float]: |
|
"""Analyze mask quality metrics.""" |
|
h, w = mask.shape[:2] |
|
total_pixels = h * w |
|
|
|
|
|
coverage = np.sum(mask > 0.5) / total_pixels |
|
|
|
|
|
mask_binary = (mask > 0.5).astype(np.uint8) |
|
|
|
|
|
contours, _ = cv2.findContours( |
|
mask_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE |
|
) |
|
|
|
|
|
hole_area = 0 |
|
if len(contours) > 0: |
|
|
|
filled = np.zeros_like(mask_binary) |
|
cv2.drawContours(filled, contours, -1, 1, -1) |
|
|
|
|
|
holes = filled - mask_binary |
|
hole_area = np.sum(holes) / np.sum(filled) if np.sum(filled) > 0 else 0 |
|
|
|
|
|
gradient_x = cv2.Sobel(mask, cv2.CV_64F, 1, 0, ksize=3) |
|
gradient_y = cv2.Sobel(mask, cv2.CV_64F, 0, 1, ksize=3) |
|
gradient_mag = np.sqrt(gradient_x**2 + gradient_y**2) |
|
|
|
|
|
gradient_smoothness = 1.0 - np.std(gradient_mag) / (np.mean(gradient_mag) + 1e-6) |
|
accuracy = np.clip(gradient_smoothness, 0, 1) |
|
|
|
|
|
if alpha is not None: |
|
diff = np.abs(alpha - mask) |
|
confidence = 1.0 - np.mean(diff) |
|
else: |
|
|
|
hist, _ = np.histogram(mask.flatten(), bins=10, range=(0, 1)) |
|
hist = hist / hist.sum() |
|
|
|
confidence = (hist[0] + hist[-1]) / 2.0 |
|
|
|
return { |
|
'coverage': coverage, |
|
'accuracy': accuracy, |
|
'confidence': confidence, |
|
'hole_ratio': hole_area |
|
} |
|
|
|
def _compute_overall_quality(self, metrics: QualityMetrics) -> float: |
|
"""Compute weighted overall quality score.""" |
|
weights = { |
|
'edge': 0.25, |
|
'temporal': 0.25, |
|
'mask': 0.25, |
|
'detail': 0.25 |
|
} |
|
|
|
|
|
edge_score = np.mean([ |
|
metrics.edge_accuracy, |
|
metrics.edge_smoothness, |
|
metrics.edge_completeness |
|
]) |
|
|
|
temporal_score = np.mean([ |
|
metrics.temporal_stability, |
|
metrics.temporal_consistency, |
|
1.0 - metrics.flicker_score |
|
]) |
|
|
|
mask_score = np.mean([ |
|
metrics.mask_accuracy, |
|
metrics.mask_confidence, |
|
1.0 - metrics.hole_ratio |
|
]) |
|
|
|
detail_score = np.mean([ |
|
metrics.detail_preservation, |
|
metrics.hair_detail_score, |
|
metrics.texture_quality |
|
]) |
|
|
|
|
|
overall = ( |
|
weights['edge'] * edge_score + |
|
weights['temporal'] * temporal_score + |
|
weights['mask'] * mask_score + |
|
weights['detail'] * detail_score |
|
) |
|
|
|
|
|
penalty = len(metrics.warnings) * 0.05 |
|
overall = max(0, overall - penalty) |
|
|
|
return np.clip(overall, 0, 1) |
|
|
|
def _compute_confidence(self, metrics: QualityMetrics) -> float: |
|
"""Compute processing confidence.""" |
|
|
|
factors = [] |
|
|
|
|
|
factors.append(metrics.edge_accuracy) |
|
|
|
|
|
factors.append(metrics.temporal_stability) |
|
|
|
|
|
factors.append(1.0 - metrics.hole_ratio) |
|
|
|
|
|
factors.append(metrics.mask_confidence) |
|
|
|
|
|
warning_factor = 1.0 if len(metrics.warnings) == 0 else 0.8 |
|
factors.append(warning_factor) |
|
|
|
return np.mean(factors) |
|
|
|
def _generate_warnings(self, metrics: QualityMetrics): |
|
"""Generate warnings based on quality thresholds.""" |
|
for metric_name, threshold in self.config.warning_thresholds.items(): |
|
if hasattr(metrics, metric_name): |
|
value = getattr(metrics, metric_name) |
|
if value < threshold: |
|
metrics.warnings.append( |
|
f"Low {metric_name.replace('_', ' ')}: {value:.1%} < {threshold:.1%}" |
|
) |
|
|
|
def _save_report(self, metrics: QualityMetrics): |
|
"""Save quality report to file.""" |
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
|
report_path = Path(self.config.report_dir) / f"quality_report_{timestamp}.json" |
|
|
|
report = { |
|
'timestamp': timestamp, |
|
'frame_count': self.frame_count, |
|
'metrics': metrics.to_dict(), |
|
'config': { |
|
'temporal_window': self.config.temporal_window, |
|
'edge_threshold': self.config.edge_threshold, |
|
'min_confidence': self.config.min_confidence |
|
} |
|
} |
|
|
|
with open(report_path, 'w') as f: |
|
json.dump(report, f, indent=2) |
|
|
|
logger.info(f"Quality report saved to {report_path}") |
|
|
|
def get_statistics(self) -> Dict[str, Any]: |
|
"""Get quality statistics over time.""" |
|
if not self.metrics_history: |
|
return {} |
|
|
|
|
|
all_metrics = list(self.metrics_history) |
|
|
|
stats = { |
|
'average_quality': np.mean([m.overall_quality for m in all_metrics]), |
|
'min_quality': np.min([m.overall_quality for m in all_metrics]), |
|
'max_quality': np.max([m.overall_quality for m in all_metrics]), |
|
'std_quality': np.std([m.overall_quality for m in all_metrics]), |
|
'total_warnings': sum(len(m.warnings) for m in all_metrics), |
|
'frames_analyzed': len(all_metrics) |
|
} |
|
|
|
return stats |
|
|
|
|
|
class EdgeQualityAnalyzer: |
|
"""Analyzes edge quality in masks.""" |
|
|
|
def analyze(self, image: np.ndarray, mask: np.ndarray, |
|
alpha: Optional[np.ndarray] = None) -> Dict[str, float]: |
|
"""Analyze edge quality metrics.""" |
|
|
|
if len(image.shape) == 3: |
|
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) |
|
else: |
|
gray = image |
|
|
|
|
|
image_edges = cv2.Canny(gray, 50, 150) / 255.0 |
|
|
|
|
|
mask_uint8 = (mask * 255).astype(np.uint8) |
|
mask_edges = cv2.Canny(mask_uint8, 50, 150) / 255.0 |
|
|
|
|
|
overlap = np.logical_and(image_edges > 0, mask_edges > 0) |
|
accuracy = np.sum(overlap) / (np.sum(mask_edges) + 1e-6) |
|
|
|
|
|
contours, _ = cv2.findContours( |
|
mask_uint8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE |
|
) |
|
|
|
smoothness = 1.0 |
|
if len(contours) > 0: |
|
|
|
for contour in contours: |
|
perimeter = cv2.arcLength(contour, True) |
|
if perimeter > 0: |
|
|
|
epsilon = 0.02 * perimeter |
|
approx = cv2.approxPolyDP(contour, epsilon, True) |
|
|
|
|
|
complexity = len(approx) / (perimeter / 10 + 1) |
|
smoothness = min(smoothness, 1.0 / (1.0 + complexity)) |
|
|
|
|
|
if np.sum(image_edges) > 0: |
|
|
|
kernel = np.ones((5, 5), np.uint8) |
|
mask_edges_dilated = cv2.dilate(mask_edges, kernel, iterations=1) |
|
|
|
covered = np.logical_and(image_edges > 0, mask_edges_dilated > 0) |
|
completeness = np.sum(covered) / np.sum(image_edges) |
|
else: |
|
completeness = 1.0 |
|
|
|
return { |
|
'accuracy': np.clip(accuracy, 0, 1), |
|
'smoothness': np.clip(smoothness, 0, 1), |
|
'completeness': np.clip(completeness, 0, 1) |
|
} |
|
|
|
|
|
class TemporalQualityAnalyzer: |
|
"""Analyzes temporal consistency and stability.""" |
|
|
|
def analyze(self, mask_buffer: deque, frame_buffer: deque) -> Dict[str, float]: |
|
"""Analyze temporal quality metrics.""" |
|
if len(mask_buffer) < 2: |
|
return {'stability': 1.0, 'consistency': 1.0, 'flicker': 0.0} |
|
|
|
masks = list(mask_buffer) |
|
|
|
|
|
differences = [] |
|
for i in range(1, len(masks)): |
|
diff = np.abs(masks[i] - masks[i-1]) |
|
differences.append(np.mean(diff)) |
|
|
|
|
|
avg_diff = np.mean(differences) |
|
stability = 1.0 - min(avg_diff * 2, 1.0) |
|
|
|
|
|
mask_stack = np.stack(masks, axis=0) |
|
variance = np.var(mask_stack, axis=0) |
|
consistency = 1.0 - np.mean(variance) |
|
|
|
|
|
flicker = 0.0 |
|
if len(differences) >= 3: |
|
|
|
for i in range(1, len(differences) - 1): |
|
if differences[i] < differences[i-1] * 0.5 and differences[i] < differences[i+1] * 0.5: |
|
flicker += 0.1 |
|
elif differences[i] > differences[i-1] * 2 and differences[i] > differences[i+1] * 2: |
|
flicker += 0.1 |
|
|
|
flicker = min(flicker, 1.0) |
|
|
|
return { |
|
'stability': np.clip(stability, 0, 1), |
|
'consistency': np.clip(consistency, 0, 1), |
|
'flicker': np.clip(flicker, 0, 1) |
|
} |
|
|
|
|
|
class DetailPreservationAnalyzer: |
|
"""Analyzes how well details are preserved.""" |
|
|
|
def analyze(self, original: np.ndarray, processed: np.ndarray, |
|
mask: np.ndarray) -> Dict[str, float]: |
|
"""Analyze detail preservation metrics.""" |
|
|
|
if len(original.shape) == 3: |
|
orig_gray = cv2.cvtColor(original, cv2.COLOR_BGR2GRAY) |
|
proc_gray = cv2.cvtColor(processed, cv2.COLOR_BGR2GRAY) |
|
else: |
|
orig_gray = original |
|
proc_gray = processed |
|
|
|
|
|
mask_binary = mask > 0.5 |
|
|
|
|
|
overall = 1.0 |
|
if np.any(mask_binary): |
|
|
|
orig_masked = orig_gray * mask_binary |
|
proc_masked = proc_gray * mask_binary |
|
|
|
try: |
|
overall = skmetrics.structural_similarity( |
|
orig_masked, proc_masked, |
|
data_range=255 |
|
) |
|
except: |
|
overall = 0.8 |
|
|
|
|
|
hair_detail = self._analyze_hair_details(orig_gray, proc_gray, mask) |
|
|
|
|
|
texture = self._analyze_texture_quality(orig_gray, proc_gray, mask_binary) |
|
|
|
return { |
|
'overall': np.clip(overall, 0, 1), |
|
'hair_detail': np.clip(hair_detail, 0, 1), |
|
'texture': np.clip(texture, 0, 1) |
|
} |
|
|
|
def _analyze_hair_details(self, orig: np.ndarray, proc: np.ndarray, |
|
mask: np.ndarray) -> float: |
|
"""Analyze hair detail preservation.""" |
|
|
|
kernel = np.array([[-1, -1, -1], |
|
[-1, 8, -1], |
|
[-1, -1, -1]], dtype=np.float32) |
|
|
|
orig_details = cv2.filter2D(orig, -1, kernel) |
|
proc_details = cv2.filter2D(proc, -1, kernel) |
|
|
|
|
|
edges = cv2.Canny((mask * 255).astype(np.uint8), 50, 150) |
|
edge_mask = edges > 0 |
|
|
|
if np.any(edge_mask): |
|
|
|
orig_edge_details = np.abs(orig_details[edge_mask]) |
|
proc_edge_details = np.abs(proc_details[edge_mask]) |
|
|
|
|
|
if len(orig_edge_details) > 0 and len(proc_edge_details) > 0: |
|
correlation = np.corrcoef( |
|
orig_edge_details.flatten(), |
|
proc_edge_details.flatten() |
|
)[0, 1] |
|
|
|
return (correlation + 1) / 2 |
|
|
|
return 0.8 |
|
|
|
def _analyze_texture_quality(self, orig: np.ndarray, proc: np.ndarray, |
|
mask: np.ndarray) -> float: |
|
"""Analyze texture preservation quality.""" |
|
|
|
window_size = 5 |
|
|
|
def local_variance(img): |
|
mean = cv2.blur(img, (window_size, window_size)) |
|
sqr_mean = cv2.blur(img**2, (window_size, window_size)) |
|
variance = sqr_mean - mean**2 |
|
return np.sqrt(np.maximum(variance, 0)) |
|
|
|
orig_texture = local_variance(orig.astype(np.float32)) |
|
proc_texture = local_variance(proc.astype(np.float32)) |
|
|
|
|
|
if np.any(mask): |
|
orig_masked_texture = orig_texture[mask] |
|
proc_masked_texture = proc_texture[mask] |
|
|
|
if len(orig_masked_texture) > 0: |
|
|
|
texture_diff = np.abs(orig_masked_texture - proc_masked_texture) |
|
max_texture = np.maximum(orig_masked_texture, proc_masked_texture) + 1e-6 |
|
|
|
similarity = 1.0 - np.mean(texture_diff / max_texture) |
|
return similarity |
|
|
|
return 0.8 |
|
|
|
|
|
class ArtifactDetector: |
|
"""Detects various artifacts in processed frames.""" |
|
|
|
def detect(self, frame: np.ndarray, mask: np.ndarray) -> Dict[str, Any]: |
|
"""Detect artifacts in frame and mask.""" |
|
artifacts = { |
|
'found': False, |
|
'types': [], |
|
'locations': [] |
|
} |
|
|
|
|
|
if self._detect_halo(frame, mask): |
|
artifacts['found'] = True |
|
artifacts['types'].append('halo') |
|
|
|
|
|
if self._detect_color_bleeding(frame, mask): |
|
artifacts['found'] = True |
|
artifacts['types'].append('color_bleeding') |
|
|
|
|
|
if self._detect_blockiness(mask): |
|
artifacts['found'] = True |
|
artifacts['types'].append('blockiness') |
|
|
|
|
|
if self._detect_noise(mask): |
|
artifacts['found'] = True |
|
artifacts['types'].append('noise') |
|
|
|
return artifacts |
|
|
|
def _detect_halo(self, frame: np.ndarray, mask: np.ndarray) -> bool: |
|
"""Detect halo artifacts around edges.""" |
|
|
|
kernel = np.ones((5, 5), np.uint8) |
|
dilated = cv2.dilate((mask > 0.5).astype(np.uint8), kernel, iterations=2) |
|
|
|
|
|
halo_region = dilated - (mask > 0.5).astype(np.uint8) |
|
|
|
if np.any(halo_region): |
|
|
|
halo_pixels = frame[halo_region > 0] |
|
if len(halo_pixels) > 0: |
|
mean_brightness = np.mean(halo_pixels) |
|
|
|
|
|
overall_brightness = np.mean(frame) |
|
|
|
|
|
if abs(mean_brightness - overall_brightness) > 30: |
|
return True |
|
|
|
return False |
|
|
|
def _detect_color_bleeding(self, frame: np.ndarray, mask: np.ndarray) -> bool: |
|
"""Detect color bleeding at edges.""" |
|
|
|
edges = cv2.Canny((mask * 255).astype(np.uint8), 50, 150) |
|
kernel = np.ones((3, 3), np.uint8) |
|
edge_region = cv2.dilate(edges, kernel, iterations=1) > 0 |
|
|
|
if np.any(edge_region) and len(frame.shape) == 3: |
|
|
|
edge_pixels = frame[edge_region] |
|
|
|
if len(edge_pixels) > 0: |
|
|
|
color_std = np.std(edge_pixels, axis=0) |
|
|
|
if np.max(color_std) > 50: |
|
return True |
|
|
|
return False |
|
|
|
def _detect_blockiness(self, mask: np.ndarray) -> bool: |
|
"""Detect blocky artifacts in mask.""" |
|
|
|
grad_x = np.abs(np.diff(mask, axis=1)) |
|
grad_y = np.abs(np.diff(mask, axis=0)) |
|
|
|
|
|
if grad_x.size > 0 and grad_y.size > 0: |
|
|
|
fft_x = np.fft.fft2(grad_x) |
|
fft_y = np.fft.fft2(grad_y) |
|
|
|
|
|
spectrum_x = np.abs(fft_x) |
|
spectrum_y = np.abs(fft_y) |
|
|
|
|
|
blockiness_score = (np.max(spectrum_x) + np.max(spectrum_y)) / (spectrum_x.size + spectrum_y.size) |
|
|
|
if blockiness_score > 0.1: |
|
return True |
|
|
|
return False |
|
|
|
def _detect_noise(self, mask: np.ndarray) -> bool: |
|
"""Detect noise artifacts in mask.""" |
|
|
|
mean = cv2.blur(mask, (3, 3)) |
|
sqr_mean = cv2.blur(mask**2, (3, 3)) |
|
variance = sqr_mean - mean**2 |
|
|
|
|
|
smooth_regions = (mask > 0.3) & (mask < 0.7) |
|
|
|
if np.any(smooth_regions): |
|
noise_level = np.mean(variance[smooth_regions]) |
|
|
|
if noise_level > 0.05: |
|
return True |
|
|
|
return False |
|
|
|
|
|
class MetricsTracker: |
|
"""Tracks metrics over time for reporting.""" |
|
|
|
def __init__(self, window_size: int = 100): |
|
self.window_size = window_size |
|
self.metrics_history = deque(maxlen=window_size) |
|
self.frame_times = deque(maxlen=window_size) |
|
|
|
def add(self, metrics: QualityMetrics, frame_time: float): |
|
"""Add metrics to tracker.""" |
|
self.metrics_history.append(metrics) |
|
self.frame_times.append(frame_time) |
|
|
|
def get_trends(self) -> Dict[str, List[float]]: |
|
"""Get metric trends over time.""" |
|
if not self.metrics_history: |
|
return {} |
|
|
|
trends = { |
|
'overall_quality': [], |
|
'edge_accuracy': [], |
|
'temporal_stability': [], |
|
'detail_preservation': [] |
|
} |
|
|
|
for metrics in self.metrics_history: |
|
trends['overall_quality'].append(metrics.overall_quality) |
|
trends['edge_accuracy'].append(metrics.edge_accuracy) |
|
trends['temporal_stability'].append(metrics.temporal_stability) |
|
trends['detail_preservation'].append(metrics.detail_preservation) |
|
|
|
return trends |
|
|
|
def get_average_fps(self) -> float: |
|
"""Get average FPS from frame times.""" |
|
if len(self.frame_times) < 2: |
|
return 0.0 |
|
|
|
time_diffs = [self.frame_times[i] - self.frame_times[i-1] |
|
for i in range(1, len(self.frame_times))] |
|
|
|
avg_time = np.mean(time_diffs) |
|
return 1.0 / avg_time if avg_time > 0 else 0.0 |
|
|
|
|
|
class QualityReport: |
|
"""Generates quality reports.""" |
|
|
|
@staticmethod |
|
def generate(metrics: QualityMetrics, |
|
statistics: Dict[str, Any], |
|
output_path: Optional[str] = None) -> str: |
|
"""Generate comprehensive quality report.""" |
|
report = [] |
|
report.append("=" * 60) |
|
report.append("BACKGROUNDFX PRO - QUALITY REPORT") |
|
report.append("=" * 60) |
|
report.append("") |
|
|
|
|
|
report.append(f"Overall Quality: {metrics.overall_quality:.1%}") |
|
report.append(f"Processing Confidence: {metrics.processing_confidence:.1%}") |
|
report.append("") |
|
|
|
|
|
report.append("DETAILED METRICS:") |
|
report.append("-" * 40) |
|
report.append(f"Edge Accuracy: {metrics.edge_accuracy:.1%}") |
|
report.append(f"Edge Smoothness: {metrics.edge_smoothness:.1%}") |
|
report.append(f"Edge Completeness: {metrics.edge_completeness:.1%}") |
|
report.append("") |
|
report.append(f"Temporal Stability: {metrics.temporal_stability:.1%}") |
|
report.append(f"Temporal Consistency: {metrics.temporal_consistency:.1%}") |
|
report.append(f"Flicker Score: {metrics.flicker_score:.1%}") |
|
report.append("") |
|
report.append(f"Mask Coverage: {metrics.mask_coverage:.1%}") |
|
report.append(f"Mask Accuracy: {metrics.mask_accuracy:.1%}") |
|
report.append(f"Hole Ratio: {metrics.hole_ratio:.1%}") |
|
report.append("") |
|
report.append(f"Detail Preservation: {metrics.detail_preservation:.1%}") |
|
report.append(f"Hair Detail Score: {metrics.hair_detail_score:.1%}") |
|
report.append(f"Texture Quality: {metrics.texture_quality:.1%}") |
|
report.append("") |
|
|
|
|
|
if metrics.warnings: |
|
report.append("WARNINGS:") |
|
report.append("-" * 40) |
|
for warning in metrics.warnings: |
|
report.append(f"⚠️ {warning}") |
|
report.append("") |
|
|
|
|
|
if statistics: |
|
report.append("STATISTICS:") |
|
report.append("-" * 40) |
|
report.append(f"Average Quality: {statistics.get('average_quality', 0):.1%}") |
|
report.append(f"Min Quality: {statistics.get('min_quality', 0):.1%}") |
|
report.append(f"Max Quality: {statistics.get('max_quality', 0):.1%}") |
|
report.append(f"Std Deviation: {statistics.get('std_quality', 0):.3f}") |
|
report.append(f"Total Warnings: {statistics.get('total_warnings', 0)}") |
|
report.append(f"Frames Analyzed: {statistics.get('frames_analyzed', 0)}") |
|
|
|
report.append("") |
|
report.append("=" * 60) |
|
|
|
report_text = "\n".join(report) |
|
|
|
|
|
if output_path: |
|
with open(output_path, 'w') as f: |
|
f.write(report_text) |
|
|
|
return report_text |
|
|
|
|
|
|
|
__all__ = [ |
|
'QualityAnalyzer', |
|
'QualityMetrics', |
|
'QualityConfig', |
|
'MetricsTracker', |
|
'QualityReport', |
|
'EdgeQualityAnalyzer', |
|
'TemporalQualityAnalyzer', |
|
'DetailPreservationAnalyzer', |
|
'ArtifactDetector' |
|
] |