Project structure:

DeepFit_TEST2/
├── backend/
│   ├── DeepFit/
│   │   ├── DeepFitClassifier.py
│   │   └── deepfit_classifier_v3.tflite
│   ├── main.py
│   ├── pushup_counter.py
│   ├── renderer.py
│   ├── requirements.txt
│   └── utils.py
├── frontend/
│   ├── index.html
│   ├── script.js
│   └── styles.css
└── V2_GenerateProjectShort.py


Project files:

backend/main.py
```py
import os
import base64
import cv2
import numpy as np
from flask import Flask, render_template
from flask_socketio import SocketIO, emit
from pushup_counter import PushUpCounter
import eventlet
import json
from pyngrok import ngrok

# Необходимо использовать eventlet с Flask-SocketIO
eventlet.monkey_patch()

app = Flask(__name__, static_folder='../frontend', template_folder='../frontend')
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='eventlet')

# Инициализация PushUpCounter
counter = PushUpCounter()

@app.route('/')
def index():
    return render_template('index.html')

@socketio.on('connect')
def handle_connect():
    print('Client connected')
    emit('status', {'status': 'Connected to server'})

@socketio.on('image')
def handle_image(data):
    try:
        # Декодирование base64 изображения
        header, encoded = data.split(',', 1)
        decoded = base64.b64decode(encoded)
        np_data = np.frombuffer(decoded, np.uint8)
        frame = cv2.imdecode(np_data, cv2.IMREAD_COLOR)
        if frame is None:
            emit('result', {'status': 'error', 'message': 'Invalid image data'})
            return

        # Обработка кадра
        result = counter.process_frame(frame)

        # Формирование данных для отправки
        response = {
            'status': 'processing',
            'progress': result.get('count', 0),
            'landmarks': result.get('landmarks', []),
            'angles': result.get('angles', {}),
            'feedback': result.get('feedback', '')
        }

        # Отправка данных обратно клиенту
        emit('result', json.dumps(response))
    except Exception as e:
        print(f"Error processing frame: {e}")
        emit('result', {'status': 'error', 'message': str(e)})

@socketio.on('disconnect')
def handle_disconnect():
    print('Client disconnected')

if __name__ == '__main__':
    # Открыть туннель ngrok
    public_url = ngrok.connect(5000)
    print(f"Public URL: {public_url}")

    # Запуск Flask-SocketIO сервера
    socketio.run(app, host='0.0.0.0', port=5000)
```


backend/pushup_counter.py
```py
# backend/pushup_counter.py

import cv2
import mediapipe as mp
import numpy as np
from collections import deque
from utils import set_pose_parameters, calculate_angle, LM_DICT
import os


# Если вы используете DeepFitClassifier, импортируйте его здесь.
# Убедитесь, что структура папок соответствует
# from DeepFit.DeepFitClassifier import DeepFitClassifier

class PushUpCounter:
    def __init__(self, deepfit_model_path='DeepFit/deepfit_classifier_v3.tflite'):
        # Инициализация параметров позы
        params = set_pose_parameters()
        (mode, complexity, smooth_landmarks, enable_segmentation,
         smooth_segmentation, detectionCon, trackCon, mpPose) = params
        self.pose = mpPose.Pose(
            static_image_mode=mode,
            model_complexity=complexity,
            smooth_landmarks=smooth_landmarks,
            enable_segmentation=enable_segmentation,
            smooth_segmentation=smooth_segmentation,
            min_detection_confidence=detectionCon,
            min_tracking_confidence=trackCon
        )
        self.mpDraw = mp.solutions.drawing_utils

        # Инициализация классификатора DeepFit (опционально)
        if os.path.exists(deepfit_model_path):
            # self.classifier = DeepFitClassifier(deepfit_model_path)
            self.classifier = None  # Замените на инициализацию вашего классификатора
            print(f"Классификатор DeepFit загружен по пути: {deepfit_model_path}")
        else:
            self.classifier = None
            print(f"Классификатор DeepFit не найден по пути: {deepfit_model_path}")

        # Инициализация счетчиков
        self.reset_counters()

    def reset_counters(self):
        self.count = 0
        self.direction = 0
        self.form = 0
        self.feedback = "Bad Form. Correct Posture."
        self.frame_queue = deque(maxlen=250)

    def process_frame(self, frame):
        """
        Обрабатывает один кадр и обновляет счетчики.
        Возвращает информацию для фронтенда.

        :param frame: Полученный кадр в формате BGR
        :return: Словарь с данными
        """
        image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        results = self.pose.process(image)
        landmark_list = self.get_position(results, frame.shape[0], frame.shape[1])

        data = {
            'landmarks': landmark_list,
            'angles': {},
            'feedback': self.feedback,
            'count': self.count
        }

        if landmark_list:
            # Пример расчета углов
            shoulder = self.get_landmark(landmark_list, 11)
            elbow = self.get_landmark(landmark_list, 13)
            wrist = self.get_landmark(landmark_list, 15)
            hip = self.get_landmark(landmark_list, 23)

            if shoulder and elbow and wrist and hip:
                elbow_angle = calculate_angle(
                    (shoulder[1], shoulder[2]),
                    (elbow[1], elbow[2]),
                    (wrist[1], wrist[2])
                )
                shoulder_angle = calculate_angle(
                    (elbow[1], elbow[2]),
                    (shoulder[1], shoulder[2]),
                    (hip[1], hip[2])
                )
                # Для hip_angle можно использовать другую ключевую точку для улучшения
                hip_angle = calculate_angle(
                    (shoulder[1], shoulder[2]),
                    (hip[1], hip[2]),
                    (hip[1], hip[2])  # Можно заменить на другую точку, например, 25 или 24
                )

                data['angles'] = {
                    'elbow': elbow_angle,
                    'shoulder': shoulder_angle,
                    'hip': hip_angle
                }

                # Подсчет отжиманий
                self.update_counters(elbow_angle, shoulder_angle, hip_angle)
                data['feedback'] = self.feedback
                data['count'] = self.count

        return data

    def get_position(self, results, height, width):
        """
        Извлекает координаты ключевых точек.

        :param results: Результаты обработки Mediapipe Pose
        :param height: Высота кадра
        :param width: Ширина кадра
        :return: Список ключевых точек
        """
        landmark_list = []
        if results.pose_landmarks:
            for id, lm in enumerate(results.pose_landmarks.landmark):
                px, py = int(lm.x * width), int(lm.y * height)
                landmark_list.append([id, px, py])
        return landmark_list

    def get_landmark(self, landmark_list, landmark_id):
        """
        Получает координаты конкретной ключевой точки.

        :param landmark_list: Список ключевых точек
        :param landmark_id: ID ключевой точки
        :return: Список [id, x, y] или None
        """
        for lm in landmark_list:
            if lm[0] == landmark_id:
                return lm
        return None

    def update_counters(self, elbow_angle, shoulder_angle, hip_angle):
        """
        Обновляет счетчики отжиманий на основе углов.

        :param elbow_angle: Угол в локте
        :param shoulder_angle: Угол в плече
        :param hip_angle: Угол в бедре
        """
        # Интерполяция угла локтя для прогресс-бара и процента успешности
        pushup_success_percentage = np.interp(elbow_angle, (90, 160), (0, 100))
        # Прогресс-бар будет рассчитан на фронтенде, поэтому можно его не отправлять

        # Проверка начальной формы
        if elbow_angle > 160 and shoulder_angle > 40 and hip_angle > 160:
            self.form = 1
        else:
            self.form = 0

        # Полный цикл выполнения отжиманий
        if self.form == 1:
            if pushup_success_percentage == 0:
                if elbow_angle <= 90 and hip_angle > 160:
                    if self.direction == 0:
                        self.count += 0.5
                        self.direction = 1
                    self.feedback = "Go Up"
                else:
                    self.feedback = "Bad Form. Correct Posture."
            elif pushup_success_percentage == 100:
                if elbow_angle > 160 and shoulder_angle > 40 and hip_angle > 160:
                    if self.direction == 1:
                        self.count += 0.5
                        self.direction = 0
                    self.feedback = "Go Down"
                else:
                    self.feedback = "Bad Form. Correct Posture."
        else:
            self.feedback = "Bad Form. Correct Posture."
```


backend/renderer.py
```py
# renderer.py

import cv2


class Renderer:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def draw_annotations(self, frame, data):
        """
        Рисует аннотации на кадре на основе переданных данных.

        :param frame: Исходный кадр изображения.
        :param data: Словарь с данными для отрисовки.
        :return: Аннотированный кадр.
        """
        # Отрисовка ключевых точек
        if data['landmarks']:
            for lm in data['landmarks']:
                cv2.circle(frame, (lm[1], lm[2]), 5, (255, 0, 0), cv2.FILLED)

        # Отрисовка прогресса отжиманий
        if 'angles' in data and data['angles']:
            elbow_angle = data['angles'].get('elbow', 0)
            pushup_success_percentage = int(np.interp(elbow_angle, (90, 160), (0, 100)))
            pushup_progress_bar = int(np.interp(elbow_angle, (90, 160), (380, 50)))

            cv2.rectangle(frame, (580, 50), (600, 380), (0, 255, 0), 3)
            cv2.rectangle(frame, (580, pushup_progress_bar), (600, 380), (0, 255, 0), cv2.FILLED)
            cv2.putText(frame, f'{pushup_success_percentage}%', (565, 430),
                        cv2.FONT_HERSHEY_PLAIN, 2, (255, 0, 0), 2)

        # Отображение счёта отжиманий
        cv2.rectangle(frame, (0, 380), (100, 480), (0, 255, 0), cv2.FILLED)
        cv2.putText(frame, str(int(data['count'])), (25, 455),
                    cv2.FONT_HERSHEY_PLAIN, 5, (255, 0, 0), 5)

        # Отображение обратной связи
        cv2.rectangle(frame, (500, 0), (640, 40), (255, 255, 255), cv2.FILLED)
        cv2.putText(frame, data['feedback'], (500, 40),
                    cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 2)

        return frame
```


backend/requirements.txt
```txt
python-multipart

tensorflow[and-cuda]
matplotlib


flask
flask-socketio
eventlet
opencv-python
mediapipe
numpy
```


backend/utils.py
```py
# backend/utils.py

import mediapipe as mp
import math

# Словарь для сопоставления ключевых точек от Mediapipe с моделью классификатора
LM_DICT = {
    0: 0, 1: 10, 2: 12, 3: 14, 4: 16, 5: 11, 6: 13, 7: 15,
    8: 24, 9: 26, 10: 28, 11: 23, 12: 25, 13: 27,
    14: 5, 15: 2, 16: 8, 17: 7,
}


def set_pose_parameters():
    mode = False
    complexity = 1
    smooth_landmarks = True
    enable_segmentation = False
    smooth_segmentation = True
    detectionCon = 0.5
    trackCon = 0.5
    mpPose = mp.solutions.pose
    return mode, complexity, smooth_landmarks, enable_segmentation, smooth_segmentation, detectionCon, trackCon, mpPose


def calculate_angle(point1, point2, point3):
    """
    Вычисляет угол между тремя точками.

    :param point1: Координаты первой точки (x1, y1)
    :param point2: Координаты второй точки (x2, y2)
    :param point3: Координаты третьей точки (x3, y3)
    :return: Угол в градусах
    """
    x1, y1 = point1
    x2, y2 = point2
    x3, y3 = point3

    angle = math.degrees(
        math.atan2(y3 - y2, x3 - x2) -
        math.atan2(y1 - y2, x1 - x2)
    )

    # Корректировка угла
    if angle < 0:
        angle += 360
    if angle > 180:
        angle = 360 - angle
    return angle
```


backend/DeepFit/DeepFitClassifier.py
```py
import tensorflow as tf
import numpy as np


# def load_X(X_path):
#     file = open(X_path, "r")
#     X_ = np.array([elem for elem in [row.split(",") for row in file]], dtype=np.float32)
#     file.close()
#     return X_


def euclidean_dist(a, b):
    # This function calculates the euclidean distance between 2 point in 2-D coordinates
    # if one of two points is (0,0), dist = 0
    # a, b: input array with dimension: m, 2
    # m: number of samples
    # 2: x and y coordinate
    try:
        if a.shape[1] == 2 and a.shape == b.shape:
            # check if element of a and b is (0,0)
            bol_a = (a[:, 0] != 0).astype(int)
            bol_b = (b[:, 0] != 0).astype(int)
            dist = np.linalg.norm(a - b, axis=1)
            return (dist * bol_a * bol_b).reshape(a.shape[0], 1)
    except:
        print("[Error]: Check dimension of input vector")
        return 0


def norm_X(X):
    num_sample = X.shape[0]
    # Keypoints
    Nose = X[:, 0 * 2 : 0 * 2 + 2]
    Neck = X[:, 1 * 2 : 1 * 2 + 2]
    RShoulder = X[:, 2 * 2 : 2 * 2 + 2]
    RElbow = X[:, 3 * 2 : 3 * 2 + 2]
    RWrist = X[:, 4 * 2 : 4 * 2 + 2]
    LShoulder = X[:, 5 * 2 : 5 * 2 + 2]
    LElbow = X[:, 6 * 2 : 6 * 2 + 2]
    LWrist = X[:, 7 * 2 : 7 * 2 + 2]
    RHip = X[:, 8 * 2 : 8 * 2 + 2]
    RKnee = X[:, 9 * 2 : 9 * 2 + 2]
    RAnkle = X[:, 10 * 2 : 10 * 2 + 2]
    LHip = X[:, 11 * 2 : 11 * 2 + 2]
    LKnee = X[:, 12 * 2 : 12 * 2 + 2]
    LAnkle = X[:, 13 * 2 : 13 * 2 + 2]
    REye = X[:, 14 * 2 : 14 * 2 + 2]
    LEye = X[:, 15 * 2 : 15 * 2 + 2]
    REar = X[:, 16 * 2 : 16 * 2 + 2]
    LEar = X[:, 17 * 2 : 17 * 2 + 2]

    # Length of head
    length_Neck_LEar = euclidean_dist(Neck, LEar)
    length_Neck_REar = euclidean_dist(Neck, REar)
    length_Neck_LEye = euclidean_dist(Neck, LEye)
    length_Neck_REye = euclidean_dist(Neck, REye)
    length_Nose_LEar = euclidean_dist(Nose, LEar)
    length_Nose_REar = euclidean_dist(Nose, REar)
    length_Nose_LEye = euclidean_dist(Nose, LEye)
    length_Nose_REye = euclidean_dist(Nose, REye)
    length_head = np.maximum.reduce(
        [
            length_Neck_LEar,
            length_Neck_REar,
            length_Neck_LEye,
            length_Neck_REye,
            length_Nose_LEar,
            length_Nose_REar,
            length_Nose_LEye,
            length_Nose_REye,
        ]
    )
    # length_head      = np.sqrt(np.square((LEye[:,0:1]+REye[:,0:1])/2 - Neck[:,0:1]) + np.square((LEye[:,1:2]+REye[:,1:2])/2 - Neck[:,1:2]))

    # Length of torso
    length_Neck_LHip = euclidean_dist(Neck, LHip)
    length_Neck_RHip = euclidean_dist(Neck, RHip)
    length_torso = np.maximum(length_Neck_LHip, length_Neck_RHip)
    # length_torso     = np.sqrt(np.square(Neck[:,0:1]-(LHip[:,0:1]+RHip[:,0:1])/2) + np.square(Neck[:,1:2]-(LHip[:,1:2]+RHip[:,1:2])/2))

    # Length of right leg
    length_leg_right = euclidean_dist(RHip, RKnee) + euclidean_dist(RKnee, RAnkle)
    # length_leg_right = np.sqrt(np.square(RHip[:,0:1]-RKnee[:,0:1]) + np.square(RHip[:,1:2]-RKnee[:,1:2])) \
    # + np.sqrt(np.square(RKnee[:,0:1]-RAnkle[:,0:1]) + np.square(RKnee[:,1:2]-RAnkle[:,1:2]))

    # Length of left leg
    length_leg_left = euclidean_dist(LHip, LKnee) + euclidean_dist(LKnee, LAnkle)
    # length_leg_left = np.sqrt(np.square(LHip[:,0:1]-LKnee[:,0:1]) + np.square(LHip[:,1:2]-LKnee[:,1:2])) \
    # + np.sqrt(np.square(LKnee[:,0:1]-LAnkle[:,0:1]) + np.square(LKnee[:,1:2]-LAnkle[:,1:2]))

    # Length of leg
    length_leg = np.maximum(length_leg_right, length_leg_left)

    # Length of body
    length_body = length_head + length_torso + length_leg

    # Check all samples have length_body of 0
    length_chk = (length_body > 0).astype(int)

    # Check keypoints at origin
    keypoints_chk = (X > 0).astype(int)

    chk = length_chk * keypoints_chk

    # Set all length_body of 0 to 1 (to avoid division by 0)
    length_body[length_body == 0] = 1

    # The center of gravity
    # number of point OpenPose locates:
    num_pts = (X[:, 0::2] > 0).sum(1).reshape(num_sample, 1)
    centr_x = X[:, 0::2].sum(1).reshape(num_sample, 1) / num_pts
    centr_y = X[:, 1::2].sum(1).reshape(num_sample, 1) / num_pts

    # The  coordinates  are  normalized relative to the length of the body and the center of gravity
    X_norm_x = (X[:, 0::2] - centr_x) / length_body
    X_norm_y = (X[:, 1::2] - centr_y) / length_body

    # Stack 1st element x and y together
    X_norm = np.column_stack((X_norm_x[:, :1], X_norm_y[:, :1]))

    for i in range(1, X.shape[1] // 2):
        X_norm = np.column_stack(
            (X_norm, X_norm_x[:, i : i + 1], X_norm_y[:, i : i + 1])
        )

    # Set all samples have length_body of 0 to origin (0, 0)
    X_norm = X_norm * chk

    return X_norm


# encodings for labels in dataset
LABELS = [
    "squats",
    "lunges",
    "bicep_curls",
    "situps",
    "pushups",
    "tricep_extensions",
    "dumbbell_rows",
    "jumping_jacks",
    "dumbbell_shoulder_press",
    "lateral_shoulder_raises",
]
class_d = {}
for i, ex in enumerate(LABELS):
    class_d[ex] = i


class DeepFitClassifier:
    def __init__(self, model_path):
        """
        Load the TFLite model and allocate tensors \n
        Get input and output tensors.
        """
        self.interpreter = tf.lite.Interpreter(model_path=model_path)
        self.interpreter.allocate_tensors()
        self.input_details = self.interpreter.get_input_details()
        self.output_details = self.interpreter.get_output_details()

    def predict(self, X) -> str:
        """
        X -> (1, 36) shaped numpy array
        First 18 are the x coordinates of the 18 keypoints \n
        next 18 are the y coordinates of the 18 keypoints \n
        Returns: \n
        Winning Label
        """
        # Setup input
        X_sample = np.array(X).reshape(1, -1)
        X_sample_norm = norm_X(X_sample)
        input_data = np.array(X_sample_norm[0], np.float32).reshape(1, 36)
        # Invoke the model on the input data
        self.interpreter.set_tensor(self.input_details[0]["index"], input_data)
        self.interpreter.invoke()
        # Get the result
        output_data = self.interpreter.get_tensor(self.output_details[0]["index"])

        np_output_data = np.array(output_data)
        champ_idx = np.argmax(np_output_data)
        self.results = dict(zip(LABELS, output_data[0]))

        return LABELS[champ_idx]

    def get_results(self) -> dict:
        """
        Get a dictionary containing the probability of each label for the last predicted input
        """
        return self.results
        # return 0


```


frontend/index.html
```html
<!-- frontend/index.html -->

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Подсчет Отжиманий в Реальном Времени</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="container">
        <h1>Подсчет Отжиманий в Реальном Времени</h1>
        <div class="video-container">
            <video id="video" autoplay muted></video>
            <canvas id="overlay"></canvas>
        </div>
        <div class="status">
            <p id="feedback">Жду начала...</p>
            <p>Повторы: <span id="count">0</span></p>
            <div class="progress-bar">
                <div id="progress"></div>
            </div>
        </div>
    </div>

    <!-- Подключение jQuery -->
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <!-- Подключение Socket.IO -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.4/socket.io.min.js"
            integrity="sha512-JgC5fCimjqBvBBbkI8RoSZ7K3PoK/sqiymsdliFWhFE+tMP0rXlyFTqzBYTI+9yad0tLXIQUxFEFEzzSzLCmLg=="
            crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <!-- Подключение скрипта -->
    <script src="script.js"></script>
</body>
</html>
```


frontend/script.js
```js
// frontend/script.js

$(document).ready(function() {
    const video = document.getElementById('video');
    const overlay = document.getElementById('overlay');
    const ctx = overlay.getContext('2d');
    const feedback = $('#feedback');
    const countDisplay = $('#count');
    const progressBar = $('#progress');

    // Подключение к WebSocket серверу
    const socket = io.connect(`http://${window.location.host}`);

    socket.on('connect', function() {
        console.log('Подключено к серверу');
    });

    socket.on('status', function(data) {
        console.log(data.status);
    });

    socket.on('result', function(data) {
        try {
            const parsedData = JSON.parse(data);
            // Обновление статуса и счётчика
            feedback.text(parsedData.feedback);
            countDisplay.text(parsedData.progress);

            // Обновление прогресс-бара
            const progressPercentage = Math.min(Math.max(parsedData.progress, 0), 100);
            progressBar.css('width', `${progressPercentage}%`);

            // Отрисовка аннотаций
            drawAnnotations(parsedData.landmarks);
        } catch (e) {
            console.error('Ошибка при обработке данных:', e);
        }
    });

    socket.on('disconnect', function() {
        console.log('Отключено от сервера');
    });

    // Функция для отрисовки аннотаций
    function drawAnnotations(landmarks) {
        // Очистка канваса
        ctx.clearRect(0, 0, overlay.width, overlay.height);

        if (landmarks && landmarks.length > 0) {
            // Преобразование списка ключевых точек в объект для удобства доступа
            const lm = {};
            landmarks.forEach(point => {
                lm[point[0]] = {x: point[1], y: point[2]};
            });

            // Нарисовать круги на ключевых точках
            for (const id in lm) {
                const x = lm[id].x;
                const y = lm[id].y;
                ctx.beginPath();
                ctx.arc(x, y, 5, 0, 2 * Math.PI);
                ctx.fillStyle = 'red';
                ctx.fill();
            }

            // Нарисовать линии между ключевыми точками (по связям Mediapipe)
            const connections = [
                [11, 13], [13, 15],  // Левая рука
                [12, 14], [14, 16],  // Правая рука
                [11, 23], [12, 24],  // Туловище
                [23, 25], [24, 26],  // Ноги
                // Добавьте другие связи по необходимости
                [11, 12], [23, 24],
                [25, 26],
                [13, 11], [14, 12],
                // Вспомогательные связи для более полной визуализации
            ];

            connections.forEach(pair => {
                const [start, end] = pair;
                if (lm[start] && lm[end]) {
                    ctx.beginPath();
                    ctx.moveTo(lm[start].x, lm[start].y);
                    ctx.lineTo(lm[end].x, lm[end].y);
                    ctx.strokeStyle = 'blue';
                    ctx.lineWidth = 2;
                    ctx.stroke();
                }
            });
        }
    }

    // Захват видео с веб-камеры
    navigator.mediaDevices.getUserMedia({ video: true, audio: false })
        .then(function(stream) {
            video.srcObject = stream;
            video.play();
            // Настройка размера канваса после загрузки видео
            video.onloadedmetadata = function() {
                overlay.width = video.videoWidth;
                overlay.height = video.videoHeight;
                // Запуск отправки кадров
                sendFrames();
            };
        })
        .catch(function(err) {
            console.log("Ошибка доступа к камере: " + err);
        });

    // Функция для отправки кадров на сервер
    function sendFrames() {
        const FPS = 10; // Частота отправки кадров

        setInterval(function() {
            // Создание временного канваса для захвата кадра
            const tempCanvas = document.createElement('canvas');
            tempCanvas.width = video.videoWidth;
            tempCanvas.height = video.videoHeight;
            const tempCtx = tempCanvas.getContext('2d');
            tempCtx.drawImage(video, 0, 0, tempCanvas.width, tempCanvas.height);
            const dataURL = tempCanvas.toDataURL('image/jpeg');

            // Отправка изображения через WebSocket
            if (socket.connected) {
                socket.emit('image', dataURL);
            }
        }, 1000 / FPS);
    }
});
```


frontend/styles.css
```css
/* frontend/styles.css */

body {
    font-family: Arial, sans-serif;
    background-color: #f0f2f5;
    text-align: center;
    margin: 0;
    padding: 0;
}

.container {
    padding: 20px;
}

h1 {
    color: #333;
}

.video-container {
    position: relative;
    display: inline-block;
}

#video {
    width: 640px;
    height: 480px;
    background-color: #000;
}

#overlay {
    position: absolute;
    top: 0;
    left: 0;
}

.status {
    margin-top: 20px;
}

#feedback {
    font-size: 1.2em;
    color: #555;
}

.progress-bar {
    width: 300px;
    height: 25px;
    background-color: #ddd;
    border-radius: 5px;
    margin: 10px auto;
    overflow: hidden;
}

#progress {
    height: 100%;
    width: 0;
    background-color: #4caf50;
    transition: width 0.5s;
}

#count {
    font-size: 2em;
    color: #333;
}
```


Вот что пишется мне в консоли после запуска main:
```
2024-11-02 19:27:57.209031: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:485] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
2024-11-02 19:27:57.249880: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:8454] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
2024-11-02 19:27:57.262311: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1452] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
2024-11-02 19:27:57.286897: I tensorflow/core/platform/cpu_feature_guard.cc:210] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: AVX2 AVX512F FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
2024-11-02 19:27:58.501709: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT
An exception was thrown while monkey_patching for eventlet. to fix this error make sure you run eventlet.monkey_patch() before importing any other modules.
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 490, in _upgrade_instances
    new = upgrade_or_traverse(v)
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 467, in upgrade_or_traverse
    _upgrade_instances(obj, klass, upgrade, visited, old_to_new)
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 481, in _upgrade_instances
    container_vars = vars(container)
  File "/usr/local/lib/python3.10/dist-packages/werkzeug/local.py", line 318, in __get__
    obj = instance._get_current_object()
  File "/usr/local/lib/python3.10/dist-packages/werkzeug/local.py", line 519, in _get_current_object
    raise RuntimeError(unbound_message) from None
RuntimeError: Working outside of application context.

This typically means that you attempted to use functionality that needed
the current application. To solve this, set up an application context
with app.app_context(). See the documentation for more information.
An exception was thrown while monkey_patching for eventlet. to fix this error make sure you run eventlet.monkey_patch() before importing any other modules.
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 490, in _upgrade_instances
    new = upgrade_or_traverse(v)
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 467, in upgrade_or_traverse
    _upgrade_instances(obj, klass, upgrade, visited, old_to_new)
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 481, in _upgrade_instances
    container_vars = vars(container)
  File "/usr/local/lib/python3.10/dist-packages/werkzeug/local.py", line 318, in __get__
    obj = instance._get_current_object()
  File "/usr/local/lib/python3.10/dist-packages/werkzeug/local.py", line 519, in _get_current_object
    raise RuntimeError(unbound_message) from None
RuntimeError: Working outside of application context.

This typically means that you attempted to use functionality that needed
the current application. To solve this, set up an application context
with app.app_context(). See the documentation for more information.
An exception was thrown while monkey_patching for eventlet. to fix this error make sure you run eventlet.monkey_patch() before importing any other modules.
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 490, in _upgrade_instances
    new = upgrade_or_traverse(v)
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 467, in upgrade_or_traverse
    _upgrade_instances(obj, klass, upgrade, visited, old_to_new)
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 481, in _upgrade_instances
    container_vars = vars(container)
  File "/usr/local/lib/python3.10/dist-packages/werkzeug/local.py", line 318, in __get__
    obj = instance._get_current_object()
  File "/usr/local/lib/python3.10/dist-packages/werkzeug/local.py", line 519, in _get_current_object
    raise RuntimeError(unbound_message) from None
RuntimeError: Working outside of request context.

This typically means that you attempted to use functionality that needed
an active HTTP request. Consult the documentation on testing for
information about how to avoid this problem.
An exception was thrown while monkey_patching for eventlet. to fix this error make sure you run eventlet.monkey_patch() before importing any other modules.
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 490, in _upgrade_instances
    new = upgrade_or_traverse(v)
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 467, in upgrade_or_traverse
    _upgrade_instances(obj, klass, upgrade, visited, old_to_new)
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 481, in _upgrade_instances
    container_vars = vars(container)
  File "/usr/local/lib/python3.10/dist-packages/werkzeug/local.py", line 318, in __get__
    obj = instance._get_current_object()
  File "/usr/local/lib/python3.10/dist-packages/werkzeug/local.py", line 519, in _get_current_object
    raise RuntimeError(unbound_message) from None
RuntimeError: Working outside of request context.

This typically means that you attempted to use functionality that needed
an active HTTP request. Consult the documentation on testing for
information about how to avoid this problem.
An exception was thrown while monkey_patching for eventlet. to fix this error make sure you run eventlet.monkey_patch() before importing any other modules.
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 490, in _upgrade_instances
    new = upgrade_or_traverse(v)
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 467, in upgrade_or_traverse
    _upgrade_instances(obj, klass, upgrade, visited, old_to_new)
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 481, in _upgrade_instances
    container_vars = vars(container)
  File "/usr/local/lib/python3.10/dist-packages/werkzeug/local.py", line 318, in __get__
    obj = instance._get_current_object()
  File "/usr/local/lib/python3.10/dist-packages/werkzeug/local.py", line 519, in _get_current_object
    raise RuntimeError(unbound_message) from None
RuntimeError: Working outside of application context.

This typically means that you attempted to use functionality that needed
the current application. To solve this, set up an application context
with app.app_context(). See the documentation for more information.
An exception was thrown while monkey_patching for eventlet. to fix this error make sure you run eventlet.monkey_patch() before importing any other modules.
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 490, in _upgrade_instances
    new = upgrade_or_traverse(v)
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 467, in upgrade_or_traverse
    _upgrade_instances(obj, klass, upgrade, visited, old_to_new)
  File "/usr/local/lib/python3.10/dist-packages/eventlet/patcher.py", line 481, in _upgrade_instances
    container_vars = vars(container)
  File "/usr/local/lib/python3.10/dist-packages/werkzeug/local.py", line 318, in __get__
    obj = instance._get_current_object()
  File "/usr/local/lib/python3.10/dist-packages/werkzeug/local.py", line 519, in _get_current_object
    raise RuntimeError(unbound_message) from None
RuntimeError: Working outside of request context.

This typically means that you attempted to use functionality that needed
an active HTTP request. Consult the documentation on testing for
information about how to avoid this problem.
1 RLock(s) were not greened, to fix this error make sure you run eventlet.monkey_patch() before importing any other modules.
Классификатор DeepFit не найден по пути: DeepFit/deepfit_classifier_v3.tflite
INFO: Created TensorFlow Lite XNNPACK delegate for CPU.
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
W0000 00:00:1730575681.307470    6107 inference_feedback_manager.cc:114] Feedback manager requires a model with a single signature inference. Disabling support for feedback tensors.
Your ngrok-agent version "2.3.41" is too old. The minimum supported agent version for your account is "3.2.0". Please update to a newer version with `ngrok update`, by downloading from https://ngrok.com/download, or by updating your SDK version. Paid accounts are currently excluded from minimum agent version requirements. To begin handling traffic immediately without updating your agent, upgrade to a paid plan: https://dashboard.ngrok.com/billing/subscription.

ERR_NGROK_121

W0000 00:00:1730575681.390890    6108 inference_feedback_manager.cc:114] Feedback manager requires a model with a single signature inference. Disabling support for feedback tensors.
```