-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 68bb7cd
Showing
13 changed files
with
1,520 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
on: | ||
push: | ||
branches: | ||
- master | ||
workflow_dispatch: | ||
|
||
jobs: | ||
publish: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v2 | ||
- name: Set up Python | ||
uses: actions/setup-python@v2 | ||
with: | ||
python-version: '3.12' | ||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install poetry | ||
poetry install | ||
- name: Publish to PyPI | ||
run: | | ||
poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} | ||
poetry publish --build |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
__pycache__/ | ||
*.pyc | ||
*.pyo | ||
|
||
/dist |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
<center> | ||
|
||
# FlipAnimGen 📽️ | ||
|
||
## Dithering animation generator from video files for Flipper Zero written in Python 🐍 | ||
|
||
![CodeStyle](https://img.shields.io/badge/code%20style-black-black) | ||
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flipanimgen) | ||
|
||
![GitHub contributors](https://img.shields.io/github/contributors/D4n13l3k00/flipanimgen) | ||
![GitHub License](https://img.shields.io/github/license/D4n13l3k00/flipanimgen) | ||
|
||
</center> | ||
|
||
### Tested on 🧪 | ||
|
||
- [X] Windows 11 | ||
- [ ] Linux | ||
- [ ] MacOS | ||
|
||
### Installation 📦 | ||
|
||
Before installing the package, make sure you have installed [Python](https://www.python.org/downloads/) and | ||
[Git](https://git-scm.com/downloads) on your system. | ||
|
||
```bash | ||
# Via pipx (recommend) | ||
pip install pipx -U | ||
pipx install flipanimgen | ||
|
||
# Via pip (not recommended due to conflicts with other package versions) | ||
pip install flipanimgen | ||
``` | ||
|
||
### Usage 🎯 | ||
|
||
```bash | ||
flipanimgen --help | ||
|
||
# Example | ||
flipanimgen -i input_animation.mp4 -o output_animation_folder | ||
``` | ||
|
||
After running the command, the program will generate a folder with the animation frames in the specified directory. | ||
|
||
Then you can use [FlipperAM](https://github.com/Ooggle/FlipperAnimationManager) to generate a manifest for adding the | ||
animation to Flipper Zero | ||
|
||
### License 📜 | ||
|
||
This project is licensed under the GNU AGPLv3 License - see the [LICENSE](LICENSE) file for details. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# python -m flipanimgen -i <video_name> -o <output_folder> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from flipanimgen.cli import cli | ||
|
||
cli() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import argparse | ||
|
||
from flipanimgen.main import main | ||
|
||
|
||
def cli(): | ||
parser = argparse.ArgumentParser( | ||
description="FlipAnimGen - Dithering animation generator from video files for Flipper Zero written in Python" | ||
) | ||
parser.add_argument( | ||
"--input", "-i", help="Input video path", type=str, required=True | ||
) | ||
parser.add_argument( | ||
"--output", | ||
"-o", | ||
help="Output folder path for animation", | ||
type=str, | ||
default="flipanimgen-output", | ||
required=False, | ||
) | ||
parsed_args = parser.parse_args() | ||
if not parsed_args.input.endswith(".mp4"): | ||
print("[red]Error:[/red] Input file must be .mp4") | ||
exit(1) | ||
main(parsed_args) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
from numba import jit | ||
import numpy as np | ||
|
||
|
||
@jit(nopython=True) | ||
def clamp(color): | ||
return max(0, min(255, color)) | ||
|
||
|
||
@jit(nopython=True) | ||
def floyd_steinberg_dither(image): | ||
height, width = (image.shape[0], image.shape[1]) | ||
|
||
for y in range(0, height - 1): | ||
for x in range(1, width - 1): | ||
old_p = image[y, x] | ||
new_p = np.round(old_p / 255.0) * 255 | ||
image[y, x] = new_p | ||
|
||
quant_error_p = old_p - new_p | ||
|
||
image[y, x + 1] = clamp( | ||
image[y, x + 1] + quant_error_p * 0.4375 | ||
) # 7 / 16.0 | ||
image[y + 1, x - 1] = clamp( | ||
image[y + 1, x - 1] + quant_error_p * 0.1875 | ||
) # 3 / 16.0 | ||
image[y + 1, x] = clamp( | ||
image[y + 1, x] + quant_error_p * 0.3125 | ||
) # 5 / 16.0 | ||
image[y + 1, x + 1] = clamp( | ||
image[y + 1, x + 1] + quant_error_p * 0.0625 | ||
) # 1 / 16.0 | ||
|
||
return image |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
from pathlib import Path | ||
from shutil import rmtree | ||
|
||
import cv2 | ||
from rich import print | ||
|
||
from flipanimgen.dithering import floyd_steinberg_dither | ||
from flipanimgen.metadata import generate_meta | ||
from flipanimgen.ofw import install_ofw, copy_anim_to_ofw, compile_animation | ||
|
||
|
||
def main(args): | ||
input_video_path = Path(args.input) | ||
|
||
if not input_video_path.exists(): | ||
print(f"[bold red]Input video not found:[/] {input_video_path}") | ||
return | ||
|
||
temp_folder = Path.cwd() / "flipanimgen_temp" | ||
print(f"[bold cyan]Temp folder:[/] {temp_folder}") | ||
temp_folder.mkdir(exist_ok=True) | ||
|
||
frames_folder = temp_folder / "animation" | ||
print(f"[bold cyan]Frames folder:[/] {frames_folder}") | ||
frames_folder.mkdir(exist_ok=True) | ||
|
||
output_path = Path(args.output) | ||
if output_path.exists(): | ||
rmtree(output_path, ignore_errors=True) | ||
print(f"[bold cyan]Output path:[/] {output_path}") | ||
output_path.mkdir(exist_ok=True) | ||
|
||
cap = cv2.VideoCapture(str(input_video_path)) | ||
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) | ||
width, height = ( | ||
int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), | ||
int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)), | ||
) | ||
fps = cap.get(cv2.CAP_PROP_FPS) | ||
|
||
current_frame = 0 | ||
print(f"[bold yellow]Processing frames:[/] {total}") | ||
while cap.isOpened(): | ||
ret, frame = cap.read() | ||
if ret: | ||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) | ||
|
||
if width / height > 2: | ||
new_width = height * 2 | ||
frame = frame[ | ||
:, | ||
int((width - new_width) / 2) : int((width + new_width) / 2), | ||
] | ||
else: | ||
new_height = width / 2 | ||
frame = frame[ | ||
int((height - new_height) / 2) : int((height + new_height) / 2), | ||
:, | ||
] | ||
|
||
frame = cv2.resize(frame, (128, 64)) | ||
|
||
dithered_frame = floyd_steinberg_dither(frame) | ||
|
||
cv2.imwrite( | ||
str(frames_folder / f"frame_{current_frame}.png"), | ||
dithered_frame, | ||
) | ||
|
||
current_frame += 1 | ||
print(f"[bold yellow]Progress:[/] {current_frame}/{total}", end="\r") | ||
else: | ||
break | ||
|
||
print() | ||
|
||
cap.release() | ||
cv2.destroyAllWindows() | ||
|
||
print(f"[bold yellow]Generating meta.txt[/]") | ||
meta = generate_meta( | ||
frames_count=current_frame, | ||
framerate=int(fps), | ||
) | ||
|
||
with open(frames_folder / "meta.txt", "w") as f: | ||
f.write(meta) | ||
|
||
install_ofw(temp_folder) | ||
copy_anim_to_ofw(frames_folder, temp_folder) | ||
compile_animation(temp_folder, output_path) | ||
|
||
rmtree(frames_folder, ignore_errors=True) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
def generate_meta(frames_count: int, framerate: int) -> str: | ||
duration = int(frames_count / framerate * 1000) | ||
frames_order = " ".join(map(str, range(0, frames_count))) | ||
|
||
return """ | ||
Filetype: Flipper Animation | ||
Version: 1 | ||
Width: 128 | ||
Height: 64 | ||
Passive frames: {frames_count} | ||
Active frames: 0 | ||
Frames order: {frames_order} | ||
Active cycles: 0 | ||
Frame rate: {framerate} | ||
Duration: {duration} | ||
Active cooldown: 0 | ||
Bubble slots: 0 | ||
""".format( | ||
frames_count=frames_count, | ||
duration=duration, | ||
frames_order=frames_order, | ||
framerate=framerate, | ||
).strip() | ||
|
||
|
||
def get_manifest() -> str: | ||
return """ | ||
Filetype: Flipper Animation Manifest | ||
Version: 1 | ||
Name: animation | ||
Min butthurt: 0 | ||
Max butthurt: 14 | ||
Min level: 1 | ||
Max level: 3 | ||
Weight: 8 | ||
""".strip() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import os | ||
import shutil | ||
import subprocess | ||
from pathlib import Path | ||
from typing import Union | ||
|
||
from rich import print | ||
|
||
from flipanimgen.metadata import get_manifest | ||
|
||
OFW_DIR = "flipperzero-firmware-release" | ||
|
||
|
||
def install_ofw(temp_path: Union[Path, str]): | ||
if not isinstance(temp_path, Path): | ||
temp_path = Path(temp_path) | ||
|
||
ofw_path = temp_path / OFW_DIR | ||
|
||
if ofw_path.exists(): | ||
print("[yellow]OFW already installed, skipping...[/]") | ||
return | ||
|
||
print("[yellow]Downloading OFW...[/]") | ||
subprocess.run( | ||
[ | ||
"git", | ||
"clone", | ||
"https://github.com/flipperdevices/flipperzero-firmware", | ||
ofw_path, | ||
"--depth=1", | ||
] | ||
) | ||
print("[yellow]OFW downloaded![/]") | ||
|
||
|
||
def copy_anim_to_ofw(new_animation_path: Union[Path, str], temp_path: Union[Path, str]): | ||
if not isinstance(new_animation_path, Path): | ||
new_animation_path = Path(new_animation_path) | ||
if not isinstance(temp_path, Path): | ||
temp_path = Path(temp_path) | ||
|
||
ofw_path = temp_path / OFW_DIR | ||
animations_path = ofw_path / "assets" / "dolphin" / "external" | ||
|
||
print("[yellow]Cleaning up old animations...[/]") | ||
for file in animations_path.iterdir(): | ||
if file.is_file(): | ||
file.unlink() | ||
elif file.is_dir(): | ||
shutil.rmtree(file) | ||
|
||
print("[yellow]Copying new animation...[/]") | ||
shutil.copytree(str(new_animation_path), str(animations_path / "animation")) | ||
|
||
print("[yellow]Generating manifest...[/]") | ||
manifest_path = animations_path / "manifest.txt" | ||
with manifest_path.open("w") as f: | ||
f.write(get_manifest()) | ||
|
||
|
||
def compile_animation(temp_path: Union[Path, str], output_path: Union[Path, str]): | ||
if not isinstance(temp_path, Path): | ||
temp_path = Path(temp_path) | ||
if not isinstance(output_path, Path): | ||
output_path = Path(output_path) | ||
|
||
print("[yellow]Compiling animation...[/]") | ||
|
||
cwd = temp_path / OFW_DIR | ||
|
||
old_dir = os.getcwd() | ||
os.chdir(cwd) | ||
command = [ | ||
"fbt.cmd" if os.name == "nt" else "fbt", | ||
"icons", | ||
"proto", | ||
"dolphin_internal", | ||
"dolphin_ext", | ||
"resources", | ||
] | ||
subprocess.run(command) | ||
os.chdir(old_dir) | ||
|
||
ofw_path = temp_path / OFW_DIR | ||
compiled_animations_path = ofw_path / "build" | ||
build_name = compiled_animations_path.iterdir().__next__() | ||
compiled_animations_path = ( | ||
compiled_animations_path | ||
/ build_name | ||
/ "assets" | ||
/ "compiled" | ||
/ "dolphin" | ||
/ "animation" | ||
) | ||
|
||
shutil.copytree(str(compiled_animations_path), str(output_path), dirs_exist_ok=True) |
Oops, something went wrong.