Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
fa27b32
A user-friendly GUI (beta version) (#149)
AYLARDJ Dec 6, 2024
055d8a4
A user-friendly GUI (beta version) (#149)
AYLARDJ Dec 6, 2024
8267bec
Merge branch 'Gui' of https://github.com/perfanalytics/pose2sim into Gui
davidpagnon Dec 6, 2024
6ba83c8
Beta version of GUI (#179)
AYLARDJ May 5, 2025
fc7a1a8
Merge branch 'main' into Gui
davidpagnon May 9, 2025
4874b6b
solved all merge conflicts
davidpagnon May 9, 2025
3fcf257
ready to test now
davidpagnon May 9, 2025
9d981c4
first edits (to the wrong file?)
davidpagnon May 9, 2025
3809b93
intro animation, displays center of screen (#183)
hunminkim98 May 9, 2025
4641270
Few edits (see pull request #180)
davidpagnon May 11, 2025
5997275
Merge branch 'Gui' of https://github.com/perfanalytics/Pose2Sim into Gui
davidpagnon May 11, 2025
9773d49
small edits
davidpagnon May 11, 2025
1eb1dd1
pose2sim_installer.py: First attempt at making a one-click install
davidpagnon May 12, 2025
32fec86
Added a simple installation GUI
davidpagnon May 12, 2025
a7ae106
Calibration:
davidpagnon May 12, 2025
7f19bfd
little bug fix in calib
davidpagnon May 13, 2025
7ef7dfe
Calibration:
davidpagnon May 15, 2025
fef4ce2
Worsk if no left side to be swapped with (single hand, for exampe)
davidpagnon May 20, 2025
cf80613
Auto mode for face blurring and some improvements for the script. (#190)
hunminkim98 Jun 27, 2025
d726604
Add conda environment configuration for Pose2Sim
davidpagnon Sep 18, 2025
eca0229
GUI: conflicts resolved, ready for review (#198)
AYLARDJ Oct 6, 2025
1bcde06
Merge branch 'main' into Gui
davidpagnon Oct 20, 2025
ded06f2
remove output calibration files
davidpagnon Oct 22, 2025
7510f7b
removed deep-sort-realtime dependency
davidpagnon Oct 22, 2025
d714af8
Added logo to intro, dark/light switch, working translation, faster l…
davidpagnon Oct 24, 2025
b9c14cb
Merge branch 'main' into Gui
davidpagnon Oct 24, 2025
354c635
Added logo in sidebar
davidpagnon Oct 24, 2025
282e25e
Merge branch 'Gui' of https://github.com/perfanalytics/Pose2Sim into …
davidpagnon Oct 24, 2025
6cee938
Missing import
davidpagnon Oct 25, 2025
33149f3
Add trc_rotate and trc_scale CLI scripts and functions
davidpagnon Nov 2, 2025
8e65272
More robust selection of Qualisys video cameras
davidpagnon Nov 6, 2025
0c61fc4
custom pose between double quote for easier handling by GUI
davidpagnon Nov 21, 2025
88bc833
changed head segment measurement to neck to nose *1.5
davidpagnon Nov 21, 2025
e85a747
fixed bug on empty frame with Mac CoreMLExecutionProvider, added_setu…
davidpagnon Nov 21, 2025
1059776
added intrinsic and extrinsic folders removed by mistake
davidpagnon Nov 25, 2025
abff455
Reestablish neck_to_head measurement, with 0.8% corrective factor
davidpagnon Nov 26, 2025
fb0b6e7
extrinsic images or videos don't need to be in specific folders anymore
davidpagnon Dec 1, 2025
cc469f8
fixed edge case when no detection at all on video
davidpagnon Dec 4, 2025
c30683a
Filter some numpy warnings
davidpagnon Dec 4, 2025
5d3bd20
no need to restart calibration from scratch anymore when mistakes
davidpagnon Dec 4, 2025
f63f591
more robust json_display script
davidpagnon Dec 10, 2025
d429ad7
Edited signature of indices_of_first_last_non_nan_chunks for future c…
davidpagnon Dec 10, 2025
a51dc1b
Merge branch 'main' into Gui
davidpagnon Dec 10, 2025
6dd61d9
revert debug save in marker augmentation
davidpagnon Dec 10, 2025
39ef1eb
Merge branch 'Gui' of https://github.com/perfanalytics/Pose2Sim into …
davidpagnon Dec 10, 2025
5312e85
calibration_type = 'convert' by default
davidpagnon Dec 10, 2025
140543f
minor formatting changes
davidpagnon Dec 10, 2025
2d1d31d
This commit might need to be reverted
davidpagnon Dec 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
374 changes: 374 additions & 0 deletions GUI/app.py

Large diffs are not rendered by default.

Binary file added GUI/assets/pose2sim_logo.ico
Binary file not shown.
Binary file added GUI/assets/pose2sim_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2,380 changes: 2,380 additions & 0 deletions GUI/blur.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions GUI/cache/citation_data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"10.21105/joss.04362": {"title": "Pose2Sim: An open-source Python package for multiview markerless kinematics", "citation_count": 42, "last_updated": "2025-03-17"}, "10.3390/s22072712": {"title": "Pose2Sim: An End-to-End Workflow for 3D Markerless Sports Kinematics\u2014Part 2: Accuracy", "citation_count": 35, "last_updated": "2025-03-17"}, "10.3390/s21196530": {"title": "Pose2Sim: An End-to-End Workflow for 3D Markerless Sports Kinematics\u2014Part 1: Robustness", "citation_count": 48, "last_updated": "2025-03-17"}, "10.21105/joss.06849": {"title": "Sports2D: Compute 2D human pose and angles from a video or a webcam", "citation_count": 8, "last_updated": "2025-03-17"}}
113 changes: 113 additions & 0 deletions GUI/config_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from pathlib import Path
import toml


class ConfigGenerator:
def __init__(self):
# Load templates from files to avoid comment issues
self.config_3d_template_path = Path('templates') / '3d_config_template.toml'
self.config_2d_template_path = Path('templates') /'2d_config_template.toml'

# Create templates directory if it doesn't exist
Path('templates').mkdir(parents=True, exist_ok=True)

# Write the template files if they don't exist
self.create_template_files()

def create_template_files(self):
"""Create template files if they don't exist"""
# Create 3D template file
if not self.config_3d_template_path.exists():
with open(self.config_3d_template_path, 'w', encoding='utf-8') as f:
toml.dump(self.get_3d_template(), f)

# Create 2D template file
if not self.config_2d_template_path.exists():
with open(self.config_2d_template_path, 'w', encoding='utf-8') as f:
toml.dump(self.get_2d_template(), f)

def get_3d_template(self):
"""Return the 3D configuration template"""
from Pose2Sim import Pose2Sim
config_template_3d = toml.load(Path(Pose2Sim.__file__).parent / 'Demo_SinglePerson' / 'Config.toml')
return config_template_3d

def get_2d_template(self):
"""Return the 2D configuration template"""
from Sports2D import Sports2D
config_template_2d = toml.load(Path(Sports2D.__file__).parent / 'Demo/Config_demo.toml')
return config_template_2d


def generate_2d_config(self, config_path, settings):
"""Generate configuration file for 2D analysis"""
try:
# Load the template
config = toml.load(self.config_2d_template_path)

# Debug print to check settings
print("2D Settings being applied:", settings)

# Update sections recursively
for section_name, section_data in settings.items():
if section_name not in config:
config[section_name] = {}

self.update_nested_section(config[section_name], section_data)

# Write the updated config with pretty formatting
with open(config_path, 'w', encoding='utf-8') as f:
toml.dump(config, f)

print(f"2D Config file saved successfully to {config_path}")
return True
except Exception as e:
print(f"Error generating 2D config: {e}")
import traceback
traceback.print_exc()
return False

def generate_3d_config(self, config_path, settings):
"""Generate configuration file for 3D analysis"""
try:
# Parse the template
config = toml.load(self.config_3d_template_path)

# Debug print to check settings
print("3D Settings being applied:", settings)

# Update sections recursively
for section_name, section_data in settings.items():
if section_name not in config:
config[section_name] = {}

self.update_nested_section(config[section_name], section_data)

# Write the updated config with pretty formatting
with open(config_path, 'w', encoding='utf-8') as f:
toml.dump(config, f)

print(f"3D Config file saved successfully to {config_path}")
return True
except Exception as e:
print(f"Error generating 3D config: {e}")
import traceback
traceback.print_exc()
return False

def update_nested_section(self, config_section, settings_section):
"""Recursively update nested sections of the configuration file"""
if not isinstance(settings_section, dict):
return

for key, value in settings_section.items():
if isinstance(value, dict):
# If the key doesn't exist in the config section, create it
if key not in config_section:
config_section[key] = {}

# Recursively update the subsection
self.update_nested_section(config_section[key], value)
else:
# Update the value
config_section[key] = value
245 changes: 245 additions & 0 deletions GUI/intro.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import tkinter as tk
import customtkinter as ctk

class IntroWindow:
"""Displays an animated introduction window for the Pose2Sim GUI."""
def __init__(self, color='dark'):
"""Initializes the IntroWindow.

Args:
color (str, optional): The color theme for the window.
Can be 'light' or 'dark'. Defaults to 'dark'.
"""
# Set color parameters based on choice
if color.lower() == 'light':
self.main_color = 'black'
self.shadow_color = '#404040' # Dark gray
self.main_color_value = 0
self.shadow_color_value = 64
self.bg_color = '#F0F0F0' # Light gray
elif color.lower() == 'dark':
self.main_color = 'white'
self.shadow_color = '#AAAAAA' # Light gray
self.main_color_value = 255
self.shadow_color_value = 170
self.bg_color = '#1A1A1A' # Very dark gray


# Create the intro window
self.root = ctk.CTk()
self.root.title("Welcome to Pose2Sim")

# Get screen dimensions
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()

# Set window size (80% of screen size)
# window_width = int(screen_width * 0.7)
# window_height = int(screen_height * 0.7)

# Size should be same as app.py
window_width = 1300
window_height = 800

# Calculate position for center of screen
x = (screen_width - window_width) // 2
y = (screen_height - window_height) // 2

# Set window size and position
self.root.geometry(f"{window_width}x{window_height}+{x}+{y}")

# Set background color
self.root.configure(fg_color=self.bg_color)

# Create canvas for animation
self.canvas = tk.Canvas(self.root, bg=self.bg_color, highlightthickness=0)
self.canvas.pack(expand=True, fill='both')

# Create individual letters with initial opacity
letters = ['P', 'o', 's', 'e', '2', 'S', 'i', 'm']
self.text_ids = []
self.shadow_ids = [] # Add shadow text IDs
spacing = 50 # Adjust spacing between letters
total_width = len(letters) * spacing
start_x = window_width/2 - total_width/2

for i, letter in enumerate(letters):
# Adjust font size for P and S
font_size = 78 if letter in ['P', '2', 'S'] else 70

if letter == 'i' or letter == 'm':
spacing = 49
elif letter == 'i':
spacing = 55
elif letter == 'S':
spacing = 51
elif letter == 's':
spacing = 52
elif letter == 'o':
spacing = 54
# Create shadow text (slightly offset)
shadow_id = self.canvas.create_text(
start_x + i * spacing + 2, # Offset by 2 pixels right
window_height/2 + 2, # Offset by 2 pixels down
text=letter,
font=('Helvetica', font_size, 'bold'),
fill=self.shadow_color,
state='hidden'
)
self.shadow_ids.append(shadow_id)

# Create main text
text_id = self.canvas.create_text(
start_x + i * spacing,
window_height/2,
text=letter,
font=('Helvetica', font_size, 'bold'),
fill=self.main_color,
state='hidden'
)

self.text_ids.append(text_id)
spacing = 50 # Reset spacing for other letters

# Store animation parameters
self.opacity = 0
self.fadein_step = 0.008 # Time step for fade-in/out
self.fadeout_step = 0.0018
self.current_group = 0 # Track current group (0: Pose, 1: 2, 2: Sim)
self.animation_done = False
self.after_id = None

# Create subtitle text
subtitle = "markerless motion capture solution"
subtitle_font_size = 26

self.subtitle_shadow_id = self.canvas.create_text(
window_width/2 - 30 + 1,
window_height/2 + 60 + 1,
text=subtitle,
font=('Helvetica', subtitle_font_size),
fill=self.shadow_color,
state='hidden'
)

self.subtitle_id = self.canvas.create_text(
window_width/2 - 30,
window_height/2 + 60,
text=subtitle,
font=('Helvetica', subtitle_font_size),
fill=self.main_color,
state='hidden'
)

# Define letter groups (including shadows)
self.groups = [
list(zip(self.text_ids[:4], self.shadow_ids[:4])), # Pose
list(zip([self.text_ids[4]], [self.shadow_ids[4]])), # 2
list(zip(self.text_ids[5:], self.shadow_ids[5:])) # Sim
]

# Add subtitle as the 4th group
self.groups.append([(self.subtitle_id, self.subtitle_shadow_id)])

# Bind window close event
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

# Start the fade-in animation after a short delay
self.after_id = self.root.after(150, self.fade_in)

def on_closing(self):
"""Handles the window closing event."""
if self.after_id:
self.root.after_cancel(self.after_id)
self.animation_done = True
self.root.destroy()

def fade_in(self):
"""Animates the fade-in effect for the text elements."""
if not self.root.winfo_exists():
return
if self.current_group < len(self.groups):
if self.opacity < 1:
self.opacity += self.fadein_step
# Make current group visible and set opacity
for text_id, shadow_id in self.groups[self.current_group]:
self.canvas.itemconfig(shadow_id, state='normal')
self.canvas.itemconfig(text_id, state='normal')

# Calculate color values based on mode
if self.main_color == 'black':
# Light mode (black text on #F0F0F0 background)
main_r = int(240 * (1 - self.opacity) + 0 * self.opacity) # Fade from bg color (240) to black (0)
shadow_r = int(240 * (1 - self.opacity) + 64 * self.opacity) # Fade from bg color to shadow
hex_color = f'#{main_r:02x}{main_r:02x}{main_r:02x}'
shadow_color = f'#{shadow_r:02x}{shadow_r:02x}{shadow_r:02x}'
elif self.main_color == 'white':
# Dark mode (white text on #1A1A1A background)
main_r = int(26 * (1 - self.opacity) + 255 * self.opacity) # Fade from bg color (26) to white (255)
shadow_r = int(26 * (1 - self.opacity) + self.shadow_color_value * self.opacity) # Fade from bg to shadow
hex_color = f'#{main_r:02x}{main_r:02x}{main_r:02x}'
shadow_color = f'#{shadow_r:02x}{shadow_r:02x}{shadow_r:02x}'

self.canvas.itemconfig(shadow_id, fill=shadow_color)
self.canvas.itemconfig(text_id, fill=hex_color)
self.after_id = self.root.after(1, self.fade_in)
else:
self.opacity = 0
self.current_group += 1
self.after_id = self.root.after(1, self.fade_in)
else:
self.opacity = 1
self.fade_out()

def fade_out(self):
"""Animates the fade-out effect for the text elements and closes the window."""
if not self.root.winfo_exists():
return
if self.opacity > 0:
self.opacity -= self.fadeout_step
# Update all letters opacity together

# Calculate color values based on mode
if self.main_color == 'black':
# Light mode (black text on #F0F0F0 background)
main_r = int(240 * (1 - self.opacity) + 0 * self.opacity) # Fade from bg color (240) to black (0)
shadow_r = int(240 * (1 - self.opacity) + 64 * self.opacity) # Fade from bg color to shadow
hex_color = f'#{main_r:02x}{main_r:02x}{main_r:02x}'
shadow_color = f'#{shadow_r:02x}{shadow_r:02x}{shadow_r:02x}'
elif self.main_color == 'white':
# Dark mode (white text on #1A1A1A background)
main_r = int(26 * (1 - self.opacity) + 255 * self.opacity) # Fade from bg color (26) to white (255)
shadow_r = int(26 * (1 - self.opacity) + self.shadow_color_value * self.opacity) # Fade from bg to shadow
hex_color = f'#{main_r:02x}{main_r:02x}{main_r:02x}'
shadow_color = f'#{shadow_r:02x}{shadow_r:02x}{shadow_r:02x}'


for text_id, shadow_id in zip(self.text_ids, self.shadow_ids):
self.canvas.itemconfig(shadow_id, fill=shadow_color)
self.canvas.itemconfig(text_id, fill=hex_color)
self.canvas.itemconfig(self.subtitle_shadow_id, fill=shadow_color)
self.canvas.itemconfig(self.subtitle_id, fill=hex_color)
self.after_id = self.root.after(1, self.fade_out)
else:
self.animation_done = True
if self.root.winfo_exists():
self.on_closing()

def run(self):
"""Runs the main event loop for the intro window.

Returns:
bool: True when the animation is complete and the window is closed.
"""
self.root.mainloop()

if self.after_id:
self.root.after_cancel(self.after_id)

self.animation_done = True
return self.animation_done

if __name__ == "__main__":

intro = IntroWindow('dark')
intro.run()
Loading