File size: 7,778 Bytes
a2c2f9c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c245bb5
a2c2f9c
 
 
62e6699
 
a2c2f9c
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#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()