#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Created on Mon Jun 9 12:55:13 2025 @author: amit """ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Orchestration Pipeline for WBC Classification This file handles model loading, image preprocessing, and prediction orchestration @author: amit """ import tensorflow as tf import numpy as np import cv2 from tensorflow.keras.models import load_model from PIL import Image import io import logging from typing import List, Dict, Tuple, Optional import os import glob from pathlib import Path from tensorflow.keras.models import load_model import tensorflow as tf import pandas as pd # Define the custom functions def split_attention(x): return tf.split(x, num_or_size_splits=2, axis=1) def split_attention_output_shape(input_shape): filters = input_shape[-1] // 2 return [input_shape[:-1] + (filters,), input_shape[:-1] + (filters,)] # Define custom objects custom_objects = { 'split_attention': split_attention, 'split_attention_output_shape': split_attention_output_shape } # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class WBCClassificationPipeline: """ Complete pipeline for WBC classification including model loading, image preprocessing, and prediction generation """ def __init__(self, model_path: str): """ Initialize the pipeline with model path Args: model_path (str): Path to the trained .keras model file """ self.model_path = model_path self.model = None self.class_names = ['Basophil', 'Eosinophil', 'Lymphocyte', 'Monocyte', 'Neutrophil'] self.input_shape = (128, 128, 3) # Updated to correct input shape (was 112, now 128) self.is_loaded = False def load_model(self) -> bool: """ Load the trained model with custom objects Returns: bool: True if model loaded successfully, False otherwise """ try: logger.info(f"Loading model from: {self.model_path}") # Define custom functions for your model def split_attention(x): return tf.split(x, num_or_size_splits=2, axis=1) def split_attention_output_shape(input_shape): filters = input_shape[-1] // 2 return [input_shape[:-1] + (filters,), input_shape[:-1] + (filters,)] # Define custom objects dictionary custom_objects = { 'split_attention': split_attention, 'split_attention_output_shape': split_attention_output_shape } # Load the model with custom objects self.model = load_model(self.model_path, custom_objects=custom_objects) self.is_loaded = True logger.info("Model loaded successfully!") logger.info(f"Model input shape: {self.model.input_shape}") logger.info(f"Model output shape: {self.model.output_shape}") return True except Exception as e: logger.error(f"Failed to load model: {str(e)}") self.is_loaded = False return False def preprocess_image(self, image_data: bytes) -> Optional[np.ndarray]: """ Preprocess uploaded image for model prediction Args: image_data (bytes): Raw image data from file upload Returns: np.ndarray: Preprocessed image array ready for prediction """ try: # Convert bytes to PIL Image image = Image.open(io.BytesIO(image_data)) # Convert to RGB if necessary if image.mode != 'RGB': image = image.convert('RGB') # Convert PIL to numpy array image_array = np.array(image) # Resize image to model input shape (128x128) - UPDATED FROM 112x112 resized_image = cv2.resize(image_array, (self.input_shape[0], self.input_shape[1])) # Normalize pixel values to [0, 1] normalized_image = resized_image.astype(np.float32) / 255.0 # Add batch dimension preprocessed_image = np.expand_dims(normalized_image, axis=0) logger.info(f"Image preprocessed successfully. Shape: {preprocessed_image.shape}") return preprocessed_image except Exception as e: logger.error(f"Failed to preprocess image: {str(e)}") return None def preprocess_image_from_path(self, image_path: str) -> Optional[np.ndarray]: """ Preprocess image from file path (for local testing) Args: image_path (str): Path to image file Returns: np.ndarray: Preprocessed image array ready for prediction """ try: # Read image file as bytes with open(image_path, 'rb') as f: image_data = f.read() return self.preprocess_image(image_data) except Exception as e: logger.error(f"Failed to preprocess image from path {image_path}: {str(e)}") return None def predict_single_image(self, image_data: bytes) -> Dict: """ Predict WBC class for a single image Args: image_data (bytes): Raw image data Returns: Dict: Prediction results with class, confidence, and all probabilities """ try: if not self.is_loaded: raise Exception("Model not loaded. Call load_model() first.") # Preprocess the image preprocessed_image = self.preprocess_image(image_data) if preprocessed_image is None: raise Exception("Failed to preprocess image") # Make prediction predictions = self.model.predict(preprocessed_image, verbose=0) # Get prediction probabilities probabilities = predictions[0] # Get predicted class index and confidence predicted_class_index = np.argmax(probabilities) confidence = float(probabilities[predicted_class_index]) predicted_class = self.class_names[predicted_class_index] # Create probability dictionary for all classes all_probabilities = { class_name: float(prob) for class_name, prob in zip(self.class_names, probabilities) } # Sort probabilities in descending order sorted_probabilities = dict( sorted(all_probabilities.items(), key=lambda x: x[1], reverse=True) ) result = { 'predicted_class': predicted_class, 'confidence': confidence, 'all_probabilities': sorted_probabilities, 'predicted_class_index': int(predicted_class_index) } logger.info(f"Prediction successful: {predicted_class} ({confidence:.3f})") return result except Exception as e: logger.error(f"Prediction failed: {str(e)}") raise e def predict_single_image_from_path(self, image_path: str) -> Dict: """ Predict WBC class for a single image from file path (for local testing) Args: image_path (str): Path to image file Returns: Dict: Prediction results with class, confidence, and all probabilities """ try: # Read image file as bytes with open(image_path, 'rb') as f: image_data = f.read() return self.predict_single_image(image_data) except Exception as e: logger.error(f"Failed to predict image from path {image_path}: {str(e)}") raise e def predict_batch_images(self, images_data: List[Tuple[str, bytes]]) -> List[Dict]: """ Predict WBC classes for multiple images Args: images_data (List[Tuple[str, bytes]]): List of tuples containing (filename, image_data) Returns: List[Dict]: List of prediction results for each image """ results = [] for filename, image_data in images_data: try: logger.info(f"Processing image: {filename}") # Get prediction for single image prediction_result = self.predict_single_image(image_data) # Add filename to result result = { 'filename': filename, 'success': True, 'results': prediction_result } except Exception as e: logger.error(f"Failed to process {filename}: {str(e)}") result = { 'filename': filename, 'success': False, 'error': str(e) } results.append(result) logger.info(f"Batch processing completed. Processed {len(results)} images.") return results def predict_batch_from_folder(self, folder_path: str, image_extensions: List[str] = None) -> List[Dict]: """ Predict WBC classes for all images in a folder (for local testing) Args: folder_path (str): Path to folder containing images image_extensions (List[str]): List of image extensions to process Returns: List[Dict]: List of prediction results for each image """ if image_extensions is None: image_extensions = ['*.jpg', '*.jpeg', '*.png', '*.bmp', '*.tiff'] # Get all image files from folder image_files = [] for ext in image_extensions: pattern = os.path.join(folder_path, ext) image_files.extend(glob.glob(pattern, recursive=False)) # Also check uppercase extensions pattern = os.path.join(folder_path, ext.upper()) image_files.extend(glob.glob(pattern, recursive=False)) if not image_files: logger.warning(f"No image files found in {folder_path}") return [] logger.info(f"Found {len(image_files)} images in {folder_path}") # Process each image results = [] for image_path in image_files: try: filename = os.path.basename(image_path) logger.info(f"Processing image: {filename}") # Get prediction for single image prediction_result = self.predict_single_image_from_path(image_path) # Add filename and path to result result = { 'filename': filename, 'filepath': image_path, 'success': True, 'results': prediction_result } except Exception as e: logger.error(f"Failed to process {image_path}: {str(e)}") result = { 'filename': os.path.basename(image_path), 'filepath': image_path, 'success': False, 'error': str(e) } results.append(result) logger.info(f"Batch processing completed. Processed {len(results)} images.") return results def get_model_info(self) -> Dict: """ Get information about the loaded model Returns: Dict: Model information including classes, input shape, etc. """ return { 'is_loaded': self.is_loaded, 'model_path': self.model_path, 'class_names': self.class_names, 'num_classes': len(self.class_names), 'input_shape': self.input_shape } def validate_image_format(self, image_data: bytes) -> Tuple[bool, str]: """ Validate if the uploaded file is a valid image Args: image_data (bytes): Raw image data Returns: Tuple[bool, str]: (is_valid, error_message) """ try: image = Image.open(io.BytesIO(image_data)) # Check if it's a valid image format valid_formats = ['JPEG', 'PNG', 'BMP', 'TIFF', 'GIF'] if image.format not in valid_formats: return False, f"Unsupported image format: {image.format}" # Check image dimensions width, height = image.size if width < 32 or height < 32: return False, "Image too small. Minimum size is 32x32 pixels" if width > 4096 or height > 4096: return False, "Image too large. Maximum size is 4096x4096 pixels" return True, "Valid image" except Exception as e: return False, f"Invalid image file: {str(e)}" # Global pipeline instance pipeline = None def initialize_pipeline(model_path: str) -> bool: """ Initialize the global pipeline instance Args: model_path (str): Path to the model file Returns: bool: True if initialization successful """ global pipeline try: pipeline = WBCClassificationPipeline(model_path) success = pipeline.load_model() return success except Exception as e: logger.error(f"Failed to initialize pipeline: {str(e)}") return False def get_pipeline() -> Optional[WBCClassificationPipeline]: """ Get the global pipeline instance Returns: WBCClassificationPipeline: The pipeline instance or None if not initialized """ global pipeline return pipeline def load_configuration(): """ Load configuration settings for the pipeline You can modify this function to load from config files, environment variables, etc. Returns: Dict: Configuration dictionary """ config = { 'model_path': 'path/to/your/model.keras', # Update this path 'test_image_path': None, # Single image for testing 'test_folder_path': None, # Folder containing test images 'image_extensions': ['*.jpg', '*.jpeg', '*.png', '*.bmp', '*.tiff'], 'log_level': 'INFO' } # You can override these with environment variables or config files # Example: # config['model_path'] = os.getenv('WBC_MODEL_PATH', config['model_path']) return config def test_single_image(pipeline_instance: WBCClassificationPipeline, image_path: str): """ Test pipeline with a single image Args: pipeline_instance: Initialized pipeline image_path (str): Path to test image """ print(f"\n{'='*50}") print("TESTING SINGLE IMAGE") print(f"{'='*50}") try: result = pipeline_instance.predict_single_image_from_path(image_path) print(f"Image: {os.path.basename(image_path)}") print(f"Predicted Class: {result['predicted_class']}") print(f"Confidence: {result['confidence']:.4f}") print("\nAll Probabilities:") for class_name, prob in result['all_probabilities'].items(): print(f" {class_name}: {prob:.4f}") except Exception as e: print(f"Error testing single image: {str(e)}") def test_batch_images(pipeline_instance: WBCClassificationPipeline, folder_path: str): """ Test pipeline with batch of images from folder Args: pipeline_instance: Initialized pipeline folder_path (str): Path to folder containing test images """ print(f"\n{'='*50}") print("TESTING BATCH IMAGES") print(f"{'='*50}") try: results = pipeline_instance.predict_batch_from_folder(folder_path) print(f"Processed {len(results)} images from {folder_path}") print("\nResults Summary:") print("-" * 80) successful_predictions = 0 for result in results: if result['success']: successful_predictions += 1 pred_result = result['results'] print(f"{result['filename']:<30} | {pred_result['predicted_class']:<12} | {pred_result['confidence']:.4f}") else: print(f"{result['filename']:<30} | ERROR: {result['error']}") print(f"\nSuccess Rate: {successful_predictions}/{len(results)} ({100*successful_predictions/len(results):.1f}%)") except Exception as e: print(f"Error testing batch images: {str(e)}") def print_model_info(pipeline_instance: WBCClassificationPipeline): """ Print model information Args: pipeline_instance: Initialized pipeline """ print(f"\n{'='*50}") print("MODEL INFORMATION") print(f"{'='*50}") info = pipeline_instance.get_model_info() print(f"Model Loaded: {info['is_loaded']}") print(f"Model Path: {info['model_path']}") print(f"Input Shape: {info['input_shape']}") print(f"Number of Classes: {info['num_classes']}") print(f"Class Names: {', '.join(info['class_names'])}") def main(): """ Main function for testing the pipeline """ print("WBC Classification Pipeline - Testing Mode") print("=" * 60) # Load configuration config = load_configuration() # Update these paths according to your setup MODEL_PATH = "Models/99.73(accuracy).keras" # Update this path TEST_IMAGE_PATH = "/home/amit/White_blood_application/uploads" # Update this path TEST_FOLDER_PATH = "/home/amit/White_blood_application/uploads" # Update this path # You can override the paths from config if 'model_path' in config and config['model_path'] != 'path/to/your/model.keras': MODEL_PATH = config['model_path'] # Check if model file exists if not os.path.exists(MODEL_PATH): print(f"ERROR: Model file not found at {MODEL_PATH}") print("Please update the MODEL_PATH variable with the correct path to your model.") return # Initialize pipeline print(f"Initializing pipeline with model: {MODEL_PATH}") pipeline_instance = WBCClassificationPipeline(MODEL_PATH) # Load model if not pipeline_instance.load_model(): print("Failed to load model. Exiting.") return # Print model information print_model_info(pipeline_instance) # Test single image if path provided and exists if TEST_IMAGE_PATH and os.path.exists(TEST_IMAGE_PATH): test_single_image(pipeline_instance, TEST_IMAGE_PATH) else: print(f"\nSkipping single image test (file not found: {TEST_IMAGE_PATH})") # Test batch images if folder exists if TEST_FOLDER_PATH and os.path.exists(TEST_FOLDER_PATH): test_batch_images(pipeline_instance, TEST_FOLDER_PATH) else: print(f"\nSkipping batch test (folder not found: {TEST_FOLDER_PATH})") print(f"\n{'='*60}") print("Testing completed!") print("Update the file paths above to test with your actual data.") print(f"{'='*60}")