# Copyright (C) 2023 Deforum LLC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, version 3 of the License. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # Contact the authors: https://deforum.github.io/ # TODO: deduplicate upscaling/interp/vid2depth code import os, gc import numpy as np import cv2 from pathlib import Path from tqdm import tqdm from PIL import Image, ImageOps, ImageChops from modules.shared import cmd_opts, device as sh_device from modules import devices import shutil from .frame_interpolation import clean_folder_name from rife.inference_video import duplicate_pngs_from_folder from .video_audio_utilities import get_quick_vid_info, vid2frames, ffmpeg_stitch_video def process_depth_vid_upload_logic(file, mode, thresholding, threshold_value, threshold_value_max, adapt_block_size, adapt_c, invert, end_blur, midas_weight_vid2depth, vid_file_name, keep_imgs, f_location, f_crf, f_preset, f_models_path): print("got a request to *vid2depth* an existing video.") in_vid_fps, _, _ = get_quick_vid_info(file.name) folder_name = clean_folder_name(Path(vid_file_name).stem) outdir_no_tmp = os.path.join(os.getcwd(), 'outputs', 'frame-depth', folder_name) i = 1 while os.path.exists(outdir_no_tmp): outdir_no_tmp = os.path.join(os.getcwd(), 'outputs', 'frame-depth', folder_name + '_' + str(i)) i += 1 outdir = os.path.join(outdir_no_tmp, 'tmp_input_frames') os.makedirs(outdir, exist_ok=True) vid2frames(video_path=file.name, video_in_frame_path=outdir, overwrite=True, extract_from_frame=0, extract_to_frame=-1, numeric_files_output=True, out_img_format='png') process_video_depth(mode, thresholding, threshold_value, threshold_value_max, adapt_block_size, adapt_c, invert, end_blur, midas_weight_vid2depth, orig_vid_fps=in_vid_fps, real_audio_track=file.name, raw_output_imgs_path=outdir, img_batch_id=None, ffmpeg_location=f_location, ffmpeg_crf=f_crf, ffmpeg_preset=f_preset, f_models_path=f_models_path, keep_depth_imgs=keep_imgs, orig_vid_name=folder_name) def process_video_depth(mode, thresholding, threshold_value, threshold_value_max, adapt_block_size, adapt_c, invert, end_blur, midas_weight_vid2depth, orig_vid_fps, real_audio_track, raw_output_imgs_path, img_batch_id, ffmpeg_location, ffmpeg_crf, ffmpeg_preset, f_models_path, keep_depth_imgs, orig_vid_name): devices.torch_gc() print("Vid2depth progress (it's OK if it finishes before 100%):") upscaled_path = os.path.join(raw_output_imgs_path, 'depth_frames') if orig_vid_name is not None: # upscaling a video (deforum or unrelated) custom_upscale_path = "{}_{}".format(upscaled_path, orig_vid_name) else: # upscaling after a deforum run: custom_upscale_path = "{}_{}".format(upscaled_path, img_batch_id) temp_convert_raw_png_path = os.path.join(raw_output_imgs_path, "tmp_depth_folder") duplicate_pngs_from_folder(raw_output_imgs_path, temp_convert_raw_png_path, img_batch_id, orig_vid_name) videogen = [] for f in os.listdir(temp_convert_raw_png_path): # double check for old _depth_ files, not really needed probably but keeping it for now if '_depth_' not in f: videogen.append(f) videogen.sort(key= lambda x:int(x.split('.')[0])) vid_out = None if not os.path.exists(custom_upscale_path): os.mkdir(custom_upscale_path) # Loading the chosen model if 'Mixed' in mode: model = (load_depth_model(f_models_path, midas_weight_vid2depth), load_anime_model()) elif 'Depth' in mode: model = load_depth_model(f_models_path, midas_weight_vid2depth) elif 'Anime' in mode: model = load_anime_model() else: model = None # Upscaling is a slow and demanding operation, so we don't need as much parallelization here for i in tqdm(range(len(videogen)), desc="Vid2depth"): lastframe = videogen[i] img_path = os.path.join(temp_convert_raw_png_path, lastframe) image = process_frame(model, Image.open(img_path).convert("RGB"), mode, thresholding, threshold_value, threshold_value_max, adapt_block_size, adapt_c, invert, end_blur, midas_weight_vid2depth) filename = '{}/{:0>9d}.png'.format(custom_upscale_path, i) image.save(filename) # Cleaning up and freeing the memory before stitching model = None gc.collect() devices.torch_gc() shutil.rmtree(temp_convert_raw_png_path) # stitch video from upscaled frames, and add audio if needed try: print (f"*Passing depth frames to ffmpeg...*") vid_out_path = stitch_video(img_batch_id, orig_vid_fps, custom_upscale_path, real_audio_track, ffmpeg_location, mode, thresholding, threshold_value, threshold_value_max, adapt_block_size, adapt_c, invert, end_blur, midas_weight_vid2depth, ffmpeg_crf, ffmpeg_preset, keep_depth_imgs, orig_vid_name) # remove folder with raw (non-upscaled) vid input frames in case of input VID and not PNGs if orig_vid_name is not None: shutil.rmtree(raw_output_imgs_path) except Exception as e: print(f'Video stitching gone wrong. *Vid2depth frames were saved to HD as backup!*. Actual error: {e}') gc.collect() devices.torch_gc() def process_frame(model, image, mode, thresholding, threshold_value, threshold_value_max, adapt_block_size, adapt_c, invert, end_blur, midas_weight_vid2depth): # Get grayscale foreground map if 'None' in mode: depth = process_depth(image, 'None', thresholding, threshold_value, threshold_value_max, adapt_block_size, adapt_c, invert, end_blur) elif not 'Mixed' in mode: depth = process_frame_depth(model, np.array(image), midas_weight_vid2depth) if 'Depth' in mode else process_frame_anime(model, np.array(image)) depth = process_depth(depth, mode, thresholding, threshold_value, threshold_value_max, adapt_block_size, adapt_c, invert, end_blur) else: if thresholding == 'None': raise "Mixed mode doesn't work with no thresholding!" depth_depth = process_frame_depth(model[0], np.array(image), midas_weight_vid2depth) depth_depth = process_depth(depth_depth, 'Depth', thresholding, threshold_value, threshold_value_max, adapt_block_size, adapt_c, invert, end_blur) anime_depth = process_frame_anime(model[1], np.array(image)) anime_depth = process_depth(anime_depth, 'Anime', 'Simple', 32, 255, adapt_block_size, adapt_c, invert, end_blur) depth = ImageChops.logical_or(depth_depth.convert('1'), anime_depth.convert('1')) return depth def process_depth(depth, mode, thresholding, threshold_value, threshold_value_max, adapt_block_size, adapt_c, invert, end_blur): depth = depth.convert('L') # Depth mode need inverting whereas Anime mode doesn't # (invert and 'Depth' in mode) or (not invert and not 'Depth' in mode) if (invert and 'None' in mode) or (invert is ('Depth' in mode)): depth = ImageOps.invert(depth) depth = np.array(depth) # Apply thresholding if thresholding == 'Simple': _, depth = cv2.threshold(depth, threshold_value, threshold_value_max, cv2.THRESH_BINARY) elif thresholding == 'Simple (Auto-value)': _, depth = cv2.threshold(depth, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) elif thresholding == 'Adaptive (Mean)': depth = cv2.adaptiveThreshold(depth, threshold_value_max, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, int(adapt_block_size), adapt_c) elif thresholding == 'Adaptive (Gaussian)': depth = cv2.adaptiveThreshold(depth, threshold_value_max, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, int(adapt_block_size), adapt_c) # Apply slight blur in the end to smoothen the edges after initial thresholding if end_blur > 0: depth = cv2.GaussianBlur(depth, (5, 5), end_blur) if thresholding == 'None' or end_blur == 0: # Return a graymap return Image.fromarray(depth).convert('L') else: # This commits thresholding again, but on the already processed image, so we don't need to set it up as much return Image.fromarray(cv2.threshold(depth, 127, 255, cv2.THRESH_BINARY)[1]).convert('L') def stitch_video(img_batch_id, fps, img_folder_path, audio_path, ffmpeg_location, mode, thresholding, threshold_value, threshold_value_max, adapt_block_size, adapt_c, invert, end_blur, midas_weight_vid2depth, f_crf, f_preset, keep_imgs, orig_vid_name): parent_folder = os.path.dirname(img_folder_path) grandparent_folder = os.path.dirname(parent_folder) mode = str(mode).replace('\\', '_').replace(' ', '_').replace('(', '_').replace(')', '_') mp4_path = os.path.join(grandparent_folder, str(orig_vid_name if orig_vid_name is not None else img_batch_id) +'_depth_'+f"{thresholding}") mp4_path = mp4_path + '.mp4' t = os.path.join(img_folder_path, "%09d.png") add_soundtrack = 'None' if not audio_path is None: add_soundtrack = 'File' exception_raised = False try: ffmpeg_stitch_video(ffmpeg_location=ffmpeg_location, fps=fps, outmp4_path=mp4_path, stitch_from_frame=0, stitch_to_frame=1000000, imgs_path=t, add_soundtrack=add_soundtrack, audio_path=audio_path, crf=f_crf, preset=f_preset) except Exception as e: exception_raised = True print(f"An error occurred while stitching the video: {e}") if not exception_raised and not keep_imgs: shutil.rmtree(img_folder_path) if (keep_imgs and orig_vid_name is not None) or (orig_vid_name is not None and exception_raised is True): shutil.move(img_folder_path, grandparent_folder) return mp4_path # Midas/Adabins Depth mode with the usual workflow def load_depth_model(models_path, midas_weight_vid2depth): from .depth import DepthModel device = ('cpu' if cmd_opts.lowvram or cmd_opts.medvram else sh_device) keep_in_vram = False # TODO: Future - handle this too? print('Loading Depth Model') depth_model = DepthModel(models_path, device, not cmd_opts.no_half, keep_in_vram=keep_in_vram) return depth_model # Anime Remove Background by skytnt and onnx model # https://huggingface.co/spaces/skytnt/anime-remove-background/blob/main/app.py def load_anime_model(): # Installing its deps on demand print('Checking ARB dependencies') from launch import is_installed, run_pip libs = ["onnx", "onnxruntime-gpu", "huggingface_hub"] for lib in libs: if not is_installed(lib): run_pip(f"install {lib}", lib) try: import onnxruntime as rt import huggingface_hub except Exception as e: raise f"onnxruntime has not been installed correctly! Anime Remove Background mode is unable to function. The actual exception is: {e}. Note, that you'll need internet connection for the first run!" providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] model_path = huggingface_hub.hf_hub_download("skytnt/anime-seg", "isnetis.onnx") return rt.InferenceSession(model_path, providers=providers) def get_mask(rmbg_model, img, s=1024): img = (img / 255).astype(np.float32) h, w = h0, w0 = img.shape[:-1] h, w = (s, int(s * w / h)) if h > w else (int(s * h / w), s) ph, pw = s - h, s - w img_input = np.zeros([s, s, 3], dtype=np.float32) img_input[ph // 2:ph // 2 + h, pw // 2:pw // 2 + w] = cv2.resize(img, (w, h)) img_input = np.transpose(img_input, (2, 0, 1)) img_input = img_input[np.newaxis, :] mask = rmbg_model.run(None, {'img': img_input})[0][0] mask = np.transpose(mask, (1, 2, 0)) mask = mask[ph // 2:ph // 2 + h, pw // 2:pw // 2 + w] mask = cv2.resize(mask, (w0, h0)) # TODO: pass in batches mask = (mask * 255).astype(np.uint8) return mask def process_frame_depth(depth_model, image, midas_weight): opencv_image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) depth = depth_model.predict(opencv_image, midas_weight, not cmd_opts.no_half) return depth_model.to_image(depth) def process_frame_anime(model, image): return Image.fromarray(get_mask(model, image), 'L')