def quantify_cell_cycle_phases_from_microscopy(image_paths, output_dir='./results'):
    '''
    Quantify the percentage of cells in each cell cycle phase using Calcofluor white stained microscopy images.
    
    This function processes microscopy images where cell walls/septa are stained with Calcofluor white,
    segments individual cells, extracts features, and classifies each cell into G1, S, or G2/M phase.
    
    Parameters:
    -----------
    image_paths : list of str
        List of file paths to microscopy images of cells stained with Calcofluor white
    output_dir : str, optional
        Directory to save results (default: './results')
        
    Returns:
    --------
    str
        Research log summarizing the analysis process and results
    '''
    import os
    import numpy as np
    import pandas as pd
    from skimage import io, filters, measure, segmentation, feature, morphology
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.preprocessing import StandardScaler
    
    # Create output directory if it doesn't exist
    os.makedirs(output_dir, exist_ok=True)
    
    # Initialize log
    log = "# Cell Cycle Phase Quantification Analysis Log\n\n"
    log += f"Processing {len(image_paths)} microscopy images with Calcofluor white staining.\n\n"
    
    # Initialize data structures to store results
    all_cells = []
    cell_features = []
    
    # Step 1: Process images and segment cells
    log += "## Image Processing and Cell Segmentation\n\n"
    
    for i, img_path in enumerate(image_paths):
        log += f"Processing image {i+1}/{len(image_paths)}: {os.path.basename(img_path)}\n"
        
        # Load image
        img = io.imread(img_path)
        
        # Convert to grayscale if needed
        if len(img.shape) > 2:
            img = np.mean(img, axis=2).astype(np.uint8)
        
        # Preprocessing
        filtered_img = filters.gaussian(img, sigma=1.0)
        
        # Thresholding to identify cell walls
        threshold = filters.threshold_otsu(filtered_img)
        binary = filtered_img > threshold
        
        # Clean up binary image
        binary = morphology.remove_small_objects(binary, min_size=30)
        binary = morphology.binary_closing(binary)
        
        # Segment cells
        labeled_cells = measure.label(binary)
        cell_props = measure.regionprops(labeled_cells, intensity_image=img)
        
        log += f"  - Found {len(cell_props)} cells\n"
        all_cells.extend(cell_props)
        
        # Extract features for each cell
        for cell in cell_props:
            # Basic morphological features
            area = cell.area
            perimeter = cell.perimeter
            eccentricity = cell.eccentricity
            mean_intensity = cell.mean_intensity
            
            # Cell shape features
            circularity = (4 * np.pi * area) / (perimeter ** 2) if perimeter > 0 else 0
            
            # Check for septation (indicative of S or G2/M phase)
            # Calcofluor white stains the septum more intensely
            has_septum = False
            if area > 50:  # Only check larger cells
                intensity_profile = cell.intensity_image[cell.intensity_image > 0]
                if intensity_profile.size > 0:
                    intensity_std = np.std(intensity_profile)
                    intensity_max = np.max(intensity_profile)
                    # High standard deviation and max intensity suggests septum presence
                    has_septum = (intensity_std > 0.2 * np.mean(intensity_profile) and 
                                 intensity_max > 1.5 * np.mean(intensity_profile))
            
            # Store features
            cell_features.append([
                area, perimeter, eccentricity, mean_intensity, 
                circularity, has_septum, i  # include image index
            ])
    
    log += f"\nTotal cells detected across all images: {len(all_cells)}\n\n"
    
    # Step 2: Feature engineering and classification
    log += "## Cell Cycle Phase Classification\n\n"
    
    # Convert to numpy array for easier processing
    X = np.array(cell_features)
    
    # Simple rule-based classification
    # This is a simplified approach - in practice, you would train a classifier on labeled data
    # G1: Smaller cells, no septum
    # S: Medium-sized cells, early septum formation
    # G2/M: Larger cells, clear septum, often elongated
    
    # Define classification rules based on features
    phases = []
    for i in range(X.shape[0]):
        area = X[i, 0]
        perimeter = X[i, 1]
        eccentricity = X[i, 2]
        has_septum = X[i, 5]
        
        if has_septum and area > np.median(X[:, 0]) * 1.2:
            # Larger cells with septum are likely in G2/M
            phases.append('G2/M')
        elif has_septum or (area > np.median(X[:, 0]) and eccentricity > 0.5):
            # Cells with septum or larger elongated cells are likely in S
            phases.append('S')
        else:
            # Smaller cells without septum are likely in G1
            phases.append('G1')
    
    # Calculate percentages
    unique_phases, counts = np.unique(phases, return_counts=True)
    percentages = (counts / len(phases)) * 100
    
    # Create results table
    results_df = pd.DataFrame({
        'Phase': unique_phases,
        'Count': counts,
        'Percentage': percentages
    })
    
    # Save results
    results_path = os.path.join(output_dir, 'cell_cycle_phases.csv')
    results_df.to_csv(results_path, index=False)
    
    # Log results
    log += "### Cell Cycle Phase Distribution\n\n"
    for phase, count, percentage in zip(unique_phases, counts, percentages):
        log += f"- {phase}: {count} cells ({percentage:.2f}%)\n"
    
    log += f"\nResults saved to: {results_path}\n"
    log += "\nNote: This analysis uses simplified morphological features for classification. "
    log += "For more accurate results, a supervised machine learning approach with labeled training data is recommended.\n"
    
    return log


def quantify_and_cluster_cell_motility(image_sequence_path, output_dir="./results", num_clusters=3):
    '''
    Quantify cell motility features from time-lapse microscopy images and cluster cells based on motility patterns.
    
    Parameters:
    -----------
    image_sequence_path : str
        Path to directory containing time-lapse microscopy images in sequential order
    output_dir : str
        Directory to save output files (default: "./results")
    num_clusters : int
        Number of motility pattern clusters to identify (default: 3)
        
    Returns:
    --------
    str
        Research log summarizing the analysis process and results
    '''
    import os
    import cv2
    import numpy as np
    import pandas as pd
    from sklearn.cluster import KMeans
    from scipy.spatial.distance import pdist, squareform
    from pathlib import Path
    
    # Create output directory if it doesn't exist
    os.makedirs(output_dir, exist_ok=True)
    
    # Step 1: Load image sequence
    image_files = sorted([f for f in os.listdir(image_sequence_path) 
                         if f.endswith(('.tif', '.tiff', '.png', '.jpg', '.jpeg'))])
    
    if len(image_files) < 2:
        return "Error: Insufficient images found. At least 2 time points are required."
    
    # Step 2: Initialize cell tracking
    first_image = cv2.imread(os.path.join(image_sequence_path, image_files[0]), cv2.IMREAD_GRAYSCALE)
    
    # Simple cell detection using thresholding and contour detection
    _, binary = cv2.threshold(first_image, 127, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    # Extract cell centroids from first frame
    cells = []
    for i, contour in enumerate(contours):
        if cv2.contourArea(contour) > 50:  # Filter small noise
            M = cv2.moments(contour)
            if M["m00"] != 0:
                cx = int(M["m10"] / M["m00"])
                cy = int(M["m01"] / M["m00"])
                cells.append({
                    'id': i,
                    'positions': [(cx, cy)],
                    'frame_indices': [0]
                })
    
    # Step 3: Track cells across frames
    for frame_idx, img_file in enumerate(image_files[1:], 1):
        img = cv2.imread(os.path.join(image_sequence_path, img_file), cv2.IMREAD_GRAYSCALE)
        _, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
        contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        
        # Get current frame centroids
        current_centroids = []
        for contour in contours:
            if cv2.contourArea(contour) > 50:
                M = cv2.moments(contour)
                if M["m00"] != 0:
                    cx = int(M["m10"] / M["m00"])
                    cy = int(M["m01"] / M["m00"])
                    current_centroids.append((cx, cy))
        
        # Match cells with nearest centroids in current frame
        for cell in cells:
            if len(cell['positions']) == frame_idx:  # Only track cells found in previous frame
                prev_pos = cell['positions'][-1]
                
                if current_centroids:
                    # Calculate distances to all current centroids
                    distances = [np.sqrt((prev_pos[0] - c[0])**2 + (prev_pos[1] - c[1])**2) for c in current_centroids]
                    min_idx = np.argmin(distances)
                    
                    # Only match if distance is below threshold (prevents incorrect matching)
                    if distances[min_idx] < 50:  # Threshold distance in pixels
                        cell['positions'].append(current_centroids[min_idx])
                        cell['frame_indices'].append(frame_idx)
                        current_centroids.pop(min_idx)  # Remove matched centroid
    
    # Step 4: Calculate motility features for each cell
    cell_features = []
    
    for cell in cells:
        # Only analyze cells tracked for at least 3 frames
        if len(cell['positions']) < 3:
            continue
            
        # Calculate displacements between consecutive positions
        displacements = []
        for i in range(1, len(cell['positions'])):
            prev_pos = cell['positions'][i-1]
            curr_pos = cell['positions'][i]
            displacement = np.sqrt((curr_pos[0] - prev_pos[0])**2 + (curr_pos[1] - prev_pos[1])**2)
            displacements.append(displacement)
        
        # Calculate speed (pixels per frame)
        avg_speed = np.mean(displacements)
        
        # Calculate directionality (displacement from start to end / total path length)
        start_pos = cell['positions'][0]
        end_pos = cell['positions'][-1]
        net_displacement = np.sqrt((end_pos[0] - start_pos[0])**2 + (end_pos[1] - start_pos[1])**2)
        total_path_length = np.sum(displacements)
        directionality = net_displacement / total_path_length if total_path_length > 0 else 0
        
        # Calculate mean squared displacement
        msd = np.mean([np.sqrt((cell['positions'][i][0] - start_pos[0])**2 + 
                              (cell['positions'][i][1] - start_pos[1])**2) 
                      for i in range(1, len(cell['positions']))])
        
        # Calculate track duration
        duration = cell['frame_indices'][-1] - cell['frame_indices'][0]
        
        cell_features.append({
            'cell_id': cell['id'],
            'avg_speed': avg_speed,
            'directionality': directionality,
            'msd': msd,
            'track_duration': duration,
            'track_length': len(cell['positions'])
        })
    
    # Step 5: Cluster cells based on motility features
    if len(cell_features) < num_clusters:
        return f"Error: Not enough cells ({len(cell_features)}) to form {num_clusters} clusters. Try reducing num_clusters."
    
    # Create feature matrix for clustering
    feature_df = pd.DataFrame(cell_features)
    feature_matrix = feature_df[['avg_speed', 'directionality', 'msd']].values
    
    # Normalize features
    feature_matrix = (feature_matrix - np.mean(feature_matrix, axis=0)) / np.std(feature_matrix, axis=0)
    
    # Perform k-means clustering
    kmeans = KMeans(n_clusters=num_clusters, random_state=42)
    clusters = kmeans.fit_predict(feature_matrix)
    
    # Add cluster assignments to dataframe
    feature_df['cluster'] = clusters
    
    # Save results to CSV
    results_file = os.path.join(output_dir, "cell_motility_features.csv")
    feature_df.to_csv(results_file, index=False)
    
    # Calculate cluster statistics
    cluster_stats = feature_df.groupby('cluster').agg({
        'avg_speed': ['mean', 'std'],
        'directionality': ['mean', 'std'],
        'msd': ['mean', 'std'],
        'cell_id': 'count'
    })
    
    # Save cluster statistics
    stats_file = os.path.join(output_dir, "cluster_statistics.csv")
    cluster_stats.to_csv(stats_file)
    
    # Create research log
    log = f"""
Cell Motility Quantification and Clustering Analysis
===================================================

Analysis Steps:
1. Loaded {len(image_files)} time-lapse microscopy images
2. Detected and tracked {len(cells)} initial cells across time frames
3. Calculated motility features for {len(cell_features)} cells with sufficient tracking data
4. Performed k-means clustering to identify {num_clusters} distinct motility patterns

Results Summary:
- Detected {len(cells)} cells in the first frame
- Successfully tracked {len(cell_features)} cells across multiple frames
- Clustered cells into {num_clusters} motility pattern groups

Cluster Statistics:
{cluster_stats.to_string()}

Output Files:
- Cell motility features saved to: {results_file}
- Cluster statistics saved to: {stats_file}

Analysis Notes:
- Features used for clustering: average speed, directionality, and mean squared displacement
- Cells were required to be tracked for at least 3 frames to be included in the analysis
"""
    
    # Save research log
    log_file = os.path.join(output_dir, "research_log.txt")
    with open(log_file, 'w') as f:
        f.write(log)
    
    return log

def perform_facs_cell_sorting(cell_suspension_data, fluorescence_parameter, 
                            threshold_min=None, threshold_max=None, 
                            output_file="sorted_cells.csv"):
    """
    Performs Fluorescence-Activated Cell Sorting (FACS) to enrich cell populations based on fluorescence characteristics.
    
    Parameters:
    -----------
    cell_suspension_data : str
        Path to the FCS file containing flow cytometry data
    fluorescence_parameter : str
        The fluorescence parameter to use for sorting (e.g., 'GFP', 'FITC', 'PE')
    threshold_min : float, optional
        Minimum threshold for the fluorescence parameter. Cells below this value will be excluded
    threshold_max : float, optional
        Maximum threshold for the fluorescence parameter. Cells above this value will be excluded
    output_file : str, optional
        Filename to save the sorted cell population data
        
    Returns:
    --------
    str
        Research log detailing the FACS cell sorting process
    """
    import numpy as np
    import pandas as pd
    import os
    
    # Initialize research log
    log = "# FACS-based Cell Sorting and Enrichment Research Log\n\n"
    
    try:
        # Load cell suspension data
        log += "## Loading Cell Suspension Data\n"
        if isinstance(cell_suspension_data, str):
            # If data is provided as a file path
            if cell_suspension_data.lower().endswith('.fcs'):
                try:
                    import flowkit as fk
                    fcs_data = fk.Sample(cell_suspension_data)
                    cell_df = pd.DataFrame(fcs_data.get_dataframe())
                    log += f"Successfully loaded FCS file containing {len(cell_df)} cells\n"
                except ImportError:
                    # Fallback if flowkit is not available
                    log += "FlowKit not available, attempting to load as CSV\n"
                    cell_df = pd.read_csv(cell_suspension_data)
            else:
                # Assume CSV or other tabular format
                cell_df = pd.read_csv(cell_suspension_data)
                log += f"Loaded data file containing {len(cell_df)} cells\n"
        else:
            # Assume data is already a DataFrame
            cell_df = cell_suspension_data.copy()
            log += f"Using provided DataFrame containing {len(cell_df)} cells\n"
        
        # Validate fluorescence parameter exists in data
        if fluorescence_parameter not in cell_df.columns:
            raise ValueError(f"Fluorescence parameter '{fluorescence_parameter}' not found in data")
        
        # Apply gating strategy based on fluorescence thresholds
        log += f"\n## Applying Gating Strategy for {fluorescence_parameter}\n"
        original_count = len(cell_df)
        
        # Filter cells based on min/max thresholds
        if threshold_min is not None:
            cell_df = cell_df[cell_df[fluorescence_parameter] >= threshold_min]
            log += f"Applied minimum threshold of {threshold_min}: {len(cell_df)} cells remaining\n"
        
        if threshold_max is not None:
            cell_df = cell_df[cell_df[fluorescence_parameter] <= threshold_max]
            log += f"Applied maximum threshold of {threshold_max}: {len(cell_df)} cells remaining\n"
        
        # Calculate enrichment statistics
        enrichment_percent = (len(cell_df) / original_count) * 100 if original_count > 0 else 0
        log += f"\n## Enrichment Results\n"
        log += f"Original population: {original_count} cells\n"
        log += f"Sorted population: {len(cell_df)} cells\n"
        log += f"Enrichment percentage: {enrichment_percent:.2f}%\n"
        
        # Calculate statistics of sorted population
        mean_fluorescence = cell_df[fluorescence_parameter].mean()
        median_fluorescence = cell_df[fluorescence_parameter].median()
        
        log += f"Mean {fluorescence_parameter}: {mean_fluorescence:.2f}\n"
        log += f"Median {fluorescence_parameter}: {median_fluorescence:.2f}\n"
        
        # Save sorted population to file
        cell_df.to_csv(output_file, index=False)
        log += f"\n## Output\n"
        log += f"Sorted cell population saved to: {os.path.abspath(output_file)}\n"
        
        return log
        
    except Exception as e:
        log += f"\n## Error\n"
        log += f"An error occurred during cell sorting: {str(e)}\n"
        return log
    

def analyze_flow_cytometry_immunophenotyping(fcs_file_path, gating_strategy, compensation_matrix=None, output_dir="./results"):
    """
    Analyze flow cytometry data to identify and quantify specific cell populations based on surface markers.
    
    Parameters:
    -----------
    fcs_file_path : str
        Path to the FCS file containing flow cytometry data
    gating_strategy : dict
        Dictionary defining the gating strategy. Each key is a population name, and each value is a list of tuples 
        (marker, operator, threshold). For example: {'HSCs': [('Lin', '<', 100), ('Sca1', '>', 1000), ...]}
    compensation_matrix : numpy.ndarray, optional
        Spillover/compensation matrix to correct for fluorescence overlap
    output_dir : str, optional
        Directory to save the results
        
    Returns:
    --------
    str
        Research log summarizing the analysis steps and results
    """
    import os
    import numpy as np
    from FlowCytometryTools import FCMeasurement
    import pandas as pd
    
    # Create output directory if it doesn't exist
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    
    # Load the FCS file
    sample = FCMeasurement(ID='Sample', datafile=fcs_file_path)
    
    # Apply compensation if provided
    if compensation_matrix is not None:
        sample = sample.compensate(compensation_matrix)
    
    # Initialize log
    log = f"Flow Cytometry Analysis Log\n"
    log += f"==========================\n"
    log += f"File analyzed: {os.path.basename(fcs_file_path)}\n"
    log += f"Total events: {len(sample)}\n\n"
    
    # Create a dictionary to store population counts
    populations = {}
    
    # Apply gating strategy to identify cell populations
    for pop_name, gates in gating_strategy.items():
        # Start with all events
        current_population = sample.copy()
        
        log += f"Identifying {pop_name} population:\n"
        
        # Apply each gate sequentially
        for marker, operator, threshold in gates:
            initial_count = len(current_population)
            
            if operator == '>':
                current_population = current_population.gate(f'{marker} > {threshold}')
            elif operator == '<':
                current_population = current_population.gate(f'{marker} < {threshold}')
            elif operator == 'between':
                # For 'between', threshold should be a tuple (lower, upper)
                lower, upper = threshold
                current_population = current_population.gate(f'{marker} > {lower} and {marker} < {upper}')
            
            final_count = len(current_population)
            log += f"  Gate {marker} {operator} {threshold}: {initial_count} → {final_count} events\n"
        
        # Store the final population
        populations[pop_name] = current_population
        
        # Calculate percentage of original sample
        percentage = (len(current_population) / len(sample)) * 100
        log += f"  Final {pop_name} count: {len(current_population)} events ({percentage:.2f}% of total)\n\n"
    
    # Create a summary dataframe
    summary_data = {
        'Population': [],
        'Count': [],
        'Percentage': []
    }
    
    for pop_name, population in populations.items():
        summary_data['Population'].append(pop_name)
        summary_data['Count'].append(len(population))
        summary_data['Percentage'].append((len(population) / len(sample)) * 100)
    
    summary_df = pd.DataFrame(summary_data)
    
    # Save the summary to a CSV file
    summary_file = os.path.join(output_dir, "population_summary.csv")
    summary_df.to_csv(summary_file, index=False)
    
    log += f"Summary of identified populations:\n"
    for _, row in summary_df.iterrows():
        log += f"  {row['Population']}: {row['Count']} events ({row['Percentage']:.2f}%)\n"
    
    log += f"\nDetailed results saved to: {summary_file}\n"
    
    return log

def analyze_mitochondrial_morphology_and_potential(morphology_image_path, potential_image_path, output_dir="./output"):
    '''
    Quantifies metrics of mitochondrial morphology and membrane potential from fluorescence microscopy images.
    
    Parameters:
    -----------
    morphology_image_path : str
        Path to the fluorescence microscopy image showing mitochondrial morphology (e.g., MTS-GFP)
    potential_image_path : str
        Path to the fluorescence microscopy image showing mitochondrial membrane potential (e.g., TMRE staining)
    output_dir : str, optional
        Directory to save output files, default is "./output"
    
    Returns:
    --------
    str
        A research log summarizing the analysis steps and results
    '''
    import os
    import numpy as np
    import cv2
    from skimage import io, filters, morphology, measure, util
    from scipy import ndimage
    import datetime
    
    # Create output directory if it doesn't exist
    os.makedirs(output_dir, exist_ok=True)
    
    log = []
    log.append(f"Mitochondrial Analysis - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    log.append("=" * 50)
    
    # Load images
    try:
        morph_img = io.imread(morphology_image_path)
        pot_img = io.imread(potential_image_path)
        
        log.append(f"Successfully loaded morphology image: {morphology_image_path}")
        log.append(f"Successfully loaded potential image: {potential_image_path}")
        log.append(f"Morphology image shape: {morph_img.shape}")
        log.append(f"Potential image shape: {pot_img.shape}")
    except Exception as e:
        return f"Error loading images: {str(e)}"
    
    # Convert to grayscale if needed
    if len(morph_img.shape) > 2:
        morph_img = cv2.cvtColor(morph_img, cv2.COLOR_RGB2GRAY)
        log.append("Converted morphology image to grayscale")
    if len(pot_img.shape) > 2:
        pot_img = cv2.cvtColor(pot_img, cv2.COLOR_RGB2GRAY)
        log.append("Converted potential image to grayscale")
    
    # Normalize images
    morph_img = util.img_as_float(morph_img)
    pot_img = util.img_as_float(pot_img)
    
    log.append("\nMorphology Analysis:")
    log.append("-" * 30)
    
    # Denoise morphology image
    morph_img_denoised = filters.gaussian(morph_img, sigma=1.0)
    
    # Threshold to segment mitochondria
    threshold_value = filters.threshold_otsu(morph_img_denoised)
    binary_img = morph_img_denoised > threshold_value
    
    # Remove small objects
    binary_img = morphology.remove_small_objects(binary_img, min_size=20)
    
    # Save binary image
    binary_path = os.path.join(output_dir, "binary_mitochondria.png")
    io.imsave(binary_path, util.img_as_ubyte(binary_img))
    log.append(f"Binary segmentation saved to: {binary_path}")
    
    # Skeletonize for network analysis
    skeleton = morphology.skeletonize(binary_img)
    skeleton_path = os.path.join(output_dir, "skeleton.png")
    io.imsave(skeleton_path, util.img_as_ubyte(skeleton))
    log.append(f"Skeleton image saved to: {skeleton_path}")
    
    # Calculate morphology metrics
    # 1. Count branches and junctions
    # Label the skeleton
    labeled_skeleton, num_branches = ndimage.label(skeleton)
    
    # Find branch points (pixels with more than 2 neighbors)
    kernel = np.ones((3, 3), dtype=np.uint8)
    kernel[1, 1] = 0  # Don't count the center pixel
    neighbor_count = ndimage.convolve(skeleton.astype(np.uint8), kernel)
    junction_points = (neighbor_count > 2) & skeleton
    num_junctions = np.sum(junction_points)
    
    # 2. Calculate fragmentation metrics
    labeled_objects, num_objects = ndimage.label(binary_img)
    object_sizes = ndimage.sum(binary_img, labeled_objects, range(1, num_objects + 1))
    mean_size = np.mean(object_sizes) if len(object_sizes) > 0 else 0
    
    # Calculate network connectivity (ratio of junctions to branches)
    connectivity = num_junctions / num_branches if num_branches > 0 else 0
    
    # Calculate fragmentation index (inverse of mean object size, normalized)
    fragmentation = 1 / (mean_size / np.max(object_sizes)) if mean_size > 0 and np.max(object_sizes) > 0 else 0
    
    log.append(f"Number of mitochondrial fragments: {num_objects}")
    log.append(f"Number of branches: {num_branches}")
    log.append(f"Number of junction points: {num_junctions}")
    log.append(f"Network connectivity index: {connectivity:.4f}")
    log.append(f"Fragmentation index: {fragmentation:.4f}")
    log.append(f"Mean fragment size: {mean_size:.2f} pixels")
    
    # Membrane potential analysis
    log.append("\nMembrane Potential Analysis:")
    log.append("-" * 30)
    
    # Create mask from morphology image to analyze only mitochondrial regions
    mito_mask = binary_img
    
    # Calculate potential metrics within mitochondrial regions
    pot_intensity_raw = np.mean(pot_img)
    pot_intensity_in_mito = np.mean(pot_img[mito_mask]) if np.sum(mito_mask) > 0 else 0
    
    # Calculate potential heterogeneity (standard deviation of intensity)
    pot_heterogeneity = np.std(pot_img[mito_mask]) if np.sum(mito_mask) > 0 else 0
    
    # Calculate potential distribution
    if np.sum(mito_mask) > 0:
        percentiles = [10, 25, 50, 75, 90]
        pot_percentiles = np.percentile(pot_img[mito_mask], percentiles)
        percentile_str = ", ".join([f"{p}th: {v:.4f}" for p, v in zip(percentiles, pot_percentiles)])
    else:
        percentile_str = "No mitochondrial regions detected"
    
    log.append(f"Overall mean TMRE intensity: {pot_intensity_raw:.4f}")
    log.append(f"Mean TMRE intensity in mitochondria: {pot_intensity_in_mito:.4f}")
    log.append(f"TMRE intensity heterogeneity (std): {pot_heterogeneity:.4f}")
    log.append(f"TMRE intensity percentiles: {percentile_str}")
    
    # Create a combined results image
    # Overlay potential intensity on morphology
    overlay = np.zeros((morph_img.shape[0], morph_img.shape[1], 3), dtype=np.float32)
    overlay[..., 0] = morph_img  # Red channel - morphology
    overlay[..., 2] = pot_img    # Blue channel - potential
    overlay = np.clip(overlay, 0, 1)
    
    overlay_path = os.path.join(output_dir, "morphology_potential_overlay.png")
    io.imsave(overlay_path, util.img_as_ubyte(overlay))
    log.append(f"Overlay image saved to: {overlay_path}")
    
    # Save quantitative results to CSV
    import csv
    results_path = os.path.join(output_dir, "mitochondrial_analysis_results.csv")
    with open(results_path, 'w', newline='') as csvfile:
        writer = csv.writer(csvfile)
        writer.writerow(['Metric', 'Value'])
        writer.writerow(['Number of fragments', num_objects])
        writer.writerow(['Number of branches', num_branches])
        writer.writerow(['Number of junctions', num_junctions])
        writer.writerow(['Network connectivity', connectivity])
        writer.writerow(['Fragmentation index', fragmentation])
        writer.writerow(['Mean fragment size', mean_size])
        writer.writerow(['Mean TMRE intensity', pot_intensity_in_mito])
        writer.writerow(['TMRE heterogeneity', pot_heterogeneity])
    
    log.append(f"Quantitative results saved to: {results_path}")
    log.append("\nAnalysis complete.")
    
    return "\n".join(log)