Created
December 15, 2024 08:27
-
-
Save naufalso/cc0746aa6794bed4aa073e98484e2337 to your computer and use it in GitHub Desktop.
Image Clarity Analysis
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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