Taichi 模拟结果导出实战:图片、视频与 PLY 序列文件的完整指南
【免费下载链接】taichiProductive, portable, and performant GPU programming in Python.项目地址: https://gitcode.com/GitHub_Trending/ta/taichi
导读
Taichi 为 Python GPU 编程提供了从仿真计算到视觉呈现的一站式工具链。当你的物理模拟、流体或粒子系统运行结束后,如何将ti.field中的结果导出为图片、视频,甚至能被 Houdini、Blender 等 DCC 软件读取的 PLY 序列文件,是连接「计算」与「展示」的关键一环。本文基于 Taichi 仓库中的 export_results.md 教程,结合 tools 模块 的真实源码,系统讲解ti.GUI.show、ti.tools.imwrite、ti.tools.VideoManager、ti.tools.PLYWriter以及ti video/ti gif命令行工具的使用方法与底层原理,读完即可在自己的项目中完成图像帧导出、视频合成与网格序列输出的完整工作流。
一、导出图片的两种途径
Taichi 提供两条导出图片的路径,适用场景不同:
ti.GUI.show(filename):适合「边仿真边实时预览、同时保存关键帧」的场景,调用一次即可完成窗口渲染与文件落盘;ti.tools.imwrite(img, filename):适合「不需要弹窗、直接保存计算结果的图像」的场景,例如离线渲染或批量后处理。
两种方式下,图片的实际格式完全由filename的后缀决定。Taichi 当前支持png、jpg与bmp三种格式,官方推荐优先使用无损的png。
1.1 使用ti.GUI.show(filename)导出帧
下面的例子创建一个 512×512 的随机噪点画布,在 1000 次迭代中逐帧保存:
import taichi as ti import os ti.init() pixels = ti.field(ti.u8, shape=(512, 512, 3)) @ti.kernel def paint(): for i, j, k in pixels: pixels[i, j, k] = ti.random() * 255 iterations = 1000 gui = ti.GUI("Random pixels", res=512) # mainloop for i in range(iterations): paint() gui.set_image(pixels) filename = f'frame_{i:05d}.png' # create filename with suffix png print(f'Frame {i} is recorded in {filename}') gui.show(filename) # export and show in GUI运行后,当前目录下会生成frame_00000.png、frame_00001.png等一系列图片。
从源码看,gui.py 中show(file)的实现逻辑是:先调用self.core.update()刷新窗口内容;当传入file时再调用self.core.screenshot(file)将当前帧写入文件,随后帧计数加一并清空画布。因此每次调用gui.show(filename)都等价于「截屏 + 存档」。
要点:
gui.show()不带参数时仅刷新窗口不保存文件;传入文件名后缀为png/jpg/bmp时才会触发落盘。这也意味着「一边实时预览、一边录制视频帧」是完全可行的。
1.2 使用ti.tools.imwrite直接写盘
如果不希望唤起 GUI 窗口,可以直接调用ti.tools.imwrite:
import taichi as ti ti.init() pixels = ti.field(ti.u8, shape=(512, 512, 3)) @ti.kernel def set_pixels(): for i, j, k in pixels: pixels[i, j, k] = ti.random() * 255 set_pixels() filename = f'imwrite_export.png' ti.tools.imwrite(pixels.to_numpy(), filename) print(f'The image has been saved to {filename}')ti.tools.imwrite支持以下输入类型:
- Taichi 字段:
ti.field、ti.Vector.field、ti.Matrix.field; - NumPy 数组:
np.ndarray(字段会先通过to_numpy()转出)。
输出格式同样由文件后缀决定,支持png、jpg、bmp。
通道数决定图像类型
输出图像是灰度、RGB 还是 RGBA,完全取决于输入字段第三维的长度(即field.shape[2]):
| 输入字段形状 | 导出图像类型 |
|---|---|
(w, h)或(w, h, 1) | 灰度图(grayscale) |
(w, h, 3) | RGB 彩色图 |
(w, h, 4) | RGBA 彩色图 |
数据类型决定像素映射规则
原文档特别提醒:Taichi 字段自带ti.u8、ti.f32等数据类型,不同类型在imwrite中的行为不同。这一点在 image.py 的cook_image_to_bytes函数中有精确实现:
- 整数类型(
uint16/uint32/uint64):按像素值 // (类型上限 // 256)线性映射到[0, 255]区间。例如ti.u16的 65535 会映射为 255; - 浮点类型(
float32/float64):先clip(img, 0, 1)截断到[0.0, 1.0],再乘以 255。即浮点字段的像素值应规范在 0~1 之间,超出部分会被截断; uint8:直接使用,无需转换;- 其他类型:抛出
ValueError。
处理完数据类型后,二维数组会被补成(w, h, 1)的三维形状,最后通过swapaxes(0, 1)[::-1, :]调整坐标方向,再经底层_ti_core.imwrite写入文件。
扩展阅读:GUI 系统 中还介绍了与
imwrite配套的imread、imresize、imshow等辅助函数,分别用于读图、缩放图像与在 GUI 窗口或 Jupyter Notebook 中直接显示图像,实现完整的图像 I/O 闭环。
二、将 PNG 序列转换为视频:ti video
当你通过上述方式导出了一系列png帧(例如000000.png、000001.png、000002.png……),可以方便地用ti video命令把它们合并成单个video.mp4:
# 默认把当前工作目录下的所有 *.png 按文件名排序合成为 video.mp4 ti video # 指定 40 FPS 输出 ti video -f40 # 指定输出路径 ti video -o output/my_video.mp4该命令定义在 _main.py 中,参数行为如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
inputs(位置参数) | 当前目录下全部*.png(排序后) | 也可显式传入多个 PNG 文件 |
-o, --output | ./video.mp4 | 输出 MP4 文件路径 |
-f, --framerate | 24 | 输出视频帧率 |
-c, --crf | 20 | 恒定速率因子,范围 1~51:1 近乎无损,20 为默认,51 画质最差 |
底层通过 video.py 的make_video调用 ffmpeg:将输入帧统一拷贝到临时目录并重命名为%06d.png,然后以libx264 + profile:v high编码为yuv420p像素格式的 MP4。若输入尺寸为奇数(无法被 2 整除),会自动减 1 以保证 H.264 编码兼容性。
三、导出视频:ti.tools.VideoManager
3.1 前置依赖:ffmpeg
视频导出工具链依赖ffmpeg。若机器上未安装,请先参照本文末尾「安装 ffmpeg」一节完成安装。
3.2 基本用法
ti.tools.VideoManager可以把逐帧写入的图像合成为mp4或gif:
import taichi as ti ti.init() pixels = ti.field(ti.u8, shape=(512, 512, 3)) @ti.kernel def paint(): for i, j, k in pixels: pixels[i, j, k] = ti.random() * 255 result_dir = "./results" video_manager = ti.tools.VideoManager(output_dir=result_dir, framerate=24, automatic_build=False) for i in range(50): paint() pixels_img = pixels.to_numpy() video_manager.write_frame(pixels_img) print(f'\rFrame {i+1}/50 is recorded', end='') print() print('Exporting .mp4 and .gif videos...') video_manager.make_video(gif=True, mp4=True) print(f'MP4 video is saved to {video_manager.get_output_filename(".mp4")}') print(f'GIF video is saved to {video_manager.get_output_filename(".gif")}')运行后,./results/目录下会出现生成的视频文件。
3.3 构造参数与工作流程解析
对照 video.py 的VideoManager实现,各参数与内部行为如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
output_dir | 必填 | 帧文件与视频的输出目录;帧图会存放在其下的frames/子目录 |
video_filename | None | 视频文件名(如sim.mp4),缺省为video.mp4 |
width/height | None | 视频分辨率,需同时指定或同时省略;省略时取第一帧的尺寸 |
post_processor | None | 实现process(img)方法的对象,可对每帧做后处理 |
framerate | 24 | 视频帧率 |
automatic_build | True | 是否在写帧过程中自动增量合成视频 |
工作流程分三步:
write_frame(img):接收np.ndarray帧,内部以%06d.png模板(即000000.png、000001.png……)命名后调用imwrite存入output_dir/frames/。实现中会检查宽高是否为奇数,若是则打印警告并丢弃最后一行/列(保证 H.264 兼容);若设置了post_processor则先处理再写入;make_video(mp4=True, gif=True):调用 ffmpeg,以-framerate {framerate} -c:v libx264 -profile:v high -crf 1 -pix_fmt yuv420p参数合成 MP4(crf 1意味着近无损的高质量输出);若gif=True,则进一步调用mp4_to_gif基于 palettegen / paletteuse 双趟法生成 GIF;get_output_filename(suffix):按output_dir与video_filename计算最终输出路径,例如./results/video.mp4。
若automatic_build=True,write_frame会在第 4、8、16……帧(checkpoint 每轮翻倍)自动调用一次make_video,适合长时录制时边跑边出片;文档示例中显式设为False,由用户在循环结束后统一合成。
提示:帧文件目录可通过
get_frame_directory()获取,clean_frames()可清理已记录的帧图,write_frames(images)支持一次性写入帧列表。
四、将视频转换为 GIF:ti gif
需要把 MP4 转成 GIF 发布到论坛时,使用ti gif:
# 将 video.mp4 转为同名 video.gif(帧率默认 24) ti gif -i video.mp4 # 以 40 FPS 生成 GIF ti gif -i video.mp4 -f40从 _main.py 可以看到,-i/--input为必填参数且必须是以.mp4结尾的文件(否则报错filename must be of type .mp4),-f/--framerate默认 24。输出文件自动取输入文件的同名.gif后缀,底层调用mp4_to_gif,通过 ffmpeg 的两步 palette 策略得到质量更稳的动图。
五、导出 PLY 网格序列:ti.tools.PLYWriter
PLY(Polygon File Format)是点云与网格数据的通用格式。ti.tools.PLYWriter可以把 Taichi 字段中的顶点位置、颜色、法线等通道写入ply文件,供 Blender、Houdini、MeshLab 等三维软件读取,是粒子系统、MPM 等无网格模拟结果可视化的标准出口。
5.1 完整示例:导出 10 帧随机运动立方体
import taichi as ti import numpy as np ti.init(arch=ti.cpu) num_vertices = 1000 pos = ti.Vector.field(3, dtype=ti.f32, shape=(10, 10, 10)) rgba = ti.Vector.field(4, dtype=ti.f32, shape=(10, 10, 10)) @ti.kernel def place_pos(): for i, j, k in pos: pos[i, j, k] = 0.1 * ti.Vector([i, j, k]) @ti.kernel def move_particles(): for i, j, k in pos: pos[i, j, k] += ti.Vector([0.1, 0.1, 0.1]) @ti.kernel def fill_rgba(): for i, j, k in rgba: rgba[i, j, k] = ti.Vector( [ti.random(), ti.random(), ti.random(), ti.random()]) place_pos() series_prefix = "example.ply" for frame in range(10): move_particles() fill_rgba() # now adding each channel only supports passing individual np.array # so converting into np.ndarray, reshape # remember to use a temp var to store so you dont have to convert back np_pos = np.reshape(pos.to_numpy(), (num_vertices, 3)) np_rgba = np.reshape(rgba.to_numpy(), (num_vertices, 4)) # create a PLYWriter writer = ti.tools.PLYWriter(num_vertices=num_vertices) writer.add_vertex_pos(np_pos[:, 0], np_pos[:, 1], np_pos[:, 2]) writer.add_vertex_rgba( np_rgba[:, 0], np_rgba[:, 1], np_rgba[:, 2], np_rgba[:, 3]) writer.export_frame_ascii(frame, series_prefix)运行后在当前目录得到example_000000.ply、example_000001.ply…… 共 10 个帧文件。接下来把PLYWriter的用法拆解为 4 步。
5.2 第一步:初始化PLYWriter
# num_vertices 必须是正整数 # num_faces 可选,默认 0 # face_type 只能为 "tri" 或 "quad",默认 "tri" # 上一个例子:创建包含 1000 个顶点、0 个三角面的 writer num_vertices = 1000 writer = ti.tools.PLYWriter(num_vertices=num_vertices) # 下面的例子:创建包含 20 个顶点、5 个四边形面的 writer writer2 = ti.tools.PLYWriter(num_vertices=20, num_faces=5, face_type="quad")对照 np2ply.py 的实现,构造函数还支持comment参数(默认"created by PLYWriter",会写入 PLY 文件头部的comment行),并包含三条断言:num_vertices > 0、num_faces >= 0、face_type仅为tri或quad。
5.3 第二步:添加必需通道
顶点与面各有一个必需通道:顶点必须有位置坐标,面必须有顶点索引列表。
以一个 20 顶点、12 个四边形面的 2D 网格为例:
# 网格拓扑示意: # y # | # z---/ # x # 19---15---11---07---03 # | | | | | # 18---14---10---06---02 # | | | | | # 17---13---19---05---01 # | | | | | # 16---12---08---04---00 writer = ti.tools.PLYWriter(num_vertices=20, num_faces=12, face_type="quad") # 顶点位置:将 3 个 np.array(x、y、z)传入 x = np.zeros(20) y = np.array(list(np.arange(0, 4))*5) z = np.repeat(np.arange(5), 4) writer.add_vertex_pos(x, y, z) # 面索引:每个面包含的顶点编号列表 indices = np.array([0, 1, 5, 4]*12)+np.repeat( np.array(list(np.arange(0, 3))*4)+4*np.repeat(np.arange(4), 3), 4) writer.add_faces(indices)源码中add_vertex_pos(x, y, z)实际是分别以"x"、"y"、"z"为键调用add_vertex_channel,类型固定为float;add_faces(indices)会把一维索引数组 reshape 成(num_faces, vert_per_face)并转为int32,其中tri每面 3 个索引、quad每面 4 个索引,总数必须严格等于vert_per_face * num_faces。
5.4 第三步:添加可选通道
# 自定义顶点通道:传入 key、受支持的数据类型和数据 np.array vdata = np.random.rand(20) writer.add_vertex_channel("vdata1", "double", vdata) # 自定义面通道 foo_data = np.zeros(12) writer.add_face_channel("foo_key", "foo_data_type", foo_data) # error! 因为 "foo_data_type" 不是受支持的类型。 # 受支持的类型为: # ['char', 'uchar', 'short', 'ushort', 'int', 'uint', 'float', 'double'] # PLYWriter 内置了多个常用通道的辅助函数 # 顶点颜色:使用 0~1 区间的 float/double 表示 r、g、b、alpha r = np.random.rand(20) g = np.random.rand(20) b = np.random.rand(20) alpha = np.random.rand(20) writer.add_vertex_color(r, g, b) writer.add_vertex_alpha(alpha) # 等价写法: # add_vertex_rgba(r, g, b, alpha) # 顶点法线 writer.add_vertex_normal(np.ones(20), np.zeros(20), np.zeros(20)) # 顶点 id 与 piece(分组 id) writer.add_vertex_id() writer.add_vertex_piece(np.ones(20)) # 面 id 与 piece(分组 id) writer.add_face_id() # 将所有面划入组 1 writer.add_face_piece(np.ones(12))对照源码,add_vertex_channel/add_face_channel的底层规则值得注意:
- 数据类型映射:PLY 的 8 种类型
['char', 'uchar', 'short', 'ushort', 'int', 'uint', 'float', 'double']分别映射到 NumPy 的int8/uint8/int16/uint16/int32/uint32/float32/float64(见 np2ply.py);传入未知类型会打印Unknown type ... skipping this channel并跳过该通道,不会直接崩溃; - 多维数据自动展开:若传入的数据是二维数组,会按列拆分为
key_1、key_2…… 等多个单列通道,便于一次性写入向量分量; - 重复键警告:通道键重复时会打印
WARNING: duplicate key ... detected; - 便捷函数:
add_vertex_normal写入nx/ny/nz,add_vertex_color写入red/green/blue,add_vertex_alpha写入Alpha(注意首字母大写),add_vertex_rgba为前两者的组合;add_vertex_id/add_face_id自动写入np.arange索引,add_vertex_piece/add_face_piece用于给顶点/面标记分组 ID(PLY 中常配合piece实现多物体合并)。
5.5 第四步:导出文件
series_prefix = "example.ply" series_prefix_ascii = "example_ascii.ply" # 导出单个文件,使用 ascii 以便直接阅读内容 writer.export_ascii(series_prefix_ascii) # 或者使用二进制格式以获得更好性能 # writer.export(series_prefix) # 导出一系列文件,例如 10 帧 for frame in range(10): # 每帧写入如 "example_000000.ply" 的文件 writer.export_frame_ascii(frame, series_prefix_ascii) # 或使用二进制 # writer.export_frame(frame, series_prefix) # 更新位置/颜色 x = x + 0.1*np.random.rand(20) y = y + 0.1*np.random.rand(20) z = z + 0.1*np.random.rand(20) r = np.random.rand(20) g = np.random.rand(20) b = np.random.rand(20) alpha = np.random.rand(20) # 重新填充 writer = ti.tools.PLYWriter(num_vertices=20, num_faces=12, face_type="quad") writer.add_vertex_pos(x, y, z) writer.add_faces(indices) writer.add_vertex_channel("vdata1", "double", vdata) writer.add_vertex_color(r, g, b) writer.add_vertex_alpha(alpha) writer.add_vertex_normal(np.ones(20), np.zeros(20), np.zeros(20)) writer.add_vertex_id() writer.add_vertex_piece(np.ones(20)) writer.add_face_id() writer.add_face_piece(np.ones(12))导出的四种方法对应两种格式:
| 方法 | 格式 | 文件命名 |
|---|---|---|
export(path) | 二进制(binary_little_endian或binary_big_endian,取决于平台字节序) | 即path本身 |
export_ascii(path) | ASCII 文本 | 即path本身 |
export_frame(series_num, path) | 二进制 | path去除.ply后加_{series_num:06d}.ply |
export_frame_ascii(series_num, path) | ASCII 文本 | 同上 |
从 np2ply.py 可以看到,每种导出前都会执行sanity_check():确保顶点通道中包含x、y、z三个必需键;若存在面,则逐索引校验其落在[0, num_vertices)区间内,防止写出越界引用的损坏文件。文件头会按 PLY 规范写入element vertex、element face、property list uchar int vertex_indices等声明。
提示:帧号使用 6 位补零(
%06d),因此 10 帧会输出example_000000.ply~example_000009.ply,便于外部软件按字典序识别帧序列。
5.6 将 PLY 序列导入 Houdini 与 Blender
Houdini支持导入共享同一前缀/后缀的 PLY 序列,export_frame系列方法生成的命名恰好满足该要求。导入时点击File -> Import -> Geometry,导航到帧结果所在目录,序列会折叠为单个条目(形如example_$F6.ply (0-9)),双击该条目即可完成导入,Houdini 会自动按帧号推进。
Blender需要借助名为 Stop-motion-OBJ 的第三方插件来加载序列结果:Blender 2.80+ 用户应安装该插件的最新版本;Blender 2.79 及更早版本则需使用v1.1.1版本,插件作者提供了详细的安装与使用文档。装好插件后即可按帧序列方式把 Taichi 导出的 PLY 动画导入 Blender 继续做材质、打光与渲染。
六、安装 ffmpeg
ti video、ti gif与VideoManager均依赖 ffmpeg,以下按平台说明安装方法。
6.1 Windows
- 从 ffmpeg 官网下载发行压缩包(形如
ffmpeg-2020xxx.zip); - 解压到某个目录,例如
D:/YOUR_FFMPEG_FOLDER; - 重要:将
D:/YOUR_FFMPEG_FOLDER/bin添加到系统PATH环境变量; - 打开
cmd或PowerShell,执行以下命令验证:
ffmpeg -version若能打印出版本信息,说明安装成功。
6.2 Linux
大多数 Linux 发行版已原生附带ffmpeg,若ffmpeg命令已存在则无需操作。否则按发行版选择安装方式:
Ubuntu:
sudo apt-get update sudo apt-get install ffmpegCentOS / RHEL:
sudo yum install ffmpeg ffmpeg-develArch Linux:
pacman -S ffmpeg验证安装:
ffmpeg -h6.3 macOS
使用 Homebrew 安装:
brew install ffmpeg七、小结:按需选择导出方案
| 需求 | 推荐方案 | 底层实现 |
|---|---|---|
| 边实时预览边存图 | ti.GUI.show(filename) | gui.py |
| 无窗口直接存图 | ti.tools.imwrite | image.py |
| PNG 序列合成 MP4 | ti video | _main.py + video.py |
| 逐帧录制并合成视频 | ti.tools.VideoManager | video.py |
| MP4 转 GIF | ti gif | _main.py |
| 点云/网格序列导出 | ti.tools.PLYWriter | np2ply.py |
这套工具链覆盖了「计算字段 → 图像帧 → 视频 → 三维网格序列」的完整导出路径:仿真中逐帧imwrite或VideoManager.write_frame落盘,结束后用ti video或make_video合成 MP4/GIF 用于演示与分享;需要进入 Blender、Houdini 做进一步可视化的场景,则用PLYWriter输出带颜色、法线、分组信息的帧序列。配合 GUI 系统 中的imread、imresize、imshow等辅助函数,你可以把 Taichi 的每一帧计算结果无缝接入主流 CG 工作流。
【免费下载链接】taichiProductive, portable, and performant GPU programming in Python.项目地址: https://gitcode.com/GitHub_Trending/ta/taichi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考