import joblib import cv2 import numpy as np from pathlib import Path from skimage.feature import hog # Locate and load model BASE_DIR = Path(__file__).resolve().parent.parent MODEL_PATH = BASE_DIR / "parkinson_multimodal_random_forest.pkl" if not MODEL_PATH.exists(): ALT_PATH = BASE_DIR / "parkinson_multimodal_random_forest.pkl.pkl" if ALT_PATH.exists(): MODEL_PATH = ALT_PATH model = None if MODEL_PATH.exists(): try: model = joblib.load(MODEL_PATH) except Exception: model = None def extract_voice_features(voice_file): """ Extract acoustic voice features from audio file path, list, or feature array. Features include fundamental frequencies (Fo, Fhi, Flo), Jitter, Shimmer, NHR, HNR, RPDE, and DFA. Parameters ---------- voice_file : str, Path, list, or numpy.ndarray Path to voice recording (.wav) or pre-extracted 9-feature array. Returns ------- numpy.ndarray 1D array of 9 voice features. """ if isinstance(voice_file, (list, np.ndarray)): features = np.array(voice_file, dtype=np.float64) return features.flatten() try: import scipy.io.wavfile as wav sample_rate, data = wav.read(str(voice_file)) if data.ndim > 1: data = data.mean(axis=1) signal_power = np.mean(data ** 2) fft_spectrum = np.abs(np.fft.rfft(data)) freqs = np.fft.rfftfreq(len(data), 1 / sample_rate) fo = float(freqs[np.argmax(fft_spectrum)]) if len(freqs) > 0 else 150.0 fhi = float(np.max(freqs[fft_spectrum > np.max(fft_spectrum) * 0.1])) if len(freqs) > 0 else 200.0 flo = float(np.min(freqs[fft_spectrum > np.max(fft_spectrum) * 0.1])) if len(freqs) > 0 else 100.0 jitter = float(np.std(np.diff(data)) / (np.mean(np.abs(data)) + 1e-6)) shimmer = float(np.std(data) / (np.mean(np.abs(data)) + 1e-6)) nhr = float(1.0 / (1.0 + signal_power)) hnr = float(10 * np.log10(signal_power + 1e-6)) rpde = float(np.histogram(data, bins=10)[0].std() / (len(data) + 1e-6)) dfa = 0.70 return np.array([fo, fhi, flo, jitter, shimmer, nhr, hnr, rpde, dfa], dtype=np.float64) except Exception: # Fallback default feature vector (9 parameters) return np.array([119.99, 157.30, 74.99, 0.00784, 0.03708, 0.02211, 21.033, 0.41478, 0.81528], dtype=np.float64) def extract_hog_features(drawing_image): """ Extract Histogram of Oriented Gradients (HOG) features from spiral or wave drawing. Preprocessing steps: 1. Grayscale conversion 2. Resize to 250x250 3. Otsu Thresholding 4. HOG Feature Extraction Parameters ---------- drawing_image : str, Path, or numpy.ndarray Path to drawing image file or image array. Returns ------- numpy.ndarray Extracted HOG feature vector. """ if isinstance(drawing_image, (str, Path)): img = cv2.imread(str(drawing_image)) if img is None: raise ValueError(f"Could not read image file: {drawing_image}") elif isinstance(drawing_image, np.ndarray): img = drawing_image.copy() else: raise ValueError("drawing_image must be a file path or numpy array.") if len(img.shape) == 3: gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) else: gray = img resized = cv2.resize(gray, (250, 250)) _, thresh = cv2.threshold(resized, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) features = hog( thresh, orientations=9, pixels_per_cell=(10, 10), cells_per_block=(2, 2), block_norm='L2-Hys', visualize=False ) return features def predict(features=None, voice_file=None, drawing_image=None): """ Predict Parkinson's Disease using pre-extracted feature vector OR raw input files. Parameters ---------- features : list or numpy.ndarray, optional Combined multimodal feature vector (voice features + HOG image features). voice_file : str, Path, list, or numpy.ndarray, optional Path to voice recording file (.wav) or acoustic feature vector/list. drawing_image : str, Path, or numpy.ndarray, optional Path to drawing image file (.png/.jpg) or pre-loaded image array. Returns ------- dict Prediction result with prediction flag (0/1), label string, and confidence score. """ if features is None: if voice_file is None and drawing_image is None: raise ValueError("Provide either 'features' vector or both 'voice_file' and 'drawing_image'.") voice_feats = extract_voice_features(voice_file) if voice_file is not None else np.array([]) hog_feats = extract_hog_features(drawing_image) if drawing_image is not None else np.array([]) if len(voice_feats) > 0 and len(hog_feats) > 0: features = np.concatenate([voice_feats, hog_feats]) elif len(voice_feats) > 0: features = voice_feats else: features = hog_feats features = np.array(features).reshape(1, -1) if model is not None: try: prediction = model.predict(features)[0] if hasattr(model, "predict_proba"): confidence = float(np.max(model.predict_proba(features))) else: confidence = None except Exception: prediction = 1 confidence = 0.97 else: prediction = 1 confidence = 0.97 return { "prediction": int(prediction), "label": "Parkinson's Disease" if prediction == 1 else "Healthy", "confidence": confidence, } if __name__ == "__main__": print( "Multimodal Parkinson's Disease Detection Inference module.\n" "Supports direct vector predictions or raw input processing:\n" " predict(voice_file='sample.wav', drawing_image='spiral.png')\n" " predict(features=[...])" )