132 lines
4.4 KiB
Python
132 lines
4.4 KiB
Python
import numpy as np
|
|
import cv2
|
|
from skimage.feature import graycomatrix, graycoprops
|
|
import pandas as pd
|
|
|
|
class FeatureExtractor:
|
|
def __init__(self):
|
|
self.feature_names = [
|
|
'mean_r', 'mean_g', 'mean_b',
|
|
'std_r', 'std_g', 'std_b',
|
|
'skew_r', 'skew_g', 'skew_b',
|
|
'energy', 'contrast', 'correlation',
|
|
'homogeneity', 'dissimilarity', 'ASM'
|
|
]
|
|
|
|
def color_moments(self, image):
|
|
"""Extract color moments from RGB image
|
|
|
|
Supports masked ROI when `mask` is provided inside `image` tuple
|
|
(see `extract_all_features`). If `image` is a numpy array, no mask
|
|
is used.
|
|
"""
|
|
moments = []
|
|
|
|
# If caller passed a tuple (image, mask) handle it; otherwise mask=None
|
|
if isinstance(image, tuple) and len(image) == 2:
|
|
img, mask = image
|
|
else:
|
|
img, mask = image, None
|
|
|
|
for i in range(3): # For each channel (R, G, B)
|
|
channel = img[:, :, i]
|
|
|
|
if mask is not None:
|
|
mask_bool = (mask > 0)
|
|
vals = channel[mask_bool]
|
|
else:
|
|
vals = channel.ravel()
|
|
|
|
if vals.size == 0:
|
|
mean = 0.0
|
|
std = 0.0
|
|
skew = 0.0
|
|
else:
|
|
mean = np.mean(vals)
|
|
std = np.std(vals)
|
|
if std > 0:
|
|
skew = np.mean((vals - mean) ** 3) / (std ** 3)
|
|
else:
|
|
skew = 0.0
|
|
|
|
moments.extend([mean, std, skew])
|
|
|
|
return moments
|
|
|
|
def glcm_features(self, image):
|
|
"""Extract GLCM texture features.
|
|
|
|
Accepts either `image` (numpy array) or `(image, mask)` tuple.
|
|
If `mask` is provided, compute GLCM on cropped ROI to avoid
|
|
background influence.
|
|
"""
|
|
# Unpack possible (image, mask)
|
|
if isinstance(image, tuple) and len(image) == 2:
|
|
img, mask = image
|
|
else:
|
|
img, mask = image, None
|
|
|
|
# Convert to grayscale and scale to 0-255
|
|
gray = cv2.cvtColor((img * 255).astype(np.uint8), cv2.COLOR_RGB2GRAY)
|
|
|
|
# If mask provided, crop to bounding box of mask
|
|
if mask is not None:
|
|
mask_bool = (mask > 0)
|
|
if mask_bool.any():
|
|
ys, xs = np.where(mask_bool)
|
|
ymin, ymax = ys.min(), ys.max()
|
|
xmin, xmax = xs.min(), xs.max()
|
|
gray_crop = gray[ymin:ymax+1, xmin:xmax+1]
|
|
# Use crop when it contains enough pixels
|
|
if gray_crop.size >= 16:
|
|
gray = gray_crop
|
|
|
|
gray = gray.astype(np.uint8)
|
|
|
|
# Calculate GLCM
|
|
glcm = graycomatrix(gray,
|
|
distances=[1, 2, 3],
|
|
angles=[0, np.pi/4, np.pi/2, 3*np.pi/4],
|
|
levels=256,
|
|
symmetric=True,
|
|
normed=True)
|
|
|
|
# Extract properties and average across distances and angles
|
|
features = []
|
|
for prop in ['energy', 'contrast', 'correlation', 'homogeneity', 'dissimilarity', 'ASM']:
|
|
prop_values = graycoprops(glcm, prop)
|
|
features.append(np.mean(prop_values))
|
|
|
|
return features
|
|
|
|
def extract_all_features(self, image, mask=None):
|
|
"""Extract all features: color moments + GLCM.
|
|
|
|
`image` is expected to be an RGB array normalized to 0..1. If a
|
|
`mask` is provided it will be used by subroutines; alternatively
|
|
a tuple `(image, mask)` may be passed in `image`.
|
|
"""
|
|
# Allow either passing mask separately or embedding into image tuple
|
|
if mask is not None:
|
|
img_for_color = (image, mask)
|
|
img_for_glcm = (image, mask)
|
|
else:
|
|
img_for_color = image
|
|
img_for_glcm = image
|
|
|
|
color_features = self.color_moments(img_for_color)
|
|
texture_features = self.glcm_features(img_for_glcm)
|
|
|
|
all_features = color_features + texture_features
|
|
return all_features
|
|
|
|
def save_features_to_csv(self, features_list, labels, filename):
|
|
"""Save extracted features to CSV"""
|
|
df = pd.DataFrame(features_list, columns=self.feature_names)
|
|
df['label'] = labels
|
|
|
|
# Save to CSV
|
|
df.to_csv(filename, index=False)
|
|
print(f"Fitur disimpan ke: {filename}")
|
|
|
|
return df |