|
import streamlit as st |
|
import cv2 |
|
import numpy as np |
|
from PIL import Image |
|
from rembg import remove |
|
import matplotlib.pyplot as plt |
|
import io |
|
import os |
|
import tempfile |
|
|
|
|
|
st.set_page_config( |
|
page_title="SpotRadar", |
|
page_icon="π", |
|
layout="wide" |
|
) |
|
|
|
|
|
st.title("π SpotRadar - Phase 1 Demo") |
|
st.write("Upload an image of a fruit to analyze dark spots and blemishes.") |
|
|
|
|
|
def remove_background(input_image): |
|
|
|
img_byte_arr = io.BytesIO() |
|
input_image.save(img_byte_arr, format='PNG') |
|
input_bytes = img_byte_arr.getvalue() |
|
|
|
|
|
output_bytes = remove(input_bytes) |
|
|
|
|
|
return Image.open(io.BytesIO(output_bytes)) |
|
|
|
|
|
def create_white_bg_spots(mask, original_image): |
|
|
|
white_bg = np.ones_like(original_image) * 255 |
|
|
|
|
|
result = original_image.copy() |
|
|
|
|
|
result[mask == 0] = [255, 255, 255] |
|
|
|
return result |
|
|
|
|
|
def segment_dark_spots(image_array): |
|
|
|
if len(image_array.shape) == 2 or image_array.shape[2] == 1: |
|
image_rgb = cv2.cvtColor(image_array, cv2.COLOR_GRAY2RGB) |
|
else: |
|
image_rgb = cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB) |
|
|
|
|
|
rgb_ranges = { |
|
"light_brown": ([139, 69, 19], [165, 105, 45]), |
|
"dark_brown": ([75, 45, 10], [110, 75, 35]), |
|
"black": ([0, 0, 0], [30, 30, 30]), |
|
"gray_green": ([100, 120, 100], [150, 160, 140]), |
|
"yellow_brown": ([150, 120, 20], [200, 160, 60]), |
|
"dark_purple": ([75, 0, 50], [120, 40, 80]), |
|
"speckled_brown": ([120, 80, 50], [150, 110, 80]) |
|
} |
|
|
|
|
|
final_mask = np.zeros(image_rgb.shape[:2], dtype=np.uint8) |
|
|
|
|
|
for color_name, (lower, upper) in rgb_ranges.items(): |
|
|
|
lower_bound = np.array(lower, dtype=np.uint8) |
|
upper_bound = np.array(upper, dtype=np.uint8) |
|
mask = cv2.inRange(image_rgb, lower_bound, upper_bound) |
|
|
|
|
|
final_mask = cv2.bitwise_or(final_mask, mask) |
|
|
|
|
|
segmented_image = cv2.bitwise_and(image_rgb, image_rgb, mask=final_mask) |
|
|
|
|
|
white_bg_image = create_white_bg_spots(final_mask, image_rgb) |
|
|
|
|
|
spot_info = analyze_spots(image_rgb, final_mask) |
|
|
|
return segmented_image, white_bg_image, final_mask, spot_info |
|
|
|
|
|
def analyze_spots(original_image, mask): |
|
|
|
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask, connectivity=8) |
|
|
|
|
|
spot_info = [] |
|
total_image_pixels = original_image.shape[0] * original_image.shape[1] |
|
|
|
for i in range(1, num_labels): |
|
|
|
area = stats[i, cv2.CC_STAT_AREA] |
|
|
|
|
|
if area < 10: |
|
continue |
|
|
|
|
|
spot_mask = (labels == i).astype(np.uint8) |
|
|
|
|
|
mean_color = cv2.mean(original_image, mask=spot_mask) |
|
mean_color_rgb = (int(mean_color[0]), int(mean_color[1]), int(mean_color[2])) |
|
|
|
|
|
spot_percentage = (area / total_image_pixels) * 100 |
|
|
|
|
|
x = stats[i, cv2.CC_STAT_LEFT] |
|
y = stats[i, cv2.CC_STAT_TOP] |
|
w = stats[i, cv2.CC_STAT_WIDTH] |
|
h = stats[i, cv2.CC_STAT_HEIGHT] |
|
|
|
|
|
spot_info.append({ |
|
'spot_number': i, |
|
'size_pixels': area, |
|
'color_rgb': mean_color_rgb, |
|
'coverage_percentage': spot_percentage, |
|
'bbox': (x, y, w, h), |
|
'centroid': (int(centroids[i][0]), int(centroids[i][1])) |
|
}) |
|
|
|
return spot_info |
|
|
|
|
|
def pil_to_cv2(pil_image): |
|
return cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR) |
|
|
|
|
|
def cv2_to_pil(cv2_image): |
|
return Image.fromarray(cv2.cvtColor(cv2_image, cv2.COLOR_BGR2RGB)) |
|
|
|
|
|
def draw_spot_annotations(image, spot_info): |
|
|
|
annotated_image = image.copy() |
|
|
|
|
|
for spot in spot_info: |
|
x, y, w, h = spot['bbox'] |
|
cv2.rectangle(annotated_image, (x, y), (x+w, y+h), (0, 255, 0), 2) |
|
cv2.putText(annotated_image, f"#{spot['spot_number']}", |
|
(x, y-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) |
|
|
|
return annotated_image |
|
|
|
|
|
uploaded_file = st.file_uploader("Choose an image...", type=["jpg", "jpeg", "png"]) |
|
|
|
|
|
st.sidebar.header("Settings") |
|
remove_bg = st.sidebar.checkbox("Remove Background", value=True) |
|
min_spot_size = st.sidebar.slider("Minimum Spot Size (pixels)", 5, 100, 10) |
|
show_annotations = st.sidebar.checkbox("Show Spot Annotations", value=True) |
|
|
|
|
|
if uploaded_file is not None: |
|
|
|
image = Image.open(uploaded_file) |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
st.subheader("Original Image") |
|
st.image(image, use_container_width=True) |
|
|
|
|
|
if remove_bg: |
|
with st.spinner('Removing background...'): |
|
no_bg_img = remove_background(image) |
|
|
|
with col2: |
|
st.subheader("Background Removed") |
|
st.image(no_bg_img, use_container_width=True) |
|
|
|
|
|
img_for_processing = pil_to_cv2(no_bg_img) |
|
else: |
|
img_for_processing = pil_to_cv2(image) |
|
|
|
|
|
with st.spinner('Analyzing spots...'): |
|
segmented_img, white_bg_spots, mask, spot_info = segment_dark_spots(img_for_processing) |
|
|
|
|
|
filtered_spot_info = [spot for spot in spot_info if spot['size_pixels'] >= min_spot_size] |
|
|
|
|
|
total_coverage = sum(spot['coverage_percentage'] for spot in filtered_spot_info) |
|
|
|
|
|
col3, col4 = st.columns(2) |
|
|
|
with col3: |
|
st.subheader("Detected Dark Spots") |
|
st.image(segmented_img, use_container_width=True) |
|
|
|
with col4: |
|
st.subheader("Spots on White Background") |
|
if show_annotations and filtered_spot_info: |
|
annotated_img = draw_spot_annotations(white_bg_spots, filtered_spot_info) |
|
st.image(annotated_img, use_container_width=True) |
|
else: |
|
st.image(white_bg_spots, use_container_width=True) |
|
|
|
|
|
st.subheader("Analysis Results") |
|
|
|
col_metrics1, col_metrics2, col_metrics3 = st.columns(3) |
|
|
|
with col_metrics1: |
|
st.metric("Total Spots", len(filtered_spot_info)) |
|
|
|
with col_metrics2: |
|
st.metric("Total Coverage", f"{total_coverage:.2f}%") |
|
|
|
with col_metrics3: |
|
if len(filtered_spot_info) > 0: |
|
largest_spot = max(filtered_spot_info, key=lambda x: x['size_pixels']) |
|
st.metric("Largest Spot Size", f"{largest_spot['size_pixels']} pixels") |
|
else: |
|
st.metric("Largest Spot Size", "0 pixels") |
|
|
|
|
|
if filtered_spot_info: |
|
st.subheader("Spot Details") |
|
|
|
|
|
spot_data = [] |
|
for spot in filtered_spot_info: |
|
color_hex = "#{:02x}{:02x}{:02x}".format(*spot['color_rgb']) |
|
spot_data.append({ |
|
"Spot #": spot['spot_number'], |
|
"Size (pixels)": spot['size_pixels'], |
|
"Color": color_hex, |
|
"Coverage (%)": f"{spot['coverage_percentage']:.2f}%" |
|
}) |
|
|
|
|
|
tab1, tab2 = st.tabs(["Table View", "Detailed View"]) |
|
|
|
with tab1: |
|
|
|
st.dataframe(spot_data) |
|
|
|
with tab2: |
|
|
|
for spot in filtered_spot_info: |
|
with st.expander(f"Spot #{spot['spot_number']} - {spot['size_pixels']} pixels"): |
|
col_a, col_b = st.columns([1, 3]) |
|
|
|
with col_a: |
|
|
|
color_hex = "#{:02x}{:02x}{:02x}".format(*spot['color_rgb']) |
|
st.markdown(f"<div style='background-color: {color_hex}; width: 50px; height: 50px; border-radius: 5px;'></div>", unsafe_allow_html=True) |
|
|
|
with col_b: |
|
st.write(f"Size: {spot['size_pixels']} pixels") |
|
st.write(f"Coverage: {spot['coverage_percentage']:.2f}%") |
|
st.write(f"RGB Color: {spot['color_rgb']}") |
|
st.write(f"Position: {spot['centroid']}") |
|
else: |
|
st.info("No spots detected with the current settings. Try adjusting the minimum spot size.") |
|
|
|
|
|
with st.expander("How to use this app"): |
|
st.write(""" |
|
1. Upload an image of a fruit with visible dark spots or blemishes. |
|
2. The app will automatically remove the background (if selected) and detect dark spots. |
|
3. Adjust the minimum spot size to filter out noise or small spots. |
|
4. Toggle spot annotations to see numbered bounding boxes around the detected spots. |
|
5. View detailed information about each spot in the 'Spot Details' section. |
|
|
|
This app can help in: |
|
- Assessing fruit quality |
|
- Tracking disease progression |
|
- Quantifying surface blemishes |
|
""") |
|
|
|
|
|
st.markdown("---") |
|
st.markdown("<p style='text-align: center;'>Developed by Manith Marapperuma</p>", unsafe_allow_html=True) |