FirstFrame 1.0.1__py3-none-any.whl
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.
- LastFrame/__init__.py +4 -0
- LastFrame/__main__.py +4 -0
- LastFrame/cli.py +113 -0
- firstframe-1.0.1.dist-info/METADATA +158 -0
- firstframe-1.0.1.dist-info/RECORD +8 -0
- firstframe-1.0.1.dist-info/WHEEL +5 -0
- firstframe-1.0.1.dist-info/entry_points.txt +2 -0
- firstframe-1.0.1.dist-info/top_level.txt +1 -0
LastFrame/__init__.py
ADDED
LastFrame/__main__.py
ADDED
LastFrame/cli.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
from __future__ import print_function, unicode_literals
|
|
3
|
+
|
|
4
|
+
from ChronicleLogger import ChronicleLogger
|
|
5
|
+
import sys
|
|
6
|
+
import cv2
|
|
7
|
+
import os
|
|
8
|
+
import glob
|
|
9
|
+
|
|
10
|
+
def get_last_frame(video_path, output_path):
|
|
11
|
+
"""Extract the last frame from video and save it."""
|
|
12
|
+
cap = cv2.VideoCapture(video_path)
|
|
13
|
+
if not cap.isOpened():
|
|
14
|
+
print("Error: Cannot open video file.")
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
# Method 1: Try by frame count (most accurate)
|
|
18
|
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
19
|
+
if total_frames > 1:
|
|
20
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, total_frames - 1)
|
|
21
|
+
ret, frame = cap.read()
|
|
22
|
+
if ret:
|
|
23
|
+
cv2.imwrite(output_path, frame)
|
|
24
|
+
cap.release()
|
|
25
|
+
print(f"Success: Last frame saved as '{output_path}'")
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
# Method 2: Fallback - seek to 99% of video
|
|
29
|
+
cap.set(cv2.CAP_PROP_POS_AVI_RATIO, 0.99)
|
|
30
|
+
ret, frame = cap.read()
|
|
31
|
+
if ret:
|
|
32
|
+
cv2.imwrite(output_path, frame)
|
|
33
|
+
cap.release()
|
|
34
|
+
print(f"Success: Last frame saved as '{output_path}' (using time-based seek)")
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
# Method 3: Use millisecond seek
|
|
38
|
+
fps = cap.get(cv2.CAP_PROP_FPS)
|
|
39
|
+
if fps > 0:
|
|
40
|
+
duration_ms = (total_frames / fps) * 1000
|
|
41
|
+
cap.set(cv2.CAP_PROP_POS_MSEC, duration_ms - 100) # 0.1s before end
|
|
42
|
+
ret, frame = cap.read()
|
|
43
|
+
if ret:
|
|
44
|
+
cv2.imwrite(output_path, frame)
|
|
45
|
+
cap.release()
|
|
46
|
+
print(f"Success: Last frame saved as '{output_path}' (millisecond seek)")
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
cap.release()
|
|
50
|
+
print("Failed: Could not extract the last frame.")
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
# Find all .mp4 files in current directory (case-insensitive)
|
|
56
|
+
mp4_files = glob.glob("*.mp4") + glob.glob("*.MP4") + glob.glob("*.Mp4")
|
|
57
|
+
mp4_files = sorted(set(mp4_files)) # Remove duplicates, sort nicely
|
|
58
|
+
|
|
59
|
+
if not mp4_files:
|
|
60
|
+
print("No .mp4 videos found in the current folder!")
|
|
61
|
+
print(f"Current directory: {os.getcwd()}")
|
|
62
|
+
input("\nPress Enter to exit...")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
# Display numbered list
|
|
66
|
+
print("\nFound MP4 videos:\n")
|
|
67
|
+
for i, filename in enumerate(mp4_files, 1):
|
|
68
|
+
size_mb = os.path.getsize(filename) / (1024 * 1024)
|
|
69
|
+
print(f" {i:2d}. {filename} ({size_mb:.1f} MB)")
|
|
70
|
+
|
|
71
|
+
# Get user choice
|
|
72
|
+
while True:
|
|
73
|
+
try:
|
|
74
|
+
choice = input(f"\nEnter video number (1-{len(mp4_files)}): ").strip()
|
|
75
|
+
if not choice:
|
|
76
|
+
print("Please enter a number.")
|
|
77
|
+
continue
|
|
78
|
+
idx = int(choice) - 1
|
|
79
|
+
if 0 <= idx < len(mp4_files):
|
|
80
|
+
selected_video = mp4_files[idx]
|
|
81
|
+
break
|
|
82
|
+
else:
|
|
83
|
+
print(f"Please enter a number between 1 and {len(mp4_files)}.")
|
|
84
|
+
except ValueError:
|
|
85
|
+
print("Invalid input. Please type a number.")
|
|
86
|
+
|
|
87
|
+
# Suggest default output: either output.png or video_name_last.png
|
|
88
|
+
base_name = os.path.splitext(selected_video)[0]
|
|
89
|
+
default_output = "output.png" if os.path.exists("output.png") == False else f"{base_name}_last.png"
|
|
90
|
+
|
|
91
|
+
print(f"\nSelected: {selected_video}")
|
|
92
|
+
|
|
93
|
+
# Ask for output filename
|
|
94
|
+
user_output = input(f"Enter output image filename [default: {default_output}]: ").strip()
|
|
95
|
+
if not user_output:
|
|
96
|
+
output_path = default_output
|
|
97
|
+
else:
|
|
98
|
+
# Ensure it has an image extension
|
|
99
|
+
if not user_output.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.tiff')):
|
|
100
|
+
output_path = user_output + ".png"
|
|
101
|
+
else:
|
|
102
|
+
output_path = user_output
|
|
103
|
+
|
|
104
|
+
print(f"\nExtracting last frame from '{selected_video}'...")
|
|
105
|
+
get_last_frame(selected_video, output_path)
|
|
106
|
+
|
|
107
|
+
print(f"\nDone! Image saved as: {output_path}")
|
|
108
|
+
input("\nPress Enter to exit...")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
main()
|
|
113
|
+
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: FirstFrame
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: FirstFrame is a simple command-line tool for extracting the last frame from MP4 video files using OpenCV (cv2)
|
|
5
|
+
Author-email: Wilgat Wong <wilgat.wong@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Wilgat/FirstFrame
|
|
8
|
+
Project-URL: Repository, https://github.com/Wilgat/FirstFrame
|
|
9
|
+
Project-URL: Issues, https://github.com/Wilgat/FirstFrame/issues
|
|
10
|
+
Keywords: logging,sudo,chronicle,example
|
|
11
|
+
Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: ChronicleLogger>=1.2.3
|
|
14
|
+
Requires-Dist: opencv-python
|
|
15
|
+
|
|
16
|
+
# FirstFrame
|
|
17
|
+
|
|
18
|
+
## Overview
|
|
19
|
+
|
|
20
|
+
**FirstFrame** is a lightweight command-line tool that extracts the **first frame** from MP4 video files using OpenCV (`cv2`).
|
|
21
|
+
|
|
22
|
+
It automatically scans the current directory for `.mp4` files (case-insensitive), displays an interactive numbered list with file sizes, lets the user select a video, and saves the first frame as an image file (PNG by default; supports JPG, JPEG, BMP, TIFF).
|
|
23
|
+
|
|
24
|
+
The tool features clear prompts, basic error handling, and a pause at the end — perfect for quick thumbnail generation, cover image creation, or inspecting video starts without loading the entire file.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- Finds all `.mp4` / `.MP4` / `.Mp4` files in the current folder using `glob`
|
|
29
|
+
- Shows numbered list with file sizes (in MB) for easy selection
|
|
30
|
+
- Extracts the very first frame reliably with a single `cap.read()` call
|
|
31
|
+
- Smart default output filename: `output.png` (if free) or `{video_name}_first.png`
|
|
32
|
+
- Automatically adds `.png` extension if user input lacks an image suffix
|
|
33
|
+
- Clean resource handling (`cap.release()`) — no temporary files
|
|
34
|
+
- User-friendly success/error messages and final confirmation
|
|
35
|
+
|
|
36
|
+
## Prerequisites
|
|
37
|
+
|
|
38
|
+
- Python 3.8+ (recommended: 3.10–3.12)
|
|
39
|
+
- `opencv-python` (installed via pip)
|
|
40
|
+
- Optional: virtual environment (`venv` or `uv`)
|
|
41
|
+
|
|
42
|
+
No C compiler or Cython is required for normal usage — the project is currently pure Python.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
### Development / Source Install (Editable)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# 1. Clone or download the repo
|
|
50
|
+
git clone <your-repo-url>
|
|
51
|
+
cd FirstFrame
|
|
52
|
+
|
|
53
|
+
# 2. (Recommended) Create & activate virtual environment
|
|
54
|
+
python3 -m venv venv
|
|
55
|
+
source venv/bin/activate # Windows: venv\Scripts\activate
|
|
56
|
+
|
|
57
|
+
# 3. Install dependencies
|
|
58
|
+
pip install opencv-python
|
|
59
|
+
# If you later add ChronicleLogger or others → add them here
|
|
60
|
+
|
|
61
|
+
# 4. Install project in editable mode
|
|
62
|
+
pip install -e .
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Now you can run it with:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python -m FirstFrame
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Future PyPI Release (when ready)
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install firstframe
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
(You would then publish it via `python -m build` + twine, and configure `console_scripts` in `pyproject.toml` for a `firstframe` command.)
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
Place your MP4 videos in the current directory and run:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
python -m FirstFrame
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Example session:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
Found MP4 videos:
|
|
91
|
+
|
|
92
|
+
1. intro.mp4 (12.4 MB)
|
|
93
|
+
2. demo.MP4 (45.8 MB)
|
|
94
|
+
|
|
95
|
+
Enter video number (1-2): 1
|
|
96
|
+
|
|
97
|
+
Selected: intro.mp4
|
|
98
|
+
Enter output image filename [default: output.png]:
|
|
99
|
+
|
|
100
|
+
Extracting first frame from 'intro.mp4'...
|
|
101
|
+
Success: First frame saved as 'output.png'
|
|
102
|
+
|
|
103
|
+
Done! Image saved as: output.png
|
|
104
|
+
|
|
105
|
+
Press Enter to exit...
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Or specify a custom name:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
Enter output image filename [default: intro_first.png]: cover.jpg
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The tool saves the image and waits for you to press Enter before closing.
|
|
115
|
+
|
|
116
|
+
For scripting / automation:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from FirstFrame.__main__ import main
|
|
120
|
+
main()
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Project Structure
|
|
124
|
+
|
|
125
|
+
Follows modern Python packaging conventions with `src/` layout:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
FirstFrame/
|
|
129
|
+
├── README.md
|
|
130
|
+
├── pyproject.toml # (add when packaging properly)
|
|
131
|
+
├── src/
|
|
132
|
+
│ └── FirstFrame/
|
|
133
|
+
│ ├── __init__.py
|
|
134
|
+
│ └── __main__.py # Main logic + entry point
|
|
135
|
+
└── (optional future folders)
|
|
136
|
+
├── tests/
|
|
137
|
+
└── docs/
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Development Notes
|
|
141
|
+
|
|
142
|
+
- The core logic lives in `src/FirstFrame/__main__.py`
|
|
143
|
+
- To add features (batch mode, other formats, progress bar…): edit the `main()` and `get_first_frame()` functions
|
|
144
|
+
- Logging: `ChronicleLogger` is imported but unused — either integrate it or remove the import
|
|
145
|
+
- Tests: Consider adding `tests/` folder + `pytest` later
|
|
146
|
+
- Packaging: When ready, add `pyproject.toml` with `build-system = {requires = ["hatchling"], build-backend = "hatchling.build"}` (or setuptools)
|
|
147
|
+
|
|
148
|
+
## Troubleshooting
|
|
149
|
+
|
|
150
|
+
- **Cannot open video** → Check file path, integrity, and codec support in your OpenCV build
|
|
151
|
+
- **No MP4 files found** → Make sure videos are in the current working directory (`pwd`)
|
|
152
|
+
- **OpenCV not found** → `pip install opencv-python`
|
|
153
|
+
- **Permission denied on save** → Check write permissions in current folder
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT License
|
|
158
|
+
(Add a `LICENSE` file with the standard MIT text when you publish or share the repo.)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
LastFrame/__init__.py,sha256=bgv2y1LnxxM75lyCZQOD8n840ZfMIBUS4boSzy0zk9s,85
|
|
2
|
+
LastFrame/__main__.py,sha256=0g3iknXOS9gZUcpL_trgAcuCJnZZKjdsT_xt61WOVb4,60
|
|
3
|
+
LastFrame/cli.py,sha256=RIPcM2E05uQcIi6TyJ9XNr8MDounI-FVlp9Sy_JJtPU,3810
|
|
4
|
+
firstframe-1.0.1.dist-info/METADATA,sha256=ATNGZjKVOO5vIHMaiZbYvsKW1M9E24FMGxRuQrMieRg,4826
|
|
5
|
+
firstframe-1.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
firstframe-1.0.1.dist-info/entry_points.txt,sha256=mSDW-CTqz9esvqLH97SyNmbkio0YQ6y6IO76Vtbgq3w,52
|
|
7
|
+
firstframe-1.0.1.dist-info/top_level.txt,sha256=ViqkFFJ2aTDbEaUya4XACENmuH1tCf5Sm7Bun6oKclM,10
|
|
8
|
+
firstframe-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
LastFrame
|