signlang-segmenter 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- signlang_segmenter-0.1.0/LICENSE +21 -0
- signlang_segmenter-0.1.0/PKG-INFO +144 -0
- signlang_segmenter-0.1.0/README.md +117 -0
- signlang_segmenter-0.1.0/setup.cfg +4 -0
- signlang_segmenter-0.1.0/setup.py +25 -0
- signlang_segmenter-0.1.0/signlang_segmenter/__init__.py +4 -0
- signlang_segmenter-0.1.0/signlang_segmenter/pose/__init__.py +1 -0
- signlang_segmenter-0.1.0/signlang_segmenter/video/__init__.py +17 -0
- signlang_segmenter-0.1.0/signlang_segmenter/video/optical_flow/__init__.py +15 -0
- signlang_segmenter-0.1.0/signlang_segmenter/video/optical_flow/exporter.py +111 -0
- signlang_segmenter-0.1.0/signlang_segmenter/video/optical_flow/models.py +24 -0
- signlang_segmenter-0.1.0/signlang_segmenter/video/optical_flow/motion_analyzer.py +87 -0
- signlang_segmenter-0.1.0/signlang_segmenter/video/optical_flow/segmenter.py +122 -0
- signlang_segmenter-0.1.0/signlang_segmenter/video/optical_flow/utils.py +99 -0
- signlang_segmenter-0.1.0/signlang_segmenter/video/optical_flow/visualization.py +67 -0
- signlang_segmenter-0.1.0/signlang_segmenter.egg-info/PKG-INFO +144 -0
- signlang_segmenter-0.1.0/signlang_segmenter.egg-info/SOURCES.txt +18 -0
- signlang_segmenter-0.1.0/signlang_segmenter.egg-info/dependency_links.txt +1 -0
- signlang_segmenter-0.1.0/signlang_segmenter.egg-info/requires.txt +4 -0
- signlang_segmenter-0.1.0/signlang_segmenter.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mohamed Yehia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: signlang-segmenter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python library for sign language video and pose segmentation.
|
|
5
|
+
Home-page: https://github.com/24-mohamedyehia/signlang-segmenter
|
|
6
|
+
Author: Mohamed Yehia
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: opencv-python==4.11.0.86
|
|
15
|
+
Requires-Dist: numpy==1.26.4
|
|
16
|
+
Requires-Dist: matplotlib==3.7.3
|
|
17
|
+
Requires-Dist: notebook==6.5.4
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: classifier
|
|
20
|
+
Dynamic: description
|
|
21
|
+
Dynamic: description-content-type
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
Dynamic: requires-dist
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
Dynamic: summary
|
|
27
|
+
|
|
28
|
+
# signlang-segmenter
|
|
29
|
+
|
|
30
|
+
A Python library for sign language segmentation.
|
|
31
|
+
|
|
32
|
+
## Optical Flow
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
## Important Naming Note
|
|
36
|
+
|
|
37
|
+
- Distribution/package name: `signlang-segmenter`
|
|
38
|
+
- Python import name: `signlang_segmenter`
|
|
39
|
+
|
|
40
|
+
Python imports use underscores, not dashes.
|
|
41
|
+
|
|
42
|
+
## Current Layout
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
signlang-segmenter/
|
|
46
|
+
├── signlang_segmenter/
|
|
47
|
+
│ ├── __init__.py
|
|
48
|
+
│ ├── video/
|
|
49
|
+
│ │ ├── __init__.py
|
|
50
|
+
│ │ └── optical_flow/
|
|
51
|
+
│ │ ├── __init__.py
|
|
52
|
+
│ │ ├── segmenter.py
|
|
53
|
+
│ │ ├── motion_analyzer.py
|
|
54
|
+
│ │ ├── models.py
|
|
55
|
+
│ │ ├── utils.py
|
|
56
|
+
│ │ ├── exporter.py
|
|
57
|
+
│ │ └── visualization.py
|
|
58
|
+
│ └── pose/
|
|
59
|
+
│ └── __init__.py
|
|
60
|
+
├── examples/
|
|
61
|
+
│ └── basic_pipeline.ipynb
|
|
62
|
+
├── setup.py
|
|
63
|
+
└── README.md
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
### Option 1: Install the library to use it
|
|
69
|
+
|
|
70
|
+
If you only want to use the package in your own project, install it directly from GitHub:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python -m pip install "git+https://github.com/24-mohamedyehia/signlang-segmenter.git"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Option 2: Install the project for development
|
|
77
|
+
|
|
78
|
+
If you want to modify the code and contribute, use an isolated environment and editable install:
|
|
79
|
+
|
|
80
|
+
1. Install Miniconda if needed.
|
|
81
|
+
2. Run:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git clone https://github.com/24-mohamedyehia/signlang-segmenter.git
|
|
85
|
+
cd signlang-segmenter
|
|
86
|
+
conda create -n signlang-segmenter python=3.11 -y
|
|
87
|
+
conda activate signlang-segmenter
|
|
88
|
+
python -m pip install --upgrade pip
|
|
89
|
+
python -m pip install -e .
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Quick Import Check
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import signlang_segmenter
|
|
97
|
+
import signlang_segmenter.video
|
|
98
|
+
import signlang_segmenter.pose
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Segmentation API (MVP)
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from signlang_segmenter.video import VideoSegmenter, SegmentExporter
|
|
105
|
+
|
|
106
|
+
segmenter = VideoSegmenter(
|
|
107
|
+
roi_mode="full",
|
|
108
|
+
smooth_window=11,
|
|
109
|
+
min_len_sec=0.30,
|
|
110
|
+
merge_gap_sec=0.45,
|
|
111
|
+
pad_before_frames=10,
|
|
112
|
+
pad_after_frames=12,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
segments, info = segmenter.segment(VIDEO_PATH)
|
|
116
|
+
print(f"Found {len(segments)} segments: {segments}")
|
|
117
|
+
|
|
118
|
+
exporter = SegmentExporter(out_dir="../output/segments_out")
|
|
119
|
+
exporter.export(VIDEO_PATH, segments)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Visualization
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from signlang_segmenter.video import plot_motion_segments
|
|
126
|
+
|
|
127
|
+
plot_motion_segments(segments, info)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The plot includes:
|
|
131
|
+
|
|
132
|
+
- motion raw curve
|
|
133
|
+
- motion smooth curve
|
|
134
|
+
- adaptive high/low thresholds
|
|
135
|
+
- shaded spans for detected segments
|
|
136
|
+
|
|
137
|
+
The `info` dict returned by `VideoSegmenter.segment` must include:
|
|
138
|
+
|
|
139
|
+
- `motion_raw`
|
|
140
|
+
- `motion_smooth`
|
|
141
|
+
- `fps`
|
|
142
|
+
- `frame_idx`
|
|
143
|
+
- `th_high_arr`
|
|
144
|
+
- `th_low_arr`
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# signlang-segmenter
|
|
2
|
+
|
|
3
|
+
A Python library for sign language segmentation.
|
|
4
|
+
|
|
5
|
+
## Optical Flow
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## Important Naming Note
|
|
9
|
+
|
|
10
|
+
- Distribution/package name: `signlang-segmenter`
|
|
11
|
+
- Python import name: `signlang_segmenter`
|
|
12
|
+
|
|
13
|
+
Python imports use underscores, not dashes.
|
|
14
|
+
|
|
15
|
+
## Current Layout
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
signlang-segmenter/
|
|
19
|
+
├── signlang_segmenter/
|
|
20
|
+
│ ├── __init__.py
|
|
21
|
+
│ ├── video/
|
|
22
|
+
│ │ ├── __init__.py
|
|
23
|
+
│ │ └── optical_flow/
|
|
24
|
+
│ │ ├── __init__.py
|
|
25
|
+
│ │ ├── segmenter.py
|
|
26
|
+
│ │ ├── motion_analyzer.py
|
|
27
|
+
│ │ ├── models.py
|
|
28
|
+
│ │ ├── utils.py
|
|
29
|
+
│ │ ├── exporter.py
|
|
30
|
+
│ │ └── visualization.py
|
|
31
|
+
│ └── pose/
|
|
32
|
+
│ └── __init__.py
|
|
33
|
+
├── examples/
|
|
34
|
+
│ └── basic_pipeline.ipynb
|
|
35
|
+
├── setup.py
|
|
36
|
+
└── README.md
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
### Option 1: Install the library to use it
|
|
42
|
+
|
|
43
|
+
If you only want to use the package in your own project, install it directly from GitHub:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python -m pip install "git+https://github.com/24-mohamedyehia/signlang-segmenter.git"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Option 2: Install the project for development
|
|
50
|
+
|
|
51
|
+
If you want to modify the code and contribute, use an isolated environment and editable install:
|
|
52
|
+
|
|
53
|
+
1. Install Miniconda if needed.
|
|
54
|
+
2. Run:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/24-mohamedyehia/signlang-segmenter.git
|
|
58
|
+
cd signlang-segmenter
|
|
59
|
+
conda create -n signlang-segmenter python=3.11 -y
|
|
60
|
+
conda activate signlang-segmenter
|
|
61
|
+
python -m pip install --upgrade pip
|
|
62
|
+
python -m pip install -e .
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Quick Import Check
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import signlang_segmenter
|
|
70
|
+
import signlang_segmenter.video
|
|
71
|
+
import signlang_segmenter.pose
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Segmentation API (MVP)
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from signlang_segmenter.video import VideoSegmenter, SegmentExporter
|
|
78
|
+
|
|
79
|
+
segmenter = VideoSegmenter(
|
|
80
|
+
roi_mode="full",
|
|
81
|
+
smooth_window=11,
|
|
82
|
+
min_len_sec=0.30,
|
|
83
|
+
merge_gap_sec=0.45,
|
|
84
|
+
pad_before_frames=10,
|
|
85
|
+
pad_after_frames=12,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
segments, info = segmenter.segment(VIDEO_PATH)
|
|
89
|
+
print(f"Found {len(segments)} segments: {segments}")
|
|
90
|
+
|
|
91
|
+
exporter = SegmentExporter(out_dir="../output/segments_out")
|
|
92
|
+
exporter.export(VIDEO_PATH, segments)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Visualization
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from signlang_segmenter.video import plot_motion_segments
|
|
99
|
+
|
|
100
|
+
plot_motion_segments(segments, info)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The plot includes:
|
|
104
|
+
|
|
105
|
+
- motion raw curve
|
|
106
|
+
- motion smooth curve
|
|
107
|
+
- adaptive high/low thresholds
|
|
108
|
+
- shaded spans for detected segments
|
|
109
|
+
|
|
110
|
+
The `info` dict returned by `VideoSegmenter.segment` must include:
|
|
111
|
+
|
|
112
|
+
- `motion_raw`
|
|
113
|
+
- `motion_smooth`
|
|
114
|
+
- `fps`
|
|
115
|
+
- `frame_idx`
|
|
116
|
+
- `th_high_arr`
|
|
117
|
+
- `th_low_arr`
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from setuptools import find_packages, setup
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="signlang-segmenter",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
description="A Python library for sign language video and pose segmentation.",
|
|
7
|
+
long_description=open("README.md", encoding="utf-8").read(),
|
|
8
|
+
long_description_content_type="text/markdown",
|
|
9
|
+
author="Mohamed Yehia",
|
|
10
|
+
url="https://github.com/24-mohamedyehia/signlang-segmenter",
|
|
11
|
+
packages=find_packages(),
|
|
12
|
+
python_requires=">=3.10",
|
|
13
|
+
install_requires=[
|
|
14
|
+
"opencv-python==4.11.0.86",
|
|
15
|
+
"numpy==1.26.4",
|
|
16
|
+
"matplotlib==3.7.3",
|
|
17
|
+
"notebook==6.5.4"
|
|
18
|
+
],
|
|
19
|
+
classifiers=[
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
],
|
|
25
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Pose segmentation subpackage placeholder."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Video segmentation package public API."""
|
|
2
|
+
|
|
3
|
+
from .optical_flow import (
|
|
4
|
+
MotionAnalyzer,
|
|
5
|
+
Segment,
|
|
6
|
+
SegmentExporter,
|
|
7
|
+
VideoSegmenter,
|
|
8
|
+
plot_motion_segments,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Segment",
|
|
13
|
+
"MotionAnalyzer",
|
|
14
|
+
"VideoSegmenter",
|
|
15
|
+
"SegmentExporter",
|
|
16
|
+
"plot_motion_segments",
|
|
17
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Optical-flow segmentation algorithm package."""
|
|
2
|
+
|
|
3
|
+
from .exporter import SegmentExporter
|
|
4
|
+
from .models import Segment
|
|
5
|
+
from .motion_analyzer import MotionAnalyzer
|
|
6
|
+
from .segmenter import VideoSegmenter
|
|
7
|
+
from .visualization import plot_motion_segments
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Segment",
|
|
11
|
+
"MotionAnalyzer",
|
|
12
|
+
"VideoSegmenter",
|
|
13
|
+
"SegmentExporter",
|
|
14
|
+
"plot_motion_segments",
|
|
15
|
+
]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""SegmentExporter writes detected segments as individual video clips."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
import cv2
|
|
8
|
+
|
|
9
|
+
from .models import Segment
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SegmentExporter:
|
|
13
|
+
"""Export Segment objects as per-segment MP4 clip files."""
|
|
14
|
+
|
|
15
|
+
_H264_CODECS = ("avc1", "H264", "X264")
|
|
16
|
+
_WRITER_ATTEMPT_ORDER = ("mp4v", "avc1", "H264", "X264")
|
|
17
|
+
|
|
18
|
+
def __init__(self, out_dir: str = "segments_out") -> None:
|
|
19
|
+
self.out_dir = out_dir
|
|
20
|
+
|
|
21
|
+
def export(self, video_path: str, segments: list[Segment]) -> str:
|
|
22
|
+
"""Cut and export segments from a source video to out_dir."""
|
|
23
|
+
os.makedirs(self.out_dir, exist_ok=True)
|
|
24
|
+
|
|
25
|
+
cap = cv2.VideoCapture(video_path)
|
|
26
|
+
if not cap.isOpened():
|
|
27
|
+
raise FileNotFoundError(f"Cannot open video: {video_path}")
|
|
28
|
+
|
|
29
|
+
fps = cap.get(cv2.CAP_PROP_FPS)
|
|
30
|
+
if fps <= 0:
|
|
31
|
+
fps = 25.0
|
|
32
|
+
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
33
|
+
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
34
|
+
|
|
35
|
+
for i, seg in enumerate(segments, start=1):
|
|
36
|
+
self._write_segment(cap, seg, i, fps, (w, h))
|
|
37
|
+
|
|
38
|
+
cap.release()
|
|
39
|
+
return self.out_dir
|
|
40
|
+
|
|
41
|
+
def _write_segment(
|
|
42
|
+
self,
|
|
43
|
+
cap: cv2.VideoCapture,
|
|
44
|
+
seg: Segment,
|
|
45
|
+
index: int,
|
|
46
|
+
fps: float,
|
|
47
|
+
size: tuple[int, int],
|
|
48
|
+
) -> None:
|
|
49
|
+
final_path = os.path.join(
|
|
50
|
+
self.out_dir,
|
|
51
|
+
f"seg_{index:03d}_{seg.start_frame}_{seg.end_frame}.mp4",
|
|
52
|
+
)
|
|
53
|
+
raw_path = final_path.replace(".mp4", "_raw.mp4")
|
|
54
|
+
|
|
55
|
+
writer, codec_used = self._open_writer(raw_path, fps, size)
|
|
56
|
+
|
|
57
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, seg.start_frame)
|
|
58
|
+
for _ in range(seg.start_frame, seg.end_frame + 1):
|
|
59
|
+
ret, frame = cap.read()
|
|
60
|
+
if not ret:
|
|
61
|
+
break
|
|
62
|
+
writer.write(frame)
|
|
63
|
+
writer.release()
|
|
64
|
+
|
|
65
|
+
if codec_used.lower() in {c.lower() for c in self._H264_CODECS}:
|
|
66
|
+
os.replace(raw_path, final_path)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
if self._transcode_to_h264(raw_path, final_path):
|
|
70
|
+
if os.path.exists(raw_path):
|
|
71
|
+
os.remove(raw_path)
|
|
72
|
+
else:
|
|
73
|
+
os.replace(raw_path, final_path)
|
|
74
|
+
|
|
75
|
+
def _open_writer(
|
|
76
|
+
self,
|
|
77
|
+
path: str,
|
|
78
|
+
fps: float,
|
|
79
|
+
size: tuple[int, int],
|
|
80
|
+
) -> tuple[cv2.VideoWriter, str]:
|
|
81
|
+
for codec in self._WRITER_ATTEMPT_ORDER:
|
|
82
|
+
writer = cv2.VideoWriter(path, cv2.VideoWriter_fourcc(*codec), fps, size)
|
|
83
|
+
if writer.isOpened():
|
|
84
|
+
return writer, codec
|
|
85
|
+
writer.release()
|
|
86
|
+
raise RuntimeError("Could not initialize VideoWriter with available codecs.")
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _transcode_to_h264(src_path: str, dst_path: str) -> bool:
|
|
90
|
+
ffmpeg = shutil.which("ffmpeg")
|
|
91
|
+
if ffmpeg is None:
|
|
92
|
+
return False
|
|
93
|
+
cmd = [
|
|
94
|
+
ffmpeg,
|
|
95
|
+
"-y",
|
|
96
|
+
"-loglevel",
|
|
97
|
+
"error",
|
|
98
|
+
"-i",
|
|
99
|
+
src_path,
|
|
100
|
+
"-vf",
|
|
101
|
+
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
|
102
|
+
"-c:v",
|
|
103
|
+
"libx264",
|
|
104
|
+
"-pix_fmt",
|
|
105
|
+
"yuv420p",
|
|
106
|
+
"-movflags",
|
|
107
|
+
"+faststart",
|
|
108
|
+
"-an",
|
|
109
|
+
dst_path,
|
|
110
|
+
]
|
|
111
|
+
return subprocess.run(cmd).returncode == 0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class Segment:
|
|
6
|
+
"""Represents a single motion segment identified in a video."""
|
|
7
|
+
|
|
8
|
+
start_frame: int
|
|
9
|
+
end_frame: int
|
|
10
|
+
video_path: str = ""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def length_frames(self) -> int:
|
|
14
|
+
return self.end_frame - self.start_frame + 1
|
|
15
|
+
|
|
16
|
+
def to_seconds(self, fps: float) -> tuple[float, float]:
|
|
17
|
+
"""Return (start_sec, end_sec) for this segment."""
|
|
18
|
+
return self.start_frame / fps, self.end_frame / fps
|
|
19
|
+
|
|
20
|
+
def __repr__(self) -> str:
|
|
21
|
+
return (
|
|
22
|
+
f"Segment(start={self.start_frame}, end={self.end_frame}, "
|
|
23
|
+
f"len={self.length_frames})"
|
|
24
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""MotionAnalyzer computes a per-frame motion-energy curve via optical flow."""
|
|
2
|
+
|
|
3
|
+
import cv2
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MotionAnalyzer:
|
|
8
|
+
"""Extract a 1-D motion-energy signal from a video using Farneback flow."""
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
roi_mode: str = "upper",
|
|
13
|
+
resize_width: int = 320,
|
|
14
|
+
step: int = 1,
|
|
15
|
+
) -> None:
|
|
16
|
+
if roi_mode not in ("full", "upper"):
|
|
17
|
+
raise ValueError("roi_mode must be 'full' or 'upper'")
|
|
18
|
+
self.roi_mode = roi_mode
|
|
19
|
+
self.resize_width = resize_width
|
|
20
|
+
self.step = max(1, int(step))
|
|
21
|
+
|
|
22
|
+
def compute(self, video_path: str) -> tuple[np.ndarray, np.ndarray, float, int]:
|
|
23
|
+
"""Compute motion-energy for a video path."""
|
|
24
|
+
cap = cv2.VideoCapture(video_path)
|
|
25
|
+
if not cap.isOpened():
|
|
26
|
+
raise FileNotFoundError(f"Cannot open video: {video_path}")
|
|
27
|
+
|
|
28
|
+
fps = cap.get(cv2.CAP_PROP_FPS)
|
|
29
|
+
if fps <= 0:
|
|
30
|
+
fps = 25.0
|
|
31
|
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
32
|
+
|
|
33
|
+
ret, frame = cap.read()
|
|
34
|
+
if not ret:
|
|
35
|
+
cap.release()
|
|
36
|
+
raise RuntimeError("Failed to read the first frame.")
|
|
37
|
+
|
|
38
|
+
h0, w0 = frame.shape[:2]
|
|
39
|
+
scale = 1.0
|
|
40
|
+
if self.resize_width is not None and w0 > self.resize_width:
|
|
41
|
+
scale = self.resize_width / w0
|
|
42
|
+
frame = cv2.resize(frame, (int(w0 * scale), int(h0 * scale)))
|
|
43
|
+
h, w = frame.shape[:2]
|
|
44
|
+
|
|
45
|
+
prev_gray = cv2.cvtColor(self._crop_roi(frame, h), cv2.COLOR_BGR2GRAY)
|
|
46
|
+
motion: list[float] = []
|
|
47
|
+
frame_idx: list[int] = [0]
|
|
48
|
+
idx = 0
|
|
49
|
+
|
|
50
|
+
while True:
|
|
51
|
+
for _ in range(self.step):
|
|
52
|
+
ret, frame = cap.read()
|
|
53
|
+
idx += 1
|
|
54
|
+
if not ret:
|
|
55
|
+
cap.release()
|
|
56
|
+
return (
|
|
57
|
+
np.array(motion, dtype=np.float32),
|
|
58
|
+
np.array(frame_idx, dtype=int),
|
|
59
|
+
fps,
|
|
60
|
+
total_frames,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if scale != 1.0:
|
|
64
|
+
frame = cv2.resize(frame, (w, h))
|
|
65
|
+
|
|
66
|
+
gray = cv2.cvtColor(self._crop_roi(frame, h), cv2.COLOR_BGR2GRAY)
|
|
67
|
+
flow = cv2.calcOpticalFlowFarneback(
|
|
68
|
+
prev_gray,
|
|
69
|
+
gray,
|
|
70
|
+
None,
|
|
71
|
+
pyr_scale=0.5,
|
|
72
|
+
levels=3,
|
|
73
|
+
winsize=15,
|
|
74
|
+
iterations=3,
|
|
75
|
+
poly_n=5,
|
|
76
|
+
poly_sigma=1.2,
|
|
77
|
+
flags=0,
|
|
78
|
+
)
|
|
79
|
+
mag, _ = cv2.cartToPolar(flow[..., 0], flow[..., 1])
|
|
80
|
+
motion.append(float(np.mean(mag)))
|
|
81
|
+
frame_idx.append(idx)
|
|
82
|
+
prev_gray = gray
|
|
83
|
+
|
|
84
|
+
def _crop_roi(self, frame: np.ndarray, h: int) -> np.ndarray:
|
|
85
|
+
if self.roi_mode == "full":
|
|
86
|
+
return frame
|
|
87
|
+
return frame[: int(h * 0.7), :]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""VideoSegmenter detects and returns motion-based segments in a video."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from .models import Segment
|
|
6
|
+
from .motion_analyzer import MotionAnalyzer
|
|
7
|
+
from .utils import (
|
|
8
|
+
fill_short_gaps,
|
|
9
|
+
hysteresis_binarize_var,
|
|
10
|
+
mask_to_segments,
|
|
11
|
+
moving_average,
|
|
12
|
+
postprocess_segments,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VideoSegmenter:
|
|
17
|
+
"""High-level optical-flow segmentation pipeline."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
roi_mode: str = "upper",
|
|
22
|
+
resize_width: int = 320,
|
|
23
|
+
step: int = 1,
|
|
24
|
+
smooth_window: int = 7,
|
|
25
|
+
min_len_sec: float = 0.25,
|
|
26
|
+
merge_gap_sec: float = 0.15,
|
|
27
|
+
pad_before_frames: int = 0,
|
|
28
|
+
pad_after_frames: int = 0,
|
|
29
|
+
) -> None:
|
|
30
|
+
self.smooth_window = smooth_window
|
|
31
|
+
self.min_len_sec = min_len_sec
|
|
32
|
+
self.merge_gap_sec = merge_gap_sec
|
|
33
|
+
self.pad_before_frames = max(0, int(pad_before_frames))
|
|
34
|
+
self.pad_after_frames = max(0, int(pad_after_frames))
|
|
35
|
+
self._analyzer = MotionAnalyzer(
|
|
36
|
+
roi_mode=roi_mode,
|
|
37
|
+
resize_width=resize_width,
|
|
38
|
+
step=step,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def segment(self, video_path: str) -> tuple[list[Segment], dict]:
|
|
42
|
+
"""Run segmentation on video_path and return segments with diagnostics."""
|
|
43
|
+
motion, frame_idx, fps, total_frames = self._analyzer.compute(video_path)
|
|
44
|
+
motion = motion.astype(np.float32)
|
|
45
|
+
motion_s = moving_average(motion, self.smooth_window)
|
|
46
|
+
|
|
47
|
+
th_high_arr, th_low_arr = self._adaptive_thresholds(motion_s, fps)
|
|
48
|
+
active = hysteresis_binarize_var(motion_s, th_high_arr, th_low_arr)
|
|
49
|
+
|
|
50
|
+
kernel = np.ones(max(1, int(fps * 0.25)))
|
|
51
|
+
active = np.convolve(active.astype(float), kernel, mode="same") > 0
|
|
52
|
+
|
|
53
|
+
active = fill_short_gaps(active, max_gap_frames=max(1, int(fps * 0.6)))
|
|
54
|
+
segs = mask_to_segments(active)
|
|
55
|
+
|
|
56
|
+
real_segments = self._remap_to_frames(segs, frame_idx)
|
|
57
|
+
|
|
58
|
+
min_len_frames = max(1, int(self.min_len_sec * fps))
|
|
59
|
+
merge_gap_frames = max(0, int(self.merge_gap_sec * fps))
|
|
60
|
+
real_segments = postprocess_segments(
|
|
61
|
+
real_segments,
|
|
62
|
+
min_len_frames=min_len_frames,
|
|
63
|
+
merge_gap_frames=merge_gap_frames,
|
|
64
|
+
)
|
|
65
|
+
real_segments = self._pad_segments(real_segments, total_frames)
|
|
66
|
+
|
|
67
|
+
for seg in real_segments:
|
|
68
|
+
seg.video_path = video_path
|
|
69
|
+
|
|
70
|
+
info = {
|
|
71
|
+
"fps": fps,
|
|
72
|
+
"total_frames": total_frames,
|
|
73
|
+
"th_high_arr": th_high_arr,
|
|
74
|
+
"th_low_arr": th_low_arr,
|
|
75
|
+
"motion_raw": motion,
|
|
76
|
+
"motion_smooth": motion_s,
|
|
77
|
+
"frame_idx": frame_idx,
|
|
78
|
+
"active_mask": active,
|
|
79
|
+
}
|
|
80
|
+
return real_segments, info
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def _adaptive_thresholds(
|
|
84
|
+
motion_s: np.ndarray,
|
|
85
|
+
fps: float,
|
|
86
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
87
|
+
"""Compute local adaptive high and low thresholds."""
|
|
88
|
+
win = max(5, int(fps * 1.0))
|
|
89
|
+
local_mean = moving_average(motion_s, win)
|
|
90
|
+
local_var = moving_average((motion_s - local_mean) ** 2, win)
|
|
91
|
+
local_std = np.sqrt(np.maximum(local_var, 1e-8))
|
|
92
|
+
th_high = local_mean + 1.15 * local_std
|
|
93
|
+
th_low = local_mean + 0.65 * local_std
|
|
94
|
+
return th_high, th_low
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _remap_to_frames(segs: list[Segment], frame_idx: np.ndarray) -> list[Segment]:
|
|
98
|
+
"""Map motion-array segment indices back to original frame indices."""
|
|
99
|
+
real_segments: list[Segment] = []
|
|
100
|
+
for seg in segs:
|
|
101
|
+
start = int(frame_idx[seg.start_frame + 1])
|
|
102
|
+
end = int(frame_idx[seg.end_frame + 1])
|
|
103
|
+
real_segments.append(Segment(start, end))
|
|
104
|
+
return real_segments
|
|
105
|
+
|
|
106
|
+
def _pad_segments(self, segments: list[Segment], total_frames: int) -> list[Segment]:
|
|
107
|
+
"""Expand segment boundaries by configured context then merge overlaps."""
|
|
108
|
+
if not segments:
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
if self.pad_before_frames == 0 and self.pad_after_frames == 0:
|
|
112
|
+
return segments
|
|
113
|
+
|
|
114
|
+
max_end = max(0, int(total_frames) - 1)
|
|
115
|
+
expanded = [
|
|
116
|
+
Segment(
|
|
117
|
+
max(0, seg.start_frame - self.pad_before_frames),
|
|
118
|
+
min(max_end, seg.end_frame + self.pad_after_frames),
|
|
119
|
+
)
|
|
120
|
+
for seg in segments
|
|
121
|
+
]
|
|
122
|
+
return postprocess_segments(expanded, min_len_frames=1, merge_gap_frames=0)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Signal-processing utilities used by the optical-flow segmentation pipeline."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from .models import Segment
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def moving_average(x: np.ndarray, window: int) -> np.ndarray:
|
|
9
|
+
"""Smooth a 1-D array with a uniform moving average."""
|
|
10
|
+
if window <= 1:
|
|
11
|
+
return x.astype(np.float32)
|
|
12
|
+
kernel = np.ones(int(window), dtype=np.float32) / int(window)
|
|
13
|
+
return np.convolve(x, kernel, mode="same")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def hysteresis_binarize_var(
|
|
17
|
+
signal: np.ndarray,
|
|
18
|
+
th_high_arr: np.ndarray,
|
|
19
|
+
th_low_arr: np.ndarray,
|
|
20
|
+
) -> np.ndarray:
|
|
21
|
+
"""Hysteresis thresholding with per-sample adaptive thresholds."""
|
|
22
|
+
active = np.zeros_like(signal, dtype=bool)
|
|
23
|
+
on = False
|
|
24
|
+
for i, value in enumerate(signal):
|
|
25
|
+
if not on and value >= th_high_arr[i]:
|
|
26
|
+
on = True
|
|
27
|
+
elif on and value <= th_low_arr[i]:
|
|
28
|
+
on = False
|
|
29
|
+
active[i] = on
|
|
30
|
+
return active
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def fill_short_gaps(active: np.ndarray, max_gap_frames: int) -> np.ndarray:
|
|
34
|
+
"""Fill short OFF gaps between two ON regions in a boolean mask."""
|
|
35
|
+
mask = active.copy().astype(bool)
|
|
36
|
+
n = len(mask)
|
|
37
|
+
i = 0
|
|
38
|
+
while i < n:
|
|
39
|
+
if mask[i]:
|
|
40
|
+
i += 1
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
j = i
|
|
44
|
+
while j < n and not mask[j]:
|
|
45
|
+
j += 1
|
|
46
|
+
|
|
47
|
+
gap_len = j - i
|
|
48
|
+
left_true = i - 1 >= 0 and mask[i - 1]
|
|
49
|
+
right_true = j < n and mask[j]
|
|
50
|
+
if left_true and right_true and gap_len <= max_gap_frames:
|
|
51
|
+
mask[i:j] = True
|
|
52
|
+
i = j
|
|
53
|
+
|
|
54
|
+
return mask
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def mask_to_segments(active: np.ndarray) -> list[Segment]:
|
|
58
|
+
"""Convert a boolean activity mask to a list of Segment objects."""
|
|
59
|
+
segments: list[Segment] = []
|
|
60
|
+
in_segment = False
|
|
61
|
+
start = 0
|
|
62
|
+
|
|
63
|
+
for i, is_active in enumerate(active):
|
|
64
|
+
if is_active and not in_segment:
|
|
65
|
+
in_segment = True
|
|
66
|
+
start = i
|
|
67
|
+
elif (not is_active) and in_segment:
|
|
68
|
+
in_segment = False
|
|
69
|
+
segments.append(Segment(start, i - 1))
|
|
70
|
+
|
|
71
|
+
if in_segment:
|
|
72
|
+
segments.append(Segment(start, len(active) - 1))
|
|
73
|
+
|
|
74
|
+
return segments
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def postprocess_segments(
|
|
78
|
+
segments: list[Segment],
|
|
79
|
+
min_len_frames: int,
|
|
80
|
+
merge_gap_frames: int,
|
|
81
|
+
) -> list[Segment]:
|
|
82
|
+
"""Drop short segments and merge close neighboring segments."""
|
|
83
|
+
if not segments:
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
filtered = [s for s in segments if s.length_frames >= min_len_frames]
|
|
87
|
+
if not filtered:
|
|
88
|
+
return []
|
|
89
|
+
|
|
90
|
+
merged = [filtered[0]]
|
|
91
|
+
for seg in filtered[1:]:
|
|
92
|
+
prev = merged[-1]
|
|
93
|
+
gap = seg.start_frame - prev.end_frame - 1
|
|
94
|
+
if gap <= merge_gap_frames:
|
|
95
|
+
merged[-1] = Segment(prev.start_frame, max(prev.end_frame, seg.end_frame))
|
|
96
|
+
else:
|
|
97
|
+
merged.append(seg)
|
|
98
|
+
|
|
99
|
+
return merged
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Visualization helpers for motion-based segmentation outputs."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from .models import Segment
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def plot_motion_segments(
|
|
12
|
+
segments: Iterable[Segment],
|
|
13
|
+
info: dict,
|
|
14
|
+
*,
|
|
15
|
+
figsize: tuple[float, float] = (14.0, 4.0),
|
|
16
|
+
segment_alpha: float = 0.2,
|
|
17
|
+
title: str = "Motion-based segmentation (shaded = detected segments)",
|
|
18
|
+
show: bool = True,
|
|
19
|
+
):
|
|
20
|
+
"""Plot motion curves, thresholds, and shaded detected segments.
|
|
21
|
+
|
|
22
|
+
Expected keys in info: motion_raw, motion_smooth, fps, frame_idx, th_high_arr, th_low_arr.
|
|
23
|
+
"""
|
|
24
|
+
required = {
|
|
25
|
+
"motion_raw",
|
|
26
|
+
"motion_smooth",
|
|
27
|
+
"fps",
|
|
28
|
+
"frame_idx",
|
|
29
|
+
"th_high_arr",
|
|
30
|
+
"th_low_arr",
|
|
31
|
+
}
|
|
32
|
+
missing = [k for k in required if k not in info]
|
|
33
|
+
if missing:
|
|
34
|
+
raise KeyError(f"Missing required info keys: {missing}")
|
|
35
|
+
|
|
36
|
+
motion = np.asarray(info["motion_raw"])
|
|
37
|
+
motion_s = np.asarray(info["motion_smooth"])
|
|
38
|
+
th_high_arr = np.asarray(info["th_high_arr"])
|
|
39
|
+
th_low_arr = np.asarray(info["th_low_arr"])
|
|
40
|
+
fps = float(info["fps"])
|
|
41
|
+
frame_idx = np.asarray(info["frame_idx"])
|
|
42
|
+
|
|
43
|
+
if fps <= 0:
|
|
44
|
+
raise ValueError("fps must be > 0")
|
|
45
|
+
if frame_idx.size < 2:
|
|
46
|
+
raise ValueError("frame_idx must contain at least two indices")
|
|
47
|
+
|
|
48
|
+
t = frame_idx[1:] / fps
|
|
49
|
+
|
|
50
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
51
|
+
ax.plot(t, motion, label="motion raw")
|
|
52
|
+
ax.plot(t, motion_s, label="motion smooth")
|
|
53
|
+
ax.plot(t, th_high_arr, linestyle="--", label="th_high")
|
|
54
|
+
ax.plot(t, th_low_arr, linestyle="--", label="th_low")
|
|
55
|
+
|
|
56
|
+
for seg in segments:
|
|
57
|
+
ax.axvspan(seg.start_frame / fps, seg.end_frame / fps, alpha=segment_alpha)
|
|
58
|
+
|
|
59
|
+
ax.set_xlabel("Time (sec)")
|
|
60
|
+
ax.set_ylabel("Motion energy")
|
|
61
|
+
ax.legend()
|
|
62
|
+
ax.set_title(title)
|
|
63
|
+
|
|
64
|
+
if show:
|
|
65
|
+
plt.show()
|
|
66
|
+
|
|
67
|
+
return fig, ax
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: signlang-segmenter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python library for sign language video and pose segmentation.
|
|
5
|
+
Home-page: https://github.com/24-mohamedyehia/signlang-segmenter
|
|
6
|
+
Author: Mohamed Yehia
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: opencv-python==4.11.0.86
|
|
15
|
+
Requires-Dist: numpy==1.26.4
|
|
16
|
+
Requires-Dist: matplotlib==3.7.3
|
|
17
|
+
Requires-Dist: notebook==6.5.4
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: classifier
|
|
20
|
+
Dynamic: description
|
|
21
|
+
Dynamic: description-content-type
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
Dynamic: requires-dist
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
Dynamic: summary
|
|
27
|
+
|
|
28
|
+
# signlang-segmenter
|
|
29
|
+
|
|
30
|
+
A Python library for sign language segmentation.
|
|
31
|
+
|
|
32
|
+
## Optical Flow
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
## Important Naming Note
|
|
36
|
+
|
|
37
|
+
- Distribution/package name: `signlang-segmenter`
|
|
38
|
+
- Python import name: `signlang_segmenter`
|
|
39
|
+
|
|
40
|
+
Python imports use underscores, not dashes.
|
|
41
|
+
|
|
42
|
+
## Current Layout
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
signlang-segmenter/
|
|
46
|
+
├── signlang_segmenter/
|
|
47
|
+
│ ├── __init__.py
|
|
48
|
+
│ ├── video/
|
|
49
|
+
│ │ ├── __init__.py
|
|
50
|
+
│ │ └── optical_flow/
|
|
51
|
+
│ │ ├── __init__.py
|
|
52
|
+
│ │ ├── segmenter.py
|
|
53
|
+
│ │ ├── motion_analyzer.py
|
|
54
|
+
│ │ ├── models.py
|
|
55
|
+
│ │ ├── utils.py
|
|
56
|
+
│ │ ├── exporter.py
|
|
57
|
+
│ │ └── visualization.py
|
|
58
|
+
│ └── pose/
|
|
59
|
+
│ └── __init__.py
|
|
60
|
+
├── examples/
|
|
61
|
+
│ └── basic_pipeline.ipynb
|
|
62
|
+
├── setup.py
|
|
63
|
+
└── README.md
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
### Option 1: Install the library to use it
|
|
69
|
+
|
|
70
|
+
If you only want to use the package in your own project, install it directly from GitHub:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python -m pip install "git+https://github.com/24-mohamedyehia/signlang-segmenter.git"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Option 2: Install the project for development
|
|
77
|
+
|
|
78
|
+
If you want to modify the code and contribute, use an isolated environment and editable install:
|
|
79
|
+
|
|
80
|
+
1. Install Miniconda if needed.
|
|
81
|
+
2. Run:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git clone https://github.com/24-mohamedyehia/signlang-segmenter.git
|
|
85
|
+
cd signlang-segmenter
|
|
86
|
+
conda create -n signlang-segmenter python=3.11 -y
|
|
87
|
+
conda activate signlang-segmenter
|
|
88
|
+
python -m pip install --upgrade pip
|
|
89
|
+
python -m pip install -e .
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Quick Import Check
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import signlang_segmenter
|
|
97
|
+
import signlang_segmenter.video
|
|
98
|
+
import signlang_segmenter.pose
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Segmentation API (MVP)
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from signlang_segmenter.video import VideoSegmenter, SegmentExporter
|
|
105
|
+
|
|
106
|
+
segmenter = VideoSegmenter(
|
|
107
|
+
roi_mode="full",
|
|
108
|
+
smooth_window=11,
|
|
109
|
+
min_len_sec=0.30,
|
|
110
|
+
merge_gap_sec=0.45,
|
|
111
|
+
pad_before_frames=10,
|
|
112
|
+
pad_after_frames=12,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
segments, info = segmenter.segment(VIDEO_PATH)
|
|
116
|
+
print(f"Found {len(segments)} segments: {segments}")
|
|
117
|
+
|
|
118
|
+
exporter = SegmentExporter(out_dir="../output/segments_out")
|
|
119
|
+
exporter.export(VIDEO_PATH, segments)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Visualization
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from signlang_segmenter.video import plot_motion_segments
|
|
126
|
+
|
|
127
|
+
plot_motion_segments(segments, info)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The plot includes:
|
|
131
|
+
|
|
132
|
+
- motion raw curve
|
|
133
|
+
- motion smooth curve
|
|
134
|
+
- adaptive high/low thresholds
|
|
135
|
+
- shaded spans for detected segments
|
|
136
|
+
|
|
137
|
+
The `info` dict returned by `VideoSegmenter.segment` must include:
|
|
138
|
+
|
|
139
|
+
- `motion_raw`
|
|
140
|
+
- `motion_smooth`
|
|
141
|
+
- `fps`
|
|
142
|
+
- `frame_idx`
|
|
143
|
+
- `th_high_arr`
|
|
144
|
+
- `th_low_arr`
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
signlang_segmenter/__init__.py
|
|
5
|
+
signlang_segmenter.egg-info/PKG-INFO
|
|
6
|
+
signlang_segmenter.egg-info/SOURCES.txt
|
|
7
|
+
signlang_segmenter.egg-info/dependency_links.txt
|
|
8
|
+
signlang_segmenter.egg-info/requires.txt
|
|
9
|
+
signlang_segmenter.egg-info/top_level.txt
|
|
10
|
+
signlang_segmenter/pose/__init__.py
|
|
11
|
+
signlang_segmenter/video/__init__.py
|
|
12
|
+
signlang_segmenter/video/optical_flow/__init__.py
|
|
13
|
+
signlang_segmenter/video/optical_flow/exporter.py
|
|
14
|
+
signlang_segmenter/video/optical_flow/models.py
|
|
15
|
+
signlang_segmenter/video/optical_flow/motion_analyzer.py
|
|
16
|
+
signlang_segmenter/video/optical_flow/segmenter.py
|
|
17
|
+
signlang_segmenter/video/optical_flow/utils.py
|
|
18
|
+
signlang_segmenter/video/optical_flow/visualization.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
signlang_segmenter
|