Project structure:

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


Project files:

main.py
```py
# backend/main.py

import eventlet

eventlet.monkey_patch()

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 json
from pyngrok import ngrok

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!' #  ВАЖНО: добавить секретный ключ
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.set_auth_token("<NGROK_AUTHTOKEN>")
    # Открыть туннель ngrok
    public_url = ngrok.connect(5000)
    print(f"Public URL: {public_url}")

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

```


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."
```


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
```


requirements.txt
```txt
python-multipart

tensorflow[and-cuda]
matplotlib


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


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
```


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


```


static/script.js
```js
// backend/static/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');
    const activateButton = $('#activate-camera');
    const videoContainer = $('.video-container');
    const statusContainer = $('.status');

    let socket;

    // Функция для инициализации Socket.IO и захвата видео
    function initializeApp() {
        // Подключение к WebSocket серверу
        socket = io.connect();

        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('Отключено от сервера');
        });

        // Захват видео с веб-камеры
        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);
                alert("Не удалось получить доступ к камере. Пожалуйста, попробуйте еще раз.");
            });

        // Отображение видео и статуса
        videoContainer.show();
        statusContainer.show();
        activateButton.hide();
    }

    // Функция для отправки кадров на сервер
    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);
    }

    // Функция для отрисовки аннотаций
    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();
                }
            });
        }
    }

    // Обработчик нажатия на кнопку "Activate Camera"
    activateButton.on('click', function() {
        initializeApp();
    });
});
```


static/styles.css
```css
/* backend/static/styles.css */

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

.container {
    padding: 20px;
}

h1 {
    color: #333;
}

.activate-button {
    padding: 10px 20px;
    font-size: 1.2em;
    background-color: #4caf50;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    margin-bottom: 20px;
}

.activate-button:hover {
    background-color: #45a049;
}

.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;
}
```


templates/index.html
```html
<!-- backend/templates/index.html -->

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Подсчет Отжиманий в Реальном Времени</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
    <div class="container">
        <h1>Подсчет Отжиманий в Реальном Времени</h1>
        <button id="activate-camera" class="activate-button">Activate Camera</button>
        <div class="video-container" style="display: none;">
            <video id="video" autoplay muted></video>
            <canvas id="overlay"></canvas>
        </div>
        <div class="status" style="display: none;">
            <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="../static/script.js"></script>
</body>
</html>
```


Привет, я разрабатываю нейросеть на python, которая бы помогала тренироваться человеку.
Она бы смотрела в реальном времени через камеру то, как он тренеруется, после чего оанализировала корректность упражнений и подсчитывала подходы.

Вот код, который относительно рабтал с видео:
```py
# 1. Установка необходимых библиотек
!pip install mediapipe opencv-python-headless

# 2. Импорт необходимых библиотек
import cv2
import mediapipe as mp
import numpy as np
import math
from collections import deque
from google.colab import files
import tempfile
import os
from IPython.display import HTML
from base64 import b64encode
import zipfile
import io

# 3. Загрузка и распаковка папки DeepFit и видеофайла
print("Пожалуйста, загрузите ZIP-архив папки DeepFit и видеофайл (например, input_video.mp4).")
uploaded = files.upload()

# Предполагается, что были загружены:
# - deepfit.zip (содержит папку DeepFit)
# - input_video.mp4

# Разархивируем DeepFit
for file in uploaded.keys():
    if file.endswith('.zip'):
        print(f"Распаковка {file}...")
        with zipfile.ZipFile(io.BytesIO(uploaded[file]), 'r') as zip_ref:
            zip_ref.extractall('.')
        print("Распаковка завершена.")

# Поиск видеофайла среди загруженных файлов
video_extensions = ['.mp4', '.avi', '.mov', '.mkv']
video_files = [filename for filename in uploaded.keys() if any(filename.lower().endswith(ext) for ext in video_extensions)]

if not video_files:
    raise ValueError("Не найдено загруженных видеофайлов. Пожалуйста, загрузите видеофайл с расширением .mp4, .avi, .mov или .mkv")
else:
    video_path = video_files[0]
    print(f"Используется видеофайл: {video_path}")

# Проверка наличия необходимого содержимого в папке DeepFit
required_files = ['DeepFitClassifier.py', 'deepfit_classifier_v3.tflite']
for req_file in required_files:
    deepfit_path = os.path.join('DeepFit', req_file)
    if not os.path.exists(deepfit_path):
        raise FileNotFoundError(f"Не найден файл: {deepfit_path}. Пожалуйста, убедитесь, что все необходимые файлы находятся в папке DeepFit.")

# Добавление папки DeepFit в системный путь для импорта
import sys
sys.path.append('DeepFit')

# Импорт DeepFitClassifier после загрузки и распаковки
from DeepFitClassifier import DeepFitClassifier

# 4. Определение всех необходимых функций и параметров

# Mapping dictionary to map keypoints from Mediapipe to our Classifier model
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 get_pose(img, results, draw=True):
    if results.pose_landmarks:
        if draw:
            mpDraw = mp.solutions.drawing_utils
            mpDraw.draw_landmarks(img, results.pose_landmarks, mpPose.POSE_CONNECTIONS)
    return img

def get_position(img, results, height, width, draw=True):
    landmark_list = []
    if results.pose_landmarks:
        for id, landmark in enumerate(results.pose_landmarks.landmark):
            # finding height, width of the image printed
            landmark_pixel_x, landmark_pixel_y = int(landmark.x * width), int(landmark.y * height)
            landmark_list.append([id, landmark_pixel_x, landmark_pixel_y])
            if draw:
                cv2.circle(img, (landmark_pixel_x, landmark_pixel_y), 5, (255, 0, 0), cv2.FILLED)
    return landmark_list

def get_angle(img, landmark_list, point1, point2, point3, draw=True):
    # Retrieve landmark coordinates from point identifiers
    try:
        x1, y1 = landmark_list[point1][1:]
        x2, y2 = landmark_list[point2][1:]
        x3, y3 = landmark_list[point3][1:]
    except IndexError:
        return 0  # Возвращать 0, если ключевая точка отсутствует

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

    # Handling angle edge cases: Obtuse and negative angles
    if angle < 0:
        angle += 360
        if angle > 180:
            angle = 360 - angle
    elif angle > 180:
        angle = 360 - angle

    if draw:
        # Drawing lines between the three points
        cv2.line(img, (x1, y1), (x2, y2), (255,255,255), 3)
        cv2.line(img, (x3, y3), (x2, y2), (255,255,255), 3)

        # Drawing circles at intersection points of lines
        cv2.circle(img, (x1, y1), 5, (75,0,130), cv2.FILLED)
        cv2.circle(img, (x1, y1), 15, (75,0,130), 2)
        cv2.circle(img, (x2, y2), 5, (75,0,130), cv2.FILLED)
        cv2.circle(img, (x2, y2), 15, (75,0,130), 2)
        cv2.circle(img, (x3, y3), 5, (75,0,130), cv2.FILLED)
        cv2.circle(img, (x3, y3), 15, (75,0,130), 2)

        # Show angles between lines
        cv2.putText(img, str(int(angle)), (x2-50, y2+50),
                    cv2.FONT_HERSHEY_PLAIN, 2, (0,0,255), 2)
    return angle

def convert_mediapipe_keypoints_for_model(lm_dict, landmark_list):
    inp_pushup = []
    for index in range(0, 36):
        if index < 18:
            try:
                inp_pushup.append(round(landmark_list[lm_dict[index]][1], 3))
            except IndexError:
                inp_pushup.append(0.0)
        else:
            try:
                inp_pushup.append(round(landmark_list[lm_dict[index - 18]][2], 3))
            except IndexError:
                inp_pushup.append(0.0)
    return inp_pushup

# Предположим, что вы хотите использовать классификатор DeepFit для определения типа упражнения
# Если этот функционал не нужен, можно проигнорировать соответствующие части кода

def set_video_feed_variables(video_path):
    # Используем загруженный видеофайл
    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        raise IOError(f"Не удалось открыть видеофайл: {video_path}")
    count = 0
    direction = 0
    form = 0
    feedback = "Bad Form. Correct Posture."
    frame_queue = deque(maxlen=250)
    clf = DeepFitClassifier('DeepFit/deepfit_classifier_v3.tflite')  # Убедитесь, что путь корректный
    return cap, count, direction, form, feedback, frame_queue, clf

def main():
    # Настройка параметров позы
    mode, complexity, smooth_landmarks, enable_segmentation, smooth_segmentation, detectionCon, trackCon, mpPose = set_pose_parameters()
    pose = mpPose.Pose(mode, complexity, smooth_landmarks,
                       enable_segmentation, smooth_segmentation,
                       detectionCon, trackCon)

    # Настройка видеофида
    cap, count, direction, form, feedback, frame_queue, clf = set_video_feed_variables(video_path)

    # Настройка параметров видео для сохранения результата
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    output_filename = 'output_video.mp4'
    fps = cap.get(cv2.CAP_PROP_FPS)
    if fps == 0:
        fps = 30  # Значение по умолчанию, если не удалось получить FPS
        print("Не удалось определить FPS видео. Используется значение по умолчанию: 30")
    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    out = cv2.VideoWriter(output_filename, fourcc, fps, (width, height))

    frame_count = 0
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    print(f"Total frames in input video: {total_frames}")

    while cap.isOpened():
        # Получение кадра из видео
        ret, img = cap.read()
        if not ret:
            break
        frame_count += 1
        print(f"Processing frame {frame_count} of {total_frames}", end='\r')

        # Конвертация из BGR (используемого cv2) в RGB (используемого Mediapipe)
        results = pose.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))

        # Получение позы и рисование ключевых точек
        img = get_pose(img, results, False)

        # Получение списка ключевых точек
        landmark_list = get_position(img, results, height, width, False)

        # Если ключевые точки существуют, получить соответствующие углы и выполнить подсчёт повторений
        if len(landmark_list) != 0:
            elbow_angle = get_angle(img, landmark_list, 11, 13, 15)
            shoulder_angle = get_angle(img, landmark_list, 13, 11, 23)
            hip_angle = get_angle(img, landmark_list, 11, 23, 25)

            # Интерполяция угла локтя для прогресс-бара и процента успешности
            pushup_success_percentage = np.interp(elbow_angle, (90, 160), (0, 100))
            pushup_progress_bar = np.interp(elbow_angle, (90, 160), (380, 50))

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

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

                if pushup_success_percentage == 100:
                    if elbow_angle > 160 and shoulder_angle > 40 and hip_angle > 160:
                        feedback = "Go Down"
                        if direction == 1:
                            count += 0.5
                            direction = 0
                    else:
                        feedback = "Bad Form. Correct Posture."

            print(f"Reps: {int(count)}")

            # Рисование прогресс-бара отжиманий
            if form == 1:
                cv2.rectangle(img, (580, 50), (600, 380), (0, 255, 0), 3)
                cv2.rectangle(img, (580, int(pushup_progress_bar)), (600, 380), (0, 255, 0), cv2.FILLED)
                cv2.putText(img, f'{int(pushup_success_percentage)}%', (565, 430), cv2.FONT_HERSHEY_PLAIN, 2,
                            (255, 0, 0), 2)

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

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

        # Запись аннотированного кадра в выходной файл
        out.write(img)

    # Завершение обработки видео
    cap.release()
    out.release()
    print("\nProcessing complete.")

    # Функция для отображения видео в Colab
    def display_video(video_path):
        mp4 = open(video_path, 'rb').read()
        data_url = "data:video/mp4;base64," + b64encode(mp4).decode()
        return HTML(f'''
            <video width="800" height="600" controls>
                <source src="{data_url}" type="video/mp4">
            </video>
        ''')

    # Проверка наличия выходного файла
    if os.path.exists(output_filename):
        # Отображение выходного видео
        display_video(output_filename)
    else:
        print(f"Не удалось создать выходной файл видео: {output_filename}")

if __name__ == "__main__":
    main()
```

Однако, я превратил это все в сервер, который принимает на вход изображения и возвращает все необходимое, чтобы отрисовывать всю информацию на фронтенде, однако, что-то не так:
- Отрисовка скелета не выровнена с изображением камеры, наблюдается offset
- Он совершенно не подсчитывает количество подходов
- Он должен писать, хорошо ли делается упражнение или плохо.

Можешь просмотреть что тут не так?