Skip to content

Commit 056703a

Browse files
committed
Release 7.0
1 parent a8d1883 commit 056703a

File tree

13 files changed

+312
-51
lines changed

13 files changed

+312
-51
lines changed

.github/workflows/build.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Build TinyTouch
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
tags:
7+
- "v*"
8+
9+
jobs:
10+
build-linux:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-python@v5
15+
with:
16+
python-version: "3.12"
17+
- name: Install deps
18+
run: |
19+
python -m pip install --upgrade pip setuptools wheel
20+
pip install -r requirements.txt
21+
- name: Build (PyInstaller)
22+
run: |
23+
python -m PyInstaller TinyTouch.spec
24+
- name: Package
25+
run: |
26+
cd dist
27+
zip -r TinyTouch-linux-x64.zip TinyTouch
28+
- uses: actions/upload-artifact@v4
29+
with:
30+
name: TinyTouch-linux-x64
31+
path: dist/TinyTouch-linux-x64.zip
32+
33+
build-windows:
34+
runs-on: windows-latest
35+
steps:
36+
- uses: actions/checkout@v4
37+
- uses: actions/setup-python@v5
38+
with:
39+
python-version: "3.12"
40+
- name: Install deps
41+
run: |
42+
python -m pip install --upgrade pip setuptools wheel
43+
pip install -r requirements.txt
44+
- name: Build (PyInstaller)
45+
run: |
46+
python -m PyInstaller TinyTouch.spec
47+
- name: Package
48+
run: |
49+
Compress-Archive -Path dist\\TinyTouch.exe -DestinationPath dist\\TinyTouch-windows-x64.zip
50+
- uses: actions/upload-artifact@v4
51+
with:
52+
name: TinyTouch-windows-x64
53+
path: dist/TinyTouch-windows-x64.zip

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Labeled_data/cat3_mp4/data/**
4141
Thumbs.db
4242

4343
# Ignore virtual environments
44+
.venv/
4445
venv/
4546
.env/
4647
TinyTouch_env/
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"frame": 307, "total_frames": 323}
1+
{"frame": 248, "total_frames": 323}

Labeled_data/cat3/export/cat3_metadata.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
"Video Name": "cat3",
44
"Labeling Mode": "Normal",
55
"Frame Rate": 25.0,
6-
"Zones Covered With Clothes": null,
6+
"Zones Covered With Clothes": [
7+
"L",
8+
"K",
9+
"J"
10+
],
711
"Param Labels": {
812
"Parameter_1": "P1",
913
"Parameter_2": "P2",

README.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ This application is designed for behavioral researchers to code self-contact (se
2121
2. Double-click on `LabelingApplicationx_x.exe` to launch the application.
2222

2323
---
24-
### Linux Installation
24+
### Linux Installation
2525

2626
#### Step 1: Clone the Repository
2727

@@ -47,9 +47,25 @@ pip install -r requirements.txt
4747
```
4848
#### Step 4: Run the Application
4949

50-
```bash
51-
python3 src/main.py
52-
```
50+
```bash
51+
python3 src/main.py
52+
```
53+
54+
---
55+
### Linux App (single file, PyInstaller)
56+
57+
This produces a single executable you can double-click. Build on Linux only.
58+
59+
```bash
60+
python3.12 -m venv .venv
61+
source .venv/bin/activate
62+
python -m pip install --upgrade pip setuptools wheel
63+
pip install -r requirements.txt
64+
python -m PyInstaller TinyTouch.spec
65+
```
66+
67+
The output binary is in `dist/TinyTouch`. On first launch, `config.json` is copied next to the
68+
executable so you can edit it without unpacking anything.
5369

5470
## Application Workflow
5571

TinyTouch.spec

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# -*- mode: python ; coding: utf-8 -*-
2+
3+
import os
4+
import sys
5+
6+
spec_path = os.path.abspath(sys.argv[0]) if sys.argv else os.path.abspath("TinyTouch.spec")
7+
project_root = os.path.abspath(os.path.dirname(spec_path))
8+
src_root = os.path.join(project_root, "src")
9+
10+
datas = [
11+
(os.path.join(project_root, "config.json"), "."),
12+
(os.path.join(project_root, "icons"), "icons"),
13+
]
14+
15+
a = Analysis(
16+
["src/main.py"],
17+
pathex=[src_root],
18+
binaries=[],
19+
datas=datas,
20+
hiddenimports=[
21+
"PIL._tkinter_finder",
22+
"PIL._tkinter",
23+
],
24+
hookspath=[],
25+
hooksconfig={},
26+
runtime_hooks=[],
27+
excludes=[],
28+
win_no_prefer_redirects=False,
29+
win_private_assemblies=False,
30+
cipher=None,
31+
noarchive=False,
32+
)
33+
34+
pyz = PYZ(a.pure, a.zipped_data, cipher=None)
35+
36+
exe = EXE(
37+
pyz,
38+
a.scripts,
39+
a.binaries,
40+
a.datas,
41+
[],
42+
name="TinyTouch",
43+
console=True,
44+
disable_windowed_traceback=False,
45+
argv_emulation=False,
46+
target_arch=None,
47+
codesign_identity=None,
48+
entitlements_file=None,
49+
)

src/analysis.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import plotly.graph_objects as go
1313
from plotly.subplots import make_subplots
1414

15+
from config_utils import load_config
16+
from resource_utils import resource_path
1517

1618
LIMBS = ["LH", "RH", "LL", "RL"]
1719

@@ -505,8 +507,7 @@ def _create_touch_duration_histogram(touch_durations_list, frame_rate, limbs, ou
505507

506508
def _read_new_template_flag():
507509
try:
508-
with open("config.json", "r") as f:
509-
cfg = json.load(f)
510+
cfg = load_config()
510511
return bool(cfg.get("new_template", False))
511512
except Exception:
512513
return False
@@ -519,7 +520,7 @@ def _zone_sort_key(zone: str):
519520

520521

521522
def _get_zone_list(new_template: bool):
522-
zones_dir = "icons/zones3_new_template" if new_template else "icons/zones3"
523+
zones_dir = resource_path("icons/zones3_new_template" if new_template else "icons/zones3")
523524
zones = []
524525
try:
525526
for filename in os.listdir(zones_dir):
@@ -597,17 +598,17 @@ def do_analysis(folder_path, output_folder, name, debug, frame_rate):
597598

598599
if new_template:
599600
image_paths = [
600-
"icons/LH_new_template.png",
601-
"icons/RH_new_template.png",
602-
"icons/LL_new_template.png",
603-
"icons/RL_new_template.png",
601+
resource_path("icons/LH_new_template.png"),
602+
resource_path("icons/RH_new_template.png"),
603+
resource_path("icons/LL_new_template.png"),
604+
resource_path("icons/RL_new_template.png"),
604605
]
605606
else:
606607
image_paths = [
607-
"icons/LH.png",
608-
"icons/RH.png",
609-
"icons/LL.png",
610-
"icons/RL.png",
608+
resource_path("icons/LH.png"),
609+
resource_path("icons/RH.png"),
610+
resource_path("icons/LL.png"),
611+
resource_path("icons/RL.png"),
611612
]
612613
_plot_touch_visualization_all_4(limb_rows, image_paths, output_folder)
613614

src/cloth_app.py

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,72 @@
11
import tkinter as tk
22
from PIL import Image, ImageTk
33

4+
from resource_utils import resource_path
5+
6+
DEFAULT_CLOTH_DIAGRAM_SCALE = 1.0
7+
DEFAULT_CLOTH_DOT_RADIUS = 7
8+
49

510
class ClothApp:
6-
def __init__(self, master, on_close_callback):
11+
def __init__(
12+
self,
13+
master,
14+
on_close_callback,
15+
initial_points=None,
16+
diagram_scale=DEFAULT_CLOTH_DIAGRAM_SCALE,
17+
dot_radius=DEFAULT_CLOTH_DOT_RADIUS,
18+
):
719
# Vytvoření nového okna pomocí Toplevel
820
self.top_level = tk.Toplevel(master)
921
self.top_level.title("Clothes App")
10-
self.top_level.geometry("225x348") # Double the original dimensions for the window size
1122
self.on_close_callback = on_close_callback
23+
self.diagram_scale = float(diagram_scale)
24+
self.dot_radius = int(dot_radius)
1225

13-
self.f = tk.Frame(self.top_level, bg='red')
26+
self.f = tk.Frame(self.top_level, bg='lightgrey')
1427
self.f.grid(row=1, column=0, sticky="nsew")
1528

1629
self.dots = {}
17-
self.img = Image.open("icons/diagram.png")
18-
self.img = self.img.resize((int(self.img.width*0.5), int(self.img.height*0.5)), Image.LANCZOS)
30+
self.img = Image.open(resource_path("icons/diagram.png"))
31+
self.img = self.img.resize(
32+
(int(self.img.width * self.diagram_scale), int(self.img.height * self.diagram_scale)),
33+
Image.LANCZOS,
34+
)
1935
self.photo2 = ImageTk.PhotoImage(self.img)
20-
self.canvas2 = tk.Canvas(self.f, width=self.img.width, height=self.img.height, bg='red')
36+
self.canvas2 = tk.Canvas(self.f, width=self.img.width, height=self.img.height, bg='lightgrey')
2137

22-
self.canvas2.pack()
38+
self.canvas2.pack(padx=10, pady=10)
2339
self.canvas2.create_image(0, 0, anchor="nw", image=self.photo2)
2440
self.canvas2.bind("<Button-1>", self.add_dot) # Left click to add a dot
2541
self.canvas2.bind("<Button-2>", self.remove_dot) # Middle click to remove a dot
2642
self.top_level.protocol("WM_DELETE_WINDOW", self.on_close)
2743

44+
if initial_points:
45+
for x, y in initial_points:
46+
self._create_dot(x, y)
47+
48+
win_w = self.img.width + 20
49+
win_h = self.img.height + 20
50+
self.top_level.geometry(f"{win_w}x{win_h}")
51+
2852
def on_close(self):
2953
# Callback with dots data on close
30-
self.on_close_callback(self.dots)
54+
self.on_close_callback(self.dots, self.diagram_scale)
3155
self.top_level.destroy()
3256

57+
def _create_dot(self, x, y):
58+
dot_id = self.canvas2.create_oval(
59+
x - self.dot_radius,
60+
y - self.dot_radius,
61+
x + self.dot_radius,
62+
y + self.dot_radius,
63+
fill="red",
64+
)
65+
self.dots[dot_id] = (x, y)
66+
return dot_id
67+
3368
def add_dot(self, event):
34-
dot_id = self.canvas2.create_oval(event.x-5, event.y-5, event.x+5, event.y+5, fill="red")
35-
self.dots[dot_id] = (event.x, event.y)
69+
self._create_dot(event.x, event.y)
3670
print("INFO: Clothes dots: ", self.dots)
3771

3872
def remove_dot(self, event):

src/config_utils.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,60 @@
44
"""
55

66
import json
7+
import os
8+
import shutil
79
from PIL import Image, ImageTk
810

11+
from resource_utils import get_app_dir, resource_path
12+
13+
14+
def get_config_path() -> str:
15+
return os.path.join(get_app_dir(), "config.json")
16+
17+
18+
def _ensure_config_file() -> str:
19+
config_path = get_config_path()
20+
if os.path.exists(config_path):
21+
return config_path
22+
23+
bundled_config = resource_path("config.json")
24+
if os.path.exists(bundled_config):
25+
try:
26+
shutil.copyfile(bundled_config, config_path)
27+
return config_path
28+
except Exception:
29+
return bundled_config
30+
31+
return config_path
32+
933

1034
def load_config():
1135
try:
12-
with open('config.json', 'r') as file:
36+
config_path = _ensure_config_file()
37+
with open(config_path, 'r') as file:
1338
return json.load(file)
1439
except Exception:
1540
return {}
1641

1742

1843
def save_config(config: dict) -> None:
19-
with open('config.json', 'w') as file:
44+
config_path = _ensure_config_file()
45+
with open(config_path, 'w') as file:
2046
json.dump(config, file, indent=2, sort_keys=False)
2147

2248

2349
def load_config_flags():
24-
with open('config.json', 'r') as file:
50+
config_path = _ensure_config_file()
51+
with open(config_path, 'r') as file:
2552
config = json.load(file)
2653
NEW_TEMPLATE = config.get('new_template', False)
2754
minimal_touch_length = config.get('minimal_touch_length', '280')
2855
return NEW_TEMPLATE, minimal_touch_length
2956

3057

3158
def load_perf_config():
32-
with open('config.json', 'r') as file:
59+
config_path = _ensure_config_file()
60+
with open(config_path, 'r') as file:
3361
config = json.load(file)
3462
enabled = bool(config.get('perf_enabled', False))
3563
log_every_s = float(config.get('perf_log_every_s', 2.0))
@@ -38,7 +66,8 @@ def load_perf_config():
3866

3967

4068
def load_display_limits():
41-
with open('config.json', 'r') as file:
69+
config_path = _ensure_config_file()
70+
with open(config_path, 'r') as file:
4271
config = json.load(file)
4372
max_w = config.get('max_display_width', 0)
4473
max_h = config.get('max_display_height', 0)
@@ -61,7 +90,8 @@ def load_parameter_names_into(video_obj, par_buttons, limb_par_buttons):
6190
par_buttons: dict {1: button, 2: button, 3: button}
6291
limb_par_buttons: dict {1: button, 2: button, 3: button}
6392
"""
64-
with open('config.json', 'r') as file:
93+
config_path = _ensure_config_file()
94+
with open(config_path, 'r') as file:
6595
config = json.load(file)
6696
p1 = config.get('parameter1', 'Parameter 1')
6797
p2 = config.get('parameter2', 'Parameter 2')

0 commit comments

Comments
 (0)