ForestAI-TreeExtraction / utils /segmentation.py
thedynamicpacif
Improved image processing and added feature selection
5cf313a
raw
history blame
8.81 kB
"""
Segmentation utilities for image processing inspired by CLIPSeg techniques.
This is a simplified version that does not require the full transformers library.
"""
import os
import logging
import numpy as np
import cv2
from PIL import Image
from utils.geospatial import extract_contours, simplify_polygons, regularize_polygons, merge_nearby_polygons
def segment_by_color_threshold(image_path, output_path=None,
threshold=127, color_channel=1,
smoothing_sigma=1.0):
"""
Segment an image based on color thresholding.
This is a simple segmentation inspired by more complex models like CLIPSeg.
Args:
image_path (str): Path to the input image
output_path (str, optional): Path to save the segmentation mask
threshold (int): Pixel intensity threshold (0-255)
color_channel (int): Color channel to use for thresholding (0=R, 1=G, 2=B)
smoothing_sigma (float): Gaussian smoothing sigma
Returns:
numpy.ndarray: Segmentation mask
"""
try:
# Read the image
img = cv2.imread(image_path)
if img is None:
# Try using PIL if OpenCV fails
pil_img = Image.open(image_path).convert('RGB')
img = np.array(pil_img)
img = img[:, :, ::-1] # RGB to BGR for OpenCV compatibility
# Split channels and use the specified channel for segmentation
b, g, r = cv2.split(img)
channels = [r, g, b]
if 0 <= color_channel < 3:
channel = channels[color_channel]
else:
# Use grayscale if invalid channel specified
channel = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Apply Gaussian blur to reduce noise
if smoothing_sigma > 0:
channel = cv2.GaussianBlur(channel, (0, 0), smoothing_sigma)
# Apply thresholding to create binary mask
_, mask = cv2.threshold(channel, threshold, 255, cv2.THRESH_BINARY)
# Save the mask if output path is provided
if output_path:
cv2.imwrite(output_path, mask)
logging.info(f"Saved segmentation mask to {output_path}")
return mask
except Exception as e:
logging.error(f"Error in segmentation: {str(e)}")
return None
def segment_by_adaptive_threshold(image_path, output_path=None,
block_size=11, c=2,
smoothing_sigma=1.0):
"""
Segment an image using adaptive thresholding for better handling of
lighting variations.
Args:
image_path (str): Path to the input image
output_path (str, optional): Path to save the segmentation mask
block_size (int): Size of the pixel neighborhood for threshold calculation
c (int): Constant subtracted from the mean
smoothing_sigma (float): Gaussian smoothing sigma
Returns:
numpy.ndarray: Segmentation mask
"""
try:
# Read the image
img = cv2.imread(image_path)
if img is None:
# Try using PIL if OpenCV fails
pil_img = Image.open(image_path).convert('RGB')
img = np.array(pil_img)
img = img[:, :, ::-1] # RGB to BGR for OpenCV compatibility
# Convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Apply Gaussian blur to reduce noise
if smoothing_sigma > 0:
gray = cv2.GaussianBlur(gray, (0, 0), smoothing_sigma)
# Apply adaptive thresholding
mask = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, block_size, c
)
# Save the mask if output path is provided
if output_path:
cv2.imwrite(output_path, mask)
logging.info(f"Saved segmentation mask to {output_path}")
return mask
except Exception as e:
logging.error(f"Error in segmentation: {str(e)}")
return None
def segment_by_otsu(image_path, output_path=None, smoothing_sigma=1.0):
"""
Segment an image using Otsu's automatic thresholding method.
Args:
image_path (str): Path to the input image
output_path (str, optional): Path to save the segmentation mask
smoothing_sigma (float): Gaussian smoothing sigma
Returns:
numpy.ndarray: Segmentation mask
"""
try:
# Read the image
img = cv2.imread(image_path)
if img is None:
# Try using PIL if OpenCV fails
pil_img = Image.open(image_path).convert('RGB')
img = np.array(pil_img)
img = img[:, :, ::-1] # RGB to BGR for OpenCV compatibility
# Convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Apply Gaussian blur to reduce noise
if smoothing_sigma > 0:
gray = cv2.GaussianBlur(gray, (0, 0), smoothing_sigma)
# Apply Otsu's thresholding
_, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# Save the mask if output path is provided
if output_path:
cv2.imwrite(output_path, mask)
logging.info(f"Saved segmentation mask to {output_path}")
return mask
except Exception as e:
logging.error(f"Error in segmentation: {str(e)}")
return None
def segment_and_extract_features(image_path, output_mask_path=None,
feature_type="buildings",
min_area=50, simplify_tolerance=2.0,
merge_distance=5.0):
"""
Complete pipeline for segmentation and feature extraction.
Args:
image_path (str): Path to the input image
output_mask_path (str, optional): Path to save the segmentation mask
feature_type (str): Type of features to extract ("buildings", "trees", "water", "roads")
min_area (int): Minimum feature area to keep
simplify_tolerance (float): Tolerance for polygon simplification
merge_distance (float): Distance for merging nearby polygons
Returns:
tuple: (mask, polygons) - Segmentation mask and list of simplified Shapely polygons
"""
# Choose segmentation method based on feature type
if feature_type.lower() == "buildings":
# Buildings typically have clean edges and good contrast
mask = segment_by_adaptive_threshold(
image_path, output_mask_path,
block_size=15, c=2, smoothing_sigma=1.0
)
elif feature_type.lower() == "trees" or feature_type.lower() == "vegetation":
# Trees typically strong in green channel
mask = segment_by_color_threshold(
image_path, output_mask_path,
threshold=140, color_channel=1, smoothing_sigma=1.5
)
elif feature_type.lower() == "water":
# Water typically has distinct spectral properties
mask = segment_by_color_threshold(
image_path, output_mask_path,
threshold=120, color_channel=0, smoothing_sigma=2.0
)
else:
# Default to Otsu for unknown feature types
mask = segment_by_otsu(
image_path, output_mask_path, smoothing_sigma=1.0
)
if mask is None:
logging.error("Segmentation failed")
return None, []
# Save mask temporarily if needed for contour extraction
temp_mask_path = None
if not output_mask_path:
temp_mask_path = os.path.join(
os.path.dirname(image_path),
f"{os.path.splitext(os.path.basename(image_path))[0]}_mask.png"
)
cv2.imwrite(temp_mask_path, mask)
mask_path = temp_mask_path
else:
mask_path = output_mask_path
# Extract contours from the mask
polygons = extract_contours(mask_path, min_area=min_area)
logging.info(f"Extracted {len(polygons)} initial polygons")
# Clean up temporary file if created
if temp_mask_path and os.path.exists(temp_mask_path):
os.remove(temp_mask_path)
# Simplify polygons
polygons = simplify_polygons(polygons, tolerance=simplify_tolerance)
# If buildings, regularize them to make more rectangular
if feature_type.lower() == "buildings":
polygons = regularize_polygons(polygons)
# Merge nearby polygons to reduce count
polygons = merge_nearby_polygons(polygons, distance_threshold=merge_distance)
logging.info(f"After processing: {len(polygons)} polygons")
return mask, polygons