|
|
import cv2
|
|
|
import numpy as np
|
|
|
from skimage.feature import graycomatrix, graycoprops
|
|
|
import os
|
|
|
from glob import glob
|
|
|
from PIL import Image, UnidentifiedImageError
|
|
|
|
|
|
class GLCMFeatureExtractor:
|
|
|
def __init__(self, distances=[1, 3, 5], angles=[0, np.pi/4, np.pi/2, 3*np.pi/4]):
|
|
|
self.distances = distances
|
|
|
self.angles = angles
|
|
|
|
|
|
def preprocess_xray(self, img_path):
|
|
|
"""Robust image loading with multiple fallbacks"""
|
|
|
try:
|
|
|
|
|
|
img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
|
|
|
if img is None:
|
|
|
|
|
|
try:
|
|
|
with Image.open(img_path) as pil_img:
|
|
|
img = np.array(pil_img.convert('L'))
|
|
|
except (IOError, UnidentifiedImageError) as e:
|
|
|
raise ValueError(f"PIL cannot read image: {img_path}") from e
|
|
|
|
|
|
|
|
|
if img.size == 0:
|
|
|
raise ValueError(f"Empty image: {img_path}")
|
|
|
|
|
|
|
|
|
img = cv2.resize(img, (256, 256))
|
|
|
|
|
|
|
|
|
img = img.astype(np.float32)
|
|
|
min_val = np.min(img)
|
|
|
max_val = np.max(img)
|
|
|
|
|
|
|
|
|
if max_val - min_val < 1e-5:
|
|
|
img = np.zeros_like(img)
|
|
|
else:
|
|
|
img = (img - min_val) / (max_val - min_val) * 255
|
|
|
|
|
|
return img.astype(np.uint8)
|
|
|
except Exception as e:
|
|
|
print(f"Error processing {img_path}: {str(e)}")
|
|
|
return None
|
|
|
|
|
|
def extract_features(self, img):
|
|
|
"""Extract GLCM features with validation"""
|
|
|
if img is None:
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
|
|
|
glcm = graycomatrix(
|
|
|
img,
|
|
|
distances=self.distances,
|
|
|
angles=self.angles,
|
|
|
levels=256,
|
|
|
symmetric=True,
|
|
|
normed=True
|
|
|
)
|
|
|
|
|
|
|
|
|
features = []
|
|
|
props = ['contrast', 'dissimilarity', 'homogeneity',
|
|
|
'energy', 'correlation', 'ASM']
|
|
|
|
|
|
for prop in props:
|
|
|
feat = graycoprops(glcm, prop)
|
|
|
features.extend(feat.flatten())
|
|
|
|
|
|
return np.array(features)
|
|
|
except Exception as e:
|
|
|
print(f"Feature extraction error: {str(e)}")
|
|
|
return None
|
|
|
|
|
|
def extract_from_folder(self, folder_path, max_samples=None):
|
|
|
"""Batch feature extraction with error handling"""
|
|
|
features = []
|
|
|
labels = []
|
|
|
class_name = os.path.basename(folder_path)
|
|
|
|
|
|
|
|
|
image_paths = []
|
|
|
for ext in ('*.png', '*.jpg', '*.jpeg', '*.dcm', '*.tif', '*.bmp'):
|
|
|
image_paths.extend(glob(os.path.join(folder_path, ext)))
|
|
|
|
|
|
if not image_paths:
|
|
|
print(f"Warning: No images found in {folder_path}")
|
|
|
return np.array([]), np.array([])
|
|
|
|
|
|
|
|
|
if max_samples and len(image_paths) > max_samples:
|
|
|
image_paths = np.random.choice(image_paths, max_samples, replace=False)
|
|
|
|
|
|
|
|
|
for img_path in image_paths:
|
|
|
img = self.preprocess_xray(img_path)
|
|
|
if img is None:
|
|
|
continue
|
|
|
|
|
|
feat = self.extract_features(img)
|
|
|
if feat is not None:
|
|
|
features.append(feat)
|
|
|
labels.append(class_name)
|
|
|
|
|
|
print(f"Successfully processed {len(features)}/{len(image_paths)} images in {folder_path}")
|
|
|
return np.array(features), np.array(labels)
|
|
|
|