[Python] How to optimize the weight for TV filter?

Discussão em 'Python' iniciado por Stack, Janeiro 17, 2025.

  1. Stack

    Stack Membro Participativo

    I have 2d data which has background noise and assembled high values. I'm trying to apply the TV filter to denoise the data. Is there a suitable method to avoid over-denoising the data?

    I have tried to check the MSE value like this:

    import numpy as np
    from skimage.restoration import denoise_tv_chambolle
    import matplotlib.pyplot as plt
    from sklearn.metrics import mean_squared_error
    from scipy import stats

    def create_mask_from_quantiles(image, lower_quantile=0.1, upper_quantile=0.9):
    """
    Create a mask based on quantile values to exclude extreme values.

    Parameters:
    -----------
    image : 2D numpy array
    Input image
    lower_quantile : float
    Lower quantile threshold (0-1)
    upper_quantile : float
    Upper quantile threshold (0-1)

    Returns:
    --------
    mask : 2D boolean array
    True for values within quantile range
    """
    lower_thresh = np.quantile(image, lower_quantile)
    upper_thresh = np.quantile(image, upper_quantile)
    return (image >= lower_thresh) & (image <= upper_thresh)

    def tune_tv_chambolle(noisy_image, weight_range=None, n_weights=10,
    lower_quantile=0.1, upper_quantile=0.9, multiplier=2, plot=True):
    """
    Find suitable weight parameter for TV Chambolle denoising with quantile masking.

    Parameters:
    -----------
    noisy_image : 2D numpy array
    Input image with noise
    weight_range : tuple, optional
    Range of weights to test (min, max)
    n_weights : int
    Number of weights to test
    lower_quantile : float
    Lower quantile for masking
    upper_quantile : float
    Upper quantile for masking
    multiplier: float
    STD*multiplier for weight_max
    plot : bool
    Whether to plot the results
    """
    # Create mask for background noise evaluation
    mask = create_mask_from_quantiles(noisy_image, lower_quantile, upper_quantile)

    if weight_range is None:
    # Estimate initial range based on masked image statistics
    masked_image = noisy_image[mask]
    noise_std = np.std(masked_image)
    weight_range = (noise_std/10, noise_std*multiplier)
    print('weight_range: ', weight_range)

    weights = np.linspace(weight_range[0], weight_range[1], n_weights)
    denoised_images = []
    metrics = {
    'total_variation': [],
    'mse_with_original': [],
    'entropy': [],
    'masked_mse': []
    }

    # Calculate baseline metrics
    baseline_tv = np.sum(np.abs(np.diff(noisy_image, axis=0))) + \
    np.sum(np.abs(np.diff(noisy_image, axis=1)))

    # Test different weights
    for weight in weights:
    denoised = denoise_tv_chambolle(noisy_image, weight=weight,
    channel_axis=None)
    denoised_images.append(denoised)

    # Calculate metrics for masked region
    tv = np.sum(np.abs(np.diff(denoised, axis=0))) + \
    np.sum(np.abs(np.diff(denoised, axis=1)))
    metrics['total_variation'].append(tv)
    metrics['mse_with_original'].append(
    mean_squared_error(noisy_image, denoised))
    metrics['masked_mse'].append(
    mean_squared_error(noisy_image[mask], denoised[mask]))
    metrics['entropy'].append(stats.entropy(denoised[mask].flatten()))

    # Find optimal weight using the elbow method on masked MSE
    mse_differences = np.diff(metrics['masked_mse'])
    elbow_idx = np.argmax(np.abs(np.diff(mse_differences))) + 1
    optimal_weight = weights[elbow_idx]

    if plot:
    fig, axes = plt.subplots(2, 3, figsize=(18, 12))

    # Original image
    im0 = axes[0,0].imshow(noisy_image, cmap='viridis')
    axes[0,0].set_title('Original Noisy Image')
    plt.colorbar(im0, ax=axes[0,0])

    # Mask visualization
    im1 = axes[0,1].imshow(mask, cmap='gray_r')
    axes[0,1].set_title('Quantile Mask')
    plt.colorbar(im1, ax=axes[0,1])

    # Denoised image
    optimal_denoised = denoised_images[elbow_idx]
    im2 = axes[0,2].imshow(optimal_denoised, cmap='viridis')
    axes[0,2].set_title(f'Denoised (weight={optimal_weight:.4f})')
    plt.colorbar(im2, ax=axes[0,2])

    # Metrics plots
    axes[1,0].plot(weights, metrics['total_variation'], 'b-',
    label='Total Variation')
    axes[1,0].axvline(x=optimal_weight, color='r', linestyle='--',
    label='Optimal Weight')
    axes[1,0].set_xlabel('Weight')
    axes[1,0].set_ylabel('Total Variation')
    axes[1,0].legend()

    axes[1,1].plot(weights, metrics['masked_mse'], 'g-',
    label='Masked MSE')
    axes[1,1].set_xlabel('Weight')
    axes[1,1].set_ylabel('MSE (masked region)')
    axes[1,1].legend()

    axes[1,2].plot(weights, metrics['entropy'], 'm-',
    label='Entropy (masked region)')
    axes[1,2].set_xlabel('Weight')
    axes[1,2].set_ylabel('Entropy')
    axes[1,2].legend()

    plt.suptitle(f'multiplier={multiplier}')
    plt.tight_layout()
    plt.show()

    return optimal_weight, {
    'weights': weights,
    'metrics': metrics,
    'denoised_images': denoised_images,
    'mask': mask
    }

    def generate_test_data(size=100, noise_level=0.1, wind_direction=45, wind_speed=1.0):
    """
    Generate synthetic test data with a realistic gas plume

    Parameters:
    -----------
    size : int
    Size of the output image (size x size)
    noise_level : float
    Standard deviation of the background noise
    wind_direction : float
    Wind direction in degrees (0 is East, 90 is North)
    wind_speed : float
    Relative wind speed affecting plume spread

    Returns:
    --------
    test_image : 2D numpy array
    Normalized image containing background noise and plume
    """
    # Create background noise
    background = np.random.normal(0, noise_level, (size, size))

    # Create coordinate grid
    x, y = np.meshgrid(np.linspace(-2, 2, size), np.linspace(-2, 2, size))

    # Convert wind direction to radians and calculate rotated coordinates
    theta = np.radians(wind_direction)
    x_rot = x * np.cos(theta) + y * np.sin(theta)
    y_rot = -x * np.sin(theta) + y * np.cos(theta)

    # Create elongated plume shape
    # Higher wind_speed creates more elongated plume
    sigma_x = 0.8 * wind_speed # Spread in wind direction
    sigma_y = 0.2 # Spread perpendicular to wind direction

    # Add turbulent diffusion effect
    turbulence = np.random.normal(0, 0.1, (size, size))

    # Calculate plume concentration with gaussian dispersion model
    plume = np.exp(-(x_rot**2/(2*sigma_x**2) + y_rot**2/(2*sigma_y**2)))

    # Add some random variations to make it more realistic
    plume = plume * (1 + 0.2 * turbulence)

    # Add source point with higher concentration
    source_x = size // 4
    source_y = size // 2
    plume[source_y-2:source_y+2, source_x-2:source_x+2] = 1.0

    # Combine background and plume
    test_image = background + plume

    # Add some patchiness to the plume
    patchiness = np.random.normal(0, 0.05, (size, size))
    test_image = test_image * (1 + patchiness * (plume > 0.1))

    # Normalize to 0-1 range
    test_image = (test_image - test_image.min()) / (test_image.max() - test_image.min())

    return test_image


    # Generate test data with different wind conditions
    test_image = generate_test_data(
    size=100,
    noise_level=0.1,
    wind_direction=45, # 45 degree wind direction
    wind_speed=1.5 # Moderate wind speed
    )


    multiplier = 2

    # Find optimal weight and denoise
    optimal_weight, results = tune_tv_chambolle(
    test_image,
    lower_quantile=0.2,
    upper_quantile=0.8,
    n_weights=15,
    multiplier=multiplier,
    )

    # Apply final denoising with optimal weight
    final_denoised = denoise_tv_chambolle(test_image, weight=optimal_weight,
    channel_axis=None,
    )

    # Compare original vs denoised for masked region
    mask = results['mask']
    original_std = np.std(test_image[mask])
    denoised_std = np.std(final_denoised[mask])
    noise_reduction = (1 - denoised_std/original_std) * 100

    print(f"Optimal weight: {optimal_weight:.4f}")
    print(f"Noise reduction in masked region: {noise_reduction:.1f}%")


    Here're some results using different weight_max of TV filter

    weight_max = noise_std*2


    Optimal weight: 0.0572 Noise reduction in masked region: 20.8%

    [​IMG]

    weight_max = noise_std*3


    Optimal weight: 0.0259 Noise reduction in masked region: 24.9%

    [​IMG]

    weight_max = noise_std*4


    Optimal weight: 0.0436 Noise reduction in masked region: 25.2%

    [​IMG]

    Question


    It seems noise_std*3 gets the wrong optimal weight according to the image. How to set a correct weight range to get the optimalized TV filter?

    Continue reading...

Compartilhe esta Página