CoolFace
Apppublic

hololens/stable-diffusion-webui-depthmap-script

sourceHugging Faceupdated 2y agoView on Hugging Face
1likes
core.py774 linesDownload Raw Back to src
1from pathlib import Path
2from PIL import Image
3
4try:
5    from tqdm import trange
6except:
7    from builtins import range as trange
8
9import torch, gc
10import cv2
11import os.path
12import numpy as np
13import copy
14import platform
15import math
16
17# Our code
18from src.misc import *
19from src.common_constants import GenerationOptions as go
20from src.common_constants import *
21from src.stereoimage_generation import create_stereoimages
22from src.normalmap_generation import create_normalmap
23from src.depthmap_generation import ModelHolder
24from src import backbone
25
26try:
27    # 3d-photo-inpainting imports
28    from inpaint.mesh import write_mesh, read_mesh, output_3d_photo
29    from inpaint.networks import Inpaint_Color_Net, Inpaint_Depth_Net, Inpaint_Edge_Net
30    from inpaint.utils import path_planning
31    from inpaint.bilateral_filtering import sparse_bilateral_filtering
32except Exception as e:
33    print('Impaint import failed. Impaint will not work.')
34    import traceback
35    traceback.print_exc()
36
37global video_mesh_data, video_mesh_fn
38video_mesh_data = None
39video_mesh_fn = None
40
41model_holder = ModelHolder()
42
43
44def convert_to_i16(arr):
45    # Single channel, 16 bit image. This loses some precision!
46    # uint16 conversion uses round-down, therefore values should be [0; 2**16)
47    numbytes = 2
48    max_val = (2 ** (8 * numbytes))
49    out = np.clip(arr * max_val + 0.0001, 0, max_val - 0.1)  # -0.1 from above is needed to avoid overflowing
50    return out.astype("uint16")
51
52def convert_i16_to_rgb(image, like):
53    # three channel, 8 bits per channel image
54    output = np.zeros_like(like)
55    output[:, :, 0] = image / 256.0
56    output[:, :, 1] = image / 256.0
57    output[:, :, 2] = image / 256.0
58    return output
59
60
61class CoreGenerationFunnelInp:
62    """This class takes a dictionary and creates a core_generation_funnel inp.
63    Non-applicable parameters are silently discarded (no error)"""
64    def __init__(self, values):
65        if isinstance(values, CoreGenerationFunnelInp):
66            values = values.values
67        values = {(k.name if isinstance(k, GenerationOptions) else k).lower(): v for k, v in values.items()}
68
69        self.values = {}
70        for setting in GenerationOptions:
71            name = setting.name.lower()
72            self.values[name] = values[name] if name in values else setting.df
73
74    def __getitem__(self, item):
75        if isinstance(item, GenerationOptions):
76            return self.values[item.name.lower()]
77        return self.values[item]
78
79    def __getattr__(self, item):
80        return self[item]
81
82
83def core_generation_funnel(outpath, inputimages, inputdepthmaps, inputnames, inp, ops=None):
84    if len(inputimages) == 0 or inputimages[0] is None:
85        return
86    if inputdepthmaps is None or len(inputdepthmaps) == 0:
87        inputdepthmaps: list[Image] = [None for _ in range(len(inputimages))]
88    inputdepthmaps_complete = all([x is not None for x in inputdepthmaps])
89
90    inp = CoreGenerationFunnelInp(inp)
91
92    if ops is None:
93        ops = backbone.gather_ops()
94    model_holder.update_settings(**ops)
95
96    # TODO: ideally, run_depthmap should not save meshes - that makes the function not pure
97    print(SCRIPT_FULL_NAME)
98    print(f'Backbone: {backbone.USED_BACKBONE.name}')
99
100    backbone.unload_sd_model()
101
102    # TODO: this still should not be here
103    background_removed_images = []
104    # remove on base image before depth calculation
105    if inp[go.GEN_REMBG]:
106        if inp[go.PRE_DEPTH_BACKGROUND_REMOVAL]:
107            inputimages = batched_background_removal(inputimages, inp[go.REMBG_MODEL])
108            background_removed_images = inputimages
109        else:
110            background_removed_images = batched_background_removal(inputimages, inp[go.REMBG_MODEL])
111
112    # init torch device
113    if inp[go.COMPUTE_DEVICE] == 'GPU':
114        if torch.cuda.is_available():
115            device = torch.device("cuda")
116        else:
117            print('WARNING: Cuda device was not found, cpu will be used')
118            device = torch.device("cpu")
119    else:
120        device = torch.device("cpu")
121    print("device: %s" % device)
122
123    # TODO: This should not be here
124    inpaint_imgs = []
125    inpaint_depths = []
126
127    try:
128        if not inputdepthmaps_complete:
129            print("Loading model(s) ..")
130            model_holder.ensure_models(inp[go.MODEL_TYPE], device, inp[go.BOOST], inp[go.TILING_MODE])
131        print("Computing output(s) ..")
132        # iterate over input images
133        for count in trange(0, len(inputimages)):
134            # Convert single channel input (PIL) images to rgb
135            if inputimages[count].mode == 'I':
136                inputimages[count].point(lambda p: p * 0.0039063096, mode='RGB')
137                inputimages[count] = inputimages[count].convert('RGB')
138
139            raw_prediction = None
140            """Raw prediction, as returned by a model. None if input depthmap is used."""
141            raw_prediction_invert = False
142            """True if near=dark on raw_prediction"""
143            out = None
144
145            if inputdepthmaps is not None and inputdepthmaps[count] is not None:
146                # use custom depthmap
147                dp = inputdepthmaps[count]
148                if isinstance(dp, Image.Image):
149                    if dp.width != inputimages[count].width or dp.height != inputimages[count].height:
150                        try:  # LANCZOS may fail on some formats
151                            dp = dp.resize((inputimages[count].width, inputimages[count].height), Image.Resampling.LANCZOS)
152                        except:
153                            dp = dp.resize((inputimages[count].width, inputimages[count].height))
154                    # Trying desperately to rescale image to [0;1) without actually normalizing it
155                    # Normalizing is avoided, because we want to preserve the scale of the original depthmaps
156                    # (batch mode, video mode).
157                    if len(dp.getbands()) == 1:
158                        out = np.asarray(dp, dtype="float")
159                        out_max = out.max()
160                        if out_max < 256:
161                            bit_depth = 8
162                        elif out_max < 65536:
163                            bit_depth = 16
164                        else:
165                            bit_depth = 32
166                        out /= 2.0 ** bit_depth
167                    else:
168                        out = np.asarray(dp, dtype="float")[:, :, 0]
169                        out /= 256.0
170                else:
171                    # Should be in interval [0; 1], values outside of this range will be clipped.
172                    out = np.asarray(dp, dtype="float")
173                    assert inputimages[count].height == out.shape[0], "Custom depthmap height mismatch"
174                    assert inputimages[count].width == out.shape[1], "Custom depthmap width mismatch"
175            else:
176                # override net size (size may be different for different images)
177                if inp[go.NET_SIZE_MATCH]:
178                    # Round up to a multiple of 32 to avoid potential issues
179                    # TODO: buggs for Depth Anything
180                    net_width = (inputimages[count].width + 31) // 32 * 32
181                    net_height = (inputimages[count].height + 31) // 32 * 32
182                else:
183                    net_width = inp[go.NET_WIDTH]
184                    net_height = inp[go.NET_HEIGHT]
185                raw_prediction, raw_prediction_invert = \
186                    model_holder.get_raw_prediction(inputimages[count], net_width, net_height)
187
188                # output
189                if abs(raw_prediction.max() - raw_prediction.min()) > np.finfo("float").eps:
190                    out = np.copy(raw_prediction)
191                    # TODO: some models may output negative values, maybe these should be clamped to zero.
192                    if raw_prediction_invert:
193                        out *= -1
194                    if inp[go.DO_OUTPUT_DEPTH_PREDICTION]:
195                        yield count, 'depth_prediction', np.copy(out)
196                    if inp[go.CLIPDEPTH]:
197                        if inp[go.CLIPDEPTH_MODE] == 'Range':
198                            out = (out - out.min()) / (out.max() - out.min())  # normalize to [0; 1]
199                            out = np.clip(out, inp[go.CLIPDEPTH_FAR], inp[go.CLIPDEPTH_NEAR])
200                        elif inp[go.CLIPDEPTH_MODE] == 'Outliers':
201                            fb, nb = np.percentile(out, [inp[go.CLIPDEPTH_FAR] * 100.0, inp[go.CLIPDEPTH_NEAR] * 100.0])
202                            out = np.clip(out, fb, nb)
203                    out = (out - out.min()) / (out.max() - out.min())  # normalize to [0; 1]
204                else:
205                    # Regretfully, the depthmap is broken and will be replaced with a black image
206                    out = np.zeros(raw_prediction.shape)
207
208            # Maybe we should not use img_output for everything, since we get better accuracy from
209            # the raw_prediction. However, it is not always supported. We maybe would like to achieve
210            # reproducibility, so depthmap of the image should be the same as generating the depthmap one more time.
211            img_output = convert_to_i16(out)
212            """Depthmap (near=bright), as uint16"""
213
214            # if 3dinpainting, store maps for processing in second pass
215            if inp[go.GEN_INPAINTED_MESH]:
216                inpaint_imgs.append(inputimages[count])
217                inpaint_depths.append(img_output)
218
219            # applying background masks after depth
220            if inp[go.GEN_REMBG]:
221                print('applying background masks')
222                background_removed_image = background_removed_images[count]
223                # maybe a threshold cut would be better on the line below.
224                background_removed_array = np.array(background_removed_image)
225                bg_mask = (background_removed_array[:, :, 0] == 0) & (background_removed_array[:, :, 1] == 0) & (
226                        background_removed_array[:, :, 2] == 0) & (background_removed_array[:, :, 3] <= 0.2)
227                img_output[bg_mask] = 0  # far value
228
229                yield count, 'background_removed', background_removed_image
230
231                if inp[go.SAVE_BACKGROUND_REMOVAL_MASKS]:
232                    bg_array = (1 - bg_mask.astype('int8')) * 255
233                    mask_array = np.stack((bg_array, bg_array, bg_array, bg_array), axis=2)
234                    mask_image = Image.fromarray(mask_array.astype(np.uint8))
235
236                    yield count, 'foreground_mask', mask_image
237
238            # A weird quirk: if user tries to save depthmap, whereas custom depthmap is used,
239            # custom depthmap will be outputed
240            if inp[go.DO_OUTPUT_DEPTH]:
241                img_depth = cv2.bitwise_not(img_output) if inp[go.OUTPUT_DEPTH_INVERT] else img_output
242                if inp[go.OUTPUT_DEPTH_COMBINE]:
243                    axis = 1 if inp[go.OUTPUT_DEPTH_COMBINE_AXIS] == 'Horizontal' else 0
244                    img_concat = Image.fromarray(np.concatenate(
245                        (inputimages[count], convert_i16_to_rgb(img_depth, inputimages[count])),
246                        axis=axis))
247                    yield count, 'concat_depth', img_concat
248                else:
249                    yield count, 'depth', Image.fromarray(img_depth)
250
251            if inp[go.GEN_STEREO]:
252                # print("Generating stereoscopic image(s)..")
253                stereoimages = create_stereoimages(
254                    inputimages[count], img_output,
255                    inp[go.STEREO_DIVERGENCE], inp[go.STEREO_SEPARATION],
256                    inp[go.STEREO_MODES],
257                    inp[go.STEREO_BALANCE], inp[go.STEREO_OFFSET_EXPONENT], inp[go.STEREO_FILL_ALGO])
258                for c in range(0, len(stereoimages)):
259                    yield count, inp[go.STEREO_MODES][c], stereoimages[c]
260
261            if inp[go.GEN_NORMALMAP]:
262                normalmap = create_normalmap(
263                    img_output,
264                    inp[go.NORMALMAP_PRE_BLUR_KERNEL] if inp[go.NORMALMAP_PRE_BLUR] else None,
265                    inp[go.NORMALMAP_SOBEL_KERNEL] if inp[go.NORMALMAP_SOBEL] else None,
266                    inp[go.NORMALMAP_POST_BLUR_KERNEL] if inp[go.NORMALMAP_POST_BLUR] else None,
267                    inp[go.NORMALMAP_INVERT]
268                )
269                yield count, 'normalmap', normalmap
270
271            if inp[go.GEN_HEATMAP]:
272                from dzoedepth.utils.misc import colorize
273                heatmap = Image.fromarray(colorize(img_output, cmap='inferno'))
274                yield count, 'heatmap', heatmap
275
276            # gen mesh
277            if inp[go.GEN_SIMPLE_MESH]:
278                print(f"\nGenerating (occluded) mesh ..")
279                basename = 'depthmap'
280                meshsimple_fi = get_uniquefn(outpath, basename, 'obj', 'simple')
281
282                depthi = raw_prediction if raw_prediction is not None else out
283                depthi_min, depthi_max = depthi.min(), depthi.max()
284                # try to map output to sensible values for non zoedepth models, boost, or custom maps
285                if inp[go.MODEL_TYPE] not in [7, 8, 9] or inp[go.BOOST] or inputdepthmaps[count] is not None:
286                    # invert if midas
287                    if inp[go.MODEL_TYPE] > 0 or inputdepthmaps[count] is not None:  # TODO: Weird
288                        depthi = depthi_max - depthi + depthi_min
289                        depth_max = depthi.max()
290                        depth_min = depthi.min()
291                    # make positive
292                    if depthi_min < 0:
293                        depthi = depthi - depthi_min
294                        depth_max = depthi.max()
295                        depth_min = depthi.min()
296                    # scale down
297                    if depthi.max() > 10.0:
298                        depthi = 4.0 * (depthi - depthi_min) / (depthi_max - depthi_min)
299                    # offset
300                    depthi = depthi + 1.0
301
302                mesh = create_mesh(inputimages[count], depthi, keep_edges=not inp[go.SIMPLE_MESH_OCCLUDE],
303                                   spherical=(inp[go.SIMPLE_MESH_SPHERICAL]))
304                mesh.export(meshsimple_fi)
305                yield count, 'simple_mesh', meshsimple_fi
306
307        print("Computing output(s) done.")
308    except Exception as e:
309        import traceback
310        if 'out of memory' in str(e).lower():
311            print(str(e))
312            suggestion = "out of GPU memory, could not generate depthmap! " \
313                         "Here are some suggestions to work around this issue:\n"
314            if inp[go.BOOST]:
315                suggestion += " * Disable BOOST (generation will be faster, but the depthmap will be less detailed)\n"
316            if backbone.USED_BACKBONE != backbone.BackboneType.STANDALONE:
317                suggestion += " * Run DepthMap in the standalone mode - without launching the SD WebUI\n"
318            if device != torch.device("cpu"):
319                suggestion += " * Select CPU as the processing device (this will be slower)\n"
320            if inp[go.MODEL_TYPE] != 6:
321                suggestion +=\
322                    " * Use a different model (generally, more memory-consuming models produce better depthmaps)\n"
323            if not inp[go.BOOST]:
324                suggestion += " * Reduce net size (this could reduce quality)\n"
325            print('Fail.\n')
326            raise Exception(suggestion)
327        else:
328            print('Fail.\n')
329            raise e
330    finally:
331        if backbone.get_opt('depthmap_script_keepmodels', True):
332            model_holder.offload()  # Swap to CPU memory
333        else:
334            model_holder.unload_models()
335        gc.collect()
336        backbone.torch_gc()
337
338    # TODO: This should not be here
339    if inp[go.GEN_INPAINTED_MESH]:
340        try:
341            mesh_fi = run_3dphoto(device, inpaint_imgs, inpaint_depths, inputnames, outpath,
342                                  inp[go.GEN_INPAINTED_MESH_DEMOS],
343                                  1, "mp4")
344            yield 0, 'inpainted_mesh', mesh_fi
345        except Exception as e:
346            print(f'{str(e)}, some issue with generating inpainted mesh')
347
348    backbone.reload_sd_model()
349    print("All done.\n")
350
351
352def get_uniquefn(outpath, basename, ext, suffix=''):
353    basecount = backbone.get_next_sequence_number(outpath, basename)
354    if basecount > 0:
355        basecount -= 1
356    if suffix != '':
357        suffix = f'-{suffix}'  # Dash is important for selecting unique filenames (see get_next_sequence_number)
358    for i in range(500):
359        fullfn = os.path.join(outpath, f"{basename}-{basecount + i:04}{suffix}.{ext}")
360        if not os.path.exists(fullfn):
361            return fullfn
362    return f"{basename}-99999{suffix}.{ext}"  # Failback, should never be executed
363
364
365def run_3dphoto(device, img_rgb, img_depth, inputnames, outpath, gen_inpainted_mesh_demos, vid_ssaa, vid_format):
366    mesh_fi = ''
367    try:
368        print("Running 3D Photo Inpainting .. ")
369        edgemodel_path = './models/3dphoto/edge_model.pth'
370        depthmodel_path = './models/3dphoto/depth_model.pth'
371        colormodel_path = './models/3dphoto/color_model.pth'
372        # create paths to model if not present
373        os.makedirs('./models/3dphoto/', exist_ok=True)
374
375        ensure_file_downloaded(
376            edgemodel_path,
377            ["https://huggingface.co/spaces/Epoching/3D_Photo_Inpainting/resolve/e389e564fd2a55cf/checkpoints/edge-model.pth",
378             "https://filebox.ece.vt.edu/~jbhuang/project/3DPhoto/model/edge-model.pth"],
379            "b1d768bd008ad5fe9f540004f870b8c3d355e4939b2009aa4db493fd313217c9")
380        ensure_file_downloaded(
381            depthmodel_path,
382            ["https://huggingface.co/spaces/Epoching/3D_Photo_Inpainting/resolve/e389e564fd2a55cf/checkpoints/depth-model.pth",
383             "https://filebox.ece.vt.edu/~jbhuang/project/3DPhoto/model/depth-model.pth"],
384            "2d0e63e89a22762ddfa8bc8c9f8c992e5532b140123274ffc6e4171baa1b76f8")
385        ensure_file_downloaded(
386            colormodel_path,
387            ["https://huggingface.co/spaces/Epoching/3D_Photo_Inpainting/resolve/e389e564fd2a55cf/checkpoints/color-model.pth",
388             "https://filebox.ece.vt.edu/~jbhuang/project/3DPhoto/model/color-model.pth"],
389            "383c9b1db70097907a6f9c8abb0303e7056f50d5456a36f34ab784592b8b2c20"
390        )
391
392        print("Loading edge model ..")
393        depth_edge_model = Inpaint_Edge_Net(init_weights=True)
394        depth_edge_weight = torch.load(edgemodel_path, map_location=torch.device(device))
395        depth_edge_model.load_state_dict(depth_edge_weight)
396        depth_edge_model = depth_edge_model.to(device)
397        depth_edge_model.eval()
398        print("Loading depth model ..")
399        depth_feat_model = Inpaint_Depth_Net()
400        depth_feat_weight = torch.load(depthmodel_path, map_location=torch.device(device))
401        depth_feat_model.load_state_dict(depth_feat_weight, strict=True)
402        depth_feat_model = depth_feat_model.to(device)
403        depth_feat_model.eval()
404        depth_feat_model = depth_feat_model.to(device)
405        print("Loading rgb model ..")
406        rgb_model = Inpaint_Color_Net()
407        rgb_feat_weight = torch.load(colormodel_path, map_location=torch.device(device))
408        rgb_model.load_state_dict(rgb_feat_weight)
409        rgb_model.eval()
410        rgb_model = rgb_model.to(device)
411
412        config = {}
413        config["gpu_ids"] = 0
414        config['extrapolation_thickness'] = 60
415        config['extrapolate_border'] = True
416        config['depth_threshold'] = 0.04
417        config['redundant_number'] = 12
418        config['ext_edge_threshold'] = 0.002
419        config['background_thickness'] = 70
420        config['context_thickness'] = 140
421        config['background_thickness_2'] = 70
422        config['context_thickness_2'] = 70
423        config['log_depth'] = True
424        config['depth_edge_dilate'] = 10
425        config['depth_edge_dilate_2'] = 5
426        config['largest_size'] = 512
427        config['repeat_inpaint_edge'] = True
428        config['ply_fmt'] = "bin"
429
430        config['save_ply'] = backbone.get_opt('depthmap_script_save_ply', False)
431        config['save_obj'] = True
432
433        if device == torch.device("cpu"):
434            config["gpu_ids"] = -1
435
436        for count in trange(0, len(img_rgb)):
437            basename = 'depthmap'
438            if inputnames is not None:
439                if inputnames[count] is not None:
440                    p = Path(inputnames[count])
441                    basename = p.stem
442
443            mesh_fi = get_uniquefn(outpath, basename, 'obj')
444
445            print(f"\nGenerating inpainted mesh .. (go make some coffee) ..")
446
447            # from inpaint.utils.get_MiDaS_samples
448            W = img_rgb[count].width
449            H = img_rgb[count].height
450            int_mtx = np.array([[max(H, W), 0, W // 2], [0, max(H, W), H // 2], [0, 0, 1]]).astype(np.float32)
451            if int_mtx.max() > 1:
452                int_mtx[0, :] = int_mtx[0, :] / float(W)
453                int_mtx[1, :] = int_mtx[1, :] / float(H)
454
455            # how inpaint.utils.read_MiDaS_depth() imports depthmap
456            disp = img_depth[count].astype(np.float32)
457            disp = disp - disp.min()
458            disp = cv2.blur(disp / disp.max(), ksize=(3, 3)) * disp.max()
459            disp = (disp / disp.max()) * 3.0
460            depth = 1. / np.maximum(disp, 0.05)
461
462            # rgb input
463            img = np.asarray(img_rgb[count])
464            if len(img.shape) > 2 and img.shape[2] == 4:
465                # convert the image from RGBA2RGB
466                img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
467
468            # run sparse bilateral filter
469            config['sparse_iter'] = 5
470            config['filter_size'] = [7, 7, 5, 5, 5]
471            config['sigma_s'] = 4.0
472            config['sigma_r'] = 0.5
473            vis_photos, vis_depths = sparse_bilateral_filtering(depth.copy(), img.copy(), config,
474                                                                num_iter=config['sparse_iter'], spdb=False)
475            depth = vis_depths[-1]
476
477            # bilat_fn = os.path.join(outpath, basename +'_bilatdepth.png')
478            # cv2.imwrite(bilat_fn, depth)
479
480            rt_info = write_mesh(img,
481                                 depth,
482                                 int_mtx,
483                                 mesh_fi,
484                                 config,
485                                 rgb_model,
486                                 depth_edge_model,
487                                 depth_edge_model,
488                                 depth_feat_model)
489
490            if rt_info is not False and gen_inpainted_mesh_demos:
491                run_3dphoto_videos(mesh_fi, basename, outpath, 300, 40,
492                                   [0.03, 0.03, 0.05, 0.03],
493                                   ['double-straight-line', 'double-straight-line', 'circle', 'circle'],
494                                   [0.00, 0.00, -0.015, -0.015],
495                                   [0.00, 0.00, -0.015, -0.00],
496                                   [-0.05, -0.05, -0.05, -0.05],
497                                   ['dolly-zoom-in', 'zoom-in', 'circle', 'swing'], False, vid_format, vid_ssaa)
498
499            backbone.torch_gc()
500
501    finally:
502        del rgb_model
503        rgb_model = None
504        del depth_edge_model
505        depth_edge_model = None
506        del depth_feat_model
507        depth_feat_model = None
508        backbone.torch_gc()
509
510    return mesh_fi
511
512
513def run_3dphoto_videos(mesh_fi, basename, outpath, num_frames, fps, crop_border, traj_types, x_shift_range,
514                       y_shift_range, z_shift_range, video_postfix, vid_dolly, vid_format, vid_ssaa):
515    import vispy
516    try:
517        if platform.system() == 'Windows':
518            vispy.use(app='PyQt5')
519        elif platform.system() == 'Darwin':
520            vispy.use('PyQt6')
521        else:
522            vispy.use(app='egl')
523    except:
524        import traceback
525        print(traceback.format_exc())
526        print('Trying an alternative...')
527        for u in ['PyQt5', 'PyQt6', 'egl']:
528            try:
529                vispy.use(app=u)
530                break
531            except:
532                print(f'On {u}')
533                print(traceback.format_exc())
534        # Honestly, I don't know if it actually helps at all
535
536    # read ply
537    global video_mesh_data, video_mesh_fn
538    if video_mesh_fn is None or video_mesh_fn != mesh_fi:
539        try:
540            del video_mesh_data
541        except:
542            print("del video_mesh_data failed")
543        video_mesh_fn = mesh_fi
544        video_mesh_data = read_mesh(mesh_fi)
545
546    verts, colors, faces, Height, Width, hFov, vFov, mean_loc_depth = video_mesh_data
547
548    original_w = output_w = W = Width
549    original_h = output_h = H = Height
550    int_mtx = np.array([[max(H, W), 0, W // 2], [0, max(H, W), H // 2], [0, 0, 1]]).astype(np.float32)
551    if int_mtx.max() > 1:
552        int_mtx[0, :] = int_mtx[0, :] / float(W)
553        int_mtx[1, :] = int_mtx[1, :] / float(H)
554
555    config = {}
556    config['video_folder'] = outpath
557    config['num_frames'] = num_frames
558    config['fps'] = fps
559    config['crop_border'] = crop_border
560    config['traj_types'] = traj_types
561    config['x_shift_range'] = x_shift_range
562    config['y_shift_range'] = y_shift_range
563    config['z_shift_range'] = z_shift_range
564    config['video_postfix'] = video_postfix
565    config['ssaa'] = vid_ssaa
566
567    # from inpaint.utils.get_MiDaS_samples
568    generic_pose = np.eye(4)
569    assert len(config['traj_types']) == len(config['x_shift_range']) == \
570           len(config['y_shift_range']) == len(config['z_shift_range']) == len(config['video_postfix']), \
571        "The number of elements in 'traj_types', 'x_shift_range', 'y_shift_range', 'z_shift_range' and \
572            'video_postfix' should be equal."
573    tgt_pose = [[generic_pose * 1]]
574    tgts_poses = []
575    for traj_idx in range(len(config['traj_types'])):
576        tgt_poses = []
577        sx, sy, sz = path_planning(config['num_frames'], config['x_shift_range'][traj_idx],
578                                   config['y_shift_range'][traj_idx],
579                                   config['z_shift_range'][traj_idx], path_type=config['traj_types'][traj_idx])
580        for xx, yy, zz in zip(sx, sy, sz):
581            tgt_poses.append(generic_pose * 1.)
582            tgt_poses[-1][:3, -1] = np.array([xx, yy, zz])
583        tgts_poses += [tgt_poses]
584    tgt_pose = generic_pose * 1
585
586    # seems we only need the depthmap to calc mean_loc_depth, which is only used when doing 'dolly'
587    # width and height are already in the ply file in the comments ..
588    # might try to add the mean_loc_depth to it too
589    # did just that
590    # mean_loc_depth = img_depth[img_depth.shape[0]//2, img_depth.shape[1]//2]
591
592    print("Generating videos ..")
593
594    normal_canvas, all_canvas = None, None
595    videos_poses, video_basename = copy.deepcopy(tgts_poses), basename
596    top = (original_h // 2 - int_mtx[1, 2] * output_h)
597    left = (original_w // 2 - int_mtx[0, 2] * output_w)
598    down, right = top + output_h, left + output_w
599    border = [int(xx) for xx in [top, down, left, right]]
600    normal_canvas, all_canvas, fn_saved = output_3d_photo(verts.copy(), colors.copy(), faces.copy(),
601                                                          copy.deepcopy(Height), copy.deepcopy(Width),
602                                                          copy.deepcopy(hFov), copy.deepcopy(vFov),
603                                                          copy.deepcopy(tgt_pose), config['video_postfix'],
604                                                          copy.deepcopy(generic_pose),
605                                                          copy.deepcopy(config['video_folder']),
606                                                          None, copy.deepcopy(int_mtx), config, None,
607                                                          videos_poses, video_basename, original_h, original_w,
608                                                          border=border, depth=None, normal_canvas=normal_canvas,
609                                                          all_canvas=all_canvas,
610                                                          mean_loc_depth=mean_loc_depth, dolly=vid_dolly,
611                                                          fnExt=vid_format)
612    return fn_saved
613
614def run_makevideo(fn_mesh, vid_numframes, vid_fps, vid_traj, vid_shift, vid_border, dolly, vid_format, vid_ssaa,
615                  outpath=None, basename=None):
616    if len(fn_mesh) == 0 or not os.path.exists(fn_mesh):
617        raise Exception("Could not open mesh.")
618
619    vid_ssaa = int(vid_ssaa)
620
621    # traj type
622    if vid_traj == 0:
623        vid_traj = ['straight-line']
624    elif vid_traj == 1:
625        vid_traj = ['double-straight-line']
626    elif vid_traj == 2:
627        vid_traj = ['circle']
628
629    num_fps = int(vid_fps)
630    num_frames = int(vid_numframes)
631    shifts = vid_shift.split(',')
632    if len(shifts) != 3:
633        raise Exception("Translate requires 3 elements.")
634    x_shift_range = [float(shifts[0])]
635    y_shift_range = [float(shifts[1])]
636    z_shift_range = [float(shifts[2])]
637
638    borders = vid_border.split(',')
639    if len(borders) != 4:
640        raise Exception("Crop Border requires 4 elements.")
641    crop_border = [float(borders[0]), float(borders[1]), float(borders[2]), float(borders[3])]
642
643    if not outpath:
644        outpath = backbone.get_outpath()
645
646    if not basename:
647        # output path and filename mess ..
648        basename = Path(fn_mesh).stem
649        
650        # unique filename
651        basecount = backbone.get_next_sequence_number(outpath, basename)
652        if basecount > 0: basecount = basecount - 1
653        fullfn = None
654        for i in range(500):
655            fn = f"{basecount + i:05}" if basename == '' else f"{basename}-{basecount + i:04}"
656            fullfn = os.path.join(outpath, f"{fn}_." + vid_format)
657            if not os.path.exists(fullfn):
658                break
659        basename = Path(fullfn).stem
660        basename = basename[:-1]
661
662    print("Loading mesh ..")
663
664    fn_saved = run_3dphoto_videos(fn_mesh, basename, outpath, num_frames, num_fps, crop_border, vid_traj, x_shift_range,
665                                  y_shift_range, z_shift_range, [''], dolly, vid_format, vid_ssaa)
666
667    return fn_saved[-1], fn_saved[-1], ''
668
669def unload_models():
670    model_holder.unload_models()
671
672
673# TODO: code borrowed from the internet to be marked as such and to reside in separate files
674
675def batched_background_removal(inimages, model_name):
676    from rembg import new_session, remove
677    print('creating background masks')
678    outimages = []
679
680    # model path and name
681    bg_model_dir = Path.joinpath(Path().resolve(), "models/rem_bg")
682    os.makedirs(bg_model_dir, exist_ok=True)
683    os.environ["U2NET_HOME"] = str(bg_model_dir)
684
685    # starting a session
686    background_removal_session = new_session(model_name)
687    for count in range(0, len(inimages)):
688        bg_remove_img = np.array(remove(inimages[count], session=background_removal_session))
689        outimages.append(Image.fromarray(bg_remove_img))
690    # The line below might be redundant
691    del background_removal_session
692    return outimages
693
694
695def pano_depth_to_world_points(depth):
696    """
697    360 depth to world points
698    given 2D depth is an equirectangular projection of a spherical image
699    Treat depth as radius
700    longitude : -pi to pi
701    latitude : -pi/2 to pi/2
702    """
703
704    # Convert depth to radius
705    radius = depth.flatten()
706
707    lon = np.linspace(-np.pi, np.pi, depth.shape[1])
708    lat = np.linspace(-np.pi / 2, np.pi / 2, depth.shape[0])
709
710    lon, lat = np.meshgrid(lon, lat)
711    lon = lon.flatten()
712    lat = lat.flatten()
713
714    # Convert to cartesian coordinates
715    x = radius * np.cos(lat) * np.cos(lon)
716    y = radius * np.cos(lat) * np.sin(lon)
717    z = radius * np.sin(lat)
718
719    pts3d = np.stack([x, y, z], axis=1)
720
721    return pts3d
722
723
724def depth_edges_mask(depth):
725    """Returns a mask of edges in the depth map.
726    Args:
727    depth: 2D numpy array of shape (H, W) with dtype float32.
728    Returns:
729    mask: 2D numpy array of shape (H, W) with dtype bool.
730    """
731    # Compute the x and y gradients of the depth map.
732    depth_dx, depth_dy = np.gradient(depth)
733    # Compute the gradient magnitude.
734    depth_grad = np.sqrt(depth_dx ** 2 + depth_dy ** 2)
735    # Compute the edge mask.
736    mask = depth_grad > 0.05
737    return mask
738
739
740def create_mesh(image, depth, keep_edges=False, spherical=False):
741    import trimesh
742    from dzoedepth.utils.geometry import depth_to_points, create_triangles
743    maxsize = backbone.get_opt('depthmap_script_mesh_maxsize', 2048)
744
745    # limit the size of the input image
746    image.thumbnail((maxsize, maxsize))
747
748    if not spherical:
749        pts3d = depth_to_points(depth[None])
750    else:
751        pts3d = pano_depth_to_world_points(depth)
752
753    pts3d = pts3d.reshape(-1, 3)
754
755    verts = pts3d.reshape(-1, 3)
756    image = np.array(image)
757    if keep_edges:
758        triangles = create_triangles(image.shape[0], image.shape[1])
759    else:
760        triangles = create_triangles(image.shape[0], image.shape[1], mask=~depth_edges_mask(depth))
761    colors = image.reshape(-1, 3)
762
763    mesh = trimesh.Trimesh(vertices=verts, faces=triangles, vertex_colors=colors)
764
765    # rotate 90deg over X when spherical
766    if spherical:
767        angle = math.pi / 2
768        direction = [1, 0, 0]
769        center = [0, 0, 0]
770        rot_matrix = trimesh.transformations.rotation_matrix(angle, direction, center)
771        mesh.apply_transform(rot_matrix)
772
773    return mesh
774