Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
Z1xus committed Jul 23, 2023
1 parent 0f2ad1d commit 4017a80
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 0 deletions.
13 changes: 13 additions & 0 deletions config.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[upscaler]
magnification_factor = 2
algorithm = XBR

[ffmpeg]
args = -c:v libx264 -preset slow -crf 15 -aq-mode 3

[output]
container = mp4
scale_factor = 200

[imageresizer]
path = .\ImageResizer.exe
200 changes: 200 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import os
import sys
import argparse
import configparser
import cv2
import subprocess
import shutil
import signal
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from tqdm import tqdm

def signal_handler(sig, frame):
exit_event.set()
print('Ctrl+C received, cleaning up...')
cleanup(temp_dir)
sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)

exit_event = threading.Event()

def cleanup(temp_directory):
for path_file in os.scandir(temp_directory):
os.remove(path_file.path)
os.rmdir(temp_directory)

def check_dependencies(config):
imageresizer_path = config['imageresizer']['path']
ffmpeg_path = 'ffmpeg'

if not (os.path.isfile(imageresizer_path) and os.access(imageresizer_path, os.X_OK)):
print(f'Error: ImageResizer must be correctly installed and executable. Current path:\nImageResizer: {imageresizer_path}')
sys.exit(1)
if not shutil.which(ffmpeg_path):
print(f'Error: ffmpeg must be correctly installed. Current path:\nffmpeg: {ffmpeg_path}')
sys.exit(1)

def get_fps(video_path):
if not os.path.isfile(video_path):
print(f'Error: input video file not found. Given path:\n{video_path}')
sys.exit(1)

video = cv2.VideoCapture(video_path)
fps = video.get(cv2.CAP_PROP_FPS)
video.release()
return fps

def get_resolution(video_path):
video = cv2.VideoCapture(video_path)
width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
video.release()
return width, height

def extract_frames(video_path, temp_directory):
vidcap = cv2.VideoCapture(video_path)
success, image = vidcap.read()
count = 0
while success:
frame_file = os.path.join(temp_directory, f"frame_{count:05}.png")
success = cv2.imwrite(frame_file, image)
if not success or not os.path.exists(frame_file):
print(f"Frame extraction failed for frame {count}")
sys.exit(1)
success, image = vidcap.read()
count += 1
return count

def upscale_frame(i, temp_directory, imageresizer_path, scale_factor, magnification_factor, algorithm, original_resolution, verbose):
new_width = int(original_resolution[0] * scale_factor)
new_height = int(original_resolution[1] * scale_factor)

in_frame = os.path.join(temp_directory, f"frame_{i:05}.png")
out_frame = os.path.join(temp_directory, f"frame_{i:05}_up.png")

try:
if scale_factor != float(magnification_factor):
cmd = [imageresizer_path, '/load', in_frame, '/resize', 'auto', f'{algorithm} {magnification_factor}x', '/resize', f'{new_width}x{new_height}', 'Lanczos', '/save', out_frame]
else:
cmd = [imageresizer_path, '/load', in_frame, '/resize', 'auto', f'{algorithm} {magnification_factor}x', '/save', out_frame]

if verbose:
print(f"Running command: {' '.join(cmd)}")

process_output = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if verbose:
print(process_output.stdout.decode('utf-8'))
print(process_output.stderr.decode('utf-8'))

except subprocess.CalledProcessError as e:
print(f'Error: Upscaling frame {i} failed with exit code {e.returncode}.')
if verbose:
print(f"Command: {' '.join(cmd)}")
print(f"Output: {e.output.decode('utf-8')}")
cleanup(temp_directory)
sys.exit(1)
except Exception as e:
print(f'Unexpected error occurred during upscaling frame {i}: {str(e)}')
if verbose:
print(f"Command: {' '.join(cmd)}")
cleanup(temp_directory)
sys.exit(1)

def upscale_frames(temp_directory, total_frames, imageresizer_path, scale_factor, magnification_factor, algorithm, original_resolution, verbose):
with ThreadPoolExecutor() as executor:
futures = [executor.submit(upscale_frame, i, temp_directory, imageresizer_path, scale_factor, magnification_factor, algorithm, original_resolution, verbose) for i in range(total_frames)]

for f in tqdm(as_completed(futures), total=total_frames, desc="Upscaling frames", unit="frame"):
pass

def get_missing_frames(total_frames, temp_directory):
return [i for i in range(total_frames) if not os.path.exists(os.path.join(temp_directory, f'frame_{i:05}_up.png'))]

def encode_video(input_video, output_name, temp_directory, total_frames, ffmpeg_args, verbose):
fps = get_fps(input_video)
frame_path_list = os.path.join(temp_directory, 'frame_%05d_up.png')

missing_frames = get_missing_frames(total_frames, temp_directory)

if missing_frames:
if len(missing_frames) > 10:
print(f"Missing upscaled frames: from {missing_frames[0]} to {missing_frames[-1]}")
else:
print(f"Missing upscaled frames: {missing_frames}")
cleanup(temp_directory)
sys.exit(1)

ffmpeg_cmd = ['ffmpeg', '-r', str(fps), '-i', frame_path_list, '-i', input_video, '-map', '0:v', '-map', '1:a']

if ffmpeg_args:
ffmpeg_cmd.extend(ffmpeg_args.split(' '))
else:
print("Falling back to the default ffmpeg arguments.")
ffmpeg_cmd.extend(['-c:v', 'libx264', '-preset', 'medium', '-tune', 'animation', '-crf', '15', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-b:a', '128k', '-shortest'])

ffmpeg_cmd.append(output_name)

try:
if verbose:
print(f"Running command: {' '.join(ffmpeg_cmd)}")
process_output = subprocess.run(ffmpeg_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(process_output.stdout.decode('utf-8'))
print(process_output.stderr.decode('utf-8'))
else:
subprocess.run(ffmpeg_cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except subprocess.CalledProcessError as e:
print(f'Error: Encoding video failed with exit code {e.returncode}.')
if verbose:
print(f"Command: {' '.join(ffmpeg_cmd)}")
print(f"Output: {e.stderr.decode('utf-8')}")
except Exception as e:
print(f'Unexpected error occurred during video encoding: {str(e)}')
if verbose:
print(f"Command: {' '.join(ffmpeg_cmd)}")

def no_overwrite(out_filename):
counter = 1
filename, file_extension = os.path.splitext(out_filename)

while os.path.exists(out_filename):
out_filename = f"{filename}({counter}){file_extension}"
counter += 1

return out_filename

if __name__ == "__main__":
parser = argparse.ArgumentParser(
description=f"xbr-video-upscaler",
epilog=f"by Z1xus <3\nhttps://github.com/z1xus/xbr-video-upscaler",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument('-i', '--input', help='Input video file', required=True)
parser.add_argument('-v', '--verbose', help='Show verbose output', action='store_true')
args = parser.parse_args()

config = configparser.ConfigParser()
config.read('config.ini')

check_dependencies(config)

temp_dir = '.temp_frames'
os.makedirs(temp_dir, exist_ok=True)

print("Extracting frames...")
total_frames = extract_frames(args.input, temp_dir)

scale_factor = float(config['output']['scale_factor']) / 100
magnification_factor = config['upscaler']['magnification_factor']
algorithm = config['upscaler']['algorithm']
original_dimensions = get_resolution(args.input)
upscale_frames(temp_dir, total_frames, config['imageresizer']['path'], scale_factor, magnification_factor, algorithm, original_dimensions, args.verbose)

out_filename = f"{os.path.splitext(args.input)[0]}_upscaled_{algorithm}{magnification_factor}x.{config['output']['container']}"
out_filename = no_overwrite(out_filename)

encode_video(args.input, out_filename, temp_dir, total_frames, config['ffmpeg']['args'], args.verbose)

cleanup(temp_dir)
44 changes: 44 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## xbr-video-upscaler
Use [ImageResizer](https://github.com/Hawkynt/2dimagefilter) to process videos.

> ⚠️ WIP, do not expect a working program
### The name lies, you can use many different pixel art scaling algorithms:
+ XBR
+ HQX
+ LQX
+ NearestNeighbor
+ Bilinear
+ Bicubic
+ and others... (see [ImageResizer wiki](https://code.google.com/archive/p/2dimagefilter/wikis/ImageScaling.wiki))

### Usage:
1. Clone the repository
```bash
git clone https://github.com/Z1xus/xbr-video-upscaler
```
2. Install dependencies
```bash
pip install -r .\requirements.txt
```
3. Change config.ini
```ini
[upscaler]
magnification_factor = 2
algorithm = XBR

[ffmpeg]
args = -c:v libx264 -preset slow -crf 15 -aq-mode 3

[output]
container = mp4
scale_factor = 200

[imageresizer]
path = .\ImageResizer.exe

```
4. Run it
```bash
python3 main.py [-h] -i INPUT [-v]
```
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
opencv_python>=4.7.0.72
opencv_python_headless>=4.8.0.74
tqdm>=4.65.0

0 comments on commit 4017a80

Please sign in to comment.