|
import streamlit as st |
|
import cv2 |
|
import numpy as np |
|
from streamlit_webrtc import webrtc_streamer, VideoProcessorBase |
|
import mediapipe as mp |
|
import av |
|
from collections import deque |
|
|
|
|
|
mp_pose = mp.solutions.pose |
|
mp_drawing = mp.solutions.drawing_utils |
|
|
|
class PoseProcessor(VideoProcessorBase): |
|
def __init__(self): |
|
self.pose = mp_pose.Pose(min_detection_confidence=0.5, |
|
min_tracking_confidence=0.5, |
|
model_complexity=1) |
|
self.status_deque = deque(maxlen=1) |
|
|
|
def analyze_posture(self, landmarks, image_shape): |
|
h, w, _ = image_shape |
|
left_shoulder = landmarks.landmark[mp_pose.PoseLandmark.LEFT_SHOULDER] |
|
right_shoulder = landmarks.landmark[mp_pose.PoseLandmark.RIGHT_SHOULDER] |
|
left_hip = landmarks.landmark[mp_pose.PoseLandmark.LEFT_HIP] |
|
right_hip = landmarks.landmark[mp_pose.PoseLandmark.RIGHT_HIP] |
|
left_ear = landmarks.landmark[mp_pose.PoseLandmark.LEFT_EAR] |
|
right_ear = landmarks.landmark[mp_pose.PoseLandmark.RIGHT_EAR] |
|
nose = landmarks.landmark[mp_pose.PoseLandmark.NOSE] |
|
|
|
sitting = left_hip.y < left_shoulder.y + 0.1 or right_hip.y < right_shoulder.y + 0.1 |
|
|
|
messages = [] |
|
|
|
head_forward = (left_ear.y > left_shoulder.y + 0.1 or right_ear.y > right_shoulder.y + 0.1) and \ |
|
(nose.y > left_shoulder.y or nose.y > right_shoulder.y) |
|
if head_forward: |
|
messages.append("• Голова наклонена вперед (текстовая шея)") |
|
|
|
shoulders_rounded = left_shoulder.x > left_hip.x + 0.05 or right_shoulder.x < right_hip.x - 0.05 |
|
if shoulders_rounded: |
|
messages.append("• Плечи ссутулены (округлены вперед)") |
|
|
|
shoulder_diff = abs(left_shoulder.y - right_shoulder.y) |
|
hip_diff = abs(left_hip.y - right_hip.y) |
|
if shoulder_diff > 0.05 or hip_diff > 0.05: |
|
messages.append("• Наклон в сторону (несимметричная осанка)") |
|
|
|
if sitting and (left_hip.y < left_shoulder.y + 0.15 or right_hip.y < right_shoulder.y + 0.15): |
|
messages.append("• Таз наклонен вперед (сидя)") |
|
|
|
if messages: |
|
report = [ |
|
f"**{'Сидя' if sitting else 'Стоя'} - обнаружены проблемы:**", |
|
*messages, |
|
"\n**Рекомендации:**", |
|
"• Держите голову прямо, уши должны быть над плечами", |
|
"• Отведите плечи назад и вниз", |
|
"• Держите спину прямой, избегайте наклонов в стороны", |
|
"• При сидении опирайтесь на седалищные бугры" |
|
] |
|
else: |
|
report = [ |
|
f"**Отличная осанка ({'сидя' if sitting else 'стоя'})!**", |
|
"Все ключевые точки находятся в правильном положении.", |
|
"\n**Совет:**", |
|
"• Продолжайте следить за осанкой в течение дня" |
|
] |
|
|
|
return "\n\n".join(report) |
|
|
|
def recv(self, frame: av.VideoFrame) -> av.VideoFrame: |
|
img = frame.to_ndarray(format="bgr24") |
|
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) |
|
|
|
results = self.pose.process(img_rgb) |
|
if results.pose_landmarks: |
|
|
|
mp_drawing.draw_landmarks( |
|
img, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, |
|
mp_drawing.DrawingSpec(color=(0, 255, 0), thickness=2, circle_radius=2), |
|
mp_drawing.DrawingSpec(color=(0, 0, 255), thickness=2) |
|
) |
|
status = self.analyze_posture(results.pose_landmarks, img.shape) |
|
self.status_deque.append(status) |
|
else: |
|
self.status_deque.append("Ключевые точки не обнаружены") |
|
|
|
return av.VideoFrame.from_ndarray(img, format="bgr24") |
|
|
|
def main(): |
|
st.set_page_config(layout="wide") |
|
st.title("📷 Анализатор осанки с веб-камеры (WebRTC)") |
|
st.write("Приложение анализирует вашу осанку в реальном времени с помощью камеры браузера.") |
|
|
|
|
|
webrtc_ctx = webrtc_streamer( |
|
key="pose-analyzer", |
|
video_processor_factory=PoseProcessor, |
|
media_stream_constraints={"video": True, "audio": False}, |
|
async_processing=True |
|
) |
|
|
|
|
|
if webrtc_ctx.video_processor: |
|
|
|
if webrtc_ctx.video_processor.status_deque: |
|
analysis_text = webrtc_ctx.video_processor.status_deque[-1] |
|
else: |
|
analysis_text = "Ожидание видео и анализа..." |
|
|
|
st.markdown("### Анализ осанки:") |
|
st.markdown(analysis_text) |
|
|
|
if __name__ == "__main__": |
|
main() |