siss 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.
- siss-0.1.0/LICENSE +21 -0
- siss-0.1.0/PKG-INFO +29 -0
- siss-0.1.0/README.md +160 -0
- siss-0.1.0/setup.cfg +4 -0
- siss-0.1.0/setup.py +34 -0
- siss-0.1.0/src/siss.egg-info/PKG-INFO +29 -0
- siss-0.1.0/src/siss.egg-info/SOURCES.txt +13 -0
- siss-0.1.0/src/siss.egg-info/dependency_links.txt +1 -0
- siss-0.1.0/src/siss.egg-info/entry_points.txt +2 -0
- siss-0.1.0/src/siss.egg-info/requires.txt +3 -0
- siss-0.1.0/src/siss.egg-info/top_level.txt +1 -0
- siss-0.1.0/src/utils/__init__.py +3 -0
- siss-0.1.0/src/utils/video_processing.py +226 -0
- siss-0.1.0/tests/test_duotone.py +90 -0
- siss-0.1.0/tests/test_halftone.py +119 -0
siss-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Michail Semoglou
|
|
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.
|
siss-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: siss
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A command-line utility for applying artistic effects to videos
|
|
5
|
+
Home-page: https://github.com/MichailSemoglou/siss
|
|
6
|
+
Author: Michail Semoglou
|
|
7
|
+
Author-email: m.semoglou@tongji.edu.cn
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Multimedia :: Video
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Requires-Python: >=3.6
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: opencv-python>=4.5.0
|
|
20
|
+
Requires-Dist: numpy>=1.20.0
|
|
21
|
+
Requires-Dist: tqdm>=4.60.0
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: author-email
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: home-page
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
siss-0.1.0/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Siss
|
|
2
|
+
|
|
3
|
+
A command-line utility for applying artistic effects to videos.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Duotone Effect**: Creates a video with two selected colors mapped to dark and light areas
|
|
11
|
+
- **Halftone Effect**: Creates a video with symbol patterns of varying sizes to represent dark and light areas
|
|
12
|
+
- **Cross-platform Compatibility**: Handles codec differences between operating systems
|
|
13
|
+
- **Progress Tracking**: Shows real-time progress during video processing
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
### Option 1: Clone and Install
|
|
18
|
+
|
|
19
|
+
1. Clone this repository:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/MichailSemoglou/siss.git
|
|
23
|
+
cd siss
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
2. Create a virtual environment (recommended):
|
|
27
|
+
```bash
|
|
28
|
+
python -m venv venv
|
|
29
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
3. Install the required dependencies:
|
|
33
|
+
```bash
|
|
34
|
+
pip install -r requirements.txt
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Option 2: Install from Source
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install git+https://github.com/MichailSemoglou/siss.git
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
### Basic Usage
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python -m src.main input_video.mp4 output_video.mp4 --effect duotone
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or if installed using pip:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
siss input_video.mp4 output_video.mp4 --effect duotone
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The tool supports various video formats including MP4, MOV, AVI, and more. The output format is determined by the file extension you specify for the output file.
|
|
58
|
+
|
|
59
|
+
### Duotone Effect
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
python -m src.main input_video.mp4 output_duotone.mp4 --effect duotone --color1 255 0 0 --color2 0 255 255
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This applies a duotone effect with red for dark areas and cyan for light areas.
|
|
66
|
+
|
|
67
|
+
### Halftone Effect
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python -m src.main input_video.mp4 output_halftone.mp4 --effect halftone --symbol_size 12 --symbol_type asterisk --color1 0 0 0 --color2 255 255 255
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This applies a halftone effect with black asterisks on a white background.
|
|
74
|
+
|
|
75
|
+
### Codec Compatibility Fix
|
|
76
|
+
|
|
77
|
+
If you encounter issues with codecs (especially on different operating systems), use the `--use-codec-fix` option:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
python -m src.main input_video.mp4 output_video.mp4 --effect duotone --use-codec-fix
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This option uses an adaptive approach to find compatible codecs for your specific system.
|
|
84
|
+
|
|
85
|
+
### Available Options
|
|
86
|
+
|
|
87
|
+
- `--effect`: Choose between `duotone` or `halftone` (required)
|
|
88
|
+
- `--color1`: First color in RGB format (default: 255 0 0, red)
|
|
89
|
+
- `--color2`: Second color in RGB format (default: 0 255 255, cyan)
|
|
90
|
+
- `--symbol_size`: Size of symbols for halftone effect (default: 10)
|
|
91
|
+
- `--symbol_type`: Type of symbol for halftone effect (choices: plus, asterisk, slash, default: plus)
|
|
92
|
+
- `--use-codec-fix`: Use adaptive codec selection for cross-platform compatibility
|
|
93
|
+
|
|
94
|
+
## Examples
|
|
95
|
+
|
|
96
|
+
### Creating a blue/yellow duotone effect:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python -m src.main video.mp4 blue_yellow.mp4 --effect duotone --color1 0 0 255 --color2 255 255 0
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Creating a halftone effect with slash symbols:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
python -m src.main video.mp4 halftone_slashes.mp4 --effect halftone --symbol_type slash --symbol_size 15
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Using MOV files:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
python -m src.main input.mov output.mov --effect duotone --color1 0 0 255 --color2 255 255 0
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Project Structure
|
|
115
|
+
|
|
116
|
+
- `src/`
|
|
117
|
+
- `main.py`: Command-line interface for the tool
|
|
118
|
+
- `duotone.py`: Contains the duotone effect implementation
|
|
119
|
+
- `halftone.py`: Contains the halftone effect implementation
|
|
120
|
+
- `codec_fix.py`: Handles cross-platform codec compatibility
|
|
121
|
+
- `utils/`
|
|
122
|
+
- `video_processing.py`: Utility functions for video handling
|
|
123
|
+
|
|
124
|
+
## Requirements
|
|
125
|
+
|
|
126
|
+
- Python 3.6+
|
|
127
|
+
- OpenCV (cv2)
|
|
128
|
+
- NumPy
|
|
129
|
+
- tqdm
|
|
130
|
+
|
|
131
|
+
## Troubleshooting
|
|
132
|
+
|
|
133
|
+
### Video Output Issues
|
|
134
|
+
|
|
135
|
+
If you encounter issues with video output:
|
|
136
|
+
|
|
137
|
+
1. Try using the `--use-codec-fix` option to automatically find a compatible codec
|
|
138
|
+
2. Check that you have the necessary codecs installed for your operating system
|
|
139
|
+
3. If creating MP4 files on Windows, try using AVI format instead
|
|
140
|
+
|
|
141
|
+
### Memory Limitations
|
|
142
|
+
|
|
143
|
+
For large videos, the tool processes frames sequentially to minimize memory usage. If you still experience memory issues:
|
|
144
|
+
|
|
145
|
+
1. Try processing a shorter clip first
|
|
146
|
+
2. Reduce the resolution of your input video
|
|
147
|
+
|
|
148
|
+
## Contributing
|
|
149
|
+
|
|
150
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
151
|
+
|
|
152
|
+
1. Fork the repository
|
|
153
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
154
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
155
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
156
|
+
5. Open a Pull Request
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
siss-0.1.0/setup.cfg
ADDED
siss-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="siss",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
description="A command-line utility for applying artistic effects to videos",
|
|
7
|
+
author="Michail Semoglou",
|
|
8
|
+
author_email="m.semoglou@tongji.edu.cn",
|
|
9
|
+
url="https://github.com/MichailSemoglou/siss",
|
|
10
|
+
packages=find_packages(where="src"),
|
|
11
|
+
package_dir={"": "src"},
|
|
12
|
+
install_requires=[
|
|
13
|
+
"opencv-python>=4.5.0",
|
|
14
|
+
"numpy>=1.20.0",
|
|
15
|
+
"tqdm>=4.60.0",
|
|
16
|
+
],
|
|
17
|
+
python_requires=">=3.6",
|
|
18
|
+
entry_points={
|
|
19
|
+
"console_scripts": [
|
|
20
|
+
"siss=src.main:main",
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
classifiers=[
|
|
24
|
+
"Development Status :: 3 - Alpha",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Topic :: Multimedia :: Video",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.6",
|
|
29
|
+
"Programming Language :: Python :: 3.7",
|
|
30
|
+
"Programming Language :: Python :: 3.8",
|
|
31
|
+
"Programming Language :: Python :: 3.9",
|
|
32
|
+
"License :: OSI Approved :: MIT License",
|
|
33
|
+
],
|
|
34
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: siss
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A command-line utility for applying artistic effects to videos
|
|
5
|
+
Home-page: https://github.com/MichailSemoglou/siss
|
|
6
|
+
Author: Michail Semoglou
|
|
7
|
+
Author-email: m.semoglou@tongji.edu.cn
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Multimedia :: Video
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Requires-Python: >=3.6
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: opencv-python>=4.5.0
|
|
20
|
+
Requires-Dist: numpy>=1.20.0
|
|
21
|
+
Requires-Dist: tqdm>=4.60.0
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: author-email
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: home-page
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
src/siss.egg-info/PKG-INFO
|
|
5
|
+
src/siss.egg-info/SOURCES.txt
|
|
6
|
+
src/siss.egg-info/dependency_links.txt
|
|
7
|
+
src/siss.egg-info/entry_points.txt
|
|
8
|
+
src/siss.egg-info/requires.txt
|
|
9
|
+
src/siss.egg-info/top_level.txt
|
|
10
|
+
src/utils/__init__.py
|
|
11
|
+
src/utils/video_processing.py
|
|
12
|
+
tests/test_duotone.py
|
|
13
|
+
tests/test_halftone.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
utils
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for video processing operations.
|
|
3
|
+
|
|
4
|
+
This module provides helper functions for common video operations like
|
|
5
|
+
loading videos, extracting frames, and saving processed results.
|
|
6
|
+
"""
|
|
7
|
+
import cv2
|
|
8
|
+
import numpy as np
|
|
9
|
+
import os
|
|
10
|
+
from tqdm import tqdm
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_codec_for_file(output_path):
|
|
14
|
+
"""
|
|
15
|
+
Determine the appropriate codec based on the file extension.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
output_path (str): Path where the video will be saved
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
str: Four character codec code
|
|
22
|
+
"""
|
|
23
|
+
# Get file extension (lowercase)
|
|
24
|
+
_, ext = os.path.splitext(output_path)
|
|
25
|
+
ext = ext.lower()
|
|
26
|
+
|
|
27
|
+
# Map extensions to codecs
|
|
28
|
+
codec_map = {
|
|
29
|
+
'.avi': 'XVID',
|
|
30
|
+
'.mp4': 'mp4v', # H.264 codec
|
|
31
|
+
'.mov': 'mp4v', # H.264 codec for MOV container
|
|
32
|
+
'.mkv': 'X264',
|
|
33
|
+
'.wmv': 'WMV2',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Default to mp4v if extension not found
|
|
37
|
+
return codec_map.get(ext, 'mp4v')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_video(video_path):
|
|
41
|
+
"""
|
|
42
|
+
Load a video file and return a VideoCapture object.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
video_path (str): Path to the video file
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
cv2.VideoCapture: OpenCV VideoCapture object
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
FileNotFoundError: If the video file cannot be opened
|
|
52
|
+
"""
|
|
53
|
+
video_capture = cv2.VideoCapture(video_path)
|
|
54
|
+
if not video_capture.isOpened():
|
|
55
|
+
raise FileNotFoundError(f"Cannot open video file: {video_path}")
|
|
56
|
+
return video_capture
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_video_properties(video_capture):
|
|
60
|
+
"""
|
|
61
|
+
Get properties of a video.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
video_capture (cv2.VideoCapture): OpenCV VideoCapture object
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
dict: Dictionary with video properties (fps, width, height, frame_count)
|
|
68
|
+
"""
|
|
69
|
+
properties = {
|
|
70
|
+
'fps': video_capture.get(cv2.CAP_PROP_FPS),
|
|
71
|
+
'width': int(video_capture.get(cv2.CAP_PROP_FRAME_WIDTH)),
|
|
72
|
+
'height': int(video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT)),
|
|
73
|
+
'frame_count': int(video_capture.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
74
|
+
}
|
|
75
|
+
return properties
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def extract_frames(video_capture, show_progress=True):
|
|
79
|
+
"""
|
|
80
|
+
Extract all frames from a video.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
video_capture (cv2.VideoCapture): OpenCV VideoCapture object
|
|
84
|
+
show_progress (bool): Whether to show a progress bar
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
list: List of frames as numpy arrays
|
|
88
|
+
"""
|
|
89
|
+
frames = []
|
|
90
|
+
|
|
91
|
+
# Get frame count for progress bar
|
|
92
|
+
frame_count = int(video_capture.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
93
|
+
|
|
94
|
+
# Create progress bar if requested
|
|
95
|
+
if show_progress:
|
|
96
|
+
progress_bar = tqdm(total=frame_count, desc="Extracting frames")
|
|
97
|
+
|
|
98
|
+
while True:
|
|
99
|
+
ret, frame = video_capture.read()
|
|
100
|
+
if not ret:
|
|
101
|
+
break
|
|
102
|
+
frames.append(frame)
|
|
103
|
+
|
|
104
|
+
# Update progress bar
|
|
105
|
+
if show_progress:
|
|
106
|
+
progress_bar.update(1)
|
|
107
|
+
|
|
108
|
+
# Close progress bar
|
|
109
|
+
if show_progress:
|
|
110
|
+
progress_bar.close()
|
|
111
|
+
|
|
112
|
+
return frames
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def save_video(output_path, frames, fps, show_progress=True):
|
|
116
|
+
"""
|
|
117
|
+
Save a list of frames as a video file.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
output_path (str): Path where the video will be saved
|
|
121
|
+
frames (list): List of frames as numpy arrays
|
|
122
|
+
fps (float): Frames per second for the output video
|
|
123
|
+
show_progress (bool): Whether to show a progress bar
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
ValueError: If no frames are provided
|
|
127
|
+
"""
|
|
128
|
+
if not frames:
|
|
129
|
+
raise ValueError("No frames to save.")
|
|
130
|
+
|
|
131
|
+
height, width, _ = frames[0].shape
|
|
132
|
+
codec = get_codec_for_file(output_path)
|
|
133
|
+
fourcc = cv2.VideoWriter_fourcc(*codec)
|
|
134
|
+
video_writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
|
135
|
+
|
|
136
|
+
# Create progress bar if requested
|
|
137
|
+
if show_progress:
|
|
138
|
+
progress_bar = tqdm(total=len(frames), desc="Saving video")
|
|
139
|
+
|
|
140
|
+
for frame in frames:
|
|
141
|
+
video_writer.write(frame)
|
|
142
|
+
|
|
143
|
+
# Update progress bar
|
|
144
|
+
if show_progress:
|
|
145
|
+
progress_bar.update(1)
|
|
146
|
+
|
|
147
|
+
# Close progress bar
|
|
148
|
+
if show_progress:
|
|
149
|
+
progress_bar.close()
|
|
150
|
+
|
|
151
|
+
video_writer.release()
|
|
152
|
+
print(f"Video saved to {output_path}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def process_video_frames(video_path, output_path, process_function, **kwargs):
|
|
156
|
+
"""
|
|
157
|
+
Process a video by applying a function to each frame.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
video_path (str): Path to the input video
|
|
161
|
+
output_path (str): Path where processed video will be saved
|
|
162
|
+
process_function (callable): Function to apply to each frame
|
|
163
|
+
The function should take a frame and return a processed frame
|
|
164
|
+
**kwargs: Additional arguments to pass to the process_function
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
def grayscale(frame):
|
|
168
|
+
return cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
169
|
+
|
|
170
|
+
process_video_frames('input.mp4', 'output.mp4', grayscale)
|
|
171
|
+
"""
|
|
172
|
+
# Load video
|
|
173
|
+
cap = load_video(video_path)
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
# Get video properties
|
|
177
|
+
props = get_video_properties(cap)
|
|
178
|
+
|
|
179
|
+
# Create output video writer with appropriate codec
|
|
180
|
+
codec = get_codec_for_file(output_path)
|
|
181
|
+
fourcc = cv2.VideoWriter_fourcc(*codec)
|
|
182
|
+
out = cv2.VideoWriter(output_path, fourcc, props['fps'],
|
|
183
|
+
(props['width'], props['height']))
|
|
184
|
+
|
|
185
|
+
# Process frames with progress bar
|
|
186
|
+
progress_bar = tqdm(total=props['frame_count'], desc="Processing frames")
|
|
187
|
+
|
|
188
|
+
while True:
|
|
189
|
+
ret, frame = cap.read()
|
|
190
|
+
if not ret:
|
|
191
|
+
break
|
|
192
|
+
|
|
193
|
+
# Apply processing function
|
|
194
|
+
processed_frame = process_function(frame, **kwargs)
|
|
195
|
+
|
|
196
|
+
# Write processed frame
|
|
197
|
+
out.write(processed_frame)
|
|
198
|
+
|
|
199
|
+
# Update progress
|
|
200
|
+
progress_bar.update(1)
|
|
201
|
+
|
|
202
|
+
# Close progress bar
|
|
203
|
+
progress_bar.close()
|
|
204
|
+
|
|
205
|
+
print(f"Processed video saved to {output_path}")
|
|
206
|
+
|
|
207
|
+
finally:
|
|
208
|
+
# Release resources
|
|
209
|
+
cap.release()
|
|
210
|
+
if 'out' in locals():
|
|
211
|
+
out.release()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def release_resources(video_capture, video_writer=None):
|
|
215
|
+
"""
|
|
216
|
+
Release video resources.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
video_capture (cv2.VideoCapture): OpenCV VideoCapture object
|
|
220
|
+
video_writer (cv2.VideoWriter, optional): OpenCV VideoWriter object
|
|
221
|
+
"""
|
|
222
|
+
if video_capture is not None:
|
|
223
|
+
video_capture.release()
|
|
224
|
+
|
|
225
|
+
if video_writer is not None:
|
|
226
|
+
video_writer.release()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for the duotone module.
|
|
3
|
+
"""
|
|
4
|
+
import unittest
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
import numpy as np
|
|
8
|
+
import cv2
|
|
9
|
+
from src.duotone import apply_duotone
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestDuotone(unittest.TestCase):
|
|
13
|
+
"""Tests for the duotone effect functions."""
|
|
14
|
+
|
|
15
|
+
def setUp(self):
|
|
16
|
+
"""Set up test environment."""
|
|
17
|
+
# Create a small test video file
|
|
18
|
+
self.temp_dir = tempfile.TemporaryDirectory()
|
|
19
|
+
self.input_path = os.path.join(self.temp_dir.name, "test_input.mp4")
|
|
20
|
+
self.output_path = os.path.join(self.temp_dir.name, "test_output.mp4")
|
|
21
|
+
|
|
22
|
+
# Create a simple test video (10 frames, solid color)
|
|
23
|
+
width, height = 320, 240
|
|
24
|
+
fps = 30
|
|
25
|
+
|
|
26
|
+
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
|
27
|
+
out = cv2.VideoWriter(self.input_path, fourcc, fps, (width, height))
|
|
28
|
+
|
|
29
|
+
# Create 10 frames
|
|
30
|
+
for i in range(10):
|
|
31
|
+
# Create gradient frame
|
|
32
|
+
frame = np.zeros((height, width, 3), dtype=np.uint8)
|
|
33
|
+
for y in range(height):
|
|
34
|
+
value = int(y * 255 / height)
|
|
35
|
+
frame[y, :] = [value, value, value]
|
|
36
|
+
|
|
37
|
+
out.write(frame)
|
|
38
|
+
|
|
39
|
+
out.release()
|
|
40
|
+
|
|
41
|
+
def tearDown(self):
|
|
42
|
+
"""Clean up test environment."""
|
|
43
|
+
self.temp_dir.cleanup()
|
|
44
|
+
|
|
45
|
+
def test_apply_duotone_basic(self):
|
|
46
|
+
"""Test basic duotone effect application."""
|
|
47
|
+
# Skip test if codec not available
|
|
48
|
+
try:
|
|
49
|
+
# Test with basic colors
|
|
50
|
+
color1 = (255, 0, 0) # Red
|
|
51
|
+
color2 = (0, 255, 255) # Cyan
|
|
52
|
+
|
|
53
|
+
apply_duotone(self.input_path, self.output_path, color1, color2)
|
|
54
|
+
|
|
55
|
+
# Verify the output exists
|
|
56
|
+
self.assertTrue(os.path.exists(self.output_path))
|
|
57
|
+
|
|
58
|
+
# Verify the output has correct content
|
|
59
|
+
cap = cv2.VideoCapture(self.output_path)
|
|
60
|
+
self.assertTrue(cap.isOpened())
|
|
61
|
+
|
|
62
|
+
# Read the first frame and check colors
|
|
63
|
+
ret, frame = cap.read()
|
|
64
|
+
self.assertTrue(ret)
|
|
65
|
+
|
|
66
|
+
# Check frame dimensions
|
|
67
|
+
self.assertEqual(frame.shape[1], 320) # Width
|
|
68
|
+
self.assertEqual(frame.shape[0], 240) # Height
|
|
69
|
+
|
|
70
|
+
# Close video
|
|
71
|
+
cap.release()
|
|
72
|
+
except cv2.error:
|
|
73
|
+
self.skipTest("Codec not available")
|
|
74
|
+
|
|
75
|
+
def test_invalid_inputs(self):
|
|
76
|
+
"""Test error handling for invalid inputs."""
|
|
77
|
+
# Test with non-existent input file
|
|
78
|
+
with self.assertRaises(FileNotFoundError):
|
|
79
|
+
apply_duotone("nonexistent.mp4", self.output_path, (255, 0, 0), (0, 255, 255))
|
|
80
|
+
|
|
81
|
+
# Test with invalid colors
|
|
82
|
+
with self.assertRaises(ValueError):
|
|
83
|
+
apply_duotone(self.input_path, self.output_path, (300, 0, 0), (0, 255, 255))
|
|
84
|
+
|
|
85
|
+
with self.assertRaises(ValueError):
|
|
86
|
+
apply_duotone(self.input_path, self.output_path, (255, 0, 0), (-10, 255, 255))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
unittest.main()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for the halftone module.
|
|
3
|
+
"""
|
|
4
|
+
import unittest
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
import numpy as np
|
|
8
|
+
import cv2
|
|
9
|
+
from src.halftone import apply_halftone
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestHalftone(unittest.TestCase):
|
|
13
|
+
"""Tests for the halftone effect functions."""
|
|
14
|
+
|
|
15
|
+
def setUp(self):
|
|
16
|
+
"""Set up test environment."""
|
|
17
|
+
# Create a small test video file
|
|
18
|
+
self.temp_dir = tempfile.TemporaryDirectory()
|
|
19
|
+
self.input_path = os.path.join(self.temp_dir.name, "test_input.mp4")
|
|
20
|
+
self.output_path = os.path.join(self.temp_dir.name, "test_output.mp4")
|
|
21
|
+
|
|
22
|
+
# Create a simple test video (10 frames, gradient color)
|
|
23
|
+
width, height = 320, 240
|
|
24
|
+
fps = 30
|
|
25
|
+
|
|
26
|
+
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
|
27
|
+
out = cv2.VideoWriter(self.input_path, fourcc, fps, (width, height))
|
|
28
|
+
|
|
29
|
+
# Create 10 frames
|
|
30
|
+
for i in range(10):
|
|
31
|
+
# Create gradient frame
|
|
32
|
+
frame = np.zeros((height, width, 3), dtype=np.uint8)
|
|
33
|
+
for y in range(height):
|
|
34
|
+
value = int(y * 255 / height)
|
|
35
|
+
frame[y, :] = [value, value, value]
|
|
36
|
+
|
|
37
|
+
out.write(frame)
|
|
38
|
+
|
|
39
|
+
out.release()
|
|
40
|
+
|
|
41
|
+
def tearDown(self):
|
|
42
|
+
"""Clean up test environment."""
|
|
43
|
+
self.temp_dir.cleanup()
|
|
44
|
+
|
|
45
|
+
def test_apply_halftone_basic(self):
|
|
46
|
+
"""Test basic halftone effect application."""
|
|
47
|
+
# Skip test if codec not available
|
|
48
|
+
try:
|
|
49
|
+
# Test with basic settings
|
|
50
|
+
color1 = (0, 0, 0) # Black
|
|
51
|
+
color2 = (255, 255, 255) # White
|
|
52
|
+
symbol_size = 8
|
|
53
|
+
|
|
54
|
+
apply_halftone(self.input_path, self.output_path, symbol_size, color1, color2)
|
|
55
|
+
|
|
56
|
+
# Verify the output exists
|
|
57
|
+
self.assertTrue(os.path.exists(self.output_path))
|
|
58
|
+
|
|
59
|
+
# Verify the output has correct content
|
|
60
|
+
cap = cv2.VideoCapture(self.output_path)
|
|
61
|
+
self.assertTrue(cap.isOpened())
|
|
62
|
+
|
|
63
|
+
# Read the first frame and check dimensions
|
|
64
|
+
ret, frame = cap.read()
|
|
65
|
+
self.assertTrue(ret)
|
|
66
|
+
|
|
67
|
+
# Check frame dimensions
|
|
68
|
+
self.assertEqual(frame.shape[1], 320) # Width
|
|
69
|
+
self.assertEqual(frame.shape[0], 240) # Height
|
|
70
|
+
|
|
71
|
+
# Close video
|
|
72
|
+
cap.release()
|
|
73
|
+
except cv2.error:
|
|
74
|
+
self.skipTest("Codec not available")
|
|
75
|
+
|
|
76
|
+
def test_symbol_types(self):
|
|
77
|
+
"""Test different symbol types."""
|
|
78
|
+
# Skip test if codec not available
|
|
79
|
+
try:
|
|
80
|
+
symbol_types = ['plus', 'asterisk', 'slash']
|
|
81
|
+
|
|
82
|
+
for symbol_type in symbol_types:
|
|
83
|
+
output_path = os.path.join(self.temp_dir.name, f"test_{symbol_type}.mp4")
|
|
84
|
+
|
|
85
|
+
apply_halftone(
|
|
86
|
+
self.input_path,
|
|
87
|
+
output_path,
|
|
88
|
+
symbol_size=8,
|
|
89
|
+
color1_rgb=(0, 0, 0),
|
|
90
|
+
color2_rgb=(255, 255, 255),
|
|
91
|
+
symbol_type=symbol_type
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Verify the output exists
|
|
95
|
+
self.assertTrue(os.path.exists(output_path))
|
|
96
|
+
except cv2.error:
|
|
97
|
+
self.skipTest("Codec not available")
|
|
98
|
+
|
|
99
|
+
def test_invalid_inputs(self):
|
|
100
|
+
"""Test error handling for invalid inputs."""
|
|
101
|
+
# Test with non-existent input file
|
|
102
|
+
with self.assertRaises(FileNotFoundError):
|
|
103
|
+
apply_halftone("nonexistent.mp4", self.output_path, 10, (0, 0, 0), (255, 255, 255))
|
|
104
|
+
|
|
105
|
+
# Test with invalid colors
|
|
106
|
+
with self.assertRaises(ValueError):
|
|
107
|
+
apply_halftone(self.input_path, self.output_path, 10, (300, 0, 0), (255, 255, 255))
|
|
108
|
+
|
|
109
|
+
# Test with invalid symbol type
|
|
110
|
+
with self.assertRaises(ValueError):
|
|
111
|
+
apply_halftone(self.input_path, self.output_path, 10, (0, 0, 0), (255, 255, 255), 'invalid_type')
|
|
112
|
+
|
|
113
|
+
# Test with invalid symbol size
|
|
114
|
+
with self.assertRaises(ValueError):
|
|
115
|
+
apply_halftone(self.input_path, self.output_path, 0, (0, 0, 0), (255, 255, 255))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
unittest.main()
|