TIFNGK_E41222722/utils/preprocessing.py

309 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import cv2
import numpy as np
from PIL import Image
def validate_cattle_image(image_path, confidence_threshold=0.75):
"""
Validasi apakah gambar adalah sapi atau bukan.
Bisa deteksi wajah sapi (dengan mata) atau bagian tubuh sapi lainnya.
Returns: (is_cattle, confidence, reason)
"""
try:
# Baca gambar
img = cv2.imread(image_path)
if img is None:
try:
pil_img = Image.open(image_path).convert('RGB')
img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
except:
return False, 0.0, "Format gambar tidak didukung"
if img is None:
return False, 0.0, "Gagal membaca gambar"
h, w = img.shape[:2]
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# ===== LAYER 1: DETEKSI MATA (OPTIONAL - hanya bonus jika ada) =====
circles = cv2.HoughCircles(
gray, cv2.HOUGH_GRADIENT,
dp=1, minDist=30,
param1=70, param2=35,
minRadius=5, maxRadius=70
)
valid_eyes = []
eye_score = 0.0
if circles is not None and len(circles[0]) > 0:
circles = np.uint16(np.around(circles))
for circle in circles[0]:
x, y, r = int(circle[0]), int(circle[1]), int(circle[2])
roi = gray[max(0, y-r):min(h, y+r), max(0, x-r):min(w, x+r)]
if roi.size > 0:
roi_mean = np.mean(roi)
roi_std = np.std(roi)
if roi_mean < 105 and roi_std > 12:
valid_eyes.append((x, y, r))
# Validasi mata jika terdeteksi (untuk bonus score)
if len(valid_eyes) >= 2:
eye_positions = [(x, y) for x, y, r in valid_eyes[:2]]
eye_x = sorted([x for x, y in eye_positions])
eye_y = [valid_eyes[i][1] for i in range(2)]
eye_distances = [abs(eye_x[1] - eye_x[0]), abs(eye_y[0] - eye_y[1])]
# Mata harus horizontal (left-right)
if eye_distances[0] >= 20 and eye_distances[1] <= eye_distances[0] * 0.3:
eye_score = 1.0
print(f"[VALIDATE] Mata terdeteksi | Spacing: h={eye_distances[0]:.0f}, v={eye_distances[1]:.0f}")
# ===== LAYER 2: VALIDASI WARNA SAPI (FLEKSIBEL) =====
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h_channel = img_hsv[:, :, 0].astype(np.float32)
s_channel = img_hsv[:, :, 1].astype(np.float32)
v_channel = img_hsv[:, :, 2].astype(np.float32)
# Warna sapi: coklat, merah, hitam, putih (bisa partial)
brown_mask = ((h_channel >= 8) & (h_channel <= 30) & (s_channel >= 30) & (v_channel >= 60))
red_mask = (((h_channel <= 3) | (h_channel >= 175)) & (s_channel >= 35) & (v_channel >= 50))
black_mask = ((v_channel < 90) & (s_channel >= 15))
white_mask = ((v_channel >= 210) & (s_channel <= 50))
cattle_color = brown_mask | red_mask | black_mask | white_mask
color_ratio = np.sum(cattle_color) / (h * w)
# Minimum warna sapi harus 30% (ketat untuk menolak bukan sapi)
if color_ratio < 0.30:
return False, 0.0, "Gambar tidak menunjukkan sapi"
# Score color: ketat untuk matching sapi
if color_ratio >= 0.50:
color_score = 1.0
elif color_ratio >= 0.40:
color_score = 0.95
elif color_ratio >= 0.30:
color_score = 0.80
else:
color_score = 0.60
print(f"[VALIDATE] Warna sapi: {color_ratio*100:.0f}%")
# ===== LAYER 3: VALIDASI TEKSTUR KULIT =====
laplacian = cv2.Laplacian(gray, cv2.CV_64F)
texture_variance = np.var(laplacian)
# Texture sapi harus dalam range spesifik 80-2200 (ketat)
if texture_variance < 80 or texture_variance > 2200:
return False, 0.0, "Gambar tidak menunjukkan tekstur sapi"
# Score texture: ketat untuk matching sapi texture
if 120 <= texture_variance <= 1800:
texture_score = 1.0
elif 90 <= texture_variance <= 2100:
texture_score = 0.90
else:
texture_score = 0.75
print(f"[VALIDATE] Tekstur variance: {texture_variance:.0f}")
# ===== LAYER 4: EDGE DETECTION (Struktur) =====
edges = cv2.Canny(gray, 50, 150)
edge_density = np.sum(edges > 0) / (h * w)
# Edge density: 2-25% ketat untuk sapi
if edge_density < 0.02 or edge_density > 0.25:
return False, 0.0, "Gambar tidak memiliki struktur sapi"
# Score edge: ketat untuk matching
if 0.05 <= edge_density <= 0.15:
edge_score = 1.0
elif 0.03 <= edge_density <= 0.22:
edge_score = 0.90
else:
edge_score = 0.70
print(f"[VALIDATE] Edge density: {edge_density*100:.1f}%")
# ===== HARD REQUIREMENTS =====
# REQUIREMENT 1: Warna harus minimal decent (tidak boleh terlalu rendah)
if color_score < 0.75:
return False, 0.0, "Warna gambar tidak sesuai sapi"
# REQUIREMENT 2: Texture harus dalam range sapi
if texture_score < 0.70:
return False, 0.0, "Tekstur gambar tidak sesuai sapi"
# REQUIREMENT 3: Edge harus terdeteksi
if edge_score < 0.65:
return False, 0.0, "Struktur gambar tidak terlihat jelas"
# ===== FINAL CALCULATION =====
# Ketat: semua feature harus bagus untuk lolos
final_confidence = (
eye_score * 0.15 + # Mata (15% - bonus)
color_score * 0.40 + # Warna (40% - paling penting)
texture_score * 0.30 + # Tekstur (30% - penting)
edge_score * 0.15 # Edge (15% - supporting)
)
print(f"[VALIDATE] Final Score: {final_confidence*100:.1f}% (threshold: {confidence_threshold*100:.0f}%)")
if final_confidence >= confidence_threshold:
return True, final_confidence, None
else:
return False, final_confidence, "Bukan sapi atau gambar kurang jelas"
except Exception as e:
print(f"[VALIDATE] ERROR: {str(e)}")
return False, 0.0, f"Error: {str(e)}"
def preprocess_image(
image_path,
target_size=(256, 256),
apply_threshold=False,
thresh_method='otsu',
thresh_val=127,
morph_ops=None,
kernel_size=3,
morph_iterations=1
):
"""
Preprocess image and optionally produce a binary mask using thresholding
and morphological operations.
Returns: (img_normalized_rgb, img_resized_bgr, mask)
- img_normalized_rgb: RGB image normalized to 0..1 (numpy float32)
- img_resized_bgr: resized BGR image as read by OpenCV (uint8)
- mask: binary mask (uint8 0/255) or None if not requested
Parameters:
- apply_threshold: if True, compute a binary mask from grayscale image
- thresh_method: 'otsu' or 'binary'
- thresh_val: threshold value used when thresh_method=='binary'
- morph_ops: list of operations to apply in order. Supported: 'erode',
'dilate', 'opening', 'closing'
- kernel_size: size of structuring element (odd integer)
- morph_iterations: number of iterations for erosion/dilation
"""
try:
# Read image using OpenCV
img = cv2.imread(image_path)
if img is None:
# Try with PIL if OpenCV fails
pil_img = Image.open(image_path).convert('RGB')
img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
# Resize image (uses width, height tuple)
img_resized = cv2.resize(img, target_size)
# Convert to RGB and normalize
img_rgb = cv2.cvtColor(img_resized, cv2.COLOR_BGR2RGB).astype(np.float32)
img_normalized = img_rgb / 255.0
mask = None
if apply_threshold or (morph_ops is not None and len(morph_ops) > 0):
# Convert to grayscale for thresholding/morphology
gray = cv2.cvtColor(img_resized, cv2.COLOR_BGR2GRAY)
# Optionally apply threshold
if apply_threshold:
if thresh_method == 'otsu':
blur = cv2.GaussianBlur(gray, (5, 5), 0)
_, mask = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
else:
_, mask = cv2.threshold(gray, int(thresh_val), 255, cv2.THRESH_BINARY)
else:
# start from simple binary of gray (non-thresholded) if morph ops requested
_, mask = cv2.threshold(gray, int(thresh_val), 255, cv2.THRESH_BINARY)
# Apply morphological operations if requested
if morph_ops:
# Ensure kernel size is odd and >=1
ks = int(kernel_size)
if ks <= 0:
ks = 1
if ks % 2 == 0:
ks += 1
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (ks, ks))
for op in morph_ops:
op_lower = op.lower()
if op_lower == 'erode':
mask = cv2.erode(mask, kernel, iterations=morph_iterations)
elif op_lower == 'dilate':
mask = cv2.dilate(mask, kernel, iterations=morph_iterations)
elif op_lower == 'opening':
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=morph_iterations)
elif op_lower == 'closing':
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=morph_iterations)
else:
# ignore unknown ops but continue
continue
return img_normalized, img_resized, mask
except Exception as e:
raise ValueError(f"Error preprocessing image {image_path}: {str(e)}")
def preprocess_pipeline(
image_path,
target_size=(128, 128),
apply_threshold=True,
thresh_method='otsu',
thresh_val=127,
):
"""
Simple preprocessing pipeline:
1. Resize gambar
2. Threshold / binarisasi
3. Ekstraksi fitur RGB dan GLCM
Returns: (img_resized_rgb, gray_processed)
- img_resized_rgb: RGB image untuk fitur RGB (uint8)
- gray_processed: grayscale hasil threshold untuk fitur GLCM (uint8)
"""
try:
# Step 1: Baca image
img = cv2.imread(image_path)
if img is None:
# Try with PIL if OpenCV fails
pil_img = Image.open(image_path).convert('RGB')
img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
if img is None:
raise ValueError(f"Gagal membaca image dari {image_path}")
# Step 2: Resize image ke target size (default 128×128)
img_resized = cv2.resize(img, target_size)
# Step 3: Threshold pada grayscale untuk memisahkan objek dari background
gray = cv2.cvtColor(img_resized, cv2.COLOR_BGR2GRAY)
if apply_threshold:
if thresh_method == 'otsu':
_, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
else:
_, mask = cv2.threshold(gray, int(thresh_val), 255, cv2.THRESH_BINARY)
else:
mask = np.full(gray.shape, 255, dtype=np.uint8)
# Terapkan mask ke RGB dan grayscale supaya fitur hanya membaca area relevan
masked_bgr = cv2.bitwise_and(img_resized, img_resized, mask=mask)
img_resized_rgb = cv2.cvtColor(masked_bgr, cv2.COLOR_BGR2RGB)
gray_processed = cv2.bitwise_and(gray, gray, mask=mask)
# Validate outputs
if img_resized_rgb is None:
raise ValueError("img_resized_rgb is None after conversion")
if gray_processed is None:
raise ValueError("gray_processed is None after thresholding")
return img_resized_rgb, gray_processed
except Exception as e:
print(f"[ERROR] preprocess_pipeline failed for {image_path}: {str(e)}")
raise ValueError(f"Error in preprocessing pipeline {image_path}: {str(e)}")