import cv2
import numpy as np
import time
from scipy.fft import fft
import matplotlib.pyplot as plt
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

# Константы
BUFFER_SIZE = 150
BPM_LOWER_LIMIT = 40.0
BPM_UPPER_LIMIT = 180.0

class PulseDetector:
    def init(self):
        # Загрузка классификатора для обнаружения лиц
        self.face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
        if self.face_cascade.empty():
            raise ValueError("Ошибка при загрузке каскада Хаара для обнаружения лиц")
        self.data_buffer = []
        self.times = []
        self.bpm = 0.0
        self.start_time = time.time()

    def detect_pulse(self, frame):
        # Преобразование кадра в оттенки серого и обнаружение лиц
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        faces = self.face_cascade.detectMultiScale(
            gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30)
        )

        if len(faces) == 0:
            print("Лицо не обнаружено")
            return None

        # Работаем с первым обнаруженным лицом
        (x, y, w, h) = faces[0]

        # Определение нескольких областей интереса (ROI) на лбу
        roi_regions = [
            (int(x + w * 0.3), int(y + h * 0.1), int(w * 0.1), int(h * 0.2)),
            (int(x + w * 0.5), int(y + h * 0.1), int(w * 0.1), int(h * 0.2)),
            (int(x + w * 0.7), int(y + h * 0.1), int(w * 0.1), int(h * 0.2))
        ]

        mean_values = []

        for (fx, fy, fw, fh) in roi_regions:
            forehead = frame[fy:fy+fh, fx:fx+fw]
            if forehead.size == 0:
                continue

            # Использование нескольких цветовых каналов
            r = np.mean(forehead[:, :, 2])
            g = np.mean(forehead[:, :, 1])
            b = np.mean(forehead[:, :, 0])
            mean_color = (r + g + b) / 3  # Усреднение значений по каналам
            mean_values.append(mean_color)

            # Отрисовка прямоугольника вокруг ROI
            cv2.rectangle(frame, (fx, fy), (fx + fw, fy + fh), (0, 255, 0), 1)

        if not mean_values:
            print("Области лба имеют нулевой размер")
            return None

        # Усреднение значений из всех областей
        mean_val = np.mean(mean_values)

        # Добавление данных в буфер
        current_time = time.time() - self.start_time
        self.times.append(current_time)
        self.data_buffer.append(mean_val)

        # Ограничение размера буфера
        if len(self.data_buffer) > BUFFER_SIZE:
            self.data_buffer.pop(0)
            self.times.pop(0)

        # Расчет BPM при достаточном количестве данных
        if len(self.data_buffer) == BUFFER_SIZE:
            self.calculate_bpm()

        # Отображение стабильности сигнала
        if len(self.data_buffer) >= BUFFER_SIZE:
            stability = np.std(self.data_buffer[-BUFFER_SIZE//10:])
            cv2.putText(frame, f"Стабильность: {stability:.2f}", (x, y + h + 30),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2)

        # Отображение значения BPM
        if self.bpm > 0:
            cv2.putText(frame, f"BPM: {self.bpm:.1f}", (x, y - 10),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2)

        return self.bpm

    def calculate_bpm(self):
        if len(self.times) < 2:
            print("Недостаточно данных для расчета BPM")
            return

        time_diff = self.times[-1] - self.times[0]
        if time_diff <= 0:
            print("Некорректный временной интервал")
            return

        fps = len(self.data_buffer) / time_diff
        if fps <= 0:
            print(f"Некорректное значение FPS: {fps}")
            return

        # Нормализация данных
        normalized_data = np.array(self.data_buffer)
        normalized_data = (normalized_data - np.mean(normalized_data)) / np.std(normalized_data)

        # Применение FFT
        N = len(normalized_data)
        yf = fft(normalized_data)
        xf = np.linspace(0.0, fps / 2.0, N // 2)
        spectrum = 2.0 / N * np.abs(yf[0:N // 2])

        # Определение доминирующей частоты в диапазоне BPM
        bpm_min = BPM_LOWER_LIMIT / 60.0
        bpm_max = BPM_UPPER_LIMIT / 60.0
        idx_min = np.argmax(xf > bpm_min) - 1
        idx_max = np.argmax(xf > bpm_max) - 1

        if idx_max <= idx_min:
            print("Ошибка в индексах частот для диапазона BPM")
            return

        freq_range = xf[idx_min:idx_max]
        spectrum_range = spectrum[idx_min:idx_max]

        if len(spectrum_range) == 0:
            print("Диапазон спектра пуст")
            return

        peak_idx = np.argmax(spectrum_range)
        peak_freq = freq_range[peak_idx]
        self.bpm = peak_freq * 60.0

        if self.bpm < BPM_LOWER_LIMIT or self.bpm > BPM_UPPER_LIMIT:
            print(f"BPM вне допустимого диапазона: {self.bpm}")
            self.bpm = 0.0

def main():
    detector = PulseDetector()
    cap = cv2.VideoCapture(0)

    if not cap.isOpened():
        print("Ошибка при открытии камеры")
        return

    root = tk.Tk()
    root.title("Определение пульса")

    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 10))
    canvas = FigureCanvasTkAgg(fig, master=root)
    canvas_widget = canvas.get_tk_widget()
    canvas_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=1)

    bpm_history = []
    time_history = []

    def update():
        ret, frame = cap.read()
        if not ret:
            print("Не удалось получить кадр")
            root.after(50, update)
            return

        bpm = detector.detect_pulse(frame)

        ax1.clear()
        ax1.imshow(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
        ax1.axis('off')

        if bpm is not None and bpm > 0:
            bpm_history.append(bpm)
            time_history.append(time.time() - detector.start_time)

            if len(bpm_history) > 100:
                bpm_history.pop(0)
                time_history.pop(0)

            ax2.clear()
            ax2.plot(time_history, bpm_history, 'b-')
            ax2.set_xlabel('Время (с)')
            ax2.set_ylabel('BPM')
            ax2.set_ylim(BPM_LOWER_LIMIT, BPM_UPPER_LIMIT)
            ax2.set_title('История измерений BPM')

        canvas.draw()
        root.after(50, update)

    def on_closing():
        cap.release()
        root.destroy()

    root.protocol("WM_DELETE_WINDOW", on_closing)
    root.after(0, update)
    root.mainloop()

if name == "main":
    main()