skydetection / app.py
hannahnguyen's picture
Update app.py
62e6699 verified
#Hannah Nguyen
#CS5330 - Lab 1: Sky Pixel Identification
import gradio as gr
import cv2
import numpy as np
# Function to calculate the threshold values for sky detection
# based on the top region of the image, which is assumed to contain the sky.
def calculate_thresholds_from_top(hsv_image, top_fraction=0.25):
# Define the top region of the image for sampling sky pixels
top_region = hsv_image[:int(hsv_image.shape[0] * top_fraction), :, :]
# Calculate the average hue, saturation, and value in the top region
# to determine the characteristic colors of the sky in the image.
avg_hue = np.mean(top_region[:, :, 0])
avg_sat = np.mean(top_region[:, :, 1])
avg_val = np.mean(top_region[:, :, 2])
# Initialize the limits to cover a broad range of sky colors.
# Adjust the ranges based on the averages calculated above.
lower_limit = np.array([0, 0, min(180, avg_val - 30)])
upper_limit = np.array([180, max(255, avg_sat + 30), 255])
# Predefined hue ranges for typical sky colors.
blue_hue_range = (100, 140)
orange_hue_range = (10, 50)
# Adjust the hue range based on whether the average hue corresponds to
# typical blue skies or orange sunset/sunrise skies.
if avg_hue > blue_hue_range[0] and avg_hue < blue_hue_range[1]:
lower_limit[0] = blue_hue_range[0]
upper_limit[0] = blue_hue_range[1]
elif avg_hue > orange_hue_range[0] and avg_hue < orange_hue_range[1]:
lower_limit[0] = orange_hue_range[0]
upper_limit[0] = orange_hue_range[1]
# Extend the saturation limits to include clouds, which have high brightness
# and low saturation.
lower_limit[1] = 0 # Include low saturation values for cloud detection.
upper_limit[1] = 255 # High saturation for clear skies or sunsets
# Extend the upper value limit to ensure we capture the brightness of the sky and clouds.
upper_limit[2] = 255
return lower_limit, upper_limit
# Function to validate the sky detection mask and adjust thresholds if necessary.
# This ensures that the amount of sky detected is within reasonable bounds.
def validate_thresholds(mask, hsv_image, initial_lower_limit, initial_upper_limit):
height, width = mask.shape
total_pixels = height * width
sky_pixels = cv2.countNonZero(mask)
# Calculate the percentage of the image identified as sky.
sky_percentage = sky_pixels / total_pixels
# Define target bounds for the percentage of sky in the image.
target_min_sky_percentage = 0.2
target_max_sky_percentage = 0.95
# If the sky percentage is within the target bounds, return the current mask.
if target_min_sky_percentage <= sky_percentage <= target_max_sky_percentage:
return mask
# If the sky percentage is outside the target bounds, adjust thresholds (1e-6 to handle edge case and avoid zero devision).
adjustment_ratio = target_min_sky_percentage / (sky_percentage + 1e-6) if sky_percentage < target_min_sky_percentage else target_max_sky_percentage / (sky_percentage + 1e-6)
# Adjust the hue and saturation ranges based on the deviation from the target sky percentage.
hue_adjustment = (initial_upper_limit[0] - initial_lower_limit[0]) * (adjustment_ratio - 1)
sat_adjustment = (initial_upper_limit[1] - initial_lower_limit[1]) * (adjustment_ratio - 1)
# Apply the adjusted limits to create a new mask.
new_lower_limit = np.array([
max(0, initial_lower_limit[0] - hue_adjustment),
max(0, initial_lower_limit[1] - sat_adjustment),
initial_lower_limit[2]
])
new_upper_limit = np.array([
min(180, initial_upper_limit[0] + hue_adjustment),
min(255, initial_upper_limit[1] + sat_adjustment),
initial_upper_limit[2]
])
# Ensure new limits are type uint8 for mask creation.
new_lower_limit = np.array(new_lower_limit, dtype=np.uint8)
new_upper_limit = np.array(new_upper_limit, dtype=np.uint8)
adjusted_mask = cv2.inRange(hsv_image, new_lower_limit, new_upper_limit)
return adjusted_mask
# Function to find the most probable horizon line using the Hough Line Transform.
# It filters out significantly vertical lines and assumes the horizon is the highest
def find_horizon_line(edges):
lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=50, minLineLength=80, maxLineGap=10)
if lines is not None:
lines = [l[0] for l in lines if abs(l[0][0] - l[0][2]) > abs(l[0][1] - l[0][3])]
# Sort lines by their midpoint y-coordinate
lines.sort(key=lambda x: (x[1]+x[3])/2)
# The horizon line is the one with the smallest y-coordinate midpoint
horizon_line = lines[0]
return horizon_line
return None
# Function to create a mask that excludes everything below the detected horizon line.
# This helps to differentiate between sky and non-sky regions, particularly in cases
# where reflections or similar colors appear below the horizon.
def mask_below_horizon(image, horizon_line):
mask = np.zeros(image.shape[:2], dtype=np.uint8)
cv2.line(mask, (horizon_line[0], horizon_line[1]), (horizon_line[2], horizon_line[3]), 255, thickness=5)
# Fill below the line to exclude anything below it
cv2.floodFill(mask, None, seedPoint=(0, horizon_line[1] + 1), newVal=255)
return mask
# The main function that processes the image for Gradio.
# It applies the steps for sky detection and outputs an image
# showing the original and sky-detected regions side by side.
def process_image_for_gradio(image):
# Convert to HSV color space
hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# Calculate thresholds from the top part of the image
lower_limit, upper_limit = calculate_thresholds_from_top(hsv_image, top_fraction=0.2)
# Ensure the limits are uint8 before using them in cv2.inRange
lower_limit = np.array(lower_limit, dtype=np.uint8)
upper_limit = np.array(upper_limit, dtype=np.uint8)
# Initial mask based on the initially calculated thresholds
initial_mask = cv2.inRange(hsv_image, lower_limit, upper_limit)
# Validate and adjust the mask if necessary
mask = validate_thresholds(initial_mask, hsv_image, lower_limit, upper_limit)
# Apply morphological operations to remove small noise
kernel_size = 5
kernel = np.ones((kernel_size, kernel_size), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
# Apply edge detection to help find the horizon line
edges = cv2.Canny(mask, 50, 150)
# Find the horizon line
horizon_line = find_horizon_line(edges)
# Create a mask to exclude everything below the horizon
if horizon_line is not None:
horizon_mask = mask_below_horizon(image, horizon_line)
# Combine the sky mask with the horizon mask
mask = cv2.bitwise_and(mask, horizon_mask)
# Bitwise-AND mask and original image to extract sky
sky = cv2.bitwise_and(image, image, mask=mask)
# Stack the original image and the result side by side
result = np.hstack((image, sky))
return result
# Gradio function that wraps around process_image_for_gradio
def gradio_interface(image):
output_image = process_image_for_gradio(image)
return output_image
# Define the Gradio interface
iface = gr.Interface(
fn=gradio_interface,
inputs=gr.components.Image(type="numpy"), # Updated input definition
outputs=gr.components.Image(type="numpy"), # Updated output definition
title="Sky Pixel Identification",
description="Upload an image to identify sky pixels. The result will show the original image and sky pixels side by side."
)
# Launch the interface
iface.launch()