Skip to content

Instantly share code, notes, and snippets.

@naufalso
Created December 15, 2024 08:27
Show Gist options
  • Save naufalso/cc0746aa6794bed4aa073e98484e2337 to your computer and use it in GitHub Desktop.
Save naufalso/cc0746aa6794bed4aa073e98484e2337 to your computer and use it in GitHub Desktop.
Image Clarity Analysis
import cv2
import numpy as np
import matplotlib.pyplot as plt
import argparse
# --------------------------
# Utility Functions
# --------------------------
def normalize(value, min_value, max_value):
"""Normalize a value to the range [0, 1]."""
return max(0.0, min(1.0, (value - min_value) / (max_value - min_value)))
# --------------------------
# Exposure Analysis Functions
# --------------------------
def spatial_exposure_analysis_adaptive(image, lower_percentile=5, upper_percentile=95):
"""
Analyze underexposed and overexposed areas with adaptive thresholds.
"""
# Calculate adaptive thresholds
lower_threshold = np.percentile(image, lower_percentile)
upper_threshold = np.percentile(image, upper_percentile)
# Binary masks for underexposed and overexposed regions
underexposed_mask = (image <= lower_threshold).astype(np.uint8) * 255
overexposed_mask = (image >= upper_threshold).astype(np.uint8) * 255
# Calculate area of underexposed and overexposed regions
num_under, _, stats_under, _ = cv2.connectedComponentsWithStats(underexposed_mask, connectivity=8)
underexposed_area = np.sum(stats_under[:, cv2.CC_STAT_AREA][1:]) / image.size
num_over, _, stats_over, _ = cv2.connectedComponentsWithStats(overexposed_mask, connectivity=8)
overexposed_area = np.sum(stats_over[:, cv2.CC_STAT_AREA][1:]) / image.size
return underexposed_area, overexposed_area, lower_threshold, upper_threshold
# --------------------------
# Clarity Analysis Function
# --------------------------
def analyze_image_clarity(image_path, weights):
"""
Analyze image clarity based on sharpness, contrast, and exposure metrics.
"""
# Load the image in grayscale
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if image is None:
return {"error": "Image not found or unsupported format"}
# Sharpness calculation (Laplacian variance)
laplacian = cv2.Laplacian(image, cv2.CV_64F)
sharpness = laplacian.var()
# Contrast calculation (standard deviation of pixel intensities)
contrast = np.std(image)
# Exposure analysis with adaptive thresholds
underexposed_area, overexposed_area, low_thresh, high_thresh = spatial_exposure_analysis_adaptive(image)
# Normalization ranges for metrics
ranges = {
"sharpness": (0, 10000),
"contrast": (0, 100),
"overexposure": (0, 0.5),
"underexposure": (0, 0.5),
}
# Normalize metrics
sharpness_norm = normalize(sharpness, *ranges["sharpness"])
contrast_norm = normalize(contrast, *ranges["contrast"])
overexposed_norm = 1.0 - normalize(overexposed_area, *ranges["overexposure"]) # Penalize larger areas
underexposed_norm = 1.0 - normalize(underexposed_area, *ranges["underexposure"]) # Penalize larger areas
# Clarity score with adjustable weights
clarity_score_norm = (
weights["sharpness"] * sharpness_norm +
weights["contrast"] * contrast_norm +
weights["overexposure"] * overexposed_norm +
weights["underexposure"] * underexposed_norm
)
# Return all metrics
return {
"sharpness": sharpness,
"sharpness_normalized": sharpness_norm,
"contrast": contrast,
"contrast_normalized": contrast_norm,
"overexposed_area": overexposed_area,
"underexposed_area": underexposed_area,
"lower_threshold": low_thresh,
"upper_threshold": high_thresh,
"final_clarity_score": clarity_score_norm,
}
# --------------------------
# Visualization Functions
# --------------------------
def visualize_exposure_masks(image_path):
"""
Visualize underexposed and overexposed areas in the image.
"""
# Load the image in grayscale
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
underexposed_area, overexposed_area, low_thresh, high_thresh = spatial_exposure_analysis_adaptive(image)
# Create masks
underexposed_mask = (image <= low_thresh).astype(np.uint8) * 255
overexposed_mask = (image >= high_thresh).astype(np.uint8) * 255
# Plot original image and masks
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
axes[0].imshow(image, cmap='gray')
axes[0].set_title('Original Image')
axes[0].axis('off')
axes[1].imshow(underexposed_mask, cmap='gray')
axes[1].set_title(f'Underexposed (≤ {low_thresh:.0f})')
axes[1].axis('off')
axes[2].imshow(overexposed_mask, cmap='gray')
axes[2].set_title(f'Overexposed (≥ {high_thresh:.0f})')
axes[2].axis('off')
plt.tight_layout()
plt.show()
# --------------------------
# Main Function with argparse
# --------------------------
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Analyze image clarity.")
parser.add_argument("image_path", type=str, help="Path to the image.")
parser.add_argument("--sharpness_weight", type=float, default=0.6, help="Weight for sharpness.")
parser.add_argument("--contrast_weight", type=float, default=0.2, help="Weight for contrast.")
parser.add_argument("--overexposure_weight", type=float, default=0.1, help="Weight for overexposure.")
parser.add_argument("--underexposure_weight", type=float, default=0.1, help="Weight for underexposure.")
args = parser.parse_args()
# Weights for clarity score
weights = {
"sharpness": args.sharpness_weight,
"contrast": args.contrast_weight,
"overexposure": args.overexposure_weight,
"underexposure": args.underexposure_weight,
}
# Analyze clarity
clarity_metrics = analyze_image_clarity(args.image_path, weights)
print("\nClarity Metrics:")
for key, value in clarity_metrics.items():
print(f"{key}: {value:.4f}" if isinstance(value, float) else f"{key}: {value}")
# Visualize exposure masks
visualize_exposure_masks(args.image_path)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment