Spaces:
Sleeping
Sleeping
Upload 12 files
Browse files- .gitattributes +1 -0
- .replit +38 -0
- animation_exporter.py +105 -0
- animation_renderer.py +61 -0
- app.py +304 -0
- database.py +61 -0
- generated-icon.png +3 -0
- pose_detector.py +271 -0
- pyproject.toml +13 -0
- replit.nix +7 -0
- skeleton_generator.py +81 -0
- utils.py +178 -0
- uv.lock +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
generated-icon.png filter=lfs diff=lfs merge=lfs -text
|
.replit
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
modules = ["python-3.11", "postgresql-16"]
|
| 2 |
+
|
| 3 |
+
[nix]
|
| 4 |
+
channel = "stable-24_05"
|
| 5 |
+
|
| 6 |
+
[workflows]
|
| 7 |
+
runButton = "Project"
|
| 8 |
+
|
| 9 |
+
[[workflows.workflow]]
|
| 10 |
+
name = "Project"
|
| 11 |
+
mode = "parallel"
|
| 12 |
+
author = "agent"
|
| 13 |
+
|
| 14 |
+
[[workflows.workflow.tasks]]
|
| 15 |
+
task = "workflow.run"
|
| 16 |
+
args = "Pose Detection App"
|
| 17 |
+
|
| 18 |
+
[[workflows.workflow]]
|
| 19 |
+
name = "Pose Detection App"
|
| 20 |
+
author = "agent"
|
| 21 |
+
|
| 22 |
+
[workflows.workflow.metadata]
|
| 23 |
+
agentRequireRestartOnSave = false
|
| 24 |
+
|
| 25 |
+
[[workflows.workflow.tasks]]
|
| 26 |
+
task = "packager.installForAll"
|
| 27 |
+
|
| 28 |
+
[[workflows.workflow.tasks]]
|
| 29 |
+
task = "shell.exec"
|
| 30 |
+
args = "streamlit run app.py --server.port 8501"
|
| 31 |
+
waitForPort = 8501
|
| 32 |
+
|
| 33 |
+
[deployment]
|
| 34 |
+
run = ["sh", "-c", "streamlit run app.py --server.port 8501"]
|
| 35 |
+
|
| 36 |
+
[[ports]]
|
| 37 |
+
localPort = 8501
|
| 38 |
+
externalPort = 80
|
animation_exporter.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import struct
|
| 3 |
+
|
| 4 |
+
class AnimationExporter:
|
| 5 |
+
SUPPORTED_FORMATS = ['uasset', 'fbx', 'bvh']
|
| 6 |
+
|
| 7 |
+
def __init__(self):
|
| 8 |
+
self.fps = 30
|
| 9 |
+
self.unreal_scale = 100.0 # Convert to Unreal units (cm)
|
| 10 |
+
self.export_format = 'uasset'
|
| 11 |
+
|
| 12 |
+
def set_export_format(self, format_name: str):
|
| 13 |
+
if format_name.lower() not in self.SUPPORTED_FORMATS:
|
| 14 |
+
raise ValueError(f"Unsupported format. Must be one of {self.SUPPORTED_FORMATS}")
|
| 15 |
+
self.export_format = format_name.lower()
|
| 16 |
+
|
| 17 |
+
def export_pose(self, skeleton_data):
|
| 18 |
+
"""
|
| 19 |
+
Export single pose to Unreal Engine compatible format
|
| 20 |
+
"""
|
| 21 |
+
if not skeleton_data:
|
| 22 |
+
return None
|
| 23 |
+
|
| 24 |
+
# Create animation data structure
|
| 25 |
+
animation_data = {
|
| 26 |
+
'version': 1,
|
| 27 |
+
'skeleton_name': 'PoseSkeletalMesh',
|
| 28 |
+
'bones': []
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
# Convert skeleton data to Unreal format
|
| 32 |
+
for bone_name, bone_data in skeleton_data.items():
|
| 33 |
+
bone = {
|
| 34 |
+
'name': bone_name,
|
| 35 |
+
'position': self._convert_position(bone_data['position']),
|
| 36 |
+
'rotation': self._convert_rotation(bone_data['rotation'])
|
| 37 |
+
}
|
| 38 |
+
animation_data['bones'].append(bone)
|
| 39 |
+
|
| 40 |
+
# Convert to binary format
|
| 41 |
+
return self._to_binary(animation_data)
|
| 42 |
+
|
| 43 |
+
def export_animation(self, animation_frames):
|
| 44 |
+
"""
|
| 45 |
+
Export animation sequence to Unreal Engine compatible format
|
| 46 |
+
"""
|
| 47 |
+
if not animation_frames:
|
| 48 |
+
return None
|
| 49 |
+
|
| 50 |
+
# Create animation sequence data structure
|
| 51 |
+
animation_data = {
|
| 52 |
+
'version': 1,
|
| 53 |
+
'skeleton_name': 'AnimationSkeletalMesh',
|
| 54 |
+
'frame_rate': self.fps,
|
| 55 |
+
'frames': []
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
# Convert each frame
|
| 59 |
+
for frame in animation_frames:
|
| 60 |
+
frame_data = []
|
| 61 |
+
for bone_name, bone_data in frame.items():
|
| 62 |
+
bone = {
|
| 63 |
+
'name': bone_name,
|
| 64 |
+
'position': self._convert_position(bone_data['position']),
|
| 65 |
+
'rotation': self._convert_rotation(bone_data['rotation'])
|
| 66 |
+
}
|
| 67 |
+
frame_data.append(bone)
|
| 68 |
+
animation_data['frames'].append(frame_data)
|
| 69 |
+
|
| 70 |
+
# Convert to binary format
|
| 71 |
+
return self._to_binary(animation_data)
|
| 72 |
+
|
| 73 |
+
def _convert_position(self, position):
|
| 74 |
+
"""
|
| 75 |
+
Convert position to Unreal Engine coordinate system
|
| 76 |
+
"""
|
| 77 |
+
return [
|
| 78 |
+
position[0] * self.unreal_scale,
|
| 79 |
+
-position[2] * self.unreal_scale,
|
| 80 |
+
position[1] * self.unreal_scale
|
| 81 |
+
]
|
| 82 |
+
|
| 83 |
+
def _convert_rotation(self, rotation):
|
| 84 |
+
"""
|
| 85 |
+
Convert rotation to Unreal Engine coordinate system (quaternion)
|
| 86 |
+
"""
|
| 87 |
+
# Simple conversion - can be enhanced for better accuracy
|
| 88 |
+
return [
|
| 89 |
+
rotation[0],
|
| 90 |
+
-rotation[2],
|
| 91 |
+
rotation[1],
|
| 92 |
+
1.0 # w component
|
| 93 |
+
]
|
| 94 |
+
|
| 95 |
+
def _to_binary(self, data):
|
| 96 |
+
"""
|
| 97 |
+
Convert animation data to binary format
|
| 98 |
+
"""
|
| 99 |
+
# Create binary header
|
| 100 |
+
header = struct.pack('4s2I', b'UEAN', 1, len(json.dumps(data)))
|
| 101 |
+
|
| 102 |
+
# Convert data to JSON and then to bytes
|
| 103 |
+
json_data = json.dumps(data).encode('utf-8')
|
| 104 |
+
|
| 105 |
+
return header + json_data
|
animation_renderer.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
from typing import List, Dict, Tuple
|
| 4 |
+
|
| 5 |
+
@dataclass
|
| 6 |
+
class KeyFrame:
|
| 7 |
+
timestamp: float
|
| 8 |
+
landmarks: np.ndarray
|
| 9 |
+
connections: List[Tuple[int, int]]
|
| 10 |
+
|
| 11 |
+
class AnimationRenderer:
|
| 12 |
+
def __init__(self, fps: int = 30):
|
| 13 |
+
self.fps = fps
|
| 14 |
+
self.keyframes: List[KeyFrame] = []
|
| 15 |
+
self.current_frame = 0
|
| 16 |
+
self.total_frames = 0
|
| 17 |
+
|
| 18 |
+
def add_keyframe(self, landmarks: np.ndarray, connections: List[Tuple[int, int]], timestamp: float):
|
| 19 |
+
"""Add a new keyframe to the animation sequence"""
|
| 20 |
+
keyframe = KeyFrame(timestamp=timestamp, landmarks=landmarks, connections=connections)
|
| 21 |
+
self.keyframes.append(keyframe)
|
| 22 |
+
self.total_frames = max(self.total_frames, int(timestamp * self.fps))
|
| 23 |
+
|
| 24 |
+
def interpolate_poses(self, start_frame: KeyFrame, end_frame: KeyFrame, alpha: float) -> np.ndarray:
|
| 25 |
+
"""Interpolate between two poses using linear interpolation"""
|
| 26 |
+
return start_frame.landmarks + alpha * (end_frame.landmarks - start_frame.landmarks)
|
| 27 |
+
|
| 28 |
+
def get_frame_at_time(self, time: float) -> Tuple[np.ndarray, List[Tuple[int, int]]]:
|
| 29 |
+
"""Get interpolated frame at specified time"""
|
| 30 |
+
if not self.keyframes:
|
| 31 |
+
return None, []
|
| 32 |
+
|
| 33 |
+
# Find surrounding keyframes
|
| 34 |
+
next_idx = 0
|
| 35 |
+
for i, kf in enumerate(self.keyframes):
|
| 36 |
+
if kf.timestamp > time:
|
| 37 |
+
next_idx = i
|
| 38 |
+
break
|
| 39 |
+
|
| 40 |
+
if next_idx == 0:
|
| 41 |
+
return self.keyframes[0].landmarks, self.keyframes[0].connections
|
| 42 |
+
|
| 43 |
+
prev_idx = next_idx - 1
|
| 44 |
+
prev_frame = self.keyframes[prev_idx]
|
| 45 |
+
next_frame = self.keyframes[next_idx]
|
| 46 |
+
|
| 47 |
+
# Calculate interpolation factor
|
| 48 |
+
alpha = (time - prev_frame.timestamp) / (next_frame.timestamp - prev_frame.timestamp)
|
| 49 |
+
interpolated_landmarks = self.interpolate_poses(prev_frame, next_frame, alpha)
|
| 50 |
+
|
| 51 |
+
return interpolated_landmarks, prev_frame.connections
|
| 52 |
+
|
| 53 |
+
def get_next_frame(self) -> Tuple[np.ndarray, List[Tuple[int, int]]]:
|
| 54 |
+
"""Get the next frame in the animation sequence"""
|
| 55 |
+
if not self.keyframes or self.current_frame >= self.total_frames:
|
| 56 |
+
return None, []
|
| 57 |
+
|
| 58 |
+
time = self.current_frame / self.fps
|
| 59 |
+
self.current_frame += 1
|
| 60 |
+
|
| 61 |
+
return self.get_frame_at_time(time)
|
app.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import cv2
|
| 4 |
+
import numpy as np
|
| 5 |
+
import tempfile
|
| 6 |
+
from typing import Optional, Tuple
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
|
| 9 |
+
from pose_detector import PoseDetector
|
| 10 |
+
from skeleton_generator import SkeletonGenerator
|
| 11 |
+
from animation_exporter import AnimationExporter
|
| 12 |
+
from utils import process_video, process_image, process_gif
|
| 13 |
+
from database import get_db, ProcessedFile, PoseData, AnimationData
|
| 14 |
+
|
| 15 |
+
def init_page():
|
| 16 |
+
"""Initialize Streamlit page configuration and styling."""
|
| 17 |
+
st.set_page_config(layout="wide", page_title="Pose Detection & Animation Generator")
|
| 18 |
+
with open('static/style.css') as f:
|
| 19 |
+
st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True)
|
| 20 |
+
|
| 21 |
+
# Theme selection
|
| 22 |
+
theme = st.sidebar.selectbox("Theme", ["Light", "Dark"], key="theme")
|
| 23 |
+
if theme == "Dark":
|
| 24 |
+
st.markdown("""
|
| 25 |
+
<style>
|
| 26 |
+
.stApp { background-color: #1E1E1E; color: #FFFFFF; }
|
| 27 |
+
</style>
|
| 28 |
+
""", unsafe_allow_html=True)
|
| 29 |
+
|
| 30 |
+
# Detection settings
|
| 31 |
+
st.sidebar.title("Settings")
|
| 32 |
+
|
| 33 |
+
# Detection settings
|
| 34 |
+
confidence_threshold = st.sidebar.slider(
|
| 35 |
+
"Detection Confidence",
|
| 36 |
+
min_value=0.0,
|
| 37 |
+
max_value=1.0,
|
| 38 |
+
value=0.5,
|
| 39 |
+
step=0.1
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# Export format selection
|
| 43 |
+
export_format = st.sidebar.selectbox(
|
| 44 |
+
"Export Format",
|
| 45 |
+
options=['uasset', 'fbx', 'bvh'],
|
| 46 |
+
key='export_format'
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
# Manual correction mode
|
| 50 |
+
enable_corrections = st.sidebar.checkbox("Enable Manual Corrections")
|
| 51 |
+
if enable_corrections:
|
| 52 |
+
st.sidebar.info("Click on landmarks in the preview to adjust their positions")
|
| 53 |
+
|
| 54 |
+
# Custom skeleton mapping
|
| 55 |
+
show_mapping = st.sidebar.expander("Skeleton Mapping")
|
| 56 |
+
with show_mapping:
|
| 57 |
+
st.text_area("Custom Mapping (JSON)", value="{}", key="custom_mapping")
|
| 58 |
+
|
| 59 |
+
st.title("Pose Detection & Animation Generator")
|
| 60 |
+
return confidence_threshold
|
| 61 |
+
|
| 62 |
+
def init_components() -> Tuple[PoseDetector, SkeletonGenerator, AnimationExporter]:
|
| 63 |
+
"""Initialize the main processing components."""
|
| 64 |
+
return PoseDetector(), SkeletonGenerator(), AnimationExporter()
|
| 65 |
+
|
| 66 |
+
def handle_upload(file_type: str, uploaded_file, components: Tuple, db_session) -> Optional[ProcessedFile]:
|
| 67 |
+
"""Process uploaded file and store results in database."""
|
| 68 |
+
pose_detector, skeleton_generator, animation_exporter = components
|
| 69 |
+
|
| 70 |
+
processed_file = ProcessedFile(
|
| 71 |
+
filename=uploaded_file.name,
|
| 72 |
+
file_type='video' if uploaded_file.type == 'image/gif' else file_type,
|
| 73 |
+
processing_status="processing"
|
| 74 |
+
)
|
| 75 |
+
db_session.add(processed_file)
|
| 76 |
+
db_session.commit()
|
| 77 |
+
db_session.refresh(processed_file)
|
| 78 |
+
|
| 79 |
+
return processed_file
|
| 80 |
+
|
| 81 |
+
def main():
|
| 82 |
+
init_page()
|
| 83 |
+
components = init_components()
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
uploaded_file = st.file_uploader(
|
| 87 |
+
"Choose an image or video file (max 50MB)",
|
| 88 |
+
type=['jpg', 'jpeg', 'png', 'mp4', 'avi', 'gif']
|
| 89 |
+
)
|
| 90 |
+
if uploaded_file is not None:
|
| 91 |
+
st.cache_data.clear() # Clear cache to prevent stale data
|
| 92 |
+
except Exception as e:
|
| 93 |
+
st.error("Network error occurred. Please try uploading again.")
|
| 94 |
+
return
|
| 95 |
+
|
| 96 |
+
if uploaded_file is None:
|
| 97 |
+
st.warning("Please upload a file to begin.")
|
| 98 |
+
return
|
| 99 |
+
|
| 100 |
+
if uploaded_file.type == 'video/mp4':
|
| 101 |
+
try:
|
| 102 |
+
st.info("Processing video... This may take a moment.")
|
| 103 |
+
file_size = len(uploaded_file.getvalue()) / (1024 * 1024) # Size in MB
|
| 104 |
+
if file_size > 50:
|
| 105 |
+
st.error("Video file size must be under 50MB. Please upload a smaller file.")
|
| 106 |
+
return
|
| 107 |
+
|
| 108 |
+
# Validate video file
|
| 109 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tfile:
|
| 110 |
+
tfile.write(uploaded_file.getvalue())
|
| 111 |
+
cap = cv2.VideoCapture(tfile.name)
|
| 112 |
+
if not cap.isOpened():
|
| 113 |
+
st.error("Invalid video file. Please try a different file.")
|
| 114 |
+
return
|
| 115 |
+
cap.release()
|
| 116 |
+
except Exception as e:
|
| 117 |
+
st.error(f"Error processing video: {str(e)}")
|
| 118 |
+
return
|
| 119 |
+
|
| 120 |
+
if uploaded_file is None:
|
| 121 |
+
return
|
| 122 |
+
|
| 123 |
+
db = next(get_db())
|
| 124 |
+
try:
|
| 125 |
+
file_type = uploaded_file.type.split('/')[0]
|
| 126 |
+
is_gif = uploaded_file.type == 'image/gif'
|
| 127 |
+
|
| 128 |
+
processed_file = handle_upload(file_type, uploaded_file, components, db)
|
| 129 |
+
|
| 130 |
+
col1, col2 = st.columns(2)
|
| 131 |
+
with col1:
|
| 132 |
+
st.subheader("Original")
|
| 133 |
+
with col2:
|
| 134 |
+
st.subheader("Processed")
|
| 135 |
+
|
| 136 |
+
try:
|
| 137 |
+
if file_type == 'image' and not is_gif:
|
| 138 |
+
process_image_upload(uploaded_file, components, processed_file, db, col1, col2)
|
| 139 |
+
elif file_type == 'video' or is_gif:
|
| 140 |
+
process_video_upload(uploaded_file, components, processed_file, db, is_gif, col1, col2)
|
| 141 |
+
except Exception as e:
|
| 142 |
+
st.error(f"Processing error: {str(e)}")
|
| 143 |
+
processed_file.processing_status = "failed"
|
| 144 |
+
db.commit()
|
| 145 |
+
return
|
| 146 |
+
|
| 147 |
+
processed_file.processing_status = "completed"
|
| 148 |
+
db.commit()
|
| 149 |
+
|
| 150 |
+
except Exception as e:
|
| 151 |
+
st.error(f"An error occurred: {str(e)}")
|
| 152 |
+
finally:
|
| 153 |
+
db.close()
|
| 154 |
+
|
| 155 |
+
def process_image_upload(uploaded_file, components, processed_file, db, col1, col2):
|
| 156 |
+
"""Handle image file upload processing."""
|
| 157 |
+
pose_detector, skeleton_generator, animation_exporter = components
|
| 158 |
+
|
| 159 |
+
file_bytes = np.asarray(bytearray(uploaded_file.read()), dtype=np.uint8)
|
| 160 |
+
image = cv2.imdecode(file_bytes, 1)
|
| 161 |
+
|
| 162 |
+
with col1:
|
| 163 |
+
st.image(cv2.cvtColor(image, cv2.COLOR_BGR2RGB), use_column_width=True)
|
| 164 |
+
|
| 165 |
+
processed_image, skeleton_data = process_image(image, pose_detector, skeleton_generator)
|
| 166 |
+
|
| 167 |
+
if not skeleton_data:
|
| 168 |
+
raise ValueError("No pose detected in the image")
|
| 169 |
+
|
| 170 |
+
save_pose_data(db, processed_file.id, skeleton_data)
|
| 171 |
+
animation_data_binary = animation_exporter.export_pose(skeleton_data)
|
| 172 |
+
save_animation_data(db, processed_file.id, skeleton_data)
|
| 173 |
+
|
| 174 |
+
with col2:
|
| 175 |
+
# Create a canvas for manual corrections
|
| 176 |
+
canvas_container = st.empty()
|
| 177 |
+
processed_rgb = cv2.cvtColor(processed_image, cv2.COLOR_BGR2RGB)
|
| 178 |
+
|
| 179 |
+
# Add manual correction controls
|
| 180 |
+
if st.button("Enable Manual Correction"):
|
| 181 |
+
st.session_state.manual_correction = True
|
| 182 |
+
st.session_state.current_landmarks = skeleton_data
|
| 183 |
+
|
| 184 |
+
if st.session_state.get('manual_correction', False):
|
| 185 |
+
# Display current joint positions
|
| 186 |
+
joints = st.session_state.current_landmarks
|
| 187 |
+
|
| 188 |
+
selected_joint = st.selectbox("Select Joint to Adjust", list(joints.keys()))
|
| 189 |
+
|
| 190 |
+
col1, col2 = st.columns(2)
|
| 191 |
+
with col1:
|
| 192 |
+
x_pos = st.slider("X Position", 0.0, 1.0, float(joints[selected_joint]['position'][0]), 0.01)
|
| 193 |
+
with col2:
|
| 194 |
+
y_pos = st.slider("Y Position", 0.0, 1.0, float(joints[selected_joint]['position'][1]), 0.01)
|
| 195 |
+
|
| 196 |
+
if st.button("Apply Changes"):
|
| 197 |
+
joints[selected_joint]['position'][0] = x_pos
|
| 198 |
+
joints[selected_joint]['position'][1] = y_pos
|
| 199 |
+
st.session_state.current_landmarks = joints
|
| 200 |
+
processed_image = pose_detector.draw_corrected_pose(image, joints)
|
| 201 |
+
processed_rgb = cv2.cvtColor(processed_image, cv2.COLOR_BGR2RGB)
|
| 202 |
+
|
| 203 |
+
if st.button("Save Corrections"):
|
| 204 |
+
save_corrected_pose(db, processed_file.id, st.session_state.current_landmarks)
|
| 205 |
+
st.success("Corrections saved successfully!")
|
| 206 |
+
|
| 207 |
+
canvas_container.image(processed_rgb, use_column_width=True)
|
| 208 |
+
|
| 209 |
+
provide_download_button(animation_data_binary)
|
| 210 |
+
|
| 211 |
+
def process_video_upload(uploaded_file, components, processed_file, db, is_gif, col1, col2):
|
| 212 |
+
"""Handle video/GIF file upload processing."""
|
| 213 |
+
pose_detector, skeleton_generator, animation_exporter = components
|
| 214 |
+
progress_bar = st.progress(0)
|
| 215 |
+
status_text = st.empty()
|
| 216 |
+
|
| 217 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix='.gif' if is_gif else '.mp4') as tfile:
|
| 218 |
+
tfile.write(uploaded_file.read())
|
| 219 |
+
video_path = tfile.name
|
| 220 |
+
|
| 221 |
+
with col1:
|
| 222 |
+
st.video(video_path)
|
| 223 |
+
|
| 224 |
+
if is_gif:
|
| 225 |
+
processed_video_path, animation_frames = process_gif(video_path, pose_detector, skeleton_generator)
|
| 226 |
+
else:
|
| 227 |
+
processed_video_path, animation_frames = process_video(video_path, pose_detector, skeleton_generator)
|
| 228 |
+
|
| 229 |
+
if not animation_frames:
|
| 230 |
+
raise ValueError("No poses detected in the video/gif")
|
| 231 |
+
|
| 232 |
+
save_video_data(db, processed_file.id, animation_frames)
|
| 233 |
+
animation_data_binary = animation_exporter.export_animation(animation_frames)
|
| 234 |
+
|
| 235 |
+
with col2:
|
| 236 |
+
if processed_video_path:
|
| 237 |
+
st.video(processed_video_path)
|
| 238 |
+
|
| 239 |
+
provide_download_button(animation_data_binary)
|
| 240 |
+
|
| 241 |
+
cleanup_temp_files(video_path, processed_video_path)
|
| 242 |
+
|
| 243 |
+
def save_pose_data(db, file_id: int, skeleton_data: dict):
|
| 244 |
+
"""Save pose data to database."""
|
| 245 |
+
pose_data = PoseData(file_id=file_id, landmarks=skeleton_data)
|
| 246 |
+
db.add(pose_data)
|
| 247 |
+
db.commit()
|
| 248 |
+
|
| 249 |
+
def save_animation_data(db, file_id: int, skeleton_data: dict):
|
| 250 |
+
"""Save animation data to database."""
|
| 251 |
+
animation_data = AnimationData(
|
| 252 |
+
file_id=file_id,
|
| 253 |
+
skeleton_data=skeleton_data
|
| 254 |
+
)
|
| 255 |
+
db.add(animation_data)
|
| 256 |
+
db.commit()
|
| 257 |
+
|
| 258 |
+
def save_video_data(db, file_id: int, animation_frames: list):
|
| 259 |
+
"""Save video frame data to database."""
|
| 260 |
+
for frame_num, frame_data in enumerate(animation_frames):
|
| 261 |
+
pose_data = PoseData(
|
| 262 |
+
file_id=file_id,
|
| 263 |
+
frame_number=frame_num,
|
| 264 |
+
landmarks=frame_data
|
| 265 |
+
)
|
| 266 |
+
db.add(pose_data)
|
| 267 |
+
db.commit()
|
| 268 |
+
|
| 269 |
+
def provide_download_button(animation_data_binary):
|
| 270 |
+
"""Provide download button for animation data."""
|
| 271 |
+
st.download_button(
|
| 272 |
+
label="Download Animation Data",
|
| 273 |
+
data=animation_data_binary,
|
| 274 |
+
file_name="animation.uasset",
|
| 275 |
+
mime="application/octet-stream"
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
def cleanup_temp_files(*file_paths):
|
| 279 |
+
"""Clean up temporary files."""
|
| 280 |
+
for file_path in file_paths:
|
| 281 |
+
if file_path:
|
| 282 |
+
try:
|
| 283 |
+
import os
|
| 284 |
+
os.unlink(file_path)
|
| 285 |
+
except Exception:
|
| 286 |
+
pass
|
| 287 |
+
|
| 288 |
+
def show_instructions():
|
| 289 |
+
"""Show usage instructions."""
|
| 290 |
+
with st.expander("Instructions"):
|
| 291 |
+
st.markdown("""
|
| 292 |
+
1. Upload an image or video file using the file uploader above
|
| 293 |
+
2. Wait for the pose detection and skeleton generation to complete
|
| 294 |
+
3. Preview the results in the right column
|
| 295 |
+
4. Download the animation data for use in Unreal Engine
|
| 296 |
+
|
| 297 |
+
Supported file formats:
|
| 298 |
+
- Images: JPG, JPEG, PNG
|
| 299 |
+
- Videos: MP4, AVI, GIF
|
| 300 |
+
""")
|
| 301 |
+
|
| 302 |
+
if __name__ == "__main__":
|
| 303 |
+
main()
|
| 304 |
+
show_instructions()
|
database.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from sqlalchemy import create_engine, Column, Integer, String, JSON, ForeignKey, DateTime, Boolean
|
| 3 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 4 |
+
from sqlalchemy.orm import sessionmaker, relationship
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
# Create database engine
|
| 8 |
+
DATABASE_URL = os.environ.get('DATABASE_URL')
|
| 9 |
+
if not DATABASE_URL:
|
| 10 |
+
raise ValueError("DATABASE_URL environment variable is not set")
|
| 11 |
+
|
| 12 |
+
engine = create_engine(DATABASE_URL)
|
| 13 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 14 |
+
Base = declarative_base()
|
| 15 |
+
|
| 16 |
+
class ProcessedFile(Base):
|
| 17 |
+
__tablename__ = "processed_files"
|
| 18 |
+
|
| 19 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 20 |
+
filename = Column(String, nullable=False)
|
| 21 |
+
file_type = Column(String, nullable=False) # 'image' or 'video'
|
| 22 |
+
processing_status = Column(String, nullable=False, default="pending")
|
| 23 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 24 |
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 25 |
+
|
| 26 |
+
pose_data = relationship("PoseData", back_populates="file")
|
| 27 |
+
animation_data = relationship("AnimationData", back_populates="file")
|
| 28 |
+
|
| 29 |
+
class PoseData(Base):
|
| 30 |
+
__tablename__ = "pose_data"
|
| 31 |
+
|
| 32 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 33 |
+
file_id = Column(Integer, ForeignKey("processed_files.id"), nullable=False)
|
| 34 |
+
frame_number = Column(Integer, default=0) # 0 for images, frame number for videos
|
| 35 |
+
landmarks = Column(JSON, nullable=False)
|
| 36 |
+
corrected_landmarks = Column(JSON)
|
| 37 |
+
is_corrected = Column(Boolean, default=False)
|
| 38 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 39 |
+
|
| 40 |
+
file = relationship("ProcessedFile", back_populates="pose_data")
|
| 41 |
+
|
| 42 |
+
class AnimationData(Base):
|
| 43 |
+
__tablename__ = "animation_data"
|
| 44 |
+
|
| 45 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 46 |
+
file_id = Column(Integer, ForeignKey("processed_files.id"), nullable=False)
|
| 47 |
+
skeleton_data = Column(JSON, nullable=False)
|
| 48 |
+
export_format = Column(String, nullable=False, default="unreal")
|
| 49 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 50 |
+
|
| 51 |
+
file = relationship("ProcessedFile", back_populates="animation_data")
|
| 52 |
+
|
| 53 |
+
# Create all tables
|
| 54 |
+
Base.metadata.create_all(bind=engine)
|
| 55 |
+
|
| 56 |
+
def get_db():
|
| 57 |
+
db = SessionLocal()
|
| 58 |
+
try:
|
| 59 |
+
yield db
|
| 60 |
+
finally:
|
| 61 |
+
db.close()
|
generated-icon.png
ADDED
|
|
Git LFS Details
|
pose_detector.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import mediapipe as mp
|
| 2 |
+
import numpy as np
|
| 3 |
+
import cv2
|
| 4 |
+
from typing import List, Tuple, Optional
|
| 5 |
+
|
| 6 |
+
class PoseDetector:
|
| 7 |
+
def __init__(self):
|
| 8 |
+
self.mp_pose = mp.solutions.pose
|
| 9 |
+
self.mp_drawing = mp.solutions.drawing_utils
|
| 10 |
+
self.mp_drawing_styles = mp.solutions.drawing_styles
|
| 11 |
+
|
| 12 |
+
# Define pose connections for smooth animation
|
| 13 |
+
self.pose_connections = [
|
| 14 |
+
# Spine Chain
|
| 15 |
+
(self.mp_pose.PoseLandmark.NOSE.value, self.mp_pose.PoseLandmark.LEFT_SHOULDER.value),
|
| 16 |
+
(self.mp_pose.PoseLandmark.NOSE.value, self.mp_pose.PoseLandmark.RIGHT_SHOULDER.value),
|
| 17 |
+
(self.mp_pose.PoseLandmark.LEFT_SHOULDER.value, self.mp_pose.PoseLandmark.RIGHT_SHOULDER.value),
|
| 18 |
+
(self.mp_pose.PoseLandmark.LEFT_SHOULDER.value, self.mp_pose.PoseLandmark.LEFT_HIP.value),
|
| 19 |
+
(self.mp_pose.PoseLandmark.RIGHT_SHOULDER.value, self.mp_pose.PoseLandmark.RIGHT_HIP.value),
|
| 20 |
+
(self.mp_pose.PoseLandmark.LEFT_HIP.value, self.mp_pose.PoseLandmark.RIGHT_HIP.value),
|
| 21 |
+
|
| 22 |
+
# Left Arm Chain
|
| 23 |
+
(self.mp_pose.PoseLandmark.LEFT_SHOULDER.value, self.mp_pose.PoseLandmark.LEFT_ELBOW.value),
|
| 24 |
+
(self.mp_pose.PoseLandmark.LEFT_ELBOW.value, self.mp_pose.PoseLandmark.LEFT_WRIST.value),
|
| 25 |
+
(self.mp_pose.PoseLandmark.LEFT_WRIST.value, self.mp_pose.PoseLandmark.LEFT_THUMB.value),
|
| 26 |
+
|
| 27 |
+
# Right Arm Chain
|
| 28 |
+
(self.mp_pose.PoseLandmark.RIGHT_SHOULDER.value, self.mp_pose.PoseLandmark.RIGHT_ELBOW.value),
|
| 29 |
+
(self.mp_pose.PoseLandmark.RIGHT_ELBOW.value, self.mp_pose.PoseLandmark.RIGHT_WRIST.value),
|
| 30 |
+
(self.mp_pose.PoseLandmark.RIGHT_WRIST.value, self.mp_pose.PoseLandmark.RIGHT_THUMB.value),
|
| 31 |
+
|
| 32 |
+
# Left Leg Chain
|
| 33 |
+
(self.mp_pose.PoseLandmark.LEFT_HIP.value, self.mp_pose.PoseLandmark.LEFT_KNEE.value),
|
| 34 |
+
(self.mp_pose.PoseLandmark.LEFT_KNEE.value, self.mp_pose.PoseLandmark.LEFT_ANKLE.value),
|
| 35 |
+
(self.mp_pose.PoseLandmark.LEFT_ANKLE.value, self.mp_pose.PoseLandmark.LEFT_FOOT_INDEX.value),
|
| 36 |
+
|
| 37 |
+
# Right Leg Chain
|
| 38 |
+
(self.mp_pose.PoseLandmark.RIGHT_HIP.value, self.mp_pose.PoseLandmark.RIGHT_KNEE.value),
|
| 39 |
+
(self.mp_pose.PoseLandmark.RIGHT_KNEE.value, self.mp_pose.PoseLandmark.RIGHT_ANKLE.value),
|
| 40 |
+
(self.mp_pose.PoseLandmark.RIGHT_ANKLE.value, self.mp_pose.PoseLandmark.RIGHT_FOOT_INDEX.value),
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
# Drawing specifications
|
| 44 |
+
self.landmark_drawing_spec = self.mp_drawing.DrawingSpec(
|
| 45 |
+
color=(0, 255, 0), # Green color
|
| 46 |
+
thickness=2,
|
| 47 |
+
circle_radius=2
|
| 48 |
+
)
|
| 49 |
+
self.connection_drawing_spec = self.mp_drawing.DrawingSpec(
|
| 50 |
+
color=(255, 255, 0), # Yellow color
|
| 51 |
+
thickness=2
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
def detect(self, image, manual_corrections=None) -> Tuple[Optional[np.ndarray], np.ndarray]:
|
| 55 |
+
"""
|
| 56 |
+
Detect pose in the given image
|
| 57 |
+
Args:
|
| 58 |
+
image: Input image
|
| 59 |
+
manual_corrections: Dictionary of landmark indices and their corrected positions
|
| 60 |
+
Returns: (landmarks, annotated_image)
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
def draw_corrected_pose(self, image: np.ndarray, corrected_joints: dict) -> np.ndarray:
|
| 64 |
+
"""Draw pose with manually corrected joint positions"""
|
| 65 |
+
annotated_image = image.copy()
|
| 66 |
+
h, w = image.shape[:2]
|
| 67 |
+
|
| 68 |
+
# Draw connections
|
| 69 |
+
for start_name, end_name in self.pose_connections:
|
| 70 |
+
if start_name in corrected_joints and end_name in corrected_joints:
|
| 71 |
+
start_pos = corrected_joints[start_name]['position']
|
| 72 |
+
end_pos = corrected_joints[end_name]['position']
|
| 73 |
+
|
| 74 |
+
start_px = (int(start_pos[0] * w), int(start_pos[1] * h))
|
| 75 |
+
end_px = (int(end_pos[0] * w), int(end_pos[1] * h))
|
| 76 |
+
|
| 77 |
+
cv2.line(annotated_image, start_px, end_px, (0, 255, 0), 3)
|
| 78 |
+
|
| 79 |
+
# Draw joints
|
| 80 |
+
for joint_name, joint_data in corrected_joints.items():
|
| 81 |
+
pos = joint_data['position']
|
| 82 |
+
px_pos = (int(pos[0] * w), int(pos[1] * h))
|
| 83 |
+
|
| 84 |
+
# Draw joint
|
| 85 |
+
cv2.circle(annotated_image, px_pos, 5, (0, 255, 255), -1)
|
| 86 |
+
|
| 87 |
+
# Draw joint name
|
| 88 |
+
cv2.putText(annotated_image, joint_name,
|
| 89 |
+
(px_pos[0], px_pos[1] - 10),
|
| 90 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
|
| 91 |
+
|
| 92 |
+
return annotated_image
|
| 93 |
+
with self.mp_pose.Pose(
|
| 94 |
+
static_image_mode=True,
|
| 95 |
+
model_complexity=2,
|
| 96 |
+
min_detection_confidence=0.5,
|
| 97 |
+
min_tracking_confidence=0.5
|
| 98 |
+
) as pose:
|
| 99 |
+
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
| 100 |
+
results = pose.process(image_rgb)
|
| 101 |
+
|
| 102 |
+
annotated_image = image.copy()
|
| 103 |
+
|
| 104 |
+
if results.pose_landmarks:
|
| 105 |
+
# Draw skeleton with more prominent visualization
|
| 106 |
+
self.landmark_drawing_spec.thickness = 4
|
| 107 |
+
self.connection_drawing_spec.thickness = 3
|
| 108 |
+
|
| 109 |
+
# Draw landmarks first
|
| 110 |
+
self.mp_drawing.draw_landmarks(
|
| 111 |
+
image=annotated_image,
|
| 112 |
+
landmark_list=results.pose_landmarks,
|
| 113 |
+
connections=self.mp_pose.POSE_CONNECTIONS,
|
| 114 |
+
landmark_drawing_spec=self.landmark_drawing_spec,
|
| 115 |
+
connection_drawing_spec=self.connection_drawing_spec
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
# Draw additional smooth connections
|
| 119 |
+
self._draw_smooth_connections(
|
| 120 |
+
annotated_image,
|
| 121 |
+
results.pose_landmarks,
|
| 122 |
+
self.pose_connections
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
landmarks = np.array([[lm.x, lm.y, lm.z] for lm in results.pose_landmarks.landmark])
|
| 126 |
+
return landmarks, annotated_image
|
| 127 |
+
|
| 128 |
+
return None, annotated_image
|
| 129 |
+
|
| 130 |
+
def _draw_smooth_connections(self, image: np.ndarray, landmarks, connections: List[Tuple[int, int]]):
|
| 131 |
+
"""Draw smooth connections between landmarks in Maya-like style"""
|
| 132 |
+
h, w = image.shape[:2]
|
| 133 |
+
|
| 134 |
+
# Maya-style bone names mapping
|
| 135 |
+
bone_names = {
|
| 136 |
+
0: "Head",
|
| 137 |
+
11: "Neck",
|
| 138 |
+
12: "Spine2",
|
| 139 |
+
23: "Hips",
|
| 140 |
+
24: "Spine",
|
| 141 |
+
13: "LeftArm",
|
| 142 |
+
14: "RightArm",
|
| 143 |
+
15: "LeftForeArm",
|
| 144 |
+
16: "RightForeArm",
|
| 145 |
+
25: "LeftLeg",
|
| 146 |
+
26: "RightLeg",
|
| 147 |
+
27: "LeftFoot",
|
| 148 |
+
28: "RightFoot",
|
| 149 |
+
31: "LeftToeBase",
|
| 150 |
+
32: "RightToeBase"
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
# Maya-style colors
|
| 154 |
+
joint_color = (0, 255, 255) # Cyan for joints
|
| 155 |
+
bone_color = (0, 255, 0) # Green for bones
|
| 156 |
+
text_color = (255, 255, 255) # White for text
|
| 157 |
+
|
| 158 |
+
for connection in connections:
|
| 159 |
+
start_idx, end_idx = connection
|
| 160 |
+
start_point = landmarks.landmark[start_idx]
|
| 161 |
+
end_point = landmarks.landmark[end_idx]
|
| 162 |
+
|
| 163 |
+
# Convert normalized coordinates to pixel coordinates
|
| 164 |
+
start_pos = (int(start_point.x * w), int(start_point.y * h))
|
| 165 |
+
end_pos = (int(end_point.x * w), int(end_point.y * h))
|
| 166 |
+
|
| 167 |
+
# Draw bone connection (thicker, Maya-style)
|
| 168 |
+
cv2.line(
|
| 169 |
+
image,
|
| 170 |
+
start_pos,
|
| 171 |
+
end_pos,
|
| 172 |
+
bone_color,
|
| 173 |
+
3, # Thicker lines for bones
|
| 174 |
+
cv2.LINE_AA
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
# Draw joints as larger circles
|
| 178 |
+
for pos, idx in [(start_pos, start_idx), (end_pos, end_idx)]:
|
| 179 |
+
# Draw joint
|
| 180 |
+
cv2.circle(
|
| 181 |
+
image,
|
| 182 |
+
pos,
|
| 183 |
+
5, # Larger radius for joints
|
| 184 |
+
joint_color,
|
| 185 |
+
-1, # Filled circle
|
| 186 |
+
cv2.LINE_AA
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
# Draw bone name if it exists in mapping
|
| 190 |
+
if idx in bone_names:
|
| 191 |
+
# Position text above the joint
|
| 192 |
+
text_pos = (pos[0], pos[1] - 10)
|
| 193 |
+
cv2.putText(
|
| 194 |
+
image,
|
| 195 |
+
bone_names[idx],
|
| 196 |
+
text_pos,
|
| 197 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
| 198 |
+
0.5, # Font scale
|
| 199 |
+
text_color,
|
| 200 |
+
1, # Thickness
|
| 201 |
+
cv2.LINE_AA
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
def detect_video_frame(self, frame):
|
| 205 |
+
"""
|
| 206 |
+
Detect pose in video frame with optimized parameters for video
|
| 207 |
+
"""
|
| 208 |
+
with self.mp_pose.Pose(
|
| 209 |
+
static_image_mode=False,
|
| 210 |
+
model_complexity=2, # Increased complexity for better accuracy
|
| 211 |
+
smooth_landmarks=True,
|
| 212 |
+
min_detection_confidence=0.3, # Lower threshold to detect more poses
|
| 213 |
+
min_tracking_confidence=0.3, # Lower threshold for better tracking
|
| 214 |
+
enable_segmentation=False # Disable segmentation to reduce overhead
|
| 215 |
+
) as pose:
|
| 216 |
+
image_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 217 |
+
results = pose.process(image_rgb)
|
| 218 |
+
|
| 219 |
+
annotated_frame = frame.copy()
|
| 220 |
+
|
| 221 |
+
if results.pose_landmarks:
|
| 222 |
+
# Draw landmarks with maximum visibility
|
| 223 |
+
self.landmark_drawing_spec.thickness = 6
|
| 224 |
+
self.connection_drawing_spec.thickness = 5
|
| 225 |
+
self.landmark_drawing_spec.circle_radius = 4
|
| 226 |
+
|
| 227 |
+
# Enhanced temporal smoothing for landmarks
|
| 228 |
+
if hasattr(self, 'previous_landmarks'):
|
| 229 |
+
smoothing_factor = 0.8 # Adjust smoothing strength (0.0-1.0)
|
| 230 |
+
for i, landmark in enumerate(results.pose_landmarks.landmark):
|
| 231 |
+
if self.previous_landmarks is not None:
|
| 232 |
+
if landmark.visibility < 0.7 or self.previous_landmarks[i].visibility > 0.8:
|
| 233 |
+
# Apply temporal smoothing
|
| 234 |
+
landmark.x = smoothing_factor * self.previous_landmarks[i].x + (1 - smoothing_factor) * landmark.x
|
| 235 |
+
landmark.y = smoothing_factor * self.previous_landmarks[i].y + (1 - smoothing_factor) * landmark.y
|
| 236 |
+
landmark.z = smoothing_factor * self.previous_landmarks[i].z + (1 - smoothing_factor) * landmark.z
|
| 237 |
+
landmark.visibility = max(landmark.visibility, self.previous_landmarks[i].visibility * 0.9)
|
| 238 |
+
|
| 239 |
+
# Store current landmarks for next frame
|
| 240 |
+
self.previous_landmarks = results.pose_landmarks.landmark
|
| 241 |
+
|
| 242 |
+
# Draw the skeleton first
|
| 243 |
+
self._draw_smooth_connections(
|
| 244 |
+
annotated_frame,
|
| 245 |
+
results.pose_landmarks,
|
| 246 |
+
self.pose_connections
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
# Then draw the landmarks
|
| 250 |
+
self.mp_drawing.draw_landmarks(
|
| 251 |
+
image=annotated_frame,
|
| 252 |
+
landmark_list=results.pose_landmarks,
|
| 253 |
+
connections=self.mp_pose.POSE_CONNECTIONS,
|
| 254 |
+
landmark_drawing_spec=self.landmark_drawing_spec,
|
| 255 |
+
connection_drawing_spec=self.connection_drawing_spec
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
landmarks = np.array([[lm.x, lm.y, lm.z] for lm in results.pose_landmarks.landmark])
|
| 259 |
+
return landmarks, annotated_frame
|
| 260 |
+
elif hasattr(self, 'previous_landmarks') and self.previous_landmarks is not None:
|
| 261 |
+
# Use previous frame's data if no detection in current frame
|
| 262 |
+
results.pose_landmarks = type('obj', (object,), {'landmark': self.previous_landmarks})
|
| 263 |
+
self._draw_smooth_connections(
|
| 264 |
+
annotated_frame,
|
| 265 |
+
results.pose_landmarks,
|
| 266 |
+
self.pose_connections
|
| 267 |
+
)
|
| 268 |
+
landmarks = np.array([[lm.x, lm.y, lm.z] for lm in self.previous_landmarks])
|
| 269 |
+
return landmarks, annotated_frame
|
| 270 |
+
|
| 271 |
+
return None, annotated_frame
|
pyproject.toml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "repl-nix-workspace"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
requires-python = ">=3.11"
|
| 6 |
+
dependencies = [
|
| 7 |
+
"mediapipe>=0.10.20",
|
| 8 |
+
"numpy>=1.26.4",
|
| 9 |
+
"opencv-python>=4.11.0.86",
|
| 10 |
+
"psycopg2-binary>=2.9.10",
|
| 11 |
+
"sqlalchemy>=2.0.37",
|
| 12 |
+
"streamlit>=1.42.0",
|
| 13 |
+
]
|
replit.nix
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{pkgs}: {
|
| 2 |
+
deps = [
|
| 3 |
+
pkgs.ffmpeg
|
| 4 |
+
pkgs.libGLU
|
| 5 |
+
pkgs.libGL
|
| 6 |
+
];
|
| 7 |
+
}
|
skeleton_generator.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
class SkeletonGenerator:
|
| 4 |
+
def __init__(self, custom_mapping=None):
|
| 5 |
+
# Define default Maya-compatible bone names and their corresponding MediaPipe indices
|
| 6 |
+
self.bone_mapping = {
|
| 7 |
+
# Root & Spine
|
| 8 |
+
'Hips': 23, # HIPS/Pelvis
|
| 9 |
+
'Spine': 24, # Lower spine
|
| 10 |
+
'Spine1': 12, # Mid spine
|
| 11 |
+
'Spine2': 11, # Upper spine
|
| 12 |
+
|
| 13 |
+
# Head & Neck
|
| 14 |
+
'Neck': 11, # BASE_NECK
|
| 15 |
+
'Head': 0, # HEAD (using nose as reference)
|
| 16 |
+
|
| 17 |
+
# Left Arm Chain
|
| 18 |
+
'LeftShoulder': 11, # LEFT_SHOULDER
|
| 19 |
+
'LeftArm': 13, # LEFT_UPPER_ARM
|
| 20 |
+
'LeftForeArm': 15, # LEFT_LOWER_ARM
|
| 21 |
+
'LeftHand': 15, # LEFT_HAND (using wrist as reference)
|
| 22 |
+
|
| 23 |
+
# Right Arm Chain
|
| 24 |
+
'RightShoulder': 12, # RIGHT_SHOULDER
|
| 25 |
+
'RightArm': 14, # RIGHT_UPPER_ARM
|
| 26 |
+
'RightForeArm': 16, # RIGHT_LOWER_ARM
|
| 27 |
+
'RightHand': 16, # RIGHT_HAND (using wrist as reference)
|
| 28 |
+
|
| 29 |
+
# Left Leg Chain
|
| 30 |
+
'LeftUpLeg': 23, # LEFT_UPPER_LEG
|
| 31 |
+
'LeftLeg': 25, # LEFT_LOWER_LEG
|
| 32 |
+
'LeftFoot': 27, # LEFT_FOOT
|
| 33 |
+
'LeftToeBase': 31, # LEFT_TOE
|
| 34 |
+
|
| 35 |
+
# Right Leg Chain
|
| 36 |
+
'RightUpLeg': 24, # RIGHT_UPPER_LEG
|
| 37 |
+
'RightLeg': 26, # RIGHT_LOWER_LEG
|
| 38 |
+
'RightFoot': 28, # RIGHT_FOOT
|
| 39 |
+
'RightToeBase': 32, # RIGHT_TOE
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
def generate_skeleton(self, landmarks):
|
| 43 |
+
"""
|
| 44 |
+
Convert MediaPipe landmarks to Maya-compatible skeleton data
|
| 45 |
+
"""
|
| 46 |
+
if landmarks is None:
|
| 47 |
+
return None
|
| 48 |
+
|
| 49 |
+
skeleton_data = {}
|
| 50 |
+
|
| 51 |
+
# Convert landmarks to skeleton joint positions
|
| 52 |
+
for bone_name, landmark_idx in self.bone_mapping.items():
|
| 53 |
+
position = landmarks[landmark_idx]
|
| 54 |
+
rotation = self._calculate_bone_rotation(landmarks, landmark_idx)
|
| 55 |
+
|
| 56 |
+
# Convert NumPy arrays to regular Python lists
|
| 57 |
+
skeleton_data[bone_name] = {
|
| 58 |
+
'position': position.tolist(),
|
| 59 |
+
'rotation': rotation.tolist()
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
return skeleton_data
|
| 63 |
+
|
| 64 |
+
def _calculate_bone_rotation(self, landmarks, landmark_idx):
|
| 65 |
+
"""
|
| 66 |
+
Calculate bone rotation based on connected landmarks
|
| 67 |
+
"""
|
| 68 |
+
# Simple rotation calculation - can be enhanced for better accuracy
|
| 69 |
+
rotation = np.zeros(3)
|
| 70 |
+
|
| 71 |
+
if landmark_idx > 0:
|
| 72 |
+
# Calculate rotation based on parent-child relationship
|
| 73 |
+
parent_idx = landmark_idx - 1
|
| 74 |
+
direction = landmarks[landmark_idx] - landmarks[parent_idx]
|
| 75 |
+
|
| 76 |
+
# Convert direction to euler angles (simplified)
|
| 77 |
+
rotation[0] = np.arctan2(direction[1], direction[2]) # pitch
|
| 78 |
+
rotation[1] = np.arctan2(direction[0], direction[2]) # yaw
|
| 79 |
+
rotation[2] = np.arctan2(direction[0], direction[1]) # roll
|
| 80 |
+
|
| 81 |
+
return rotation
|
utils.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import tempfile
|
| 3 |
+
import numpy as np
|
| 4 |
+
import os
|
| 5 |
+
from animation_renderer import AnimationRenderer
|
| 6 |
+
|
| 7 |
+
def process_image(image, pose_detector, skeleton_generator):
|
| 8 |
+
"""
|
| 9 |
+
Process single image for pose detection and skeleton generation
|
| 10 |
+
"""
|
| 11 |
+
try:
|
| 12 |
+
# Detect pose
|
| 13 |
+
landmarks, annotated_image = pose_detector.detect(image)
|
| 14 |
+
|
| 15 |
+
if landmarks is not None:
|
| 16 |
+
# Generate skeleton data
|
| 17 |
+
skeleton_data = skeleton_generator.generate_skeleton(landmarks)
|
| 18 |
+
return annotated_image, skeleton_data
|
| 19 |
+
|
| 20 |
+
return image, None
|
| 21 |
+
except Exception as e:
|
| 22 |
+
print(f"Error processing image: {str(e)}")
|
| 23 |
+
return image, None
|
| 24 |
+
|
| 25 |
+
def process_video(video_path, pose_detector, skeleton_generator):
|
| 26 |
+
"""
|
| 27 |
+
Process video for pose detection and skeleton generation with improved error handling and chunked processing
|
| 28 |
+
"""
|
| 29 |
+
cap = None
|
| 30 |
+
out = None
|
| 31 |
+
try:
|
| 32 |
+
# Optimize video processing
|
| 33 |
+
chunk_size = 5 # Process fewer frames at a time
|
| 34 |
+
buffer_size = 512*1024 # Smaller buffer for stability
|
| 35 |
+
cv2.setNumThreads(2) # Allow 2 threads for better performance
|
| 36 |
+
cv2.ocl.setUseOpenCL(False) # Disable OpenCL to prevent crashes
|
| 37 |
+
|
| 38 |
+
cap = cv2.VideoCapture(video_path)
|
| 39 |
+
if not cap.isOpened():
|
| 40 |
+
raise ValueError("Could not open video file")
|
| 41 |
+
|
| 42 |
+
# Get video properties with error checking
|
| 43 |
+
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 44 |
+
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 45 |
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 46 |
+
fps = int(cap.get(cv2.CAP_PROP_FPS)) or 30
|
| 47 |
+
|
| 48 |
+
# Limit dimensions for better performance
|
| 49 |
+
target_width = 480
|
| 50 |
+
if frame_width > target_width:
|
| 51 |
+
scale = target_width / frame_width
|
| 52 |
+
frame_width = target_width
|
| 53 |
+
frame_height = int(frame_height * scale)
|
| 54 |
+
|
| 55 |
+
# Create temporary file with better error handling
|
| 56 |
+
try:
|
| 57 |
+
temp_output = tempfile.NamedTemporaryFile(suffix='.mp4', delete=False)
|
| 58 |
+
output_path = temp_output.name
|
| 59 |
+
except Exception as e:
|
| 60 |
+
raise RuntimeError(f"Failed to create temporary file: {str(e)}")
|
| 61 |
+
|
| 62 |
+
# Set lower resolution for processing
|
| 63 |
+
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 480)
|
| 64 |
+
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 360)
|
| 65 |
+
|
| 66 |
+
if not cap.isOpened():
|
| 67 |
+
return None, None
|
| 68 |
+
|
| 69 |
+
# Get video properties
|
| 70 |
+
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 71 |
+
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 72 |
+
fps = int(cap.get(cv2.CAP_PROP_FPS))
|
| 73 |
+
if fps == 0: # Handle GIF files which might report 0 fps
|
| 74 |
+
fps = 30
|
| 75 |
+
|
| 76 |
+
# Initialize animation renderer
|
| 77 |
+
renderer = AnimationRenderer(fps=fps)
|
| 78 |
+
|
| 79 |
+
# Create temporary file for processed video
|
| 80 |
+
temp_output = tempfile.NamedTemporaryFile(suffix='.mp4', delete=False)
|
| 81 |
+
output_path = temp_output.name
|
| 82 |
+
|
| 83 |
+
# Initialize video writer with more efficient settings
|
| 84 |
+
max_dimension = 480 # Reduced max dimension
|
| 85 |
+
if frame_width > max_dimension or frame_height > max_dimension:
|
| 86 |
+
scale = min(max_dimension/frame_width, max_dimension/frame_height)
|
| 87 |
+
frame_width = int(frame_width * scale)
|
| 88 |
+
frame_height = int(frame_height * scale)
|
| 89 |
+
|
| 90 |
+
# Use MP4V codec which is more memory efficient
|
| 91 |
+
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
| 92 |
+
out = cv2.VideoWriter(output_path, fourcc, min(fps, 30), (frame_width, frame_height))
|
| 93 |
+
|
| 94 |
+
animation_frames = []
|
| 95 |
+
frame_count = 0
|
| 96 |
+
frame_time = 0.0
|
| 97 |
+
|
| 98 |
+
while cap.isOpened():
|
| 99 |
+
ret, frame = cap.read()
|
| 100 |
+
if not ret:
|
| 101 |
+
break
|
| 102 |
+
|
| 103 |
+
# Resize frame to reduce memory usage
|
| 104 |
+
if frame.shape[1] > frame_width:
|
| 105 |
+
frame = cv2.resize(frame, (frame_width, frame_height))
|
| 106 |
+
|
| 107 |
+
try:
|
| 108 |
+
# Resize frame for better detection
|
| 109 |
+
frame_height, frame_width = frame.shape[:2]
|
| 110 |
+
if frame_width > 640:
|
| 111 |
+
scale = 640 / frame_width
|
| 112 |
+
frame = cv2.resize(frame, (640, int(frame_height * scale)))
|
| 113 |
+
|
| 114 |
+
# Process frame with error handling and retry
|
| 115 |
+
retries = 3
|
| 116 |
+
while retries > 0:
|
| 117 |
+
landmarks, annotated_frame = pose_detector.detect_video_frame(frame)
|
| 118 |
+
if landmarks is not None:
|
| 119 |
+
break
|
| 120 |
+
retries -= 1
|
| 121 |
+
|
| 122 |
+
# Ensure frame is properly encoded before writing
|
| 123 |
+
if annotated_frame is not None:
|
| 124 |
+
out.write(annotated_frame)
|
| 125 |
+
else:
|
| 126 |
+
out.write(frame)
|
| 127 |
+
|
| 128 |
+
if landmarks is not None:
|
| 129 |
+
# Generate skeleton data with error checking
|
| 130 |
+
try:
|
| 131 |
+
skeleton_data = skeleton_generator.generate_skeleton(landmarks)
|
| 132 |
+
animation_frames.append(skeleton_data)
|
| 133 |
+
renderer.add_keyframe(landmarks, pose_detector.pose_connections, frame_time)
|
| 134 |
+
except Exception as e:
|
| 135 |
+
print(f"Frame {frame_count} skeleton generation error: {str(e)}")
|
| 136 |
+
if animation_frames:
|
| 137 |
+
animation_frames.append(animation_frames[-1])
|
| 138 |
+
elif animation_frames:
|
| 139 |
+
animation_frames.append(animation_frames[-1])
|
| 140 |
+
|
| 141 |
+
except Exception as e:
|
| 142 |
+
print(f"Frame {frame_count} processing error: {str(e)}")
|
| 143 |
+
continue
|
| 144 |
+
|
| 145 |
+
frame_count += 1
|
| 146 |
+
frame_time = frame_count / fps
|
| 147 |
+
|
| 148 |
+
if frame_count > 1000: # Safety limit for very long videos
|
| 149 |
+
break
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
# Release resources
|
| 153 |
+
if cap is not None:
|
| 154 |
+
cap.release()
|
| 155 |
+
if out is not None:
|
| 156 |
+
out.release()
|
| 157 |
+
|
| 158 |
+
# Convert output video to proper format using more compatible settings
|
| 159 |
+
converted_output = tempfile.NamedTemporaryFile(suffix='.mp4', delete=False)
|
| 160 |
+
os.system(f'ffmpeg -y -i {output_path} -vcodec libx264 -preset ultrafast -pix_fmt yuv420p {converted_output.name}')
|
| 161 |
+
os.unlink(output_path) # Remove the original output
|
| 162 |
+
|
| 163 |
+
return converted_output.name, animation_frames
|
| 164 |
+
|
| 165 |
+
except Exception as e:
|
| 166 |
+
print(f"Error processing video: {str(e)}")
|
| 167 |
+
# Cleanup resources
|
| 168 |
+
if cap is not None:
|
| 169 |
+
cap.release()
|
| 170 |
+
if out is not None:
|
| 171 |
+
out.release()
|
| 172 |
+
return None, None
|
| 173 |
+
|
| 174 |
+
def process_gif(gif_path, pose_detector, skeleton_generator):
|
| 175 |
+
"""
|
| 176 |
+
Process GIF for pose detection and skeleton generation
|
| 177 |
+
"""
|
| 178 |
+
return process_video(gif_path, pose_detector, skeleton_generator)
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|