#!/usr/bin/env python3 """ image_hasher.py Provides robust image hashing utilities for MorphGuard to use in metrics collection and content tracking. Supports multiple hashing algorithms for different use cases. """ import os import io import hashlib import numpy as np from PIL import Image from typing import Union, Tuple, Optional, List, Dict, Any # Try to import optional dependencies try: import cv2 CV2_AVAILABLE = True except ImportError: CV2_AVAILABLE = False try: import imagehash IMAGEHASH_AVAILABLE = True except ImportError: IMAGEHASH_AVAILABLE = False class ImageHasher: """Image hashing utility for MorphGuard""" def __init__(self, hash_size: int = 16, hash_algorithm: str = "phash"): """Initialize the image hasher Args: hash_size: Size of the hash (for perceptual hashing algorithms) hash_algorithm: Hashing algorithm to use ('md5', 'sha256', 'phash', 'dhash', 'ahash', 'whash') """ self.hash_size = hash_size self.hash_algorithm = hash_algorithm.lower() # Check if required libraries are available for perceptual hashing if self.hash_algorithm in ('phash', 'dhash', 'ahash', 'whash'): if not IMAGEHASH_AVAILABLE: print(f"Warning: {hash_algorithm} requires the 'imagehash' package") print("Falling back to SHA-256 cryptographic hashing") self.hash_algorithm = 'sha256' def hash_image(self, image_path_or_data: Union[str, bytes, np.ndarray, Image.Image]) -> str: """Generate hash for an image Args: image_path_or_data: Image to hash (path, bytes, array, or PIL Image) Returns: String representation of the hash """ # Load the image if needed img = self._load_image(image_path_or_data) if img is None: # Return a placeholder hash if image couldn't be loaded return hashlib.sha256(b'error_loading_image').hexdigest() # Generate hash based on algorithm if self.hash_algorithm == 'md5': return self._cryptographic_hash(img, 'md5') elif self.hash_algorithm == 'sha256': return self._cryptographic_hash(img, 'sha256') elif IMAGEHASH_AVAILABLE: # Use perceptual hashing if available if self.hash_algorithm == 'phash': img_hash = imagehash.phash(img, hash_size=self.hash_size) elif self.hash_algorithm == 'dhash': img_hash = imagehash.dhash(img, hash_size=self.hash_size) elif self.hash_algorithm == 'ahash': img_hash = imagehash.average_hash(img, hash_size=self.hash_size) elif self.hash_algorithm == 'whash': img_hash = imagehash.whash(img, hash_size=self.hash_size) else: # Default to perceptual hash img_hash = imagehash.phash(img, hash_size=self.hash_size) return str(img_hash) else: # Fall back to SHA-256 if perceptual hashing isn't available return self._cryptographic_hash(img, 'sha256') def hash_image_batch(self, images: List[Union[str, bytes, np.ndarray, Image.Image]]) -> List[str]: """Generate hashes for a batch of images Args: images: List of images to hash Returns: List of hash strings """ return [self.hash_image(img) for img in images] def compare_images(self, image1: Union[str, bytes, np.ndarray, Image.Image], image2: Union[str, bytes, np.ndarray, Image.Image]) -> float: """Compare two images and return similarity score Args: image1: First image to compare image2: Second image to compare Returns: Similarity score (0-1), where 1 is identical """ # For cryptographic hashes, we can only do binary comparison if self.hash_algorithm in ('md5', 'sha256'): hash1 = self.hash_image(image1) hash2 = self.hash_image(image2) return 1.0 if hash1 == hash2 else 0.0 # For perceptual hashes, we can calculate distance-based similarity if IMAGEHASH_AVAILABLE: img1 = self._load_image(image1) img2 = self._load_image(image2) if img1 is None or img2 is None: return 0.0 if self.hash_algorithm == 'phash': hash1 = imagehash.phash(img1, hash_size=self.hash_size) hash2 = imagehash.phash(img2, hash_size=self.hash_size) elif self.hash_algorithm == 'dhash': hash1 = imagehash.dhash(img1, hash_size=self.hash_size) hash2 = imagehash.dhash(img2, hash_size=self.hash_size) elif self.hash_algorithm == 'ahash': hash1 = imagehash.average_hash(img1, hash_size=self.hash_size) hash2 = imagehash.average_hash(img2, hash_size=self.hash_size) elif self.hash_algorithm == 'whash': hash1 = imagehash.whash(img1, hash_size=self.hash_size) hash2 = imagehash.whash(img2, hash_size=self.hash_size) else: hash1 = imagehash.phash(img1, hash_size=self.hash_size) hash2 = imagehash.phash(img2, hash_size=self.hash_size) # Calculate normalized hamming distance-based similarity max_bits = self.hash_size * self.hash_size hamming_distance = hash1 - hash2 similarity = 1.0 - (hamming_distance / max_bits) return float(similarity) # Fallback to direct image comparison if perceptual hashing isn't available return self._direct_image_comparison(image1, image2) def _load_image(self, image_path_or_data: Union[str, bytes, np.ndarray, Image.Image]) -> Optional[Image.Image]: """Load image from various input formats Args: image_path_or_data: Image to load (path, bytes, array, or PIL Image) Returns: PIL Image or None if loading failed """ try: # Handle different input types if isinstance(image_path_or_data, str): # Path to image file return Image.open(image_path_or_data) elif isinstance(image_path_or_data, bytes): # Raw image bytes return Image.open(io.BytesIO(image_path_or_data)) elif isinstance(image_path_or_data, np.ndarray): # NumPy array return Image.fromarray(image_path_or_data) elif isinstance(image_path_or_data, Image.Image): # Already a PIL Image return image_path_or_data else: print(f"Warning: Unsupported image type: {type(image_path_or_data)}") return None except Exception as e: print(f"Error loading image: {e}") return None def _cryptographic_hash(self, img: Image.Image, algorithm: str) -> str: """Generate a cryptographic hash of an image Args: img: PIL Image to hash algorithm: Hashing algorithm ('md5' or 'sha256') Returns: Hash string """ # Convert to bytes for consistent hashing img_bytes = io.BytesIO() img.save(img_bytes, format='PNG') img_data = img_bytes.getvalue() # Apply hash function if algorithm == 'md5': return hashlib.md5(img_data).hexdigest() else: # default to sha256 return hashlib.sha256(img_data).hexdigest() def _direct_image_comparison(self, image1: Union[str, bytes, np.ndarray, Image.Image], image2: Union[str, bytes, np.ndarray, Image.Image]) -> float: """Directly compare two images using pixel-wise comparison Args: image1: First image to compare image2: Second image to compare Returns: Similarity score (0-1) """ img1 = self._load_image(image1) img2 = self._load_image(image2) if img1 is None or img2 is None: return 0.0 # Resize images to the same dimensions size = (128, 128) # Small size for faster comparison img1 = img1.resize(size, Image.LANCZOS) img2 = img2.resize(size, Image.LANCZOS) # Convert to grayscale for simplicity img1 = img1.convert('L') img2 = img2.convert('L') # Convert to numpy arrays arr1 = np.array(img1) arr2 = np.array(img2) # Calculate mean squared error mse = np.mean((arr1 - arr2) ** 2) if mse == 0: return 1.0 # Convert MSE to similarity score (0-1) max_mse = 255.0 ** 2 # Maximum possible MSE similarity = 1.0 - (mse / max_mse) return float(similarity) def hash_file(file_path: str, algorithm: str = 'sha256') -> str: """Generate a hash for any file Args: file_path: Path to the file algorithm: Hashing algorithm ('md5' or 'sha256') Returns: Hash string """ if not os.path.exists(file_path): return "" try: with open(file_path, 'rb') as f: file_data = f.read() if algorithm == 'md5': return hashlib.md5(file_data).hexdigest() else: # default to sha256 return hashlib.sha256(file_data).hexdigest() except Exception as e: print(f"Error hashing file: {e}") return "" # Convenience functions with default settings def get_image_hash(image_path_or_data: Union[str, bytes, np.ndarray, Image.Image], algorithm: str = 'phash') -> str: """Get a hash for an image using the specified algorithm Args: image_path_or_data: Image to hash (path, bytes, array, or PIL Image) algorithm: Hashing algorithm ('md5', 'sha256', 'phash', 'dhash', 'ahash', 'whash') Returns: Hash string """ hasher = ImageHasher(hash_algorithm=algorithm) return hasher.hash_image(image_path_or_data) def compare_images(image1: Union[str, bytes, np.ndarray, Image.Image], image2: Union[str, bytes, np.ndarray, Image.Image], algorithm: str = 'phash') -> float: """Compare two images and return similarity score Args: image1: First image to compare image2: Second image to compare algorithm: Hashing algorithm for comparison Returns: Similarity score (0-1) """ hasher = ImageHasher(hash_algorithm=algorithm) return hasher.compare_images(image1, image2) # Function to get a comprehensive set of hashes for an image def get_all_hashes(image_path_or_data: Union[str, bytes, np.ndarray, Image.Image]) -> Dict[str, str]: """Get multiple hashes for a single image Args: image_path_or_data: Image to hash Returns: Dictionary of hash algorithm -> hash value """ result = {} # Cryptographic hashes md5_hasher = ImageHasher(hash_algorithm='md5') result['md5'] = md5_hasher.hash_image(image_path_or_data) sha256_hasher = ImageHasher(hash_algorithm='sha256') result['sha256'] = sha256_hasher.hash_image(image_path_or_data) # Perceptual hashes if available if IMAGEHASH_AVAILABLE: phash_hasher = ImageHasher(hash_algorithm='phash') result['phash'] = phash_hasher.hash_image(image_path_or_data) dhash_hasher = ImageHasher(hash_algorithm='dhash') result['dhash'] = dhash_hasher.hash_image(image_path_or_data) ahash_hasher = ImageHasher(hash_algorithm='ahash') result['ahash'] = ahash_hasher.hash_image(image_path_or_data) whash_hasher = ImageHasher(hash_algorithm='whash') result['whash'] = whash_hasher.hash_image(image_path_or_data) return result