TIFNGK_E41222722/utils/feature_extraction.py

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