import os import tyro import imageio import numpy as np import tqdm import torch import torch.nn as nn import torch.nn.functional as F import torchvision.transforms.functional as TF from safetensors.torch import load_file import rembg import gradio as gr import kiui from kiui.op import recenter from kiui.cam import orbit_camera from core.utils import get_rays, grid_distortion, orbit_camera_jitter from core.options import AllConfigs, Options from core.models import LTRFM_Mesh,LTRFM_NeRF from core.instant_utils.mesh_util import save_obj, save_obj_with_mtl from mvdream.pipeline_mvdream import MVDreamPipeline from diffusers import DiffusionPipeline, EulerAncestralDiscreteScheduler from huggingface_hub import hf_hub_download import spaces IMAGENET_DEFAULT_MEAN = (0.485, 0.456, 0.406) IMAGENET_DEFAULT_STD = (0.229, 0.224, 0.225) GRADIO_VIDEO_PATH = 'gradio_output.mp4' GRADIO_OBJ_PATH = 'gradio_output_rgb.obj' GRADIO_OBJ_ALBEDO_PATH = 'gradio_output_albedo.obj' GRADIO_OBJ_SHADING_PATH = 'gradio_output_shading.obj' #opt = tyro.cli(AllConfigs) ckpt_path = hf_hub_download(repo_id="rgxie/LDM", filename="LDM_6V_SDF.ckpt") opt = Options( input_size=512, down_channels=(32, 64, 128, 256, 512), down_attention=(False, False, False, False, True), up_channels=(512, 256, 128), up_attention=(True, False, False, False), volume_mode='TRF_NeRF', splat_size=64, output_size=62, #crop patch data_mode='s5', num_views=8, gradient_accumulation_steps=1, #2 mixed_precision='bf16', resume=ckpt_path, ) # model if opt.volume_mode == 'TRF_Mesh': model = LTRFM_Mesh(opt) elif opt.volume_mode == 'TRF_NeRF': model = LTRFM_NeRF(opt) else: model = LGM(opt) # resume pretrained checkpoint if opt.resume is not None: if opt.resume.endswith('safetensors'): ckpt = load_file(opt.resume, device='cpu') else: #ckpt ckpt_dict = torch.load(opt.resume, map_location='cpu') ckpt=ckpt_dict["model"] state_dict = model.state_dict() for k, v in ckpt.items(): k=k.replace('module.', '') if k in state_dict: if state_dict[k].shape == v.shape: state_dict[k].copy_(v) else: print(f'[WARN] mismatching shape for param {k}: ckpt {v.shape} != model {state_dict[k].shape}, ignored.') else: print(f'[WARN] unexpected param {k}: {v.shape}') print(f'[INFO] load resume success!') device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.half().to(device) model.eval() tan_half_fov = np.tan(0.5 * np.deg2rad(opt.fovy)) proj_matrix = torch.zeros(4, 4, dtype=torch.float32).to(device) proj_matrix[0, 0] = 1 / tan_half_fov proj_matrix[1, 1] = 1 / tan_half_fov proj_matrix[2, 2] = (opt.zfar + opt.znear) / (opt.zfar - opt.znear) proj_matrix[3, 2] = - (opt.zfar * opt.znear) / (opt.zfar - opt.znear) proj_matrix[2, 3] = 1 # load dreams pipe_text = MVDreamPipeline.from_pretrained( 'ashawkey/mvdream-sd2.1-diffusers', # remote weights torch_dtype=torch.float16, trust_remote_code=True, # local_files_only=True, ) pipe_text = pipe_text.to(device) # mvdream pipe_image = MVDreamPipeline.from_pretrained( "ashawkey/imagedream-ipmv-diffusers", # remote weights torch_dtype=torch.float16, trust_remote_code=True, # local_files_only=True, ) pipe_image = pipe_image.to(device) print('Loading 123plus model ...') pipe_image_plus = DiffusionPipeline.from_pretrained( "sudo-ai/zero123plus-v1.2", custom_pipeline="zero123plus", torch_dtype=torch.float16, trust_remote_code=True, #local_files_only=True, ) pipe_image_plus.scheduler = EulerAncestralDiscreteScheduler.from_config( pipe_image_plus.scheduler.config, timestep_spacing='trailing' ) unet_path='./pretrained/diffusion_pytorch_model.bin' print('Loading custom white-background unet ...') if os.path.exists(unet_path): unet_ckpt_path = unet_path else: unet_ckpt_path = hf_hub_download(repo_id="TencentARC/InstantMesh", filename="diffusion_pytorch_model.bin", repo_type="model") state_dict = torch.load(unet_ckpt_path, map_location='cpu') pipe_image_plus.unet.load_state_dict(state_dict, strict=True) pipe_image_plus = pipe_image_plus.to(device) # load rembg bg_remover = rembg.new_session() @spaces.GPU def generate_mv(condition_input_image, prompt, prompt_neg='', input_elevation=0, input_num_steps=30, input_seed=42, mv_moedl_option=None): # seed kiui.seed_everything(input_seed) os.makedirs(os.path.join(opt.workspace, "gradio"), exist_ok=True) # text-conditioned if condition_input_image is None: mv_image_uint8 = pipe_text(prompt, negative_prompt=prompt_neg, num_inference_steps=input_num_steps, guidance_scale=7.5, elevation=input_elevation) mv_image_uint8 = (mv_image_uint8 * 255).astype(np.uint8) # bg removal mv_image = [] for i in range(4): image = rembg.remove(mv_image_uint8[i], session=bg_remover) # [H, W, 4] # to white bg image = image.astype(np.float32) / 255 image = recenter(image, image[..., 0] > 0, border_ratio=0.2) image = image[..., :3] * image[..., -1:] + (1 - image[..., -1:]) mv_image.append(image) mv_image_grid = np.concatenate([mv_image[1], mv_image[2],mv_image[3], mv_image[0]],axis=1) input_image = np.stack([mv_image[1], mv_image[2], mv_image[3], mv_image[0]], axis=0) processed_image=None # image-conditioned (may also input text, but no text usually works too) else: condition_input_image = np.array(condition_input_image) # uint8 # bg removal carved_image = rembg.remove(condition_input_image, session=bg_remover) # [H, W, 4] mask = carved_image[..., -1] > 0 image = recenter(carved_image, mask, border_ratio=0.2) image = image.astype(np.float32) / 255.0 processed_image = image[..., :3] * image[..., 3:4] + (1 - image[..., 3:4]) if mv_moedl_option=='mvdream': mv_image = pipe_image(prompt, processed_image, negative_prompt=prompt_neg, num_inference_steps=input_num_steps, guidance_scale=5.0, elevation=input_elevation) mv_image_grid = np.concatenate([mv_image[1], mv_image[2],mv_image[3], mv_image[0]],axis=1) input_image = np.stack([mv_image[1], mv_image[2], mv_image[3], mv_image[0]], axis=0) else: from PIL import Image from einops import rearrange, repeat # input_image=input_image* 255 processed_image = Image.fromarray((processed_image * 255).astype(np.uint8)) mv_image = pipe_image_plus(processed_image, num_inference_steps=input_num_steps).images[0] mv_image = np.asarray(mv_image, dtype=np.float32) / 255.0 mv_image = torch.from_numpy(mv_image).permute(2, 0, 1).contiguous().float() # (3, 960, 640) mv_image_grid = rearrange(mv_image, 'c (n h) (m w) -> (m h) (n w) c', n=3, m=2).numpy() mv_image = rearrange(mv_image, 'c (n h) (m w) -> (n m) h w c', n=3, m=2).numpy() input_image = mv_image return mv_image_grid, processed_image, input_image @spaces.GPU def generate_3d(input_image, condition_input_image, mv_moedl_option=None, input_seed=42): kiui.seed_everything(input_seed) output_obj_rgb_path = os.path.join(opt.workspace,"gradio", GRADIO_OBJ_PATH) output_obj_albedo_path = os.path.join(opt.workspace,"gradio", GRADIO_OBJ_ALBEDO_PATH) output_obj_shading_path = os.path.join(opt.workspace,"gradio", GRADIO_OBJ_SHADING_PATH) output_video_path = os.path.join(opt.workspace,"gradio", GRADIO_VIDEO_PATH) # generate gaussians # [4, 256, 256, 3], float32 input_image = torch.from_numpy(input_image).permute(0, 3, 1, 2).float().to(device) # [4, 3, 256, 256] input_image = F.interpolate(input_image, size=(opt.input_size, opt.input_size), mode='bilinear', align_corners=False) images_input_vit = F.interpolate(input_image, size=(224, 224), mode='bilinear', align_corners=False) data = {} input_image = input_image.unsqueeze(0) # [1, 4, 9, H, W] images_input_vit=images_input_vit.unsqueeze(0) data['input_vit']=images_input_vit elevation = 0 cam_poses =[] if mv_moedl_option=='mvdream' or condition_input_image is None: azimuth = np.arange(0, 360, 90, dtype=np.int32) for azi in tqdm.tqdm(azimuth): cam_pose = torch.from_numpy(orbit_camera(elevation, azi, radius=opt.cam_radius, opengl=True)).unsqueeze(0).to(device) cam_poses.append(cam_pose) else: azimuth = np.arange(30, 360, 60, dtype=np.int32) cnt = 0 for azi in tqdm.tqdm(azimuth): if (cnt+1) % 2!= 0: elevation=-20 else: elevation=30 cam_pose = torch.from_numpy(orbit_camera(elevation, azi, radius=opt.cam_radius, opengl=True)).unsqueeze(0).to(device) cam_poses.append(cam_pose) cnt=cnt+1 cam_poses = torch.cat(cam_poses,0) radius = torch.norm(cam_poses[0, :3, 3]) cam_poses[:, :3, 3] *= opt.cam_radius / radius transform = torch.tensor([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, opt.cam_radius], [0, 0, 0, 1]], dtype=torch.float32).to(device) @ torch.inverse(cam_poses[0]) cam_poses = transform.unsqueeze(0) @ cam_poses cam_poses=cam_poses.unsqueeze(0) data['source_camera']=cam_poses with torch.no_grad(): if opt.volume_mode == 'TRF_Mesh': with torch.autocast(device_type='cuda', dtype=torch.float32): svd_volume = model.forward_svd_volume(input_image,data) else: with torch.autocast(device_type='cuda', dtype=torch.float16): svd_volume = model.forward_svd_volume(input_image,data) #time-consuming export_texmap=False mesh_out = model.extract_mesh(svd_volume,use_texture_map=export_texmap) if export_texmap: vertices, faces, uvs, mesh_tex_idx, tex_map = mesh_out for i in range(len(tex_map)): mesh_path=os.path.join(opt.workspace, name + str(i) + '_'+ str(seed)+ '.obj') save_obj_with_mtl( vertices.data.cpu().numpy(), uvs.data.cpu().numpy(), faces.data.cpu().numpy(), mesh_tex_idx.data.cpu().numpy(), tex_map[i].permute(1, 2, 0).data.cpu().numpy(), mesh_path, ) else: vertices, faces, vertex_colors = mesh_out save_obj(vertices, faces, vertex_colors[0], output_obj_rgb_path) save_obj(vertices, faces, vertex_colors[1], output_obj_albedo_path) save_obj(vertices, faces, vertex_colors[2], output_obj_shading_path) # images=[] # azimuth = np.arange(0, 360, 6, dtype=np.int32) # for azi in tqdm.tqdm(azimuth): # cam_pose = torch.from_numpy(orbit_camera(elevation, azi, radius=opt.cam_radius, opengl=True)) # if opt.volume_mode == 'TRF_Mesh': # cam_view = torch.inverse(cam_pose) # cam_view=cam_view.unsqueeze(0).unsqueeze(0).to(device) # data['w2c'] = cam_view # with torch.autocast(device_type='cuda', dtype=torch.float32): # render_images=model.render_frame(data) # else: # rays_o, rays_d = get_rays(cam_pose, opt.infer_render_size, opt.infer_render_size, opt.fovy) # [h, w, 3] # rays_o=rays_o.unsqueeze(0).unsqueeze(0).to(device)# B,V,H,W,3 # rays_d=rays_d.unsqueeze(0).unsqueeze(0).to(device) # data['all_rays_o']=rays_o # data['all_rays_d']=rays_d # with torch.autocast(device_type='cuda', dtype=torch.float16): # render_images=model.render_frame(data) # image=render_images['images_pred'] # images.append((image.squeeze(1).permute(0,2,3,1).contiguous().float().cpu().numpy() * 255).astype(np.uint8)) # images = np.concatenate(images, axis=0) # imageio.mimwrite(output_video_path, images, fps=30) return output_obj_rgb_path, output_obj_albedo_path, output_obj_shading_path #, output_video_path # gradio UI _TITLE = '''LDM: Large Tensorial SDF Model for Textured Mesh Generation''' _DESCRIPTION = ''' * Input can be text prompt, image. * The currently supported multi-view diffusion models include the image-conditioned MVdream and Zero123plus, as well as the text-conditioned Imagedream. * If you find the output unsatisfying, try using different multi-view diffusion models or seeds! * The project code is available at [https://github.com/rgxie/LDM](https://github.com/rgxie/LDM). ''' block = gr.Blocks(title=_TITLE).queue() with block: with gr.Row(): with gr.Column(scale=1): gr.Markdown('# ' + _TITLE) gr.Markdown(_DESCRIPTION) with gr.Row(variant='panel'): with gr.Column(scale=1): with gr.Tab("Image-to-3D"): # input image with gr.Row(): condition_input_image = gr.Image( label="Input Image", image_mode="RGBA", type="pil" ) processed_image = gr.Image( label="Processed Image", image_mode="RGBA", type="pil", interactive=False ) with gr.Row(): mv_moedl_option = gr.Radio([ "zero123plus", "mvdream" ], value="zero123plus", label="Multi-view Diffusion") with gr.Row(variant="panel"): gr.Examples( examples=[ os.path.join("example", img_name) for img_name in sorted(os.listdir("example")) ], inputs=[condition_input_image], fn=lambda x: process(condition_input_image=x, prompt=''), cache_examples=False, examples_per_page=20, label='Image-to-3D Examples' ) with gr.Tab("Text-to-3D"): # input prompt with gr.Row(): input_text = gr.Textbox(label="prompt") # negative prompt with gr.Row(): input_neg_text = gr.Textbox(label="negative prompt", value='ugly, blurry, pixelated obscure, unnatural colors, poor lighting, dull, unclear, cropped, lowres, low quality, artifacts, duplicate') with gr.Row(variant="panel"): gr.Examples( examples=[ "a hamburger", "a furry red fox head", "a teddy bear", "a motorbike", ], inputs=[input_text], fn=lambda x: process(condition_input_image=None, prompt=x), cache_examples=False, label='Text-to-3D Examples' ) # elevation input_elevation = gr.Slider(label="elevation", minimum=-90, maximum=90, step=1, value=0) # inference steps input_num_steps = gr.Slider(label="inference steps", minimum=1, maximum=100, step=1, value=30) # random seed input_seed = gr.Slider(label="random seed", minimum=0, maximum=100000, step=1, value=0) # gen button button_gen = gr.Button("Generate") with gr.Column(scale=1): with gr.Row(): # multi-view results mv_image_grid = gr.Image(interactive=False, show_label=False) # with gr.Row(): # output_video_path = gr.Video(label="video") with gr.Row(): output_obj_rgb_path = gr.Model3D( label="RGB Model (OBJ Format)", interactive=False, ) with gr.Row(): output_obj_albedo_path = gr.Model3D( label="Albedo Model (OBJ Format)", interactive=False, ) with gr.Row(): output_obj_shading_path = gr.Model3D( label="Shading Model (OBJ Format)", interactive=False, ) input_image = gr.State() button_gen.click(fn=generate_mv, inputs=[condition_input_image, input_text, input_neg_text, input_elevation, input_num_steps, input_seed, mv_moedl_option], outputs=[mv_image_grid, processed_image, input_image],).success( fn=generate_3d, inputs=[input_image, condition_input_image, mv_moedl_option, input_seed], outputs=[output_obj_rgb_path, output_obj_albedo_path, output_obj_shading_path] , #output_video_path ) block.launch(server_name="0.0.0.0", share=False)