Spaces:
Sleeping
Sleeping
| import cv2 | |
| import numpy as np | |
| from matplotlib import pyplot as plt | |
| class FruitDiseaseDetector: | |
| def __init__(self): | |
| self.disease_mask = None | |
| self.healthy_mask = None | |
| self.processed_image = None | |
| def remove_background(self, image): | |
| """ | |
| Remove background from fruit image using multiple segmentation techniques | |
| Returns the fruit mask and the image with background removed | |
| """ | |
| # Method 1: GrabCut algorithm (most effective for fruits) | |
| fruit_mask_grabcut = self._grabcut_segmentation(image) | |
| # Method 2: Color-based segmentation | |
| fruit_mask_color = self._color_based_segmentation(image) | |
| # Method 3: Edge-based segmentation | |
| fruit_mask_edge = self._edge_based_segmentation(image) | |
| # Combine all methods using voting | |
| combined_mask = self._combine_masks([fruit_mask_grabcut, fruit_mask_color, fruit_mask_edge]) | |
| # Post-process the mask | |
| final_mask = self._post_process_mask(combined_mask) | |
| # Apply mask to image | |
| result_image = image.copy() | |
| result_image[final_mask == 0] = [0, 0, 0] # Set background to black | |
| return final_mask, result_image | |
| def _grabcut_segmentation(self, image): | |
| """Use GrabCut algorithm for mango foreground/background separation""" | |
| height, width = image.shape[:2] | |
| # Initialize mask | |
| mask = np.zeros((height, width), np.uint8) | |
| # Define rectangle around the mango (mangoes are typically oval/elongated) | |
| # Adjust margins for mango shape - less margin on sides, more on top/bottom | |
| margin_x = int(width * 0.10) # Reduced horizontal margin for mango width | |
| margin_y = int(height * 0.12) # Slightly more vertical margin for mango length | |
| rect = (margin_x, margin_y, width - 2*margin_x, height - 2*margin_y) | |
| # Initialize background and foreground models | |
| bgd_model = np.zeros((1, 65), np.float64) | |
| fgd_model = np.zeros((1, 65), np.float64) | |
| # Apply GrabCut with more iterations for better mango segmentation | |
| cv2.grabCut(image, mask, rect, bgd_model, fgd_model, 8, cv2.GC_INIT_WITH_RECT) | |
| # Create binary mask (0 for background, 1 for foreground) | |
| fruit_mask = np.where((mask == 2) | (mask == 0), 0, 255).astype(np.uint8) | |
| return fruit_mask | |
| def _color_based_segmentation(self, image): | |
| """Segment mango using mango-specific color characteristics""" | |
| # Convert to HSV for better color segmentation | |
| hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) | |
| # Mango-specific color ranges (calibrated for mango dataset) | |
| # Green mangoes (unripe) - broader green range for mangoes | |
| lower_green1 = np.array([35, 25, 25]) | |
| upper_green1 = np.array([85, 255, 255]) | |
| # Yellow/Orange mangoes (ripe) - extended range for mango ripeness | |
| lower_yellow = np.array([10, 30, 30]) | |
| upper_yellow = np.array([35, 255, 255]) | |
| # Red/Orange mangoes (very ripe) - specific to mango varieties | |
| lower_red1 = np.array([0, 30, 30]) | |
| upper_red1 = np.array([15, 255, 255]) | |
| lower_red2 = np.array([165, 30, 30]) | |
| upper_red2 = np.array([180, 255, 255]) | |
| # Yellowish-green mangoes (semi-ripe) | |
| lower_yellow_green = np.array([25, 20, 40]) | |
| upper_yellow_green = np.array([45, 255, 255]) | |
| # Create masks for mango color ranges | |
| mask_green = cv2.inRange(hsv, lower_green1, upper_green1) | |
| mask_yellow = cv2.inRange(hsv, lower_yellow, upper_yellow) | |
| mask_red1 = cv2.inRange(hsv, lower_red1, upper_red1) | |
| mask_red2 = cv2.inRange(hsv, lower_red2, upper_red2) | |
| mask_yellow_green = cv2.inRange(hsv, lower_yellow_green, upper_yellow_green) | |
| # Combine all mango color masks | |
| fruit_mask = cv2.bitwise_or(mask_green, mask_yellow) | |
| fruit_mask = cv2.bitwise_or(fruit_mask, mask_red1) | |
| fruit_mask = cv2.bitwise_or(fruit_mask, mask_red2) | |
| fruit_mask = cv2.bitwise_or(fruit_mask, mask_yellow_green) | |
| # Include areas with moderate saturation and brightness (mango skin variations) | |
| saturation = hsv[:, :, 1] # Saturation channel | |
| value = hsv[:, :, 2] # Value channel | |
| # Mango skin can have lower saturation but still be fruit | |
| _, moderate_sat_mask = cv2.threshold(saturation, 30, 255, cv2.THRESH_BINARY) | |
| _, bright_mask = cv2.threshold(value, 50, 255, cv2.THRESH_BINARY) | |
| # Additional mask for pale/light mango areas | |
| mango_skin_mask = cv2.bitwise_and(moderate_sat_mask, bright_mask) | |
| # Final combination optimized for mangoes | |
| fruit_mask = cv2.bitwise_or(fruit_mask, mango_skin_mask) | |
| return fruit_mask | |
| def _edge_based_segmentation(self, image): | |
| """Use edge detection and contour analysis for segmentation""" | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
| # Apply Gaussian blur | |
| blurred = cv2.GaussianBlur(gray, (5, 5), 0) | |
| # Edge detection | |
| edges = cv2.Canny(blurred, 50, 150) | |
| # Find contours | |
| contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
| # Create mask | |
| mask = np.zeros(gray.shape, np.uint8) | |
| if contours: | |
| # Find the largest contour (assuming it's the fruit) | |
| largest_contour = max(contours, key=cv2.contourArea) | |
| # Only consider if the contour is reasonably large | |
| if cv2.contourArea(largest_contour) > (gray.shape[0] * gray.shape[1] * 0.1): | |
| cv2.fillPoly(mask, [largest_contour], 255) | |
| return mask | |
| def _combine_masks(self, masks): | |
| """Combine multiple masks using majority voting""" | |
| height, width = masks[0].shape | |
| combined = np.zeros((height, width), dtype=np.uint8) | |
| # Convert masks to binary (0 or 1) | |
| binary_masks = [(mask > 127).astype(np.uint8) for mask in masks] | |
| # Sum all masks | |
| mask_sum = np.sum(binary_masks, axis=0) | |
| # Use majority voting (at least 2 out of 3 methods agree) | |
| combined[mask_sum >= 2] = 255 | |
| return combined | |
| def _post_process_mask(self, mask): | |
| """Clean up the mask using morphological operations""" | |
| # Remove small noise | |
| kernel_small = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) | |
| cleaned = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel_small) | |
| # Fill small holes | |
| kernel_large = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15)) | |
| cleaned = cv2.morphologyEx(cleaned, cv2.MORPH_CLOSE, kernel_large) | |
| # Find the largest connected component (should be the main fruit) | |
| num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(cleaned, connectivity=8) | |
| if num_labels > 1: | |
| # Find largest component (excluding background) | |
| largest_label = 1 + np.argmax(stats[1:, cv2.CC_STAT_AREA]) | |
| # Create mask with only the largest component | |
| final_mask = np.zeros_like(cleaned) | |
| final_mask[labels == largest_label] = 255 | |
| else: | |
| final_mask = cleaned | |
| return final_mask | |
| def preprocess_image(self, image): | |
| """Preprocess the input image for disease detection""" | |
| # Convert to different color spaces for better analysis | |
| hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) | |
| lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB) | |
| # Apply Gaussian blur to reduce noise | |
| blurred = cv2.GaussianBlur(image, (5, 5), 0) | |
| return blurred, hsv, lab | |
| def detect_diseased_areas(self, image, fruit_mask=None): | |
| """ | |
| Detect mango diseases using color-based segmentation and texture analysis | |
| Calibrated for: Alternaria, Anthracnose, Aspergillus (Black Mould), Lasiodiplodia (Stem Rot) | |
| """ | |
| blurred, hsv, lab = self.preprocess_image(image) | |
| # Method 1: Detect Anthracnose (dark circular spots with orange/pink halos) | |
| disease_mask1 = self._detect_anthracnose(hsv, lab) | |
| # Method 2: Detect Alternaria (brown/black irregular spots) | |
| disease_mask2 = self._detect_alternaria(hsv, lab) | |
| # Method 3: Detect Aspergillus (Black Mould Rot - dark, fuzzy patches) | |
| disease_mask3 = self._detect_aspergillus(hsv, lab) | |
| # Method 4: Detect Lasiodiplodia (Stem and Rot - soft, dark areas) | |
| disease_mask4 = self._detect_lasiodiplodia(hsv, lab) | |
| # Method 5: General texture-based detection for rough/irregular surfaces | |
| disease_mask5 = self._detect_texture_anomalies(blurred) | |
| # Method 6: Edge-based detection for irregular boundaries | |
| disease_mask6 = self._detect_irregular_edges(blurred) | |
| # Combine disease-specific methods first (higher confidence) | |
| primary_disease_mask = cv2.bitwise_or(disease_mask1, disease_mask2) | |
| primary_disease_mask = cv2.bitwise_or(primary_disease_mask, disease_mask3) | |
| primary_disease_mask = cv2.bitwise_or(primary_disease_mask, disease_mask4) | |
| # Secondary detection (texture and edges) - use only if there's significant evidence | |
| secondary_mask = cv2.bitwise_and(disease_mask5, disease_mask6) | |
| # Combine primary and secondary with different weights | |
| combined_mask = cv2.bitwise_or(primary_disease_mask, secondary_mask) | |
| # Apply fruit mask to limit detection to mango area only | |
| if fruit_mask is not None: | |
| combined_mask = cv2.bitwise_and(combined_mask, fruit_mask) | |
| # Post-processing: More aggressive noise removal for better accuracy | |
| kernel_small = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) | |
| kernel_medium = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) | |
| # Remove small noise (more aggressive) | |
| combined_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_OPEN, kernel_small) | |
| combined_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_OPEN, kernel_medium) | |
| # Fill small holes but not too aggressively | |
| kernel_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) | |
| combined_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_CLOSE, kernel_close) | |
| # Filter out very small regions (likely noise) | |
| contours, _ = cv2.findContours(combined_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
| filtered_mask = np.zeros_like(combined_mask) | |
| min_area = 75 # Balanced minimum area for good detection | |
| for contour in contours: | |
| if cv2.contourArea(contour) >= min_area: | |
| cv2.fillPoly(filtered_mask, [contour], 255) | |
| return filtered_mask | |
| def _detect_anthracnose(self, hsv, lab): | |
| """Detect Anthracnose disease - dark circular spots with orange/pink halos""" | |
| # Anthracnose characteristics: Dark centers with lighter halos | |
| # Use both HSV and LAB for better detection | |
| # Dark spots in HSV (less conservative thresholds) | |
| lower_dark = np.array([0, 40, 0]) | |
| upper_dark = np.array([180, 255, 80]) | |
| dark_mask = cv2.inRange(hsv, lower_dark, upper_dark) | |
| # Reddish-brown areas (anthracnose lesions) - broader range | |
| lower_reddish = np.array([0, 50, 15]) | |
| upper_reddish = np.array([25, 255, 140]) | |
| reddish_mask = cv2.inRange(hsv, lower_reddish, upper_reddish) | |
| # LAB color space for better brown/dark detection | |
| l_channel = lab[:, :, 0] | |
| a_channel = lab[:, :, 1] | |
| # Less conservative dark areas with reddish tint | |
| _, dark_lab = cv2.threshold(l_channel, 85, 255, cv2.THRESH_BINARY_INV) | |
| _, red_lab = cv2.threshold(a_channel, 135, 255, cv2.THRESH_BINARY) | |
| lab_anthracnose = cv2.bitwise_and(dark_lab, red_lab) | |
| # Combine HSV and LAB detections - use OR instead of AND for better sensitivity | |
| anthracnose_mask = cv2.bitwise_or(dark_mask, reddish_mask) | |
| anthracnose_mask = cv2.bitwise_or(anthracnose_mask, lab_anthracnose) | |
| return anthracnose_mask | |
| def _detect_alternaria(self, hsv, lab): | |
| """Detect Alternaria disease - brown/black irregular spots with concentric rings""" | |
| # Alternaria has characteristic brown to black lesions | |
| # Less conservative brown to black spots in HSV | |
| lower_brown1 = np.array([5, 60, 5]) | |
| upper_brown1 = np.array([25, 255, 100]) | |
| lower_brown2 = np.array([0, 40, 3]) | |
| upper_brown2 = np.array([20, 255, 80]) | |
| brown_mask1 = cv2.inRange(hsv, lower_brown1, upper_brown1) | |
| brown_mask2 = cv2.inRange(hsv, lower_brown2, upper_brown2) | |
| # Very dark areas (advanced alternaria) - less conservative | |
| lower_black = np.array([0, 30, 0]) | |
| upper_black = np.array([180, 255, 50]) | |
| black_mask = cv2.inRange(hsv, lower_black, upper_black) | |
| # LAB space detection for brown lesions - less strict | |
| l_channel = lab[:, :, 0] | |
| _, dark_lab = cv2.threshold(l_channel, 70, 255, cv2.THRESH_BINARY_INV) | |
| # Combine alternaria indicators - use OR for better sensitivity | |
| alternaria_mask = cv2.bitwise_or(brown_mask1, brown_mask2) | |
| alternaria_mask = cv2.bitwise_or(alternaria_mask, black_mask) | |
| alternaria_mask = cv2.bitwise_or(alternaria_mask, dark_lab) | |
| return alternaria_mask | |
| def _detect_aspergillus(self, hsv, lab): | |
| """Detect Aspergillus (Black Mould Rot) - dark, fuzzy patches with irregular borders""" | |
| # Aspergillus appears as very dark, irregular patches with possible greenish tints | |
| # Very dark areas in HSV - less conservative | |
| lower_black = np.array([0, 20, 0]) | |
| upper_black = np.array([180, 255, 40]) | |
| black_mask = cv2.inRange(hsv, lower_black, upper_black) | |
| # Dark greenish areas (aspergillus can have greenish tint) - broader range | |
| lower_dark_green = np.array([40, 40, 3]) | |
| upper_dark_green = np.array([80, 255, 60]) | |
| dark_green_mask = cv2.inRange(hsv, lower_dark_green, upper_dark_green) | |
| # Dark bluish-green areas (specific to aspergillus) - broader range | |
| lower_blue_green = np.array([85, 30, 5]) | |
| upper_blue_green = np.array([120, 255, 80]) | |
| blue_green_mask = cv2.inRange(hsv, lower_blue_green, upper_blue_green) | |
| # LAB space for very dark areas - less conservative | |
| l_channel = lab[:, :, 0] | |
| b_channel = lab[:, :, 2] | |
| _, very_dark_lab = cv2.threshold(l_channel, 50, 255, cv2.THRESH_BINARY_INV) | |
| # Blue-green tint in LAB space (aspergillus characteristic) - broader range | |
| _, blue_tint = cv2.threshold(b_channel, 110, 255, cv2.THRESH_BINARY_INV) | |
| lab_aspergillus = cv2.bitwise_and(very_dark_lab, blue_tint) | |
| # Combine aspergillus indicators - use OR for better sensitivity | |
| aspergillus_mask = cv2.bitwise_or(black_mask, very_dark_lab) | |
| aspergillus_mask = cv2.bitwise_or(aspergillus_mask, dark_green_mask) | |
| aspergillus_mask = cv2.bitwise_or(aspergillus_mask, blue_green_mask) | |
| aspergillus_mask = cv2.bitwise_or(aspergillus_mask, lab_aspergillus) | |
| return aspergillus_mask | |
| def _detect_lasiodiplodia(self, hsv, lab): | |
| """Detect Lasiodiplodia (Stem and Rot disease) - soft, dark areas with brown/black coloration""" | |
| # Lasiodiplodia characteristics: dark, soft areas with brown/black coloration | |
| # Brown rot areas - less conservative | |
| lower_rot = np.array([8, 50, 10]) | |
| upper_rot = np.array([30, 255, 120]) | |
| rot_mask = cv2.inRange(hsv, lower_rot, upper_rot) | |
| # Dark spots with higher saturation (active rot) - less strict | |
| lower_active_rot = np.array([0, 60, 5]) | |
| upper_active_rot = np.array([20, 255, 100]) | |
| active_rot_mask = cv2.inRange(hsv, lower_active_rot, upper_active_rot) | |
| # Very dark areas (advanced lasiodiplodia) - broader range | |
| lower_very_dark = np.array([0, 30, 0]) | |
| upper_very_dark = np.array([30, 255, 60]) | |
| very_dark_mask = cv2.inRange(hsv, lower_very_dark, upper_very_dark) | |
| # LAB space for detecting rot areas - less conservative | |
| l_channel = lab[:, :, 0] | |
| a_channel = lab[:, :, 1] | |
| _, dark_rot = cv2.threshold(l_channel, 70, 255, cv2.THRESH_BINARY_INV) | |
| _, red_tint = cv2.threshold(a_channel, 130, 255, cv2.THRESH_BINARY) | |
| lab_rot = cv2.bitwise_and(dark_rot, red_tint) | |
| # Combine lasiodiplodia indicators - use OR for better sensitivity | |
| lasiodiplodia_mask = cv2.bitwise_or(rot_mask, active_rot_mask) | |
| lasiodiplodia_mask = cv2.bitwise_or(lasiodiplodia_mask, very_dark_mask) | |
| lasiodiplodia_mask = cv2.bitwise_or(lasiodiplodia_mask, lab_rot) | |
| return lasiodiplodia_mask | |
| def _detect_black_mould_rot(self, hsv, lab): | |
| """Detect Black Mould Rot - dark, fuzzy patches with irregular borders (legacy method)""" | |
| # This method is now replaced by _detect_aspergillus but kept for compatibility | |
| return self._detect_aspergillus(hsv, lab) | |
| def _detect_stem_rot(self, hsv, lab): | |
| """Detect Stem and Rot disease - soft, dark areas typically near stem end (legacy method)""" | |
| # This method is now replaced by _detect_lasiodiplodia but kept for compatibility | |
| return self._detect_lasiodiplodia(hsv, lab) | |
| def _detect_brown_spots(self, hsv): | |
| """Detect brown/dark spots typical of fruit diseases (legacy method)""" | |
| # Define HSV range for brown/dark diseased areas | |
| # Brown spots: Low saturation, low value, wide hue range | |
| lower_brown = np.array([0, 20, 20]) | |
| upper_brown = np.array([30, 255, 120]) | |
| # Create mask for brown areas | |
| brown_mask1 = cv2.inRange(hsv, lower_brown, upper_brown) | |
| # Additional brown range (reddish-brown) | |
| lower_brown2 = np.array([150, 20, 20]) | |
| upper_brown2 = np.array([180, 255, 120]) | |
| brown_mask2 = cv2.inRange(hsv, lower_brown2, upper_brown2) | |
| # Combine brown masks | |
| brown_mask = cv2.bitwise_or(brown_mask1, brown_mask2) | |
| return brown_mask | |
| def _detect_dark_spots_lab(self, lab): | |
| """Detect dark spots using LAB color space""" | |
| l_channel = lab[:, :, 0] | |
| a_channel = lab[:, :, 1] | |
| # Dark areas have low L values | |
| _, dark_mask = cv2.threshold(l_channel, 80, 255, cv2.THRESH_BINARY_INV) | |
| # Areas with high 'a' values (reddish) combined with darkness | |
| _, red_mask = cv2.threshold(a_channel, 140, 255, cv2.THRESH_BINARY) | |
| # Combine dark and reddish areas | |
| lab_mask = cv2.bitwise_and(dark_mask, red_mask) | |
| return lab_mask | |
| def _detect_texture_anomalies(self, image): | |
| """Detect texture anomalies using local binary patterns concept (calibrated for mango)""" | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
| # Calculate local standard deviation (texture measure) - optimized for mango texture | |
| kernel = np.ones((7, 7), np.float32) / 49 # Slightly smaller kernel for mango details | |
| gray_float = gray.astype(np.float32) | |
| mean = cv2.filter2D(gray_float, -1, kernel) | |
| sqr_mean = cv2.filter2D(gray_float**2, -1, kernel) | |
| # Ensure variance is non-negative (handle floating point precision errors) | |
| variance = sqr_mean - mean**2 | |
| variance = np.maximum(variance, 0) # Clamp negative values to 0 | |
| std_dev = np.sqrt(variance) | |
| # Handle NaN and infinity values | |
| std_dev = np.nan_to_num(std_dev, nan=0.0, posinf=255.0, neginf=0.0) | |
| # Normalize to 0-255 range | |
| if std_dev.max() > 0: | |
| std_dev = (std_dev / std_dev.max() * 255).astype(np.uint8) | |
| else: | |
| std_dev = std_dev.astype(np.uint8) | |
| # Higher threshold for mango diseases (less sensitive to normal texture) | |
| _, texture_mask = cv2.threshold(std_dev, 30, 255, cv2.THRESH_BINARY) | |
| # Additional texture analysis for mango-specific roughness | |
| # Use Sobel operators to detect rough areas - more conservative | |
| sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3) | |
| sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3) | |
| sobel_magnitude = np.sqrt(sobelx**2 + sobely**2) | |
| sobel_magnitude = np.uint8(sobel_magnitude / sobel_magnitude.max() * 255) | |
| # Higher threshold for rough texture detection (less sensitive) | |
| _, rough_mask = cv2.threshold(sobel_magnitude, 50, 255, cv2.THRESH_BINARY) | |
| # Combine standard deviation and sobel-based texture detection | |
| # Use AND operation to require both methods to agree (more conservative) | |
| final_texture_mask = cv2.bitwise_and(texture_mask, rough_mask) | |
| return final_texture_mask | |
| def _detect_irregular_edges(self, image): | |
| """Detect irregular edges that might indicate disease boundaries""" | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
| # Apply Canny edge detection | |
| edges = cv2.Canny(gray, 50, 150) | |
| # Dilate edges to create regions | |
| kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) | |
| dilated_edges = cv2.dilate(edges, kernel, iterations=2) | |
| return dilated_edges | |
| def calculate_disease_severity(self, image, disease_mask, fruit_mask): | |
| """Calculate disease severity as percentage of affected area""" | |
| if fruit_mask is not None: | |
| # Use provided fruit mask | |
| fruit_area = cv2.countNonZero(fruit_mask) | |
| else: | |
| # Fallback: create fruit mask (assuming fruit takes up most of the image) | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
| _, fruit_mask = cv2.threshold(gray, 30, 255, cv2.THRESH_BINARY) | |
| fruit_area = cv2.countNonZero(fruit_mask) | |
| # Calculate disease area (only within fruit region) | |
| if fruit_mask is not None: | |
| disease_in_fruit = cv2.bitwise_and(disease_mask, fruit_mask) | |
| disease_area = cv2.countNonZero(disease_in_fruit) | |
| else: | |
| disease_area = cv2.countNonZero(disease_mask) | |
| if fruit_area == 0: | |
| return 0 | |
| severity_percentage = (disease_area / fruit_area) * 100 | |
| return min(severity_percentage, 100) # Cap at 100% | |
| def classify_disease_level(self, severity_percentage): | |
| """Classify mango disease level based on severity percentage (calibrated for mango)""" | |
| if severity_percentage < 2: | |
| return "Healthy", (0, 255, 0) # Green - very strict for healthy | |
| elif severity_percentage < 8: | |
| return "Early Disease", (0, 255, 255) # Yellow - early detection | |
| elif severity_percentage < 20: | |
| return "Moderate Disease", (0, 165, 255) # Orange - moderate infection | |
| elif severity_percentage < 40: | |
| return "Severe Disease", (0, 100, 255) # Red-Orange - severe infection | |
| else: | |
| return "Critical Disease", (0, 0, 255) # Red - critical, unmarketable | |
| def process_image(self, image_path): | |
| """Main processing function with background removal (calibrated for mango diseases)""" | |
| # Load image | |
| image = cv2.imread(image_path) | |
| if image is None: | |
| raise ValueError("Could not load image") | |
| print("Step 1: Removing background (mango-optimized)...") | |
| # Remove background first | |
| fruit_mask, image_no_bg = self.remove_background(image) | |
| print("Step 2: Detecting mango diseases (Alternaria, Anthracnose, Aspergillus, Lasiodiplodia)...") | |
| # Detect diseased areas (only within mango region) | |
| disease_mask = self.detect_diseased_areas(image_no_bg, fruit_mask) | |
| print("Step 3: Calculating mango disease severity...") | |
| # Calculate severity | |
| severity = self.calculate_disease_severity(image_no_bg, disease_mask, fruit_mask) | |
| disease_level, color = self.classify_disease_level(severity) | |
| print("Step 4: Creating mango disease visualization...") | |
| # Create output visualization with bounding boxes | |
| output_image, disease_info = self._create_output_visualization(image, disease_mask, severity, disease_level, color, fruit_mask) | |
| # Store results | |
| self.disease_mask = disease_mask | |
| self.processed_image = output_image | |
| self.fruit_mask = fruit_mask | |
| self.image_no_bg = image_no_bg | |
| self.disease_info = disease_info | |
| return { | |
| 'severity_percentage': severity, | |
| 'disease_level': disease_level, | |
| 'disease_mask': disease_mask, | |
| 'output_image': output_image, | |
| 'fruit_mask': fruit_mask, | |
| 'image_no_bg': image_no_bg, | |
| 'disease_info': disease_info, | |
| 'num_diseased_regions': len(disease_info) | |
| } | |
| def _create_output_visualization(self, original, mask, severity, level, color, fruit_mask=None): | |
| """Create visualization showing detected diseased areas with bounding boxes""" | |
| # Start with original image | |
| result = original.copy() | |
| # If we have a fruit mask, dim the background | |
| if fruit_mask is not None: | |
| background = np.zeros_like(original) | |
| result = np.where(fruit_mask[..., np.newaxis] == 0, | |
| background, result) | |
| # Create colored overlay for diseased areas | |
| overlay = result.copy() | |
| disease_info = [] | |
| if np.any(mask > 0): # Only if there are diseased areas | |
| overlay[mask > 0] = color | |
| # Blend original image with overlay | |
| result = cv2.addWeighted(result, 0.7, overlay, 0.3, 0) | |
| # Find contours and draw bounding boxes | |
| contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
| # Filter contours by minimum area to avoid tiny noise | |
| min_area = 50 # Minimum area threshold | |
| valid_contours = [cnt for cnt in contours if cv2.contourArea(cnt) >= min_area] | |
| # Draw contours and bounding boxes | |
| for i, contour in enumerate(valid_contours): | |
| # Draw contour outline | |
| cv2.drawContours(result, [contour], -1, color, 2) | |
| # Get bounding rectangle | |
| x, y, w, h = cv2.boundingRect(contour) | |
| # Draw bounding box | |
| cv2.rectangle(result, (x, y), (x + w, y + h), color, 3) | |
| # Calculate area of this diseased region | |
| area = cv2.contourArea(contour) | |
| disease_info.append({ | |
| 'id': i + 1, | |
| 'bbox': (x, y, w, h), | |
| 'area': area, | |
| 'center': (x + w//2, y + h//2) | |
| }) | |
| # Add label with disease ID | |
| label = f"D{i+1}" | |
| label_size = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)[0] | |
| # Position label above bounding box | |
| label_x = x | |
| label_y = y - 10 if y - 10 > 20 else y + h + 25 | |
| # Draw label background | |
| cv2.rectangle(result, | |
| (label_x - 2, label_y - label_size[1] - 2), | |
| (label_x + label_size[0] + 2, label_y + 2), | |
| color, -1) | |
| # Draw label text | |
| cv2.putText(result, label, (label_x, label_y), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2) | |
| # Add overall statistics | |
| num_diseases = len(disease_info) | |
| text1 = f"{level}: {severity:.1f}%" | |
| text2 = f"Diseased Regions: {num_diseases}" | |
| # Main status text | |
| cv2.putText(result, text1, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, color, 2) | |
| cv2.putText(result, text2, (10, 70), cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2) | |
| # Add fruit outline | |
| if fruit_mask is not None: | |
| fruit_contours, _ = cv2.findContours(fruit_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
| cv2.drawContours(result, fruit_contours, -1, (255, 255, 255), 2) | |
| return result, disease_info | |
| def save_detailed_report(self, output_path, results): | |
| """Save detailed report of disease detection results""" | |
| report_path = output_path.replace('.jpg', '_report.txt').replace('.png', '_report.txt') | |
| with open(report_path, 'w') as f: | |
| f.write("=== FRUIT DISEASE DETECTION REPORT ===\n") | |
| f.write(f"Image processed: {output_path}\n") | |
| f.write(f"Overall Disease Level: {results['disease_level']}\n") | |
| f.write(f"Overall Severity: {results['severity_percentage']:.2f}%\n") | |
| f.write(f"Number of Diseased Regions: {results['num_diseased_regions']}\n") | |
| f.write("\n=== INDIVIDUAL DISEASE REGIONS ===\n") | |
| if results['disease_info']: | |
| for disease in results['disease_info']: | |
| f.write(f"\nDisease Region D{disease['id']}:\n") | |
| f.write(f" - Bounding Box: x={disease['bbox'][0]}, y={disease['bbox'][1]}, ") | |
| f.write(f"width={disease['bbox'][2]}, height={disease['bbox'][3]}\n") | |
| f.write(f" - Area: {disease['area']:.0f} pixels\n") | |
| f.write(f" - Center: ({disease['center'][0]}, {disease['center'][1]})\n") | |
| else: | |
| f.write("No diseased regions detected.\n") | |
| print(f"Detailed report saved: {report_path}") | |
| return report_path | |
| def save_results(self, output_path, include_mask=True, include_background_removed=True): | |
| """Save processing results""" | |
| if self.processed_image is not None: | |
| cv2.imwrite(output_path, self.processed_image) | |
| print(f"Main result saved: {output_path}") | |
| if include_mask and self.disease_mask is not None: | |
| mask_path = output_path.replace('.', '_disease_mask.') | |
| cv2.imwrite(mask_path, self.disease_mask) | |
| print(f"Disease mask saved: {mask_path}") | |
| if include_background_removed and hasattr(self, 'image_no_bg') and self.image_no_bg is not None: | |
| bg_removed_path = output_path.replace('.', '_no_background.') | |
| cv2.imwrite(bg_removed_path, self.image_no_bg) | |
| print(f"Background removed image saved: {bg_removed_path}") | |
| if hasattr(self, 'fruit_mask') and self.fruit_mask is not None: | |
| fruit_mask_path = output_path.replace('.', '_fruit_mask.') | |
| cv2.imwrite(fruit_mask_path, self.fruit_mask) | |
| print(f"Fruit mask saved: {fruit_mask_path}") | |
| # Example usage and testing function | |
| def demonstrate_disease_detection(): | |
| """Demonstrate the mango disease detection algorithm""" | |
| detector = FruitDiseaseDetector() | |
| print("Mango Disease Detection Algorithm with Background Removal & Bounding Boxes") | |
| print("========================================================================") | |
| print("This algorithm detects mango diseases using:") | |
| print("1. Mango-optimized background removal (GrabCut + Color + Edge detection)") | |
| print("2. Anthracnose detection (dark circular spots with orange/pink halos)") | |
| print("3. Alternaria detection (brown/black irregular spots with concentric rings)") | |
| print("4. Aspergillus detection (black mould with greenish tints)") | |
| print("5. Lasiodiplodia detection (stem rot with brown/black soft areas)") | |
| print("6. Mango-calibrated texture analysis for rough surfaces") | |
| print("7. Edge detection for irregular disease boundaries") | |
| print("8. Individual disease region bounding boxes") | |
| print() | |
| print("Algorithm Features for Mango:") | |
| print("- Multi-method background removal optimized for mango shape") | |
| print("- Disease-specific detection for common mango diseases") | |
| print("- Individual region labeling (D1, D2, D3, etc.)") | |
| print("- Mango-specific severity assessment (Healthy < 2%, Early < 8%, etc.)") | |
| print("- Comprehensive reporting for mango disease management") | |
| print("- Calibrated for mango color variations (green, yellow, orange, red)") | |
| print("- Specialized detection for Aspergillus (Black Mould) and Lasiodiplodia (Stem Rot)") | |
| return detector | |
| # Batch testing function for algorithm validation | |
| def test_mango_detection_algorithm(): | |
| """Test the algorithm on multiple mango samples""" | |
| detector = FruitDiseaseDetector() | |
| # Test cases with expected results - using correct file names | |
| test_cases = [ | |
| ("SenMangoFruitDDS_bgremoved/Healthy/healthy_003.jpg", "Should be Healthy"), | |
| ("SenMangoFruitDDS_bgremoved/Healthy/healthy_010.jpg", "Should be Healthy"), | |
| ("SenMangoFruitDDS_bgremoved/Alternaria/Alternaria_005.jpg", "Should detect Alternaria"), | |
| ("SenMangoFruitDDS_bgremoved/Alternaria/Alternaria_010.jpg", "Should detect Alternaria"), | |
| ("SenMangoFruitDDS_bgremoved/Anthracnose/Anthracnose_002.jpg", "Should detect Anthracnose"), | |
| ("SenMangoFruitDDS_bgremoved/Anthracnose/Anthracnose_010.jpg", "Should detect Anthracnose"), | |
| ("SenMangoFruitDDS_bgremoved/Black Mould Rot/Aspergillus_001.jpg", "Should detect Aspergillus (Black Mould)"), | |
| ("SenMangoFruitDDS_bgremoved/Black Mould Rot/Aspergillus_010.jpg", "Should detect Aspergillus (Black Mould)"), | |
| ("SenMangoFruitDDS_bgremoved/Stem and Rot/Lasiodiplodia_001.jpg", "Should detect Lasiodiplodia (Stem Rot)"), | |
| ("SenMangoFruitDDS_bgremoved/Stem and Rot/Lasiodiplodia_012.jpg", "Should detect Lasiodiplodia (Stem Rot)"), | |
| ] | |
| print("=== MANGO DISEASE DETECTION ALGORITHM VALIDATION ===") | |
| print("Testing multiple samples to validate algorithm performance") | |
| print("Diseases: Alternaria, Anthracnose, Aspergillus (Black Mould), Lasiodiplodia (Stem Rot)") | |
| print("=" * 80) | |
| results_summary = [] | |
| for i, (image_path, expected) in enumerate(test_cases, 1): | |
| print(f"\nTest {i}: {image_path}") | |
| print(f"Expected: {expected}") | |
| try: | |
| results = detector.process_image(image_path) | |
| output_path = f"test_result_{i}.jpg" | |
| result_info = { | |
| 'test_id': i, | |
| 'image_path': image_path, | |
| 'expected': expected, | |
| 'detected_level': results['disease_level'], | |
| 'severity': results['severity_percentage'], | |
| 'num_regions': results['num_diseased_regions'] | |
| } | |
| results_summary.append(result_info) | |
| print(f"Result: {results['disease_level']} ({results['severity_percentage']:.2f}%)") | |
| print(f"Regions: {results['num_diseased_regions']}") | |
| # Save test result | |
| detector.save_results(output_path, include_mask=True) | |
| except Exception as e: | |
| print(f"Error: {e}") | |
| results_summary.append({ | |
| 'test_id': i, | |
| 'image_path': image_path, | |
| 'expected': expected, | |
| 'detected_level': 'ERROR', | |
| 'severity': 0.0, | |
| 'num_regions': 0 | |
| }) | |
| print("-" * 50) | |
| # Print summary | |
| print("\n" + "=" * 80) | |
| print("TESTING SUMMARY") | |
| print("=" * 80) | |
| for result in results_summary: | |
| status = "β" if result['detected_level'] != 'Healthy' and 'Should detect' in result['expected'] else "β" if result['detected_level'] == 'Healthy' and 'Should detect' in result['expected'] else "β" | |
| print(f"Test {result['test_id']:2d}: {status} {result['detected_level']} ({result['severity']:.1f}%) - {result['expected']}") | |
| return results_summary | |
| # Usage example: | |
| if __name__ == "__main__": | |
| # Initialize mango disease detector | |
| detector = FruitDiseaseDetector() | |
| # Test different disease types - uncomment to test specific diseases: | |
| # image_path = "SenMangoFruitDDS_bgremoved/Healthy/healthy_003.jpg" # Should be Healthy | |
| # image_path = "SenMangoFruitDDS_bgremoved/Alternaria/Alternaria_005.jpg" # Should detect Alternaria | |
| # image_path = "SenMangoFruitDDS_bgremoved/Anthracnose/Anthracnose_002.jpg" # Should detect Anthracnose | |
| # image_path = "SenMangoFruitDDS_bgremoved/Black Mould Rot/Aspergillus_001.jpg" # Should detect Aspergillus | |
| image_path = "SenMangoFruitDDS_bgremoved/Stem and Rot/Lasiodiplodia_012.jpg" # Should detect Lasiodiplodia | |
| output_path = "mango_disease_detection_result.jpg" # Output file name | |
| try: | |
| print("Processing mango image with calibrated algorithm...") | |
| print(f"Input: {image_path}") | |
| print(f"Output: {output_path}") | |
| print("-" * 60) | |
| results = detector.process_image(image_path) | |
| print(f"\n=== MANGO DISEASE DETECTION RESULTS ===") | |
| print(f"Disease Level: {results['disease_level']}") | |
| print(f"Severity: {results['severity_percentage']:.2f}%") | |
| print(f"Number of Diseased Regions: {results['num_diseased_regions']}") | |
| # Print individual disease information | |
| if results['disease_info']: | |
| print(f"\n=== INDIVIDUAL DISEASE REGIONS ===") | |
| for disease in results['disease_info']: | |
| print(f"Disease D{disease['id']}: Area={disease['area']:.0f} pixels, " | |
| f"Center=({disease['center'][0]}, {disease['center'][1]})") | |
| else: | |
| print("\n=== MANGO HEALTH STATUS ===") | |
| print("No significant disease regions detected - mango appears healthy!") | |
| # Save results | |
| detector.save_results(output_path, include_mask=True) | |
| # Save detailed report | |
| detector.save_detailed_report(output_path, results) | |
| print(f"\n=== FILES SAVED ===") | |
| print(f"Main result: {output_path}") | |
| print(f"Disease mask: {output_path.replace('.', '_disease_mask.')}") | |
| print(f"Background removed: {output_path.replace('.', '_no_background.')}") | |
| print(f"Fruit mask: {output_path.replace('.', '_fruit_mask.')}") | |
| print(f"Detailed report: {output_path.replace('.jpg', '_report.txt')}") | |
| print("\nMango disease detection completed successfully!") | |
| print("The algorithm has been calibrated for mango-specific diseases:") | |
| print("- Alternaria, Anthracnose, Aspergillus (Black Mould), Lasiodiplodia (Stem Rot)") | |
| except FileNotFoundError: | |
| print(f"Error: Could not find mango image file '{image_path}'") | |
| print("Please check the file path and make sure the image exists.") | |
| print("\nAvailable test images:") | |
| print("- SenMangoFruitDDS_bgremoved/Healthy/healthy_003.jpg") | |
| print("- SenMangoFruitDDS_bgremoved/Alternaria/Alternaria_005.jpg") | |
| print("- SenMangoFruitDDS_bgremoved/Anthracnose/Anthracnose_002.jpg") | |
| except Exception as e: | |
| print(f"Error processing mango image: {e}") | |
| # Uncomment below to see algorithm demonstration | |
| # demonstrate_disease_detection() | |
| # Uncomment below to run batch testing | |
| # test_mango_detection_algorithm() |