Files
WilliamSoderberg Verified 7edb5b9cb3 Almost complete remake of the script. Changed to questionary and fixed the dovi_tool bug.
All bins have been removed. README has been spellchecked.

\

Fix formatting of FFmpeg dependency link

Fixed README
2026-03-12 14:12:41 +01:00

755 lines
29 KiB
Python

import argparse
import json
import logging
import re
import subprocess
import tkinter
import tkinter.filedialog
import tkinter.font
from pathlib import Path
from shutil import rmtree, which
import questionary
from PIL import Image, ImageChops, ImageTk
from pymediainfo import MediaInfo
from questionary import ValidationError, Validator
from rich.progress import (BarColumn, Progress, SpinnerColumn,
TaskProgressColumn, TextColumn, TimeElapsedColumn,
TimeRemainingColumn)
from winpty import PtyProcess
parser = argparse.ArgumentParser(
prog="Dolby Vision + HDR",
description="A program to combine HDR videos with Dolby Vison videos for a Dolby Vison file with HDR fallback",
epilog="Report bugs to Swedish-Wiking@GitHub"
)
parser.add_argument(
"files",
nargs="*",
help="Paths of files and folders",
type=Path
)
parser.add_argument(
"-logs",
dest="logs",
choices=["DEBUG", "INFO", "ERROR"],
help="Set verbosity level"
)
parser.add_argument(
"-maxdif",
dest="maxdif",
type=int,
help="Set maxed allowed frames to differ between videos"
)
parser.add_argument(
"--temp-folder",
dest="tempFolder",
type=Path,
help="A folder to which to save the converted ringtones",
)
args = parser.parse_args()
logging.basicConfig(format="%(levelname)s:\t%(message)s", level=logging.INFO)
logging.basicConfig(level=args.logs)
def get_exe(name: str) -> Path:
_bins = Path(__file__).parent.resolve().joinpath("bin")
if which(name) is not None: return Path(which(name)) # type: ignore
elif _bins.joinpath(name).exists(): return _bins.joinpath(name)
raise FileExistsError(f"The executable {name} does not exist on PATH")
def collect_files(list_of_paths: list[Path], filter: list[str] = [".mp4", ".mkv"]) -> list[Path]:
files = list()
for f in list_of_paths:
if f.is_file() and f.suffix in filter: files.append(f)
elif f.is_dir(): [files.append(p) for p in f.rglob("*") if p.suffix in filter]
return files
FFMPEG = get_exe("ffmpeg")
FFPROBE = get_exe("ffprobe")
MKVMERGE = get_exe("mkvmerge")
MKVEXTRACT = get_exe("mkvextract")
DOVI_TOOL = get_exe("dovi_tool.exe")
temp_workdir = (
args.tempFolder
if args.tempFolder
else Path(__file__).parent.resolve().joinpath("temp")
)
class NumberValidator(Validator):
def validate(self, document):
ok = re.match(
r"^\d+$",
document.text,
)
if not ok:
raise ValidationError(
message="Please enter a valid number",
cursor_position=len(document.text),
)
class ColorMerger():
def __init__(self, files:list[Path]) -> None:
self.files = self._tkAskForFiles() if not files else files
if len(self.files) <= 1:
logging.error("Not enough files were choosen!")
exit(1)
self.metadata = list(self._analyzeFiles())
self.printMedia()
self._checkColors()
self.matchedFiles = self._matchFiles()
def _tkAskForFiles(self) -> list[Path]:
filetypes = (
("Video files", ".mkv .mp4"),
("Matroska files", ".mkv"),
("MPEG-4 files", ".mp4"),
("All files", "*.*")
)
files = tkinter.filedialog.askopenfilenames(
title="Select files",
filetypes=filetypes
)
return [Path(file) for file in files]
def _analyzeFiles(self):
for file in sorted(self.files):
logging.info(f"Analyzing {file.name}...")
probe_cmd = [
FFPROBE,
"-hide_banner",
"-loglevel", "fatal",
"-show_error",
"-show_streams",
"-select_streams", "v:0",
"-show_private_data",
"-print_format", "json",
file
]
data = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if data.returncode != 0:
logging.warning(f"File could not be analyzed: {data.stderr}")
break
parsed_data = self.parseMetadata(data.stdout.decode("utf-8"), file)
logging.info("Sucessfully analyzed file!")
yield {"name": file.name, "path": file} | parsed_data
@staticmethod
def parseMetadata(data, file):
json_data = json.loads(data)
logging.debug(json_data)
json_data = json_data["streams"][0]
width = int(json_data["width"])
height = int(json_data["height"])
fps = json_data["avg_frame_rate"].split("/")
fps = int(fps[0]) / int(fps[1])
frameCount = 0
colorProfile = "None"
try:
frameCount = int(json_data["tags"]["NUMBER_OF_FRAMES"])
except:
try:
frameCount = int(json_data["tags"]["NUMBER_OF_FRAMES-eng"])
except:
try:
frameCount = int(json_data["nb_frames"])
except:
logging.warning("Framerate was not found in ffprobe data, using MediaInfo")
logging.warning("Analysis may take longer")
media_info = MediaInfo.parse(file)
for track in media_info.tracks:
if track.track_type == "Video":
frameCount = int(track.frame_count)
try:
if json_data["side_data_list"][0]["rpu_present_flag"] == 1:
try:
if json_data["color_transfer"] == "smpte2084":
colorProfile = "HDR+DV"
except:
colorProfile = "DV"
except:
try:
if json_data["color_transfer"] == "smpte2084":
colorProfile = "HDR"
except:
colorProfile = "None"
return {"frameCount": frameCount, "colorProfile": colorProfile, "pxWidth": width, "pxHeight": height, "frameRate": fps}
def _checkColors(self):
cProfile = {d["colorProfile"] for d in self.metadata}
if ("DV" not in cProfile and "HDR+DV" not in cProfile) or not "HDR" in cProfile:
logging.error("Not enough files with HDR or DV layers")
exit(1)
def printMedia(self) -> None:
def printInfo(profile:str) -> None:
logging.info(f"{profile} Media:")
for media in filter(lambda d: d["colorProfile"] == profile, self.metadata):
logging.info(f"{media["name"]}:\n\tFrameCount: {media["frameCount"]}\n\tWidth: {media["pxWidth"]}\n\tHeight: {media["pxHeight"]}\n\tFrameRate: {media["frameRate"]}")
for profile in {d["colorProfile"] for d in self.metadata}:
printInfo(profile)
@staticmethod
def delayFrames(hdr, dv, hybrid:bool):
logging.warning("Dolby Vision layer probably needs to be delayed")
isManual = questionary.confirm("Do you want to input value to shift frames with?", default=False).ask()
if isManual:
delayed_frames = int(
questionary.text(
"Input frames to shift Dolby Vision layer with:",
qmark = "",
validate = NumberValidator
).ask()
)
else:
comparer = CompareWindow(hdr, dv, hybrid)
delayed_frames = comparer.shifted_frames
logging.info(f"Dolby Vision layer is shifted by {delayed_frames} frames")
return delayed_frames
def _matchFiles(self):
matched_files = list()
HDRs = [media for media in self.metadata if media["colorProfile"] == "HDR"]
logging.debug(HDRs)
DVs = [media for media in self.metadata if media["colorProfile"] in ["DV", "HDR+DV"]]
logging.debug(DVs)
if args.maxdif != None:
maxDif = int(args.maxdif)
else:
maxDif = int(
questionary.text(
"Input max allowed differance in frames:",
qmark = "",
validate = NumberValidator
).ask()
)
logging.info("Matching files...")
for hdr_file in HDRs:
miss = 0
logging.info(f"Trying to match: {hdr_file["name"]}")
for dv_file in DVs:
hybrid = True if dv_file["colorProfile"] == "HDR+DV" else False
absDif = abs(hdr_file["frameCount"] - dv_file["frameCount"])
if absDif == 0:
logging.info(f"Perfect match found with: {dv_file["name"]}")
isAutomatic = questionary.confirm("Want to frame match anyways", default=False).ask()
if isAutomatic: frames_to_delay = self.delayFrames(hdr_file, dv_file, hybrid)
else: frames_to_delay = 0
match = {"HDR_FILE": hdr_file, "DV_FILE": dv_file, "framesToDelay": frames_to_delay}
matched_files.append(match)
break
elif(absDif <= maxDif):
logging.info(f"Match found but with a difference of: {absDif} frames, file matched with: {dv_file["name"]}")
isMatch = questionary.confirm("Is it a match", default=False).ask()
if isMatch:
frames_to_delay = self.delayFrames(hdr_file, dv_file, hybrid)
match = {"HDR_FILE": hdr_file, "DV_FILE": dv_file, "framesToDelay": frames_to_delay}
matched_files.append(match)
else: logging.info("Trying another...")
else: miss += 1
if miss == len(DVs):
logging.warning(f"No match found for: {hdr_file["name"]}")
logging.info("Matching process completeded")
logging.debug(matched_files)
return matched_files
def _main(self, file_pair:dict):
self.mkTemp()
rpu_json = temp_workdir.joinpath("RPU.json")
rpu = temp_workdir.joinpath("RPU.bin")
rpu_edited = temp_workdir.joinpath("RPU_EDITED.bin")
hdr_hevc = temp_workdir.joinpath("HDR.hevc")
dv_hevc = temp_workdir.joinpath("DV.hevc")
hdr_dv_hevc = temp_workdir.joinpath("HDR_DV.hevc")
isDVmp4 = (file_pair["DV_FILE"]["path"] == ".mp4")
delay_frames = file_pair["framesToDelay"]
if delay_frames < 0:
remove_frames = "0-" + str(abs(delay_frames)-1)
delay_frames = 0
else: remove_frames = ""
crop = False
crop_amount = 0
if (file_pair["HDR_FILE"]["pxHeight"] == file_pair["DV_FILE"]["pxHeight"]):
logging.debug("No crop needed for RPU-file")
elif (int(file_pair["HDR_FILE"]["pxHeight"]) > int(file_pair["DV_FILE"]["pxHeight"])):
logging.debug("Adding letterboxing to RPU-file to match with target file")
crop_amount = int((int(file_pair["HDR_FILE"]["pxHeight"]) - int(file_pair["DV_FILE"]["pxHeight"]))/2)
elif (int(file_pair["HDR_FILE"]["pxHeight"]) < int(file_pair["DV_FILE"]["pxHeight"])):
logging.debug("Croping needed for RPU-file")
crop = True
json_data = {
"active_area": {
"crop": crop,
"presets": [{
"id": 0,
"left": 0,
"right": 0,
"top": crop_amount,
"bottom": crop_amount
}]},
"remove": [
remove_frames
],
"duplicate": [{
"source": 0,
"offset": 0,
"length": delay_frames
}]}
with open(rpu_json, "w") as outfile: outfile.write(json.dumps(json_data, indent=4))
cmdExtractHDRMKV = [
MKVEXTRACT,
"tracks",
file_pair["HDR_FILE"]["path"],
"0:" + str(hdr_hevc),
"--gui-mode"] #1
cmdExtractDVMKV = [
MKVEXTRACT,
"tracks",
file_pair["DV_FILE"]["path"],
"0:" + str(dv_hevc),
"--gui-mode"] #2
cmdExtractDV = [
"ffmpeg",
"-loglevel", "error",
"-hide_banner",
"-progress", "-",
"-nostats",
"-analyzeduration", "6000M",
"-probesize", "2147M",
"-y",
"-i", file_pair["DV_FILE"]["path"],
"-an", "-c:v",
"copy",
"-f", "hevc",
dv_hevc] #2
cmdExtractRPU = [
DOVI_TOOL,
"extract-rpu",
dv_hevc,
"-o", rpu] #3
cmdRPUEdit = [
DOVI_TOOL,
"editor",
"-i", rpu,
"-j", rpu_json,
"-o", rpu_edited] #4
cmdRPUInject = [
DOVI_TOOL,
"inject-rpu",
"-i", hdr_hevc,
"--rpu-in", rpu_edited,
"-o", hdr_dv_hevc] #5
logging.info("Injection process begins...")
logging.info(f"Files used: \n\t{file_pair["HDR_FILE"]["path"]}\n\t{file_pair["DV_FILE"]["path"]}")
self.run(cmdExtractHDRMKV, title="Extracting HDR video:\t\t")
if isDVmp4: self.run(cmdExtractDV, title="Extracting DV video:\t\t", total=file_pair["DV_FILE"]["frameCount"])
else: self.run(cmdExtractDVMKV, title="Extracting DV video:\t\t")
self.run(cmdExtractRPU, title="Extracting RPU from DV file:\t")
self.run(cmdRPUEdit, "Modifying RPU-file:\t\t")
self.run(cmdRPUInject, "Injecting RPU into HDR file:\t")
self._merge(file_pair["HDR_FILE"]["path"], hdr_dv_hevc)
@staticmethod
def _run_dovi_tool(cmd, progress:Progress, title:str):
total = 100
seconds = False
if "inject-rpu" in cmd: total = 200
if "editor" in cmd: total = 1
task = progress.add_task(title, total=total, status="")
ansi_clean = re.compile(r"\x1b\[[\?\d;]*[A-Za-z]|\r|\n")
proc = PtyProcess.spawn(cmd)
output = "ERROR"
try:
while proc.isalive():
output = ansi_clean.sub("", proc.read(256)).strip()
m = re.search(r'(\d+)%', output)
if "Rewriting file with interleaved RPU NALs.." in output: seconds = True
if output and not m:
if output == "Done.":
progress.update(task_id=task, status="[green]Done")
else:
progress.update(task_id=task, status=output)
elif m:
percentage = int(m.group(1))
if seconds: percentage += 100
if percentage == total:
progress.update(task_id=task, status="[green]Done")
progress.update(task_id=task, completed=percentage)
except EOFError: pass
if proc.exitstatus == 1:
logging.error(f"The \"{title.replace(":", "")}\" command failed: {output}")
progress.remove_task(task)
else:
if "editor" in cmd:
progress.update(task_id=task, completed=total, status="[green]Done")
@staticmethod
def _run_cmd(cmd, progress:Progress, title:str, total:float):
task = progress.add_task(title, total=total, status="")
data = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True,
text=True
)
completed = 0
for line in data.stdout: # type: ignore
if "#GUI#progress" in line:
completed = int(line.replace("#GUI#progress ","").replace("%",""))
elif "frame=" in line:
completed = int(line.replace("frame=",""))/total
if completed:
progress.update(task, completed=completed)
if cmd[0] == FFMPEG and data.returncode == 0:
progress.update(task, completed=total)
def run(self, cmd, title="", total=100):
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TaskProgressColumn(),
TimeElapsedColumn(),
TimeRemainingColumn(compact=True),
TextColumn("{task.fields[status]}"),
refresh_per_second=10
) as progress:
try:
if cmd[0] in [FFMPEG, MKVEXTRACT, MKVMERGE]:
self._run_cmd(cmd, progress, title, total)
elif cmd[0] == DOVI_TOOL:
self._run_dovi_tool(cmd, progress, title)
except subprocess.CalledProcessError:
logging.error("Command failed, The process of this file will FAIL!")
raise RuntimeError
except KeyboardInterrupt:
logging.error("Command interupted, The process of this file will FAIL!")
raise InterruptedError
def _merge(self, file:Path, DoVi:Path):
file_out = file.with_name(file.name.replace(".mkv", "_HDR_DV.mkv"))
cmdMerge = [
MKVMERGE,
"--gui-mode",
"-o", file_out,
"--no-video",
file,
DoVi
]
self.run(cmdMerge, "Multiplexing hybrid file: \t")
logging.info("Files sucessfully combined")
def inject(self):
for match in self.matchedFiles:
try: self._main(match)
except RuntimeError:
logging.error("Multiplexing of file FAILED")
except InterruptedError:
logging.error("Multiplexing of file FAILED because of Human interuption")
finally:
self.cleanUp()
@staticmethod
def mkTemp():
temp_workdir.mkdir(exist_ok=True)
logging.debug(f"Temp folder created at: {temp_workdir}")
@staticmethod
def cleanUp():
rmtree(temp_workdir, ignore_errors=True)
logging.debug(f"Temp folder was removed")
class CompareWindow():
def __init__(self, hdr:dict, dv:dict, isHybrid:bool) -> None:
self.window = tkinter.Tk()
self.font = tkinter.font.Font(weight="bold")
self.color_mode = "D"
self.ref_int = tkinter.IntVar(value=1000)
self.shifted_int = tkinter.IntVar(value=0)
self.dv_file = dv["path"]
self.dv_fps = dv["frameRate"]
self.hdr_file = hdr["path"]
self.hdr_fps = hdr["frameRate"]
self.isHybrid = isHybrid
self.total_ref = hdr["frameCount"]
# Variable to track scheduled resize
self._resize_after_id = None
self.active_image, self.blend, self.difference = self._generate_image(self.ref_int.get(), self.shifted_int.get())
self._window_init()
@property
def shifted_frames(self) -> int:
return self.shifted_int.get()
def _validate_ref_int(self, action:str, text:str) -> bool:
if action in ["1","0"]:
if text.isdigit():
if int(text) > self.total_ref:
self.window.after_idle(lambda: self.ref_int.set(self.total_ref))
if int(text) <= self.total_ref:
self.window.after_idle(lambda: self.ref_int.set(int(text)))
return True
elif text == "":
self.window.after_idle(lambda: self.ref_int.set(0))
self.window.after_idle(lambda: self.ref_entry.icursor(1))
return True
else:
return False
else:
return True
def _validate_int(self, action:str, text:str, validate_state:str) -> bool:
if validate_state == "key":
valid_int = re.compile(r"^[+-]?[0-9]+$|^-{1}$")
return bool(valid_int.match(text)) if action == "1" else True
else:
valid_int = re.compile(r"^[+-]?[0-9]+$")
if not valid_int.match(text) or text == "":
self.window.after_idle(lambda: self.shifted_int.set(0))
return bool(valid_int.match(text)) if action in ["1","0"] else True
def _button_init(self) -> None:
# Open in photos
show_image_btn = tkinter.Button(self.window, text="Open picture", command=self._show_image)
show_image_btn.grid(column=0, row=0, sticky=tkinter.W, padx=5, pady=5)
# Switch view
switch_btn = tkinter.Button(self.window, text="Switch view", command=self._switch_view)
switch_btn.grid(column=0, row=1, sticky=tkinter.W, padx=5, pady=5)
# Submit value
done_btn = tkinter.Button(self.window, text="Done", command=self._done)
done_btn.grid(column=2, row=1, sticky=tkinter.E, padx=5, pady=5)
def _entry_init(self) -> None:
# Validator
valid_ref_int = (self.window.register(self._validate_ref_int),"%d", "%P")
valid_int = (self.window.register(self._validate_int),"%d", "%P", "%V")
# Reference frame
ref_label = tkinter.Label(
self.window,
text=f"Frame to refrence in HDR video (total: {self.total_ref}):",
bg="black",
fg="white",
font=self.font
)
ref_label.grid(column=0, row=0, sticky=tkinter.E, padx=5, pady=5)
self.ref_entry = tkinter.Entry(
self.window,
validate = "all",
validatecommand = valid_ref_int,
textvariable=self.ref_int
)
self.ref_entry.bind("<Return>", self._update_image)
self.ref_entry.grid(column=1, row=0, sticky=tkinter.W, padx=5, pady=5)
# Other frame
shifted_label = tkinter.Label(
self.window,
text="Frames to shift Dolby Vision Layer with:",
bg="black",
fg="white",
font=self.font
)
shifted_label.grid(column=0, row=1, sticky=tkinter.E, padx=5, pady=5)
self.shifted_entry = tkinter.Entry(
self.window,
validate = "all",
validatecommand = valid_int,
textvariable=self.shifted_int
)
self.shifted_entry.bind("<Return>", self._update_image)
self.shifted_entry.grid(column=1, row=1, sticky=tkinter.W, padx=5, pady=5)
def _canvas_init(self) -> None:
image = ImageTk.PhotoImage(self.active_image)
self.canvas = tkinter.Canvas(
self.window,
width=image.width(),
height=image.height(),
bg="black"
)
self.canvas.grid(column=0, row=3, columnspan=3, padx=5, pady=5)
self.image_id = self.canvas.create_image(0, 0, image=image, anchor="nw")
def _window_init(self):
# --- Main Window config ---
self.window.title("Frame Compare")
self.window.iconbitmap(str(Path(__file__).parent.resolve().joinpath("icon.ico")))
self.window.geometry("1200x800")
self.window.minsize(700,420)
self.window.configure(bg="black", padx=10, pady=10)
self.window.columnconfigure(0, weight=1)
self.window.columnconfigure(1, weight=1)
self.window.columnconfigure(2, weight=0)
self._button_init()
self._entry_init()
self._canvas_init()
self.window.bind("<Configure>", self._on_resize)
# --- Focus the Window ---
self.window.lift()
self.window.attributes("-topmost",True)
self.window.protocol("WM_DELETE_WINDOW", self._done)
self.window.after_idle(self.window.attributes,"-topmost",False)
self.window.mainloop()
def _done(self):
self.window.quit()
self.window.destroy()
def _update_image(self, event):
logging.debug(f"Refrence frame: {self.ref_int.get()}")
logging.debug(f"Shifted frames: {self.shifted_int.get()}")
self.active_image, self.blend, self.difference = self._generate_image(self.ref_int.get(), self.shifted_int.get())
self._resize_image()
def _show_image(self):
logging.info("Opening image in default photo viewer")
self.active_image.show()
def _on_resize(self, event):
# Cancel any scheduled resize
if self._resize_after_id:
self.window.after_cancel(self._resize_after_id)
# Schedule a new resize after 200 ms (debounce)
self._resize_after_id = self.window.after(150, self._resize_image)
def _resize_image(self):
global new_image, resized_image
win_width = self.window.winfo_width() - 40
win_height = self.window.winfo_height() - 110
resized_height = int((self.active_image.height/self.active_image.width)*win_width)
if resized_height > win_height:
resized_width = int((self.active_image.width/self.active_image.height)*win_height)
resized_image = self.active_image.resize((resized_width, win_height), Image.Resampling.LANCZOS)
self.canvas.config(width=resized_width, height=win_height)
else:
resized_image = self.active_image.resize((win_width, resized_height), Image.Resampling.LANCZOS)
self.canvas.config(width=win_width, height=resized_height)
new_image = ImageTk.PhotoImage(resized_image)
self.canvas.itemconfigure(self.image_id, image=new_image)
self._resize_after_id = None # Reset
def _switch_view(self):
if self.color_mode == "D":
logging.debug("Switching to Blended image")
self.active_image = self.blend
self.color_mode = "B"
self._resize_image()
else:
logging.debug("Switching to Difference image")
self.active_image = self.difference
self.color_mode = "D"
self._resize_image()
def _generate_image(self, ref:int, shift:int) -> tuple[Image.Image, Image.Image, Image.Image]:
logging.debug("Generating images to compare...")
vf_HDR = "zscale=t=linear,tonemap=hable,zscale=p=709:t=709:m=709"
vf_DV = "libplacebo=tonemapping=auto,zscale=t=linear,tonemap=hable,zscale=p=709:t=709:m=709"
ss_hdr = str(ref / self.hdr_fps)
ss_dv = str((ref - shift) / self.dv_fps)
ColorMerger.mkTemp()
hdr_out = temp_workdir.joinpath("HDR.bmp")
dv_out = temp_workdir.joinpath("DV.bmp")
hdr_cmd = [
FFMPEG,
"-hide_banner",
"-v", "error",
"-y",
"-ss", ss_hdr,
"-i", self.hdr_file,
"-qscale:v", "1",
"-vf", vf_HDR,
"-vframes", "1",
hdr_out
]
dv_cmd = [
FFMPEG,
"-hide_banner",
"-v", "error",
"-y",
"-ss", ss_dv,
"-i", self.dv_file,
"-qscale:v", "1",
"-vf", vf_HDR if self.isHybrid else vf_DV,
"-vframes", "1",
dv_out
]
logging.debug("Generating HDR image...")
try:
subprocess.run(hdr_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except Exception as e:
logging.error(f"Failed to generate HDR screencapture | ERROR:{e}")
logging.debug("Generating DV image...")
try:
subprocess.run(dv_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except Exception as e:
logging.error(f"Failed to generate DV screencapture | ERROR:{e}")
logging.debug("Processing Images...")
HDR_img = Image.open(hdr_out)
DV_img = Image.open(dv_out)
HDR_w, HDR_h = HDR_img.size
DV_w, DV_h = DV_img.size
crop = abs(HDR_h-DV_h)/2
if HDR_img.height < DV_img.height: DV_img = DV_img.crop((0, crop, DV_w, DV_h-crop))
elif HDR_img.height > DV_img.height: HDR_img = HDR_img.crop((0, crop, HDR_w, HDR_h-crop))
difference = Image.blend(DV_img, HDR_img, 0.5)
blend = ImageChops.difference(DV_img.convert('L'), HDR_img.convert('L'))
logging.debug("Processing done!")
active_image = difference if self.color_mode == "D" else blend
return active_image, blend, difference
if __name__ == "__main__":
injector = ColorMerger(collect_files(args.files))
injector.inject()