""" Visual Box Comparison Description: ComfyUI custom node for PDF tools and media processing Author: Eric Hiss (GitHub: EricRollei) Contact: eric@historic.camera, eric@rollei.us License: Dual License (Non-Commercial and Commercial Use) Copyright (c) 2025 Eric Hiss. All rights reserved. Dual License: 1. Non-Commercial Use: This software is licensed under the terms of the Creative Commons Attribution-NonCommercial 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/ 2. Commercial Use: For commercial use, a separate license is required. Please contact Eric Hiss at eric@historic.camera or eric@rollei.us for licensing options. Dependencies: This code depends on several third-party libraries, each with its own license. See CREDITS.md for a comprehensive list of dependencies and their licenses. Third-party code: - See CREDITS.md for complete list of dependencies """ #!/usr/bin/env python3 """ Comprehensive Multi-Method Image Detection Analysis Shows Florence2, Surya Layout, SAM+GroundingDINO results for comparison Usage: python visual_box_comparison.py path/to/test.pdf AI ASSISTANT INSTRUCTIONS: - No simplified/alternate solutions that reduce functionality - No "disable X until Y is working" suggestions - Implement complete, working solutions only - Copy patterns from existing working code in this project - Focus on robust, production-ready implementations """ import os import sys import time import numpy as np from pathlib import Path from typing import List, Dict, Tuple, Any, Optional from PIL import Image, ImageDraw, ImageFont from io import BytesIO import importlib.util import tempfile import cv2 try: import pymupdf as fitz except ImportError: import fitz # ADD MISSING TORCH IMPORT try: import torch TORCH_AVAILABLE = True except ImportError: TORCH_AVAILABLE = False print("❌ PyTorch not available") # Import analysis engine with proper path management current_dir = os.path.dirname(os.path.abspath(__file__)) if current_dir not in sys.path: sys.path.insert(0, current_dir) try: from analysis_engine import analyze_for_pdf_extraction, set_comfyui_base_path ANALYSIS_ENGINE_AVAILABLE = True print("✅ Analysis engine imported") except ImportError as e: print(f"❌ Analysis engine import failed: {e}") ANALYSIS_ENGINE_AVAILABLE = False # Try to import SAM + GroundingDINO with enhanced strategy (like spread_detection) SAM_AVAILABLE = False GROUNDINGDINO_AVAILABLE = False # Initialize results dictionary - ADD THIS results = { "total_pairs": 0, "successful_analyses": 0, "total_florence2_boxes": 0, "total_surya_regions": 0, "total_sam_dino_detections": 0, "all_crop_info": [] } def import_sam_modules(): """Import SAM modules from sam2_scripts""" global SAM_AVAILABLE if not TORCH_AVAILABLE: print("❌ SAM skipped - PyTorch not available") return False try: # Import from your actual sam2_scripts parent_dir = os.path.dirname(current_dir) if parent_dir not in sys.path: sys.path.insert(0, parent_dir) from sam2_scripts.sam2_integration import SAM2FlorenceIntegration print("✅ SAM imported from sam2_scripts.sam2_integration") return True except ImportError as e: print(f"❌ SAM import failed: {e}") return False def import_groundingdino_modules(): """Import GroundingDINO modules with fallback strategies""" global GROUNDINGDINO_AVAILABLE try: # Strategy 1: Try local GroundingDINO try: from local_groundingdino.util.slconfig import SLConfig as local_groundingdino_SLConfig from local_groundingdino.models import build_model as local_groundingdino_build_model from local_groundingdino.util.utils import clean_state_dict as local_groundingdino_clean_state_dict print("✅ GroundingDINO imported successfully (local)") return True except ImportError: pass # Strategy 2: Try standard GroundingDINO try: from groundingdino.util.slconfig import SLConfig from groundingdino.models import build_model from groundingdino.util.utils import clean_state_dict print("✅ GroundingDINO imported successfully (standard)") return True except ImportError: pass print("❌ GroundingDINO not available") return False except Exception as e: print(f"❌ GroundingDINO import failed: {e}") return False # Initialize imports SAM_AVAILABLE = import_sam_modules() GROUNDINGDINO_AVAILABLE = import_groundingdino_modules() # Try to import ComfyUI folder paths for model locations with fallback try: import folder_paths COMFYUI_BASE = os.path.dirname(folder_paths.models_dir) print(f"✅ ComfyUI models directory: {folder_paths.models_dir}") except ImportError: COMFYUI_BASE = "A:\\Comfy_Dec\\ComfyUI" # Your known path print(f"⚠️ ComfyUI folder_paths not available, using fallback: {COMFYUI_BASE}") # Create mock folder_paths for compatibility class MockFolderPaths: models_dir = os.path.join(COMFYUI_BASE, "models") folder_paths = MockFolderPaths() def get_large_font(): """Get a larger font for better visibility""" try: # Try to get a larger system font return ImageFont.truetype("arial.ttf", 16) except: try: return ImageFont.truetype("/System/Library/Fonts/Arial.ttf", 16) except: return ImageFont.load_default() def get_even_odd_pairs(doc) -> List[Tuple[int, int]]: """Get all even-odd page pairs (skip cover page)""" pairs = [] # Start from page 2 (index 1) since page 1 (index 0) is cover for i in range(1, len(doc) - 1, 2): left_idx = i # Even page (2,4,6...) = left page right_idx = i + 1 # Odd page (3,5,7...) = right page if right_idx < len(doc): pairs.append((left_idx, right_idx)) return pairs # Replace the SimpleSegmentationAnalyzer class with this direct implementation: class SimpleSegmentationAnalyzer: """Direct SAM2 + GroundingDINO analyzer - no subprocess calls""" def __init__(self, debug_mode=True): self.debug_mode = debug_mode self.available = False self.device = self._setup_device() # Model components self.sam2_model = None self.sam2_predictor = None self.dino_model = None if not TORCH_AVAILABLE: if debug_mode: print("❌ Torch not available - skipping SAM segmentation") return # Load models directly (no Florence2 to avoid conflicts) try: # Skip SAM2 loading since you deleted it sam2_loaded = False if debug_mode: print("⚠️ SAM2 loading skipped - models not installed") dino_loaded = self._load_groundingdino_model() # Make available with just GroundingDINO self.available = dino_loaded # GroundingDINO only if debug_mode: print(f"📊 Direct model loading results:") print(f" SAM2: {sam2_loaded} (skipped)") print(f" GroundingDINO: {dino_loaded}") print(f" Available: {self.available} (GroundingDINO only)") except Exception as e: if debug_mode: print(f"❌ Direct model loading failed: {e}") def _setup_device(self) -> str: """Setup compute device like your SAM2 script""" if torch.cuda.is_available(): return "cuda" elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available(): return "mps" else: return "cpu" def _load_sam2_model(self) -> bool: """Load SAM2 model - exact copy from working sam2_florence_segmentation.py""" try: model_path = self._find_sam2_model() if not model_path: if self.debug_mode: print("❌ No SAM2 model files found") return False # Exact import strategy from your working script try: import sys import os # Add the exact path from your working script sam2_node_path = os.path.join(COMFYUI_BASE, "custom_nodes", "ComfyUI-segment-anything-2", "nodes") if sam2_node_path not in sys.path: sys.path.insert(0, sam2_node_path) # Import the actual working module import sam2_nodes # Use the exact same function call self.sam2_model = sam2_nodes.load_sam2_model(model_path, self.device) # Create predictor exactly like your working script if hasattr(self.sam2_model, 'predict'): self.sam2_predictor = self.sam2_model else: self.sam2_predictor = SAM2Predictor(self.sam2_model) if self.debug_mode: print(f"✅ SAM2 loaded: {model_path}") return True except Exception as e: if self.debug_mode: print(f"❌ SAM2 loading failed: {e}") return False except Exception as e: if self.debug_mode: print(f"❌ SAM2 error: {e}") return False def _find_sam2_model(self) -> Optional[str]: """Find SAM2 model - copied from your working script""" try: sam_dir = Path(folder_paths.models_dir) / "sams" if not sam_dir.exists(): return None # Look for actual SAM2 models (not SAM1) for pt_file in sam_dir.glob("*.pt"): if "sam2" in pt_file.name.lower(): return str(pt_file) # Fallback to any .pth file for pth_file in sam_dir.glob("*.pth"): return str(pth_file) return None except Exception: return None def _load_groundingdino_model(self) -> bool: """Load GroundingDINO model directly""" try: if not GROUNDINGDINO_AVAILABLE: if self.debug_mode: print("⚠️ GroundingDINO not available") return False # Find GroundingDINO model files (fix the .cfg.py issue we found) config_path, model_path = self._find_groundingdino_model() if not (config_path and model_path): if self.debug_mode: print("⚠️ GroundingDINO model files not found") return False # Load model using existing imports try: from local_groundingdino.util.slconfig import SLConfig from local_groundingdino.models import build_model from local_groundingdino.util.utils import clean_state_dict except ImportError: from groundingdino.util.slconfig import SLConfig from groundingdino.models import build_model from groundingdino.util.utils import clean_state_dict # Load config and build model args = SLConfig.fromfile(config_path) self.dino_model = build_model(args) # Load weights checkpoint = torch.load(model_path, map_location="cpu") self.dino_model.load_state_dict( clean_state_dict(checkpoint["model"]), strict=False ) self.dino_model.to(self.device) self.dino_model.eval() if self.debug_mode: print(f"✅ GroundingDINO loaded: {model_path}") return True except Exception as e: if self.debug_mode: print(f"❌ GroundingDINO loading failed: {e}") return False def _find_groundingdino_model(self) -> tuple: """Find GroundingDINO config and model files (fixed for .cfg.py)""" try: dino_dir = Path(folder_paths.models_dir) / "grounding-dino" if not dino_dir.exists(): return None, None # Fix the config file extensions we found earlier config_variants = [ ("GroundingDINO_SwinT_OGC.cfg.py", "groundingdino_swint_ogc.pth"), ("GroundingDINO_SwinB.cfg.py", "groundingdino_swinb_cogcoor.pth"), ("GroundingDINO_SwinT_OGC.py", "groundingdino_swint_ogc.pth"), ("GroundingDINO_SwinB.py", "groundingdino_swinb_cogcoor.pth") ] for config_name, model_name in config_variants: config_file = dino_dir / config_name model_file = dino_dir / model_name if config_file.exists() and model_file.exists(): if self.debug_mode: print(f" ✅ Found GroundingDINO: {config_file}, {model_file}") return str(config_file), str(model_file) return None, None except Exception: return None, None def detect_images(self, image: Image.Image, prompt: str = "photograph . image . illustration") -> List[Dict]: """GroundingDINO detection only (no SAM2 segmentation)""" if not self.available: if self.debug_mode: print(" ⚠️ GroundingDINO analyzer not available") return [] try: # Use GroundingDINO for object detection detections = self._run_groundingdino_detection(image, prompt) if not detections: if self.debug_mode: print(" ⚠️ GroundingDINO found no objects") return [] # Mark all detections as having no masks (no SAM2) for detection in detections: detection['has_mask'] = False if self.debug_mode: print(f" ✅ GroundingDINO only: {len(detections)} detections") return detections except Exception as e: if self.debug_mode: print(f" ❌ Detection pipeline failed: {e}") return [] def _run_groundingdino_detection(self, image: Image.Image, prompt: str) -> List[Dict]: """Run GroundingDINO detection with proper inference""" try: if not self.dino_model: return [] # Import GroundingDINO inference utilities try: from local_groundingdino.util.inference import load_image, predict except ImportError: from groundingdino.util.inference import load_image, predict # Save image temporarily for GroundingDINO's load_image function with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp_file: image.save(tmp_file.name, 'JPEG') tmp_path = tmp_file.name try: # Load image using GroundingDINO's format image_source, transformed_image = load_image(tmp_path) # Run GroundingDINO prediction boxes, logits, phrases = predict( model=self.dino_model, image=transformed_image, caption=prompt, box_threshold=0.3, text_threshold=0.25, device=self.device ) # Convert to pixel coordinates H, W, _ = image_source.shape boxes_xyxy = boxes * torch.Tensor([W, H, W, H]) detections = [] for i, (box, logit, phrase) in enumerate(zip(boxes_xyxy, logits, phrases)): x1, y1, x2, y2 = box.cpu().numpy() confidence = logit.cpu().numpy() # Validate bounding box coordinates if x1 >= x2 or y1 >= y2: if self.debug_mode: print(f" ⚠️ Invalid bbox from GroundingDINO: [{x1}, {y1}, {x2}, {y2}] - skipping") continue # Ensure coordinates are within image bounds x1 = max(0, min(x1, W)) y1 = max(0, min(y1, H)) x2 = max(x1 + 1, min(x2, W)) y2 = max(y1 + 1, min(y2, H)) detections.append({ "bbox": [int(x1), int(y1), int(x2), int(y2)], "label": f"grounding_dino_{phrase}", "confidence": float(confidence) }) return detections finally: # Clean up temp file os.unlink(tmp_path) except Exception as e: if self.debug_mode: print(f" ❌ GroundingDINO detection failed: {e}") return [] def _run_sam2_on_detections(self, image: Image.Image, detections: List[Dict]) -> List[Dict]: """Run SAM2 segmentation on GroundingDINO detections""" try: if not self.sam2_predictor or not detections: return detections # Convert PIL to numpy array image_np = np.array(image) # Set image for SAM2 self.sam2_predictor.set_image(image_np) enhanced_detections = [] for detection in detections: try: bbox = detection["bbox"] x1, y1, x2, y2 = bbox # Create input box for SAM2 input_box = np.array([x1, y1, x2, y2]) # Run SAM2 prediction masks, scores, logits = self.sam2_predictor.predict( point_coords=None, point_labels=None, box=input_box, multimask_output=False ) if len(masks) > 0 and len(scores) > 0: # Add mask information to detection enhanced_detection = detection.copy() enhanced_detection['mask'] = masks[0] enhanced_detection['sam_score'] = float(scores[0]) enhanced_detection['has_mask'] = True enhanced_detections.append(enhanced_detection) else: # Keep original detection detection['has_mask'] = False enhanced_detections.append(detection) except Exception as e: if self.debug_mode: print(f" ⚠️ SAM2 failed for box {bbox}: {e}") # Keep original detection detection['has_mask'] = False enhanced_detections.append(detection) return enhanced_detections except Exception as e: if self.debug_mode: print(f" ❌ SAM2 processing failed: {e}") return detections # Add SAM2Predictor wrapper class if needed class SAM2Predictor: """Wrapper to provide predictor interface for SAM2 models""" def __init__(self, sam2_model): self.model = sam2_model self.image = None def set_image(self, image: np.ndarray): """Set image for prediction""" self.image = image if hasattr(self.model, 'set_image'): self.model.set_image(image) def predict(self, point_coords=None, point_labels=None, box=None, multimask_output=False): """Run prediction""" if hasattr(self.model, 'predict'): return self.model.predict( point_coords=point_coords, point_labels=point_labels, box=box, multimask_output=multimask_output ) else: # Implement basic prediction logic if needed raise NotImplementedError("SAM2 model doesn't provide predict interface") def create_comprehensive_visualization(joined_img: Image.Image, detection_results: Dict, pair_id: str, output_dir: str): """Create comprehensive visualization with all detection methods""" vis_img = joined_img.copy() draw = ImageDraw.Draw(vis_img) font = get_large_font() def validate_bbox(bbox): """Ensure bbox coordinates are valid [x1, y1, x2, y2] where x1 < x2 and y1 < y2""" x1, y1, x2, y2 = bbox # Swap coordinates if they're in wrong order if x1 > x2: x1, x2 = x2, x1 if y1 > y2: y1, y2 = y2, y1 # Ensure minimum size (at least 1 pixel) if x2 - x1 < 1: x2 = x1 + 1 if y2 - y1 < 1: y2 = y1 + 1 # Clamp to image bounds x1 = max(0, min(x1, vis_img.width - 1)) y1 = max(0, min(y1, vis_img.height - 1)) x2 = max(x1 + 1, min(x2, vis_img.width)) y2 = max(y1 + 1, min(y2, vis_img.height)) return [x1, y1, x2, y2] # Get results florence2_boxes = detection_results.get("florence2_rectangles", []) surya_regions = detection_results.get("surya_layout", []) sam_dino_boxes = detection_results.get("sam_dino_detections", []) # Draw Florence2 boxes in RED with thick lines for i, box in enumerate(florence2_boxes): try: bbox = validate_bbox(box["bbox"]) confidence = box.get("confidence", 1.0) draw.rectangle(bbox, outline="red", width=4) # Larger text with background for better visibility text = f"F2_{i+1}({confidence:.2f})" text_bbox = draw.textbbox((0, 0), text, font=font) text_width = text_bbox[2] - text_bbox[0] text_height = text_bbox[3] - text_bbox[1] # Draw text background text_x, text_y = bbox[0], bbox[1] - text_height - 5 draw.rectangle([text_x, text_y, text_x + text_width, text_y + text_height], fill="red") draw.text((text_x, text_y), text, fill="white", font=font) except Exception as e: print(f"⚠️ Error drawing Florence2 box {i}: {e}") # Draw ALL Surya regions in BLUE with different styles for different types color_map = { "text": "blue", "title": "navy", "heading": "darkblue", "caption": "lightblue", "image": "cyan", "figure": "turquoise", "table": "purple", "header": "gray", "footer": "darkgray" } for i, region in enumerate(surya_regions): try: bbox = validate_bbox(region["bbox"]) label = region.get("semantic_label", "unknown").lower() # Choose color based on semantic label color = "blue" # default for key, c in color_map.items(): if key in label: color = c break draw.rectangle(bbox, outline=color, width=3) # Draw label with background text = f"S_{label[:6]}_{i+1}" text_bbox = draw.textbbox((0, 0), text, font=font) text_width = text_bbox[2] - text_bbox[0] text_height = text_bbox[3] - text_bbox[1] text_x = bbox[2] - text_width text_y = bbox[1] draw.rectangle([text_x, text_y, text_x + text_width, text_y + text_height], fill=color) draw.text((text_x, text_y), text, fill="white", font=font) except Exception as e: print(f"⚠️ Error drawing Surya region {i}: {e}") # Draw SAM+DINO boxes in GREEN with validation if sam_dino_boxes: for i, box in enumerate(sam_dino_boxes): try: bbox = validate_bbox(box["bbox"]) confidence = box.get("confidence", 1.0) label = box.get("label", "unknown") draw.rectangle(bbox, outline="lime", width=3) text = f"DINO_{i+1}({confidence:.2f})" # Add debug info for the problematic box if confidence == 0 or any(coord < 0 for coord in box["bbox"]): print(f"⚠️ Suspicious DINO box {i}: original={box['bbox']}, fixed={bbox}, conf={confidence}, label={label}") draw.text((bbox[0], bbox[3] + 5), text, fill="lime", font=font) except Exception as e: print(f"⚠️ Error drawing SAM+DINO box {i}: {e}") print(f" Original bbox: {box.get('bbox', 'None')}") print(f" Box data: {box}") # Draw seam line - make it more prominent seam_x = joined_img.width // 2 draw.line([seam_x, 0, seam_x, joined_img.height], fill="yellow", width=5) # Seam label with background seam_text = "SEAM" text_bbox = draw.textbbox((0, 0), seam_text, font=font) text_width = text_bbox[2] - text_bbox[0] text_height = text_bbox[3] - text_bbox[1] draw.rectangle([seam_x-text_width//2, 20, seam_x+text_width//2, 20+text_height], fill="yellow") draw.text((seam_x-text_width//2, 20), seam_text, fill="black", font=font) # Add comprehensive summary with larger text and background summary_lines = [ f"Florence2: {len(florence2_boxes)} boxes", f"Surya: {len(surya_regions)} regions", f"SAM+DINO: {len(sam_dino_boxes)} detections" ] y_pos = 10 for line in summary_lines: text_bbox = draw.textbbox((0, 0), line, font=font) text_width = text_bbox[2] - text_bbox[0] text_height = text_bbox[3] - text_bbox[1] # Background for better readability draw.rectangle([10, y_pos, 10 + text_width + 10, y_pos + text_height + 5], fill="black") draw.text((15, y_pos + 2), line, fill="white", font=font) y_pos += text_height + 10 # Draw rejected Florence2 boxes in ORANGE (dashed style would be ideal) rejected_boxes = detection_results.get("florence2_rejected", []) if rejected_boxes: for i, box in enumerate(rejected_boxes): try: bbox = validate_bbox(box["bbox"]) confidence = box.get("confidence", 1.0) # Draw with dashed effect (multiple thin lines) for offset in range(0, 8, 2): draw.rectangle([bbox[0]+offset, bbox[1]+offset, bbox[2]+offset, bbox[3]+offset], outline="orange", width=1) text = f"F2_REJ_{i+1}({confidence:.2f})" draw.text((bbox[0], bbox[1] - 20), text, fill="orange", font=font) except Exception as e: print(f"⚠️ Error drawing rejected Florence2 box {i}: {e}") # Save visualization vis_dir = os.path.join(output_dir, "comprehensive_analysis") os.makedirs(vis_dir, exist_ok=True) output_path = os.path.join(vis_dir, f"{pair_id}_comprehensive.png") vis_img.save(output_path) return output_path def save_all_crops(joined_img: Image.Image, detection_results: Dict, pair_id: str, output_dir: str, analyzer): """Save crops from all detection methods""" florence2_boxes = detection_results.get("florence2_rectangles", []) if not florence2_boxes: return [] # Create output directories original_crops_dir = os.path.join(output_dir, "florence2_crops") refined_crops_dir = os.path.join(output_dir, "refined_crops") os.makedirs(original_crops_dir, exist_ok=True) os.makedirs(refined_crops_dir, exist_ok=True) saved_crops = [] for i, box in enumerate(florence2_boxes): bbox = box["bbox"] x1, y1, x2, y2 = bbox # Ensure bbox is within image bounds x1 = max(0, x1) y1 = max(0, y1) x2 = min(joined_img.width, x2) y2 = min(joined_img.height, y2) if x2 > x1 and y2 > y1: # Original crop cropped = joined_img.crop((x1, y1, x2, y2)) confidence = box.get("confidence", 1.0) # Save original crop orig_filename = f"{pair_id}_F2_{i+1:02d}_conf{confidence:.2f}.png" orig_path = os.path.join(original_crops_dir, orig_filename) cropped.save(orig_path) # Refine crop with Florence2 recursion try: refined_crop = refine_crop_with_florence2(cropped, analyzer) refined_filename = f"{pair_id}_refined_{i+1:02d}_conf{confidence:.2f}.png" refined_path = os.path.join(refined_crops_dir, refined_filename) refined_crop.save(refined_path) saved_crops.append({ "original": orig_path, "refined": refined_path, "confidence": confidence, "area_pct": ((x2-x1) * (y2-y1)) / (joined_img.width * joined_img.height) * 100 }) except Exception as e: print(f" ⚠️ Refinement failed for crop {i+1}: {e}") saved_crops.append({ "original": orig_path, "refined": None, "confidence": confidence, "area_pct": ((x2-x1) * (y2-y1)) / (joined_img.width * joined_img.height) * 100 }) return saved_crops def refine_crop_with_florence2(cropped_img: Image.Image, analyzer, confidence_threshold: float = 0.3) -> Image.Image: """Run Florence2 again on cropped image to get tighter bounding box""" try: # Run Florence2 on the already cropped image analysis = analyze_for_pdf_extraction(cropped_img, debug_mode=False) florence2_boxes = analysis.get("florence2_rectangles", []) if not florence2_boxes: return cropped_img # Find the best box (largest area with good confidence) best_box = None best_score = 0 for box in florence2_boxes: confidence = box.get("confidence", 1.0) bbox = box["bbox"] area = (bbox[2] - bbox[0]) * (bbox[3] - bbox[1]) # Score based on confidence and relative size score = confidence * (area / (cropped_img.width * cropped_img.height)) if score > best_score and confidence >= confidence_threshold: best_box = box best_score = score if best_box: bbox = best_box["bbox"] x1, y1, x2, y2 = bbox # Ensure bbox is within crop bounds x1 = max(0, x1) y1 = max(0, y1) x2 = min(cropped_img.width, x2) y2 = min(cropped_img.height, y2) if x2 > x1 and y2 > y1: refined_crop = cropped_img.crop((x1, y1, x2, y2)) return refined_crop return cropped_img except Exception as e: print(f" ⚠️ Refinement failed: {e}") return cropped_img # Replace the generic prompt system with PDF-optimized prompts: def get_pdf_optimized_prompts(): """Get prompts optimized for PDF illustration/photo detection and filtering""" return { "main_images": "photograph . photo . illustration . picture . image", "logos_icons": "logo . icon . symbol . emblem . badge", "decorative": "decoration . ornament . pattern . border . divider", "all_visual": "photograph . photo . illustration . picture . image . logo . icon . symbol" } # Update the SimpleSegmentationAnalyzer to support multiple prompt strategies: def detect_images_with_filtering(self, image: Image.Image, florence2_boxes: List[Dict] = None) -> Dict: """Enhanced GroundingDINO detection with Florence2 filtering and complementary detection""" if not self.available: if self.debug_mode: print(" ⚠️ GroundingDINO analyzer not available") return { "filtered_florence2": florence2_boxes or [], "additional_detections": [], "rejected_florence2": [] } prompts = get_pdf_optimized_prompts() results = { "filtered_florence2": [], "additional_detections": [], "rejected_florence2": [] } try: # Step 1: Run comprehensive GroundingDINO detection all_detections = self._run_groundingdino_detection(image, prompts["all_visual"]) if self.debug_mode: print(f" ✅ GroundingDINO found {len(all_detections)} total detections") # Step 2: Filter Florence2 boxes against GroundingDINO detections if florence2_boxes: results["filtered_florence2"], results["rejected_florence2"] = self._filter_florence2_with_dino( florence2_boxes, all_detections ) if self.debug_mode: print(f" 📋 Florence2 filtering: {len(results['filtered_florence2'])} kept, {len(results['rejected_florence2'])} rejected") # Step 3: Find additional detections (things Florence2 missed) if florence2_boxes: results["additional_detections"] = self._find_additional_detections( all_detections, florence2_boxes ) if self.debug_mode and results["additional_detections"]: print(f" 🔍 Found {len(results['additional_detections'])} additional items Florence2 missed") else: results["additional_detections"] = all_detections return results except Exception as e: if self.debug_mode: print(f" ❌ Enhanced detection failed: {e}") return { "filtered_florence2": florence2_boxes or [], "additional_detections": [], "rejected_florence2": [] } def _filter_florence2_with_dino(self, florence2_boxes: List[Dict], dino_detections: List[Dict]) -> Tuple[List[Dict], List[Dict]]: """Filter Florence2 boxes by checking if GroundingDINO confirms them as actual images""" def boxes_overlap(box1, box2, threshold=0.3): """Check if two boxes overlap significantly""" x1_1, y1_1, x2_1, y2_1 = box1 x1_2, y1_2, x2_2, y2_2 = box2 # Calculate intersection x1_i = max(x1_1, x1_2) y1_i = max(y1_1, y1_2) x2_i = min(x2_1, x2_2) y2_i = min(y2_1, y2_2) if x2_i <= x1_i or y2_i <= y1_i: return False intersection = (x2_i - x1_i) * (y2_i - y1_i) area1 = (x2_1 - x1_1) * (y2_1 - y1_1) area2 = (x2_2 - x1_2) * (y2_2 - y1_2) iou = intersection / (area1 + area2 - intersection) return iou >= threshold confirmed_boxes = [] rejected_boxes = [] for f2_box in florence2_boxes: f2_bbox = f2_box["bbox"] # Check if any GroundingDINO detection overlaps and confirms this is actually an image confirmed = False for dino_det in dino_detections: if boxes_overlap(f2_bbox, dino_det["bbox"]): # Check if GroundingDINO label suggests this is actually an image/photo/illustration dino_label = dino_det["label"].lower() if any(term in dino_label for term in ["photograph", "photo", "illustration", "picture", "image"]): confirmed = True # Add GroundingDINO confidence to Florence2 box f2_box["dino_confirmation"] = { "confidence": dino_det["confidence"], "label": dino_det["label"] } break if confirmed: confirmed_boxes.append(f2_box) else: # Add reason for rejection f2_box["rejection_reason"] = "No GroundingDINO confirmation" rejected_boxes.append(f2_box) return confirmed_boxes, rejected_boxes def _find_additional_detections(self, dino_detections: List[Dict], florence2_boxes: List[Dict]) -> List[Dict]: """Find GroundingDINO detections that don't overlap with Florence2 (things Florence2 missed)""" def boxes_overlap(box1, box2, threshold=0.2): """Check if boxes overlap (lower threshold for finding missed items)""" x1_1, y1_1, x2_1, y2_1 = box1 x1_2, y1_2, x2_2, y2_2 = box2 x1_i = max(x1_1, x1_2) y1_i = max(y1_1, y1_2) x2_i = min(x2_1, x2_2) y2_i = min(y2_1, y2_2) if x2_i <= x1_i or y2_i <= y1_i: return False intersection = (x2_i - x1_i) * (y2_i - y1_i) area1 = (x2_1 - x1_1) * (y2_1 - y1_1) overlap_ratio = intersection / area1 return overlap_ratio >= threshold additional = [] for dino_det in dino_detections: # Check if this GroundingDINO detection overlaps with any Florence2 box overlaps_with_f2 = False for f2_box in florence2_boxes: if boxes_overlap(dino_det["bbox"], f2_box["bbox"]): overlaps_with_f2 = True break # If it doesn't overlap, it's something Florence2 missed if not overlaps_with_f2: dino_det["source"] = "grounding_dino_additional" dino_det["has_mask"] = False additional.append(dino_det) return additional def process_pdf(pdf_path: str): """Process PDF with comprehensive multi-method analysis""" if not ANALYSIS_ENGINE_AVAILABLE: print("❌ Analysis engine not available - cannot run comparison") return print(f"📖 Processing PDF: {pdf_path}") # Set ComfyUI path for Florence2 try: set_comfyui_base_path(COMFYUI_BASE) print(f"✅ ComfyUI path set: {COMFYUI_BASE}") except Exception as e: print(f"⚠️ Could not set ComfyUI path: {e}") # Create output directory pdf_name = Path(pdf_path).stem output_dir = f"comprehensive_analysis_{pdf_name}_{int(time.time())}" os.makedirs(output_dir, exist_ok=True) # Initialize SAM analyzer (but it's disabled for now) sam_analyzer = SimpleSegmentationAnalyzer(debug_mode=True) print(f"📊 SAM+DINO analyzer available: {sam_analyzer.available}") with fitz.open(pdf_path) as doc: pairs = get_even_odd_pairs(doc) results["total_pairs"] = len(pairs) print(f"📊 Processing {len(pairs)} even-odd pairs...") for i, (left_idx, right_idx) in enumerate(pairs): pair_id = f"{left_idx+1:03d}_{right_idx+1:03d}" print(f" 🔄 Pair {i+1}/{len(pairs)}: pages {left_idx+1}-{right_idx+1}") try: # Extract and join pages left_page = doc[left_idx] right_page = doc[right_idx] mat = fitz.Matrix(200/72, 200/72) # 200 DPI left_pix = left_page.get_pixmap(matrix=mat, alpha=False) right_pix = right_page.get_pixmap(matrix=mat, alpha=False) left_img = Image.open(BytesIO(left_pix.tobytes("ppm"))) right_img = Image.open(BytesIO(right_pix.tobytes("ppm"))) # Join images joined_width = left_img.width + right_img.width joined_height = max(left_img.height, right_img.height) joined_img = Image.new('RGB', (joined_width, joined_height), 'white') joined_img.paste(left_img, (0, 0)) joined_img.paste(right_img, (left_img.width, 0)) # KEEP THE WORKING FLORENCE2 + SURYA ANALYSIS print(f" 📊 Running Florence2 + Surya analysis...") analysis = analyze_for_pdf_extraction(joined_img, debug_mode=False) # SAM+DINO is disabled for now - returns empty list print(f" 📊 Running SAM2 + GroundingDINO analysis...") sam_dino_results = [] if sam_analyzer.available: try: sam_dino_results = sam_analyzer.detect_images(joined_img, "photograph image illustration") if sam_dino_results and sam_analyzer.debug_mode: print(f" ✅ SAM2 found {len(sam_dino_results)} detections") except Exception as e: if sam_analyzer.debug_mode: print(f" ❌ SAM2 detection error: {e}") sam_dino_results = [] else: print(f" ⚠️ SAM2 analyzer not available") # Combine results (Florence2 + Surya working, SAM empty) detection_results = { "florence2_rectangles": analysis.get("florence2_rectangles", []), "surya_layout": analysis.get("surya_layout", []), "sam_dino_detections": sam_dino_results } # Create comprehensive visualization create_comprehensive_visualization( joined_img, detection_results, pair_id, output_dir ) # Save all crops using working Florence2 crop_info = save_all_crops( joined_img, detection_results, pair_id, output_dir, analyze_for_pdf_extraction ) # Update stats results["successful_analyses"] += 1 results["total_florence2_boxes"] += len(detection_results["florence2_rectangles"]) results["total_surya_regions"] += len(detection_results["surya_layout"]) results["total_sam_dino_detections"] += len(detection_results["sam_dino_detections"]) results["all_crop_info"].extend(crop_info) print(f" ✅ F2:{len(detection_results['florence2_rectangles'])} | " f"Surya:{len(detection_results['surya_layout'])} | " f"SAM+DINO:{len(detection_results['sam_dino_detections'])} | " f"Crops:{len(crop_info)}") except Exception as e: print(f" ❌ Error processing pair: {e}") import traceback traceback.print_exc() # Print summary print(f"\n✅ Comprehensive Analysis Complete!") print(f"📁 Output directory: {output_dir}") print(f"📊 Processed: {results['successful_analyses']}/{results['total_pairs']} pairs") print(f"📦 Detection Summary:") print(f" 🔴 Florence2: {results['total_florence2_boxes']} boxes total") print(f" 🔵 Surya Layout: {results['total_surya_regions']} regions total") print(f" 🟢 SAM+DINO: {results['total_sam_dino_detections']} detections total") print(f" 🖼️ Total crops: {len(results['all_crop_info'])}") # Analysis suggestions print(f"\n💡 Analysis Tips:") print(f" • Red boxes = Florence2 image detection") print(f" • Blue boxes = Surya layout (all types, color-coded)") print(f" • Green boxes = SAM+GroundingDINO (if available)") print(f" • Compare original vs refined crops for border trimming effectiveness") print(f" • Look for patterns in missed/spurious detections") def main(): if len(sys.argv) != 2: print("Usage: python visual_box_comparison.py path/to/test.pdf") sys.exit(1) pdf_path = sys.argv[1] if not os.path.exists(pdf_path): print(f"❌ PDF file not found: {pdf_path}") sys.exit(1) process_pdf(pdf_path) if __name__ == "__main__": main()