Spaces:
Sleeping
Sleeping
| import cv2 | |
| import numpy as np | |
| from PIL import Image | |
| import matplotlib.pyplot as plt | |
| import os | |
| from typing import Dict, Any | |
| import base64 | |
| import io | |
| class ImageAnalysisTool: | |
| """Standalone image analysis tool for SEM images""" | |
| def __init__(self): | |
| self.name = "SEM Image Analysis Tool" | |
| self.description = "Analyzes SEM images to extract microstructural information about soil cemented materials" | |
| def _run(self, image_path: str) -> Dict[str, Any]: | |
| """ | |
| Analyze SEM image and extract relevant features | |
| """ | |
| try: | |
| # Load and process image | |
| image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) | |
| if image is None: | |
| return {"error": f"Could not load image from {image_path}"} | |
| # Basic image properties | |
| height, width = image.shape | |
| mean_intensity = np.mean(image) | |
| std_intensity = np.std(image) | |
| # Convert to PIL for additional analysis | |
| pil_image = Image.fromarray(image) | |
| # Encode image for vision model | |
| image_base64 = self._encode_image_to_base64(pil_image) | |
| # Basic texture analysis | |
| texture_features = self._analyze_texture(image) | |
| # Porosity estimation (simple threshold-based) | |
| porosity_info = self._estimate_porosity(image) | |
| # Particle analysis | |
| particle_info = self._analyze_particles(image) | |
| analysis_results = { | |
| "image_properties": { | |
| "width": int(width), | |
| "height": int(height), | |
| "mean_intensity": float(mean_intensity), | |
| "std_intensity": float(std_intensity) | |
| }, | |
| "texture_features": texture_features, | |
| "porosity_analysis": porosity_info, | |
| "particle_analysis": particle_info, | |
| "image_base64": image_base64, | |
| "image_path": image_path | |
| } | |
| return analysis_results | |
| except Exception as e: | |
| return {"error": f"Error analyzing image: {str(e)}"} | |
| def _encode_image_to_base64(self, image: Image.Image) -> str: | |
| """Convert PIL image to base64 string""" | |
| buffered = io.BytesIO() | |
| image.save(buffered, format="PNG") | |
| img_str = base64.b64encode(buffered.getvalue()).decode() | |
| return img_str | |
| def _analyze_texture(self, image: np.ndarray) -> Dict[str, float]: | |
| """Analyze texture properties of the image""" | |
| # Calculate local standard deviation (texture measure) | |
| kernel = np.ones((9, 9), np.float32) / 81 | |
| mean_filtered = cv2.filter2D(image.astype(np.float32), -1, kernel) | |
| sqr_diff = (image.astype(np.float32) - mean_filtered) ** 2 | |
| texture_map = cv2.filter2D(sqr_diff, -1, kernel) | |
| return { | |
| "texture_variance": float(np.mean(texture_map)), | |
| "texture_uniformity": float(np.std(texture_map)), | |
| "contrast": float(np.max(image) - np.min(image)) | |
| } | |
| def _estimate_porosity(self, image: np.ndarray) -> Dict[str, Any]: | |
| """Estimate porosity using threshold-based segmentation""" | |
| # Use Otsu's thresholding for automatic threshold selection | |
| _, binary = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) | |
| # Calculate porosity (assuming dark regions are pores) | |
| total_pixels = image.shape[0] * image.shape[1] | |
| pore_pixels = np.sum(binary == 0) | |
| porosity_percentage = (pore_pixels / total_pixels) * 100 | |
| # Analyze pore size distribution | |
| contours, _ = cv2.findContours(255 - binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
| pore_areas = [cv2.contourArea(cnt) for cnt in contours if cv2.contourArea(cnt) > 10] | |
| return { | |
| "estimated_porosity_percent": float(porosity_percentage), | |
| "number_of_pores": len(pore_areas), | |
| "average_pore_area": float(np.mean(pore_areas)) if pore_areas else 0, | |
| "max_pore_area": float(np.max(pore_areas)) if pore_areas else 0, | |
| "min_pore_area": float(np.min(pore_areas)) if pore_areas else 0 | |
| } | |
| def _analyze_particles(self, image: np.ndarray) -> Dict[str, Any]: | |
| """Analyze particle characteristics""" | |
| # Edge detection for particle boundaries | |
| edges = cv2.Canny(image, 50, 150) | |
| # Find contours (potential particles) | |
| contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
| # Filter contours by area to remove noise | |
| min_area = 50 # minimum particle area | |
| particles = [cnt for cnt in contours if cv2.contourArea(cnt) > min_area] | |
| if not particles: | |
| return {"number_of_particles": 0} | |
| # Calculate particle properties | |
| areas = [cv2.contourArea(cnt) for cnt in particles] | |
| perimeters = [cv2.arcLength(cnt, True) for cnt in particles] | |
| # Calculate equivalent diameters | |
| equivalent_diameters = [2 * np.sqrt(area / np.pi) for area in areas] | |
| # Calculate circularity (roundness measure) | |
| circularities = [] | |
| for i, cnt in enumerate(particles): | |
| if perimeters[i] > 0: | |
| circularity = 4 * np.pi * areas[i] / (perimeters[i] ** 2) | |
| circularities.append(circularity) | |
| return { | |
| "number_of_particles": len(particles), | |
| "average_particle_area": float(np.mean(areas)), | |
| "particle_area_std": float(np.std(areas)), | |
| "average_equivalent_diameter": float(np.mean(equivalent_diameters)), | |
| "diameter_range": { | |
| "min": float(np.min(equivalent_diameters)), | |
| "max": float(np.max(equivalent_diameters)) | |
| }, | |
| "average_circularity": float(np.mean(circularities)) if circularities else 0, | |
| "circularity_std": float(np.std(circularities)) if circularities else 0 | |
| } |