vertalius commited on
Commit
1e91e1d
·
verified ·
1 Parent(s): ca7d189

Upload 12 files

Browse files
Files changed (13) hide show
  1. .gitattributes +1 -0
  2. .replit +38 -0
  3. animation_exporter.py +105 -0
  4. animation_renderer.py +61 -0
  5. app.py +304 -0
  6. database.py +61 -0
  7. generated-icon.png +3 -0
  8. pose_detector.py +271 -0
  9. pyproject.toml +13 -0
  10. replit.nix +7 -0
  11. skeleton_generator.py +81 -0
  12. utils.py +178 -0
  13. 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

  • SHA256: f4b09983ba71596a7f5001c878a245c173c6303ae095e1446062d3b06b209c37
  • Pointer size: 131 Bytes
  • Size of remote file: 441 kB
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