Project structure:

backend/
├── DeepFit/
│   ├── DeepFitClassifier.py
│   └── deepfit_classifier_v3.tflite
├── static/
│   ├── script.js
│   └── styles.css
├── templates/
│   └── index.html
├── main.py
├── pushup_counter.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'] = 'your_secret_key'  # Замените на ваш секретный ключ
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',
            'count': int(result.get('count', 0)),
            'landmarks': result.get('landmarks', []),
            'angles': result.get('angles', {}),
            'feedback': result.get('feedback', ''),
            'frame_width': frame.shape[1],
            'frame_height': frame.shape[0]
        }

        # Отправка данных обратно клиенту без двойного кодирования
        emit('result', 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("<TRUFFLEHOG_REDACTED_NGROK>")  # Замените на ваш токен ngrok
    # Открыть туннель 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)
            knee = self.get_landmark(landmark_list, 25)  # Левое колено

            if shoulder and elbow and wrist and hip and knee:
                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 = calculate_angle(
                    (shoulder[1], shoulder[2]),
                    (hip[1], hip[2]),
                    (knee[1], knee[2])
                )

                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: Угол в бедре
        """
        # Логирование углов для отладки
        print(f"Elbow Angle: {elbow_angle:.2f}, Shoulder Angle: {shoulder_angle:.2f}, Hip Angle: {hip_angle:.2f}")

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

        # Проверка начальной формы
        # Уменьшаем порог плечевого угла для большей гибкости
        if elbow_angle > 160 and shoulder_angle > 20 and hip_angle > 160:
            self.form = 1
        else:
            self.form = 0

        # Полный цикл выполнения отжиманий
        if self.form == 1:
            if pushup_success_percentage <= 10:
                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 >= 90:
                if elbow_angle > 160 and shoulder_angle > 20 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 = "Keep Going"
        else:
            self.feedback = "Bad Form. Correct Posture."

        # Логирование состояния счётчика для отладки
        print(f"Count: {self.count}, Direction: {self.direction}, Feedback: {self.feedback}")
```


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 = data;

                if(parsedData.status === 'processing') {
                    // Обновление обратной связи и счётчика
                    feedback.text(parsedData.feedback);
                    countDisplay.text(parsedData.count);

                    // Изменение цвета обратной связи в зависимости от состояния
                    if(parsedData.feedback.startsWith("Go Up") || parsedData.feedback.startsWith("Go Down")) {
                        feedback.css('color', 'green');
                    } else if(parsedData.feedback.startsWith("Bad")) {
                        feedback.css('color', 'red');
                    } else if(parsedData.feedback.startsWith("Keep")) {
                        feedback.css('color', 'orange');
                    } else {
                        feedback.css('color', 'black');
                    }

                    // Вычисление процента успешности на основе углов
                    const elbowAngle = parsedData.angles.elbow || 0;
                    const pushupSuccessPercentage = Math.min(Math.max(((elbowAngle - 90) / 70) * 100, 0), 100);
                    progressBar.css('width', `${pushupSuccessPercentage}%`);

                    // Масштабирование координат ключевых точек
                    const scaleX = overlay.width / parsedData.frame_width;
                    const scaleY = overlay.height / parsedData.frame_height;
                    const adjustedLandmarks = parsedData.landmarks.map(lm => ({
                        id: lm[0],
                        x: lm[1] * scaleX,
                        y: lm[2] * scaleY
                    }));

                    // Отрисовка аннотаций
                    drawAnnotations(adjustedLandmarks);
                }

            } catch (e) {
                console.error('Ошибка при обработке данных:', e);
            }
        });

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

        // Захват видео с веб-камеры с заданными ограничениями
        navigator.mediaDevices.getUserMedia({
            video: {
                width: { ideal: 640 },
                height: { ideal: 480 }
            },
            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.id] = {x: point.x, y: point.y};
            });

            // Нарисовать линии по связям 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]              // Ноги горизонтально
                // Добавьте другие связи по необходимости
            ];

            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();
                }
            });

            // Нарисовать круги на ключевых точках
            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();
            }
        }
    }

    // Обработчик нажатия на кнопку "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;
    max-width: 100%;
    width: 640px;
    height: 480px;
    background-color: #000;
}

#video {
    width: 100%;
    height: 100%;
    background-color: #000;
    object-fit: cover; /* Обеспечивает заполнение канваса без искажений */
}

#overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none; /* Обеспечивает, что канвас не перехватывает события */
}

.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" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <!-- Подключение скрипта -->
    <script src="../static/script.js"></script>
</body>
</html>
```


А вот исходный код, где все, вроже как, работает:
```
import cv2
import mediapipe as mp
import numpy as np
import math



cap = cv2.VideoCapture(0)
count = 0
direction = 0
form = 0
feedback = "Bad Form. Correct Posture."



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


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)

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, draw=True):
        landmark_list = []
        if results.pose_landmarks:
            for id, landmark in enumerate(results.pose_landmarks.landmark):
                #finding height, width of the image printed
                height, width, c = img.shape
                #Determining the pixels of the landmarks
                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
        x1, y1 = landmark_list[point1][1:]
        x2, y2 = landmark_list[point2][1:]
        x3, y3 = landmark_list[point3][1:]
        
        
        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
    
    
    
while cap.isOpened():
    #Getting image from camera
    ret, img = cap.read() 
    #Getting video dimensions
    width  = cap.get(3)  
    height = cap.get(4)  
    
    #Convert from BGR (used by cv2) to RGB (used by Mediapipe)
    results = pose.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    
    #Get pose and draw landmarks
    img = get_pose(img, results, False)
    
    landmark_list = get_position(img, False)
    #If landmarks exist, get the elbow, shoulder and hip. The points used are identifiers for specific joints
    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))

        #Is the form correct at the start?
        if elbow_angle > 160 and shoulder_angle > 40 and hip_angle > 160:
            form = 1
    
        #Full pushup motion
        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(count)
        
        #Draw the pushup progress bar
        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)


        #Show the pushup count
        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)
        
        #Show the pushup feedback 
        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)

        
    cv2.imshow('Pushup Counter', img)
    if cv2.waitKey(10) & 0xFF == ord('q'):
        break
        
cap.release()
cv2.destroyAllWindows()
```

Почему у меня постоянно пишется "Bad Form. Correct Posture.", хотя скелет полностью виден в кадре и я стою правильно.