217 lines
9.2 KiB
Markdown
217 lines
9.2 KiB
Markdown
|
|
# Documentation
|
|
- Class name: CameraPoseVisualizer
|
|
- Category: KJNodes/misc
|
|
- Output node: False
|
|
- Repo Ref: https://github.com/comfyanonymous/ComfyUI
|
|
|
|
CameraPoseVisualizer节点用于在3D图表中可视化相机姿态。它可以从指定的包含相机内参和坐标的文件中读取数据,或直接使用提供的相机控制姿态。该节点支持调整可视化比例、基准值,并可选择是否使用精确焦距或相对的相机到世界坐标变换,从而增强对给定空间中相机位置和方向的理解。
|
|
|
|
# Input types
|
|
## Required
|
|
- pose_file_path
|
|
- 指定包含相机姿态的文件路径,如果直接提供相机控制姿态,则为空字符串。此参数对确定要可视化的相机姿态来源至关重要。
|
|
- Comfy dtype: STRING
|
|
- Python dtype: str
|
|
- base_xval
|
|
- 确定相机金字塔可视化的基准x值,影响图表中相机表示的定位。
|
|
- Comfy dtype: FLOAT
|
|
- Python dtype: float
|
|
- zval
|
|
- 设置相机金字塔可视化的z值,影响图表中相机表示的深度定位。
|
|
- Comfy dtype: FLOAT
|
|
- Python dtype: float
|
|
- scale
|
|
- 调整整个可视化的比例,允许放大或缩小场景。
|
|
- Comfy dtype: FLOAT
|
|
- Python dtype: float
|
|
- use_exact_fx
|
|
- 布尔标志,决定是否使用相机姿态中的精确焦距或默认值,影响可视化的准确性。
|
|
- Comfy dtype: BOOLEAN
|
|
- Python dtype: bool
|
|
- relative_c2w
|
|
- 确定相机到世界坐标变换是否为相对变换,影响相机表示的方向和定位。
|
|
- Comfy dtype: BOOLEAN
|
|
- Python dtype: bool
|
|
- use_viewer
|
|
- 布尔标志,指示是否使用外部查看器进行可视化,提供替代的显示方法。
|
|
- Comfy dtype: BOOLEAN
|
|
- Python dtype: bool
|
|
## Optional
|
|
- cameractrl_poses
|
|
- 直接提供用于可视化的相机控制姿态,作为指定姿态文件路径的替代方案。
|
|
- Comfy dtype: CAMERACTRL_POSES
|
|
- Python dtype: list
|
|
|
|
# Output types
|
|
- image
|
|
- 输出是相机姿态的3D图表视觉表示,展示相机的空间排列和方向。
|
|
- Comfy dtype: IMAGE
|
|
- Python dtype: matplotlib.figure.Figure
|
|
- ui
|
|
- 输出是相机姿态的3D图表视觉表示,展示相机的空间排列和方向。
|
|
|
|
|
|
## Usage tips
|
|
- Infra type: `CPU`
|
|
- Common nodes: unknown
|
|
|
|
|
|
## Source code
|
|
```python
|
|
class CameraPoseVisualizer:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": {
|
|
"pose_file_path": ("STRING", {"default": '', "multiline": False}),
|
|
"base_xval": ("FLOAT", {"default": 0.2,"min": 0, "max": 100, "step": 0.01}),
|
|
"zval": ("FLOAT", {"default": 0.3,"min": 0, "max": 100, "step": 0.01}),
|
|
"scale": ("FLOAT", {"default": 1.0,"min": 0.01, "max": 10.0, "step": 0.01}),
|
|
"use_exact_fx": ("BOOLEAN", {"default": False}),
|
|
"relative_c2w": ("BOOLEAN", {"default": True}),
|
|
"use_viewer": ("BOOLEAN", {"default": False}),
|
|
},
|
|
"optional": {
|
|
"cameractrl_poses": ("CAMERACTRL_POSES", {"default": None}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "plot"
|
|
CATEGORY = "KJNodes/misc"
|
|
DESCRIPTION = """
|
|
Visualizes the camera poses, from Animatediff-Evolved CameraCtrl Pose
|
|
or a .txt file with RealEstate camera intrinsics and coordinates, in a 3D plot.
|
|
"""
|
|
|
|
def plot(self, pose_file_path, scale, base_xval, zval, use_exact_fx, relative_c2w, use_viewer, cameractrl_poses=None):
|
|
import matplotlib as mpl
|
|
import matplotlib.pyplot as plt
|
|
import io
|
|
from torchvision.transforms import ToTensor
|
|
|
|
x_min = -2.0 * scale
|
|
x_max = 2.0 * scale
|
|
y_min = -2.0 * scale
|
|
y_max = 2.0 * scale
|
|
z_min = -2.0 * scale
|
|
z_max = 2.0 * scale
|
|
plt.rcParams['text.color'] = '#999999'
|
|
self.fig = plt.figure(figsize=(18, 7))
|
|
self.fig.patch.set_facecolor('#353535')
|
|
self.ax = self.fig.add_subplot(projection='3d')
|
|
self.ax.set_facecolor('#353535') # Set the background color here
|
|
self.ax.grid(color='#999999', linestyle='-', linewidth=0.5)
|
|
self.plotly_data = None # plotly data traces
|
|
self.ax.set_aspect("auto")
|
|
self.ax.set_xlim(x_min, x_max)
|
|
self.ax.set_ylim(y_min, y_max)
|
|
self.ax.set_zlim(z_min, z_max)
|
|
self.ax.set_xlabel('x', color='#999999')
|
|
self.ax.set_ylabel('y', color='#999999')
|
|
self.ax.set_zlabel('z', color='#999999')
|
|
for text in self.ax.get_xticklabels() + self.ax.get_yticklabels() + self.ax.get_zticklabels():
|
|
text.set_color('#999999')
|
|
print('initialize camera pose visualizer')
|
|
|
|
if pose_file_path != "":
|
|
with open(pose_file_path, 'r') as f:
|
|
poses = f.readlines()
|
|
w2cs = [np.asarray([float(p) for p in pose.strip().split(' ')[7:]]).reshape(3, 4) for pose in poses[1:]]
|
|
fxs = [float(pose.strip().split(' ')[1]) for pose in poses[1:]]
|
|
print(poses)
|
|
elif cameractrl_poses is not None:
|
|
poses = cameractrl_poses
|
|
w2cs = [np.array(pose[7:]).reshape(3, 4) for pose in cameractrl_poses]
|
|
fxs = [pose[1] for pose in cameractrl_poses]
|
|
else:
|
|
raise ValueError("Please provide either pose_file_path or cameractrl_poses")
|
|
|
|
total_frames = len(w2cs)
|
|
transform_matrix = np.asarray([[1, 0, 0, 0], [0, 0, 1, 0], [0, -1, 0, 0], [0, 0, 0, 1]]).reshape(4, 4)
|
|
last_row = np.zeros((1, 4))
|
|
last_row[0, -1] = 1.0
|
|
|
|
w2cs = [np.concatenate((w2c, last_row), axis=0) for w2c in w2cs]
|
|
c2ws = self.get_c2w(w2cs, transform_matrix, relative_c2w)
|
|
|
|
for frame_idx, c2w in enumerate(c2ws):
|
|
self.extrinsic2pyramid(c2w, frame_idx / total_frames, hw_ratio=1/1, base_xval=base_xval,
|
|
zval=(fxs[frame_idx] if use_exact_fx else zval))
|
|
|
|
# Create the colorbar
|
|
cmap = mpl.cm.rainbow
|
|
norm = mpl.colors.Normalize(vmin=0, vmax=total_frames)
|
|
colorbar = self.fig.colorbar(mpl.cm.ScalarMappable(norm=norm, cmap=cmap), ax=self.ax, orientation='vertical')
|
|
|
|
# Change the colorbar label
|
|
colorbar.set_label('Frame', color='#999999') # Change the label and its color
|
|
|
|
# Change the tick colors
|
|
colorbar.ax.yaxis.set_tick_params(colors='#999999') # Change the tick color
|
|
|
|
# Change the tick frequency
|
|
# Assuming you want to set the ticks at every 10th frame
|
|
ticks = np.arange(0, total_frames, 10)
|
|
colorbar.ax.yaxis.set_ticks(ticks)
|
|
|
|
plt.title('')
|
|
plt.draw()
|
|
buf = io.BytesIO()
|
|
plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0)
|
|
buf.seek(0)
|
|
img = Image.open(buf)
|
|
tensor_img = ToTensor()(img)
|
|
buf.close()
|
|
tensor_img = tensor_img.permute(1, 2, 0).unsqueeze(0)
|
|
if use_viewer:
|
|
time.sleep(1)
|
|
plt.show()
|
|
return (tensor_img,)
|
|
|
|
def extrinsic2pyramid(self, extrinsic, color_map='red', hw_ratio=1/1, base_xval=1, zval=3):
|
|
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
|
|
vertex_std = np.array([[0, 0, 0, 1],
|
|
[base_xval, -base_xval * hw_ratio, zval, 1],
|
|
[base_xval, base_xval * hw_ratio, zval, 1],
|
|
[-base_xval, base_xval * hw_ratio, zval, 1],
|
|
[-base_xval, -base_xval * hw_ratio, zval, 1]])
|
|
vertex_transformed = vertex_std @ extrinsic.T
|
|
meshes = [[vertex_transformed[0, :-1], vertex_transformed[1][:-1], vertex_transformed[2, :-1]],
|
|
[vertex_transformed[0, :-1], vertex_transformed[2, :-1], vertex_transformed[3, :-1]],
|
|
[vertex_transformed[0, :-1], vertex_transformed[3, :-1], vertex_transformed[4, :-1]],
|
|
[vertex_transformed[0, :-1], vertex_transformed[4, :-1], vertex_transformed[1, :-1]],
|
|
[vertex_transformed[1, :-1], vertex_transformed[2, :-1], vertex_transformed[3, :-1], vertex_transformed[4, :-1]]]
|
|
|
|
color = color_map if isinstance(color_map, str) else plt.cm.rainbow(color_map)
|
|
|
|
self.ax.add_collection3d(
|
|
Poly3DCollection(meshes, facecolors=color, linewidths=0.3, edgecolors=color, alpha=0.25))
|
|
|
|
def customize_legend(self, list_label):
|
|
from matplotlib.patches import Patch
|
|
list_handle = []
|
|
for idx, label in enumerate(list_label):
|
|
color = plt.cm.rainbow(idx / len(list_label))
|
|
patch = Patch(color=color, label=label)
|
|
list_handle.append(patch)
|
|
plt.legend(loc='right', bbox_to_anchor=(1.8, 0.5), handles=list_handle)
|
|
|
|
def get_c2w(self, w2cs, transform_matrix, relative_c2w):
|
|
if relative_c2w:
|
|
target_cam_c2w = np.array([
|
|
[1, 0, 0, 0],
|
|
[0, 1, 0, 0],
|
|
[0, 0, 1, 0],
|
|
[0, 0, 0, 1]
|
|
])
|
|
abs2rel = target_cam_c2w @ w2cs[0]
|
|
ret_poses = [target_cam_c2w, ] + [abs2rel @ np.linalg.inv(w2c) for w2c in w2cs[1:]]
|
|
else:
|
|
ret_poses = [np.linalg.inv(w2c) for w2c in w2cs]
|
|
ret_poses = [transform_matrix @ x for x in ret_poses]
|
|
return np.array(ret_poses, dtype=np.float32)
|
|
|
|
```
|