# -*- coding: utf-8 -*- import argparse import gradio import os import torch import numpy as np import tempfile import functools import trimesh import copy from scipy.spatial.transform import Rotation from dust3r.inference import inference, load_model from dust3r.image_pairs import make_pairs from dust3r.utils.image import load_images, rgb, resize_images from dust3r.utils.device import to_numpy from dust3r.viz import add_scene_cam, CAM_COLORS, OPENGL, pts3d_to_trimesh, cat_meshes from dust3r.cloud_opt import global_aligner, GlobalAlignerMode from SAM2.sam2.build_sam import build_sam2_video_predictor import matplotlib.pyplot as plt import shutil import json from PIL import Image import math import cv2 plt.ion() # 添加 sam2 模块路径 import sys sys.path.append(os.path.join(os.path.dirname(__file__), 'SAM2')) torch.backends.cuda.matmul.allow_tf32 = True # for gpu >= Ampere and pytorch >= 1.12 batch_size = 1 ########################## 引入grounding_dino ############################# from transformers import AutoProcessor, AutoModelForZeroShotObjectDetection def get_mask_from_grounding_dino(video_dir, ann_frame_idx, ann_obj_id, input_text): # init grounding dino model from huggingface model_id = "IDEA-Research/grounding-dino-tiny" device = "cuda" if torch.cuda.is_available() else "cpu" processor = AutoProcessor.from_pretrained(model_id) grounding_model = AutoModelForZeroShotObjectDetection.from_pretrained(model_id).to(device) # setup the input image and text prompt for SAM 2 and Grounding DINO # VERY important: text queries need to be lowercased + end with a dot """ Step 2: Prompt Grounding DINO and SAM image predictor to get the box and mask for specific frame """ # prompt grounding dino to get the box coordinates on specific frame frame_names = [ p for p in os.listdir(video_dir) if os.path.splitext(p)[-1] in [".jpg", ".jpeg", ".JPG", ".JPEG"] ] # frame_names.sort(key=lambda p: os.path.splitext(p)[0]) img_path = os.path.join(video_dir, frame_names[ann_frame_idx]) image = Image.open(img_path) # run Grounding DINO on the image inputs = processor(images=image, text=input_text, return_tensors="pt").to(device) with torch.no_grad(): outputs = grounding_model(**inputs) results = processor.post_process_grounded_object_detection( outputs, inputs.input_ids, box_threshold=0.25, text_threshold=0.3, target_sizes=[image.size[::-1]] ) return results[0]["boxes"], results[0]["labels"] def get_masks_from_grounded_sam2(h, w, predictor, video_dir, input_text): inference_state = predictor.init_state(video_path=video_dir) predictor.reset_state(inference_state) ann_frame_idx = 0 # the frame index we interact with ann_obj_id = 1 # give a unique id to each object we interact with (it can be any integers) print("Running Groundding DINO......") input_boxes, OBJECTS = get_mask_from_grounding_dino(video_dir, ann_frame_idx, ann_obj_id, input_text) print("Groundding DINO run over!") if(len(OBJECTS) < 1): raise gradio.Error("The images you input do not contain the target in '{}'".format(input_text)) # 给第一个帧输入由grounding_dino输出的boxes作为prompts for object_id, (label, box) in enumerate(zip(OBJECTS, input_boxes)): _, out_obj_ids, out_mask_logits = predictor.add_new_points_or_box( inference_state=inference_state, frame_idx=ann_frame_idx, obj_id=ann_obj_id, box=box, ) break #只加入第一个box # sam2获取所有帧的分割结果 video_segments = {} # video_segments contains the per-frame segmentation results for out_frame_idx, out_obj_ids, out_mask_logits in predictor.propagate_in_video(inference_state): video_segments[out_frame_idx] = { out_obj_id: (out_mask_logits[i] > 0.0).cpu().numpy() for i, out_obj_id in enumerate(out_obj_ids) } resize_mask = resize_mask_to_img(video_segments, w, h) return resize_mask def handle_uploaded_files(uploaded_files, target_folder): # 创建目标文件夹 if not os.path.exists(target_folder): os.makedirs(target_folder) # 遍历上传的文件,移动到目标文件夹 for file in uploaded_files: file_path = file.name # 文件的临时路径 file_name = os.path.basename(file_path) # 文件名 target_path = os.path.join(target_folder, file_name) shutil.copy2(file_path, target_path) print("copy images from {} to {}".format(file_path, target_path)) return target_folder def show_mask(mask, ax, random_color=False): if random_color: color = np.concatenate([np.random.random(3), np.array([0.6])], axis=0) else: color = np.array([30 / 255, 144 / 255, 255 / 255, 0.6]) h, w = mask.shape[-2:] mask_image = mask.reshape(h, w, 1) * color.reshape(1, 1, -1) ax.imshow(mask_image) def show_mask_sam2(mask, ax, obj_id=None, random_color=False): if random_color: color = np.concatenate([np.random.random(3), np.array([0.6])], axis=0) else: cmap = plt.get_cmap("tab10") cmap_idx = 0 if obj_id is None else obj_id color = np.array([*cmap(cmap_idx)[:3], 0.6]) h, w = mask.shape[-2:] mask_image = mask.reshape(h, w, 1) * color.reshape(1, 1, -1) ax.imshow(mask_image) def show_points(coords, labels, ax, marker_size=375): pos_points = coords[labels == 1] neg_points = coords[labels == 0] ax.scatter(pos_points[:, 0], pos_points[:, 1], color='green', marker='*', s=marker_size, edgecolor='white', linewidth=1.25) ax.scatter(neg_points[:, 0], neg_points[:, 1], color='red', marker='*', s=marker_size, edgecolor='white', linewidth=1.25) def show_box(box, ax): x0, y0 = box[0], box[1] w, h = box[2] - box[0], box[3] - box[1] ax.add_patch(plt.Rectangle((x0, y0), w, h, edgecolor='green', facecolor=(0, 0, 0, 0), lw=2)) def get_args_parser(): parser = argparse.ArgumentParser() parser_url = parser.add_mutually_exclusive_group() parser_url.add_argument("--local_network", action='store_true', default=False, help="make app accessible on local network: address will be set to 0.0.0.0") parser_url.add_argument("--server_name", type=str, default="0.0.0.0", help="server url, default is 127.0.0.1") parser.add_argument("--image_size", type=int, default=512, choices=[512, 224], help="image size") parser.add_argument("--server_port", type=int, help=("will start gradio app on this port (if available). " "If None, will search for an available port starting at 7860."), default=None) parser.add_argument("--weights", type=str, default="./checkpoints/DUSt3R_ViTLarge_BaseDecoder_512_dpt.pth", required=False, help="path to the model weights") parser.add_argument("--device", type=str, default='cpu', help="pytorch device") parser.add_argument("--tmp_dir", type=str, default="./", help="value for tempfile.tempdir") return parser # 将渲染的3D保存到outfile路径 def _convert_scene_output_to_glb(outdir, imgs, pts3d, mask, focals, cams2world, cam_size=0.05, cam_color=None, as_pointcloud=False, transparent_cams=False): assert len(pts3d) == len(mask) <= len(imgs) <= len(cams2world) == len(focals) pts3d = to_numpy(pts3d) imgs = to_numpy(imgs) focals = to_numpy(focals) cams2world = to_numpy(cams2world) scene = trimesh.Scene() # full pointcloud if as_pointcloud: pts = np.concatenate([p[m] for p, m in zip(pts3d, mask)]) col = np.concatenate([p[m] for p, m in zip(imgs, mask)]) pct = trimesh.PointCloud(pts.reshape(-1, 3), colors=col.reshape(-1, 3)) scene.add_geometry(pct) else: meshes = [] for i in range(len(imgs)): meshes.append(pts3d_to_trimesh(imgs[i], pts3d[i], mask[i])) mesh = trimesh.Trimesh(**cat_meshes(meshes)) scene.add_geometry(mesh) # add each camera for i, pose_c2w in enumerate(cams2world): if isinstance(cam_color, list): camera_edge_color = cam_color[i] else: camera_edge_color = cam_color or CAM_COLORS[i % len(CAM_COLORS)] add_scene_cam(scene, pose_c2w, camera_edge_color, None if transparent_cams else imgs[i], focals[i], imsize=imgs[i].shape[1::-1], screen_width=cam_size) rot = np.eye(4) rot[:3, :3] = Rotation.from_euler('y', np.deg2rad(180)).as_matrix() scene.apply_transform(np.linalg.inv(cams2world[0] @ OPENGL @ rot)) outfile = os.path.join(outdir, 'scene.glb') print('(exporting 3D scene to', outfile, ')') scene.export(file_obj=outfile) return outfile def get_3D_model_from_scene(outdir, scene, sam2_masks, min_conf_thr=3, as_pointcloud=False, mask_sky=False, clean_depth=False, transparent_cams=False, cam_size=0.05): """ extract 3D_model (glb file) from a reconstructed scene """ if scene is None: return None # post processes if clean_depth: scene = scene.clean_pointcloud() if mask_sky: scene = scene.mask_sky() # get optimized values from scene rgbimg = scene.imgs focals = scene.get_focals().cpu() cams2world = scene.get_im_poses().cpu() # 3D pointcloud from depthmap, poses and intrinsics pts3d = to_numpy(scene.get_pts3d()) scene.min_conf_thr = float(scene.conf_trf(torch.tensor(min_conf_thr))) msk = to_numpy(scene.get_masks()) assert len(msk) == len(sam2_masks) # 将sam2输出的mask 和 dust3r输出的置信度阈值筛选后的msk取交集 for i in range(len(sam2_masks)): msk[i] = np.logical_and(msk[i], sam2_masks[i]) return _convert_scene_output_to_glb(outdir, rgbimg, pts3d, msk, focals, cams2world, as_pointcloud=as_pointcloud, transparent_cams=transparent_cams, cam_size=cam_size) # 置信度和SAM2 mask的交集 # 将视频分割成固定帧数 def video_to_frames_fix(video_path, output_folder, frame_interval=10, target_fps=6): """ 将视频转换为图像帧,并保存为 JPEG 文件。 frame_interval:保存帧的步长 target_fps: 目标帧率(每秒保存的帧数) """ # 确保输出文件夹存在 if not os.path.exists(output_folder): os.makedirs(output_folder) # 打开视频文件 cap = cv2.VideoCapture(video_path) # 获取视频总帧数 frames_num = cap.get(cv2.CAP_PROP_FRAME_COUNT) # 计算动态帧间隔 frame_interval = math.ceil(frames_num / target_fps) print(f"总帧数: {frames_num} FPS, 动态帧间隔: 每隔 {frame_interval} 帧保存一次.") frame_count = 0 saved_frame_count = 0 success, frame = cap.read() file_list = [] # 逐帧读取视频 while success: if frame_count % frame_interval == 0: # 每隔 frame_interval 帧保存一次 frame_filename = os.path.join(output_folder, f"frame_{saved_frame_count:04d}.jpg") cv2.imwrite(frame_filename, frame) file_list.append(frame_filename) saved_frame_count += 1 frame_count += 1 success, frame = cap.read() # 释放视频捕获对象 cap.release() print(f"视频处理完成,共保存了 {saved_frame_count} 帧到文件夹 '{output_folder}'.") return file_list def video_to_frames(video_path, output_folder, frame_interval=10, target_fps = 2): """ 将视频转换为图像帧,并保存为 JPEG 文件。 frame_interval:保存帧的步长 target_fps: 目标帧率(每秒保存的帧数) """ # 确保输出文件夹存在 if not os.path.exists(output_folder): os.makedirs(output_folder) # 打开视频文件 cap = cv2.VideoCapture(video_path) # 获取视频的实际帧率 actual_fps = cap.get(cv2.CAP_PROP_FPS) # 获取视频总帧数 frames_num = cap.get(cv2.CAP_PROP_FRAME_COUNT) # 计算动态帧间隔 # frame_interval = math.ceil(actual_fps / target_fps) print(f"实际帧率: {actual_fps} FPS, 动态帧间隔: 每隔 {frame_interval} 帧保存一次.") frame_count = 0 saved_frame_count = 0 success, frame = cap.read() file_list = [] # 逐帧读取视频 while success: if frame_count % frame_interval == 0: # 每隔 frame_interval 帧保存一次 frame_filename = os.path.join(output_folder, f"frame_{saved_frame_count:04d}.jpg") cv2.imwrite(frame_filename, frame) file_list.append(frame_filename) saved_frame_count += 1 frame_count += 1 success, frame = cap.read() # 释放视频捕获对象 cap.release() print(f"视频处理完成,共保存了 {saved_frame_count} 帧到文件夹 '{output_folder}'.") return file_list def overlay_mask_on_image(image, mask, color=[0, 1, 0], alpha=0.5): """ 将mask融合在image上显示。 返回融合后的图片 (H, W, 3) """ # 创建一个与image相同尺寸的全黑图像 mask_colored = np.zeros_like(image) # 将mask为True的位置赋值为指定颜色 mask_colored[mask] = color # 将彩色掩码与原图像叠加 overlay = cv2.addWeighted(image, 1 - alpha, mask_colored, alpha, 0) return overlay def get_reconstructed_video(sam2, outdir, model, device, image_size, image_mask, video_dir, schedule, niter, min_conf_thr, as_pointcloud, mask_sky, clean_depth, transparent_cams, cam_size, scenegraph_type, winsize, refid, input_text): target_dir = os.path.join(outdir, 'frames_video') file_list = video_to_frames_fix(video_dir, target_dir) scene, outfile, imgs = get_reconstructed_scene(sam2, outdir, model, device, image_size, image_mask, file_list, schedule, niter, min_conf_thr, as_pointcloud, mask_sky, clean_depth, transparent_cams, cam_size, scenegraph_type, winsize, refid, target_dir, input_text) return scene, outfile, imgs def get_reconstructed_image(sam2, outdir, model, device, image_size, image_mask, filelist, schedule, niter, min_conf_thr, as_pointcloud, mask_sky, clean_depth, transparent_cams, cam_size, scenegraph_type, winsize, refid, input_text): target_folder = handle_uploaded_files(filelist, os.path.join(outdir, 'uploaded_images')) scene, outfile, imgs = get_reconstructed_scene(sam2, outdir, model, device, image_size, image_mask, filelist, schedule, niter, min_conf_thr, as_pointcloud, mask_sky, clean_depth, transparent_cams, cam_size, scenegraph_type, winsize, refid, target_folder, input_text) return scene, outfile, imgs def get_reconstructed_scene(sam2, outdir, model, device, image_size, image_mask, filelist, schedule, niter, min_conf_thr, as_pointcloud, mask_sky, clean_depth, transparent_cams, cam_size, scenegraph_type, winsize, refid, images_folder, input_text=None): """ from a list of images, run dust3rWithSam2 inference, global aligner. then run get_3D_model_from_scene """ imgs = load_images(filelist, size=image_size) img_size = imgs[0]["true_shape"] for img in imgs[1:]: if not np.equal(img["true_shape"], img_size).all(): raise gradio.Error("Please ensure that the images you enter are of the same size") if len(imgs) == 1: imgs = [imgs[0], copy.deepcopy(imgs[0])] imgs[1]['idx'] = 1 if scenegraph_type == "swin": scenegraph_type = scenegraph_type + "-" + str(winsize) elif scenegraph_type == "oneref": scenegraph_type = scenegraph_type + "-" + str(refid) pairs = make_pairs(imgs, scene_graph=scenegraph_type, prefilter=None, symmetrize=True) output = inference(pairs, model, device, batch_size=batch_size) mode = GlobalAlignerMode.PointCloudOptimizer if len(imgs) > 2 else GlobalAlignerMode.PairViewer scene = global_aligner(output, device=device, mode=mode) lr = 0.01 if mode == GlobalAlignerMode.PointCloudOptimizer: loss = scene.compute_global_alignment(init='mst', niter=niter, schedule=schedule, lr=lr) # also return rgb, depth and confidence imgs # depth is normalized with the max value for all images # we apply the jet colormap on the confidence maps rgbimg = scene.imgs depths = to_numpy(scene.get_depthmaps()) confs = to_numpy([c for c in scene.im_conf]) cmap = plt.get_cmap('jet') depths_max = max([d.max() for d in depths]) depths = [d / depths_max for d in depths] confs_max = max([d.max() for d in confs]) confs = [cmap(d / confs_max) for d in confs] # TODO 调用SAM2获取masks h, w = rgbimg[0].shape[:-1] masks = None if not input_text or input_text.isspace(): # input_text 为空串 masks = get_masks_from_sam2(h, w, sam2, images_folder) else: masks = get_masks_from_grounded_sam2(h, w, sam2, images_folder, input_text) # gd-sam2 imgs = [] for i in range(len(rgbimg)): imgs.append(rgbimg[i]) imgs.append(rgb(depths[i])) imgs.append(rgb(confs[i])) imgs.append(overlay_mask_on_image(rgbimg[i], masks[i])) # mask融合原图,展示SAM2的分割效果 # TODO 基于SAM2的mask过滤DUST3R的3D重建模型 outfile = get_3D_model_from_scene(outdir, scene, masks, min_conf_thr, as_pointcloud, mask_sky, clean_depth, transparent_cams, cam_size) return scene, outfile, imgs def resize_mask_to_img(masks, target_width, target_height): frame_mask = [] origin_size = masks[0][1].shape # 1表示object id for frame, objects_mask in masks.items(): # 每个frame和该frame对应的分割结果 # 每个frame可能包含多个object对应的mask masks = list(objects_mask.values()) if not masks: # masks为空,即当前frame不包含object frame_mask.append(np.ones(origin_size, dtype=bool)) else: # 将当前frame包含的所有object的mask取并集 union_mask = masks[0] for mask in masks[1:]: union_mask = np.logical_or(union_mask, mask) frame_mask.append(union_mask) resized_mask = [] for mask in frame_mask: mask_image = Image.fromarray(mask.squeeze(0).astype(np.uint8) * 255) resized_mask_image = mask_image.resize((target_width, target_height), Image.NEAREST) resized_mask.append(np.array(resized_mask_image) > 0) return resized_mask def get_masks_from_sam2(h, w, predictor, video_dir): inference_state = predictor.init_state(video_path=video_dir) predictor.reset_state(inference_state) # 给一个帧添加points points = np.array([[360, 550], [340, 400]], dtype=np.float32) labels = np.array([1, 1], dtype=np.int32) _, out_obj_ids, out_mask_logits = predictor.add_new_points( inference_state=inference_state, frame_idx=0, obj_id=1, points=points, labels=labels, ) # sam2获取所有帧的分割结果 video_segments = {} # video_segments contains the per-frame segmentation results for out_frame_idx, out_obj_ids, out_mask_logits in predictor.propagate_in_video(inference_state): video_segments[out_frame_idx] = { out_obj_id: (out_mask_logits[i] > 0.0).cpu().numpy() for i, out_obj_id in enumerate(out_obj_ids) } resize_mask = resize_mask_to_img(video_segments, w, h) return resize_mask def set_scenegraph_options(inputfiles, winsize, refid, scenegraph_type): num_files = len(inputfiles) if inputfiles is not None else 1 max_winsize = max(1, (num_files - 1) // 2) if scenegraph_type == "swin": winsize = gradio.Slider(label="Scene Graph: Window Size", value=max_winsize, minimum=1, maximum=max_winsize, step=1, visible=True) refid = gradio.Slider(label="Scene Graph: Id", value=0, minimum=0, maximum=num_files - 1, step=1, visible=False) elif scenegraph_type == "oneref": winsize = gradio.Slider(label="Scene Graph: Window Size", value=max_winsize, minimum=1, maximum=max_winsize, step=1, visible=False) refid = gradio.Slider(label="Scene Graph: Id", value=0, minimum=0, maximum=num_files - 1, step=1, visible=True) else: winsize = gradio.Slider(label="Scene Graph: Window Size", value=max_winsize, minimum=1, maximum=max_winsize, step=1, visible=False) refid = gradio.Slider(label="Scene Graph: Id", value=0, minimum=0, maximum=num_files - 1, step=1, visible=False) return winsize, refid def process_images(imagesList): return None def process_videos(video): return None def upload_images_listener(image_size, file_list): if len(file_list) == 1: raise gradio.Error("Please enter images from at least two different views.") print("Uploading image[0] to ImageMask:") img_0 = load_images([file_list[0]], image_size) i1 = img_0[0]['img'].squeeze(0) rgb_img = rgb(i1) return rgb_img def upload_video_listener(image_size, video_dir): cap = cv2.VideoCapture(video_dir) success, frame = cap.read() # 第一帧 rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) Image_frame = Image.fromarray(rgb_frame) resized_frame = resize_images([Image_frame], image_size) i1 = resized_frame[0]['img'].squeeze(0) rgb_img = rgb(i1) return rgb_img def main_demo(sam2, tmpdirname, model, device, image_size, server_name, server_port): # functools.partial解析:https://blog.csdn.net/wuShiJingZuo/article/details/135018810 recon_fun_image_demo = functools.partial(get_reconstructed_image,sam2, tmpdirname, model, device, image_size) recon_fun_video_demo = functools.partial(get_reconstructed_video, sam2, tmpdirname, model, device, image_size) upload_files_fun = functools.partial(upload_images_listener,image_size) upload_video_fun = functools.partial(upload_video_listener, image_size) with gradio.Blocks() as demo1: scene = gradio.State(None) gradio.HTML('
\
DUST3R+SAM2: One touch for any segmentation in a video.
\
Choose an example below 🔥 🔥 🔥
\
Or, upload by yourself:
\
1. Upload a video to be tested to 'video'. If failed, please check the codec, we recommend h.264 by default.
2. Upload a prompt image to 'prompt' and draw a point or line on the target.
\
\
💎 SAM segments the target with any point or scribble, then SegGPT segments the whole video.
\
💎 Examples below were never trained and are randomly selected for testing in the wild.
\
💎 Current UI interface only unleashes a small part of the capabilities of SegGPT, i.e., 1-shot case.
\
Note: we only take the first 16 frames for the demo. \
\
DUST3R+SAM2: One touch for any segmentation in a video.
\
Choose an example below 🔥 🔥 🔥
\
Or, upload by yourself:
\
1. Upload a video to be tested to 'video'. If failed, please check the codec, we recommend h.264 by default.
2. Upload a prompt image to 'prompt' and draw a point or line on the target.
\
\
💎 SAM segments the target with any point or scribble, then SegGPT segments the whole video.
\
💎 Examples below were never trained and are randomly selected for testing in the wild.
\
💎 Current UI interface only unleashes a small part of the capabilities of SegGPT, i.e., 1-shot case.
\
Note: we only take the first 16 frames for the demo. \