恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
artcraft创作管线实战:从零搭建音频驱动图像扭曲的实时预览环境
首页
资讯中心
/
artcraft创作管线实战:从零搭建音频驱动图像扭曲的实时预览环境
artcraft创作管线实战:从零搭建音频驱动图像扭曲的实时预览环境
发布时间:2026/10/20 14:51:45
1. 从“artcraft”这个名字说起一个被低估的创作工具链思路第一次看到“artcraft”这个标题的时候我脑子里蹦出来的不是某个具体的软件而是一种很朴素的组合逻辑art加craft艺术加手艺。这两个词放在一起本身就带着一种张力——一边是偏感性的审美表达一边是偏理性的手工实现。做技术的人看到这个词很容易联想到一个场景手里有一堆零散的素材、代码片段、设计稿、音频文件怎么把它们像做手工一样一件一件拼成一个能跑、能看、能用的东西。我之所以对这个标题感兴趣是因为过去几年我一直在做类似的事情。不是做某一个垂直领域的工具而是做那种“把不同来源的东西粘合起来”的创作流程。比如把一段程序生成的图形导出成矢量文件再丢进设计工具里做后期或者把一段音频的波形数据提取出来映射成三维模型的顶点位移。这些事情听起来很杂但它们有一个共同点都需要一个“craft”的过程也就是手工打磨的环节。“artcraft”这个标题如果把它当成一个项目来看它大概率不是一个单一功能的工具而是一个创作管线或者创作环境。它可能包含几个核心模块素材的导入与解析、参数的调节与实时预览、最终结果的导出与二次编辑。适合谁来参考呢我觉得有三类人第一类是独立创作者比如做生成艺术、数据可视化、互动装置的人第二类是做前端或者图形方向的技术人想找一个能快速验证视觉想法的脚手架第三类是对“代码加手工”这种混合工作流感兴趣的学生或者爱好者。这篇文章我不会只讲概念我会把“artcraft”拆成几个可以落地的部分整体设计思路、核心模块的实现细节、实操流程、以及我在类似项目里踩过的坑。每一部分都会给出具体的参数、代码片段和排查方法。你不需要有很深的图形学背景只要会写一点代码、愿意动手调参数就能跟着做下来。2. 整体设计与思路拆解为什么是“管线”而不是“工具”2.1 核心需求解析从“想做点什么”到“能反复做点什么”很多人刚开始做创作类项目的时候容易陷入一个误区一上来就想做一个大而全的工具。比如想做一个“生成艺术平台”结果花了两周时间搭界面真正用来生成图形的代码只有几十行。这种思路的问题在于它把“工具”当成了目标而不是把“创作流程”当成目标。“artcraft”这个标题给我的启发是它更像一个动词而不是一个名词。它描述的是一种行为用艺术的手感去 craft 一个东西。所以我在设计类似项目的时候第一件事不是选框架而是画一张流程图素材从哪里来经过哪些处理步骤在哪里停下来让人调参最后输出成什么格式。我试过几种不同的组织方式最后发现最稳的是“三段式管线”输入层负责把各种格式的素材读进来统一成内部数据结构。比如图片转成像素矩阵音频转成采样数组文本转成字符序列。处理层负责对数据进行变换。这一层可以有很多个“节点”每个节点做一件小事比如模糊、扭曲、着色、映射。输出层负责把处理后的数据导出成目标格式。比如 PNG 序列、SVG、OBJ、WAV或者直接推到一个实时渲染窗口。这种管线式设计的好处是你可以随时在中间插入新的节点而不需要改动整个系统。比如你一开始只想做图片滤镜后来想加一个“根据音频振幅改变滤镜强度”的功能只需要在输入层加一个音频读取节点在处理层加一个映射节点输出层完全不用动。注意管线式设计的一个常见坑是“节点之间的数据格式不统一”。我建议在项目初期就定一个内部数据协议比如所有图像数据都用{width, height, channels, data}这种结构所有音频数据都用{sampleRate, samples}。这样后面加节点的时候不用每次都写转换代码。2.2 技术选型背后的考量为什么我最终选了“脚本加实时预览”的组合在技术选型上我试过三种方案第一种是纯前端方案用 Canvas 或者 WebGL 做渲染用 JavaScript 写处理逻辑。优点是跨平台、容易分享缺点是性能受限于浏览器处理大尺寸图片或者高采样率音频的时候会卡。第二种是纯桌面方案用 Python 加 OpenGL或者用 C 加 Qt。优点是性能好缺点是开发周期长而且调试起来比较麻烦改一行代码要重新编译。第三种是混合方案核心处理逻辑用脚本语言写比如 Python 或者 JavaScript渲染和交互用原生或者 Web 技术。脚本负责“craft”的部分也就是参数调节和逻辑编排渲染负责“art”的部分也就是实时预览。我最后选了第三种具体来说是 Python 加 moderngl 加 imgui 的组合。Python 负责数据处理和节点编排moderngl 负责 GPU 渲染imgui 负责参数面板。这样我可以在 Python 里快速写一个处理节点然后在 imgui 里拖一个滑块立刻看到渲染结果。整个流程不需要重启程序也不需要重新编译。这个组合的另一个好处是它很容易和现有的生态对接。比如我想用 numpy 做矩阵运算直接用想用 PIL 读图片直接用想用 librosa 读音频也直接用。这些库在 Python 里都是现成的不需要自己造轮子。2.3 优势与边界它能做什么不能做什么“artcraft”这种管线式创作环境优势在于灵活性和可扩展性。你可以用它做很多事情生成艺术用数学函数或者噪声生成图形然后叠加滤镜和色彩映射。数据可视化把 CSV 或者 JSON 数据映射成几何图形或者粒子系统。音频可视化把音频的频谱或者波形映射成三维模型或者二维图形。图像处理批量处理图片比如做风格迁移、色彩分级、纹理合成。但它也有边界。它不适合做那种需要极高实时性的东西比如游戏引擎或者视频编辑软件。它的定位是“创作辅助工具”而不是“生产工具”。你可以用它快速验证一个想法然后把验证好的逻辑移植到更专业的工具里。另外它也不适合做那种需要复杂交互的东西。比如你想做一个完整的绘图软件有图层、蒙版、笔刷这些功能那用“artcraft”这种管线式架构会很吃力。因为绘图软件的交互逻辑是事件驱动的而管线式架构是数据流驱动的两者在模型上有本质区别。3. 核心细节解析与实操要点从零搭一个最小可用的 artcraft 环境3.1 环境准备依赖安装与版本选择我假设你用的是 Python 3.10 或者更高版本。低版本的 Python 在类型提示和异步支持上有些限制虽然不影响核心功能但写起来会别扭一些。需要安装的库不多但有几个版本要注意pip install moderngl moderngl-window imgui-bundle numpy pillowmoderngl负责 GPU 渲染。我用的版本是 5.10.0这个版本对 OpenGL 4.3 的支持比较稳定。moderngl-window负责窗口管理和输入事件。版本 2.4.6。imgui-bundle负责参数面板。版本 1.5.0。这个包把 imgui 和 moderngl 的集成都做好了省了很多事。numpy负责矩阵运算。版本 1.26.4。pillow负责图片读写。版本 10.3.0。如果你用的是 macOS需要注意 OpenGL 的版本限制。macOS 最高只支持 OpenGL 4.1所以 moderngl 的某些特性可能用不了。我建议在 macOS 上开发的时候尽量用 OpenGL 3.3 的核心模式兼容性最好。提示安装 moderngl 的时候如果遇到编译错误大概率是缺少 OpenGL 的开发头文件。在 Ubuntu 上可以装libgl1-mesa-dev和libglu1-mesa-dev在 Windows 上一般不需要额外装因为 moderngl 有预编译的 wheel。3.2 内部数据协议定一个所有节点都认的“通用语言”前面提到过管线式设计最关键的是数据格式统一。我定义了一个很简单的协议所有节点都按这个协议来读写数据。对于图像数据我用一个字典来表示image_data { width: 1920, height: 1080, channels: 4, data: np.zeros((1080, 1920, 4), dtypenp.float32) }data的形状是(height, width, channels)数值范围是 0.0 到 1.0。用 float32 而不是 uint8是为了在多次处理之后不会出现精度损失。比如你连续做五次模糊每次都用 uint8 的话误差会累积得很明显。对于音频数据我用另一个字典audio_data { sample_rate: 44100, samples: np.zeros((44100 * 10, 2), dtypenp.float32) }samples的形状是(num_samples, num_channels)数值范围是 -1.0 到 1.0。同样用 float32。对于几何数据我用一个更简单的结构mesh_data { vertices: np.zeros((num_vertices, 3), dtypenp.float32), indices: np.zeros((num_triangles, 3), dtypenp.uint32), uvs: np.zeros((num_vertices, 2), dtypenp.float32) }这些协议不需要一开始就定得很完美但一定要在写第一个节点之前定下来。我试过先写节点再定协议结果写到第三个节点的时候发现前两个节点的数据格式不兼容只能回头重写。3.3 节点编排用有向无环图组织处理流程节点编排的核心是一个有向无环图DAG。每个节点有若干个输入端口和输出端口数据从输入端口流入经过处理从输出端口流出。节点之间通过端口连接形成一个数据流网络。我用一个很简单的类来表示节点class Node: def __init__(self, name, func, inputs, outputs): self.name name self.func func self.inputs inputs self.outputs outputs self.input_data {} self.output_data {} def process(self): args [self.input_data[port] for port in self.inputs] results self.func(*args) for port, result in zip(self.outputs, results): self.output_data[port] resultfunc是一个纯函数接收输入数据返回输出数据。这样设计的好处是节点本身不保存状态所有状态都在数据里。你可以随时重新执行整个图而不需要担心节点之间的状态污染。编排图的时候我用一个拓扑排序来确定节点的执行顺序。具体来说从没有输入的节点开始依次执行然后把输出传给下游节点。如果图里有环拓扑排序会失败这时候需要检查是不是哪里连错了。注意节点执行的时候如果某个节点的输入数据还没准备好不要直接跳过而是应该报错。我一开始为了“容错”让节点在输入缺失的时候返回空数据结果调试的时候花了很久才发现是上游节点没执行。后来改成严格模式缺输入就抛异常问题反而好找了。3.4 实时预览把处理结果画到屏幕上实时预览是“artcraft”体验的核心。没有实时预览调参数就是盲调效率会低很多。我用 moderngl 写了一个简单的渲染器把图像数据上传到纹理然后用一个全屏四边形画出来。着色器很简单// vertex shader #version 330 core in vec2 in_position; in vec2 in_texcoord; out vec2 v_texcoord; void main() { gl_Position vec4(in_position, 0.0, 1.0); v_texcoord in_texcoord; } // fragment shader #version 330 core uniform sampler2D u_texture; in vec2 v_texcoord; out vec4 f_color; void main() { f_color texture(u_texture, v_texcoord); }这个渲染器每帧做两件事第一检查上游节点的输出数据有没有变化第二如果有变化把新数据上传到纹理然后重绘。为了减少不必要的上传我用一个版本号来标记数据的变化。每次节点处理完数据版本号加一。渲染器比较版本号如果没变就不上传。这个机制看起来很简单但实际用起来很有效。我试过在 1920x1080 的分辨率下每秒处理 30 帧CPU 占用率不到 20%。如果每帧都上传纹理CPU 占用率会翻倍。3.5 参数面板用 imgui 做即时调节参数面板我用 imgui 来做。每个节点可以注册自己的参数比如“模糊半径”、“色彩强度”、“噪声频率”。这些参数用滑块、颜色选择器、下拉菜单来调节。imgui 的集成很简单moderngl-window 已经提供了现成的接口。你只需要在每一帧的渲染循环里调用imgui.render()和imgui.draw()然后把参数变化写回节点。我习惯把参数分成两类一类是“结构参数”比如节点的连接方式、输入输出的数量另一类是“数值参数”比如滑块的值。结构参数一般不改数值参数经常改。所以我把数值参数放在面板的上半部分结构参数放在下半部分用折叠面板收起来。提示imgui 的滑块默认范围是 0 到 1但很多参数需要更大的范围。比如模糊半径可能需要 0 到 100。这时候可以用imgui.slider_float(radius, value, 0.0, 100.0)来指定范围。另外如果参数是整数用imgui.slider_int不要用slider_float然后取整那样会有精度问题。4. 实操过程与核心环节实现做一个“音频驱动图像扭曲”的完整案例4.1 案例目标把音频振幅映射成图像扭曲强度这个案例的目标是读入一张图片和一段音频根据音频的振幅来扭曲图片。振幅大的时候扭曲强振幅小的时候扭曲弱。最终输出一个视频序列或者实时预览。这个案例涉及三个核心节点音频读取节点、图像读取节点、扭曲节点。音频读取节点输出振幅数组图像读取节点输出图像数据扭曲节点根据振幅数组对图像做正弦扭曲。4.2 音频读取节点从 WAV 文件到振幅数组音频读取我用的是 Python 标准库里的wave模块没有用 librosa因为wave足够简单而且不需要额外安装。import wave import numpy as np def read_audio(filepath): with wave.open(filepath, rb) as wf: sample_rate wf.getframerate() num_channels wf.getnchannels() num_frames wf.getnframes() raw_data wf.readframes(num_frames) samples np.frombuffer(raw_data, dtypenp.int16) samples samples.reshape(-1, num_channels) samples samples.astype(np.float32) / 32768.0 return { sample_rate: sample_rate, samples: samples }然后计算振幅。振幅我用的是 RMS均方根每 1024 个采样点算一个值def compute_amplitude(audio_data, window_size1024): samples audio_data[samples] mono np.mean(samples, axis1) num_windows len(mono) // window_size amplitudes np.zeros(num_windows, dtypenp.float32) for i in range(num_windows): window mono[i * window_size:(i 1) * window_size] amplitudes[i] np.sqrt(np.mean(window ** 2)) return amplitudes这个振幅数组的长度是num_windows每个值对应一段时间窗口内的能量。如果音频是 10 秒采样率 44100窗口大小 1024那么振幅数组的长度大约是 430。注意wave模块只能读 PCM 格式的 WAV 文件。如果你的音频是 MP3 或者 AAC需要先用其他工具转成 WAV。我一般用 ffmpeg 转ffmpeg -i input.mp3 -acodec pcm_s16le -ar 44100 output.wav。这个命令把音频转成 16 位 PCM采样率 44100。4.3 图像读取节点从 PNG 到浮点矩阵图像读取我用 Pillowfrom PIL import Image import numpy as np def read_image(filepath): img Image.open(filepath).convert(RGBA) data np.array(img, dtypenp.float32) / 255.0 return { width: img.width, height: img.height, channels: 4, data: data }这里把图像转成 RGBA 四通道数值范围 0.0 到 1.0。如果你的图片没有透明通道Alpha 通道会自动填充为 1.0。4.4 扭曲节点用振幅控制正弦扭曲的强度扭曲节点的核心是一个正弦位移函数。对于图像上的每个像素(x, y)我们根据振幅a计算一个新的位置def distort(image_data, amplitudes, time_index): width image_data[width] height image_data[height] data image_data[data] amplitude amplitudes[time_index % len(amplitudes)] strength amplitude * 50.0 # 把振幅映射到像素位移 frequency 0.02 y_coords, x_coords np.meshgrid( np.arange(height), np.arange(width), indexingij ) offset_x np.sin(y_coords * frequency) * strength offset_y np.cos(x_coords * frequency) * strength new_x np.clip(x_coords offset_x, 0, width - 1).astype(np.int32) new_y np.clip(y_coords offset_y, 0, height - 1).astype(np.int32) distorted data[new_y, new_x] return { width: width, height: height, channels: 4, data: distorted }这个函数的参数有两个需要调strength的系数和frequency。系数决定扭曲的最大位移频率决定扭曲的疏密。我试过系数 50、频率 0.02在 1920x1080 的图片上效果比较自然。如果系数太大图片会扭曲得看不出原样如果频率太高会出现摩尔纹。提示np.clip很重要如果不加new_x和new_y可能超出图像边界导致索引错误。另外用np.int32而不是np.int64可以减少内存占用。4.5 实时预览与导出把处理结果保存成 PNG 序列实时预览的部分前面已经讲过这里重点讲导出。导出我用 Pillow 的save方法def save_image(image_data, filepath): data (image_data[data] * 255.0).clip(0, 255).astype(np.uint8) img Image.fromarray(data, modeRGBA) img.save(filepath)如果要导出序列就在每一帧处理完之后调用save_image文件名加上帧号。比如output_0001.png、output_0002.png。导出序列的时候我建议用 PNG 而不是 JPEG。PNG 是无损的而且支持透明通道。JPEG 虽然文件小但会有压缩伪影尤其是在扭曲边缘的地方。4.6 参数调节的实操记录从“能看”到“好看”的调参过程我第一次跑通这个案例的时候效果很糟糕。图片扭曲得像被揉过的纸完全看不出原来的内容。后来我调整了几个参数第一把strength的系数从 50 降到 20。振幅本身的范围是 0 到 1乘以 50 之后最大位移是 50 像素对于 1920 宽的图片来说太大了。降到 20 之后最大位移是 20 像素看起来更自然。第二把frequency从 0.02 降到 0.01。频率太高会导致相邻像素的位移方向相反产生锯齿。降到 0.01 之后扭曲的过渡更平滑。第三加了一个时间平滑。振幅数组的相邻值可能跳变很大直接用来控制扭曲强度会导致画面闪烁。我用了一个简单的滑动平均smoothed np.convolve(amplitudes, np.ones(5)/5, modesame)这个操作把振幅数组做了 5 点滑动平均画面稳定了很多。注意调参的时候不要一次改多个参数。我习惯一次只改一个然后观察效果。如果同时改三个参数你很难判断哪个参数起了作用。另外调参的时候最好用同一张图片和同一段音频这样才有可比性。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 渲染窗口黑屏从上下文创建到着色器编译的排查路径黑屏是 moderngl 最常见的问题。我遇到过好几次每次原因都不一样。下面是我总结的排查路径现象可能原因排查方法窗口全黑无报错着色器编译失败但没检查调用program ctx.program(...)后检查program是否为空窗口全黑有报错顶点数据没上传检查vbo是否绑定了正确的数据窗口显示纯色纹理没上传检查texture.write(data)是否执行窗口显示花屏纹理格式不匹配检查texture的components和dtype是否和data一致我踩过最坑的一次是着色器编译失败但 moderngl 默认不抛异常只是返回一个空程序。后来我在创建程序之后加了一行assert program问题立刻暴露了。5.2 性能瓶颈什么时候该用 GPU什么时候该用 CPU“artcraft”这种管线式环境性能瓶颈通常出现在两个地方数据上传和节点计算。数据上传的瓶颈在于每次节点输出变化都要把数据从 CPU 内存传到 GPU 显存。如果数据量很大比如 4K 图片上传一次可能要几十毫秒。这时候可以考虑用 PBOPixel Buffer Object做异步上传或者把多个节点的计算合并到一个着色器里。节点计算的瓶颈在于如果某个节点的计算量很大而且是在 CPU 上跑的会拖慢整个管线。比如前面提到的扭曲节点用 numpy 做索引操作在 1920x1080 的图片上大概要 20 毫秒。如果每秒要处理 30 帧这个节点就占了 60% 的时间。这时候可以考虑把扭曲逻辑写成着色器在 GPU 上跑。GPU 做这种逐像素的操作速度会快很多。我一般的判断标准是如果某个节点的计算复杂度是 O(像素数)而且每个像素的计算是独立的那就放到 GPU 上。如果计算涉及全局统计或者复杂的依赖关系那就留在 CPU 上。5.3 内存泄漏节点反复执行时的资源释放管线式环境里节点会被反复执行。如果节点里创建了临时对象但没有释放内存会慢慢涨上去。我遇到过几次内存泄漏最后发现是 numpy 数组没有及时释放。Python 的垃圾回收机制一般能处理这种情况但如果数组被全局变量或者闭包引用了就不会被回收。我的做法是在节点处理完之后显式地把临时数组置为Nonedef process(self): temp np.zeros((1000, 1000), dtypenp.float32) # ... 使用 temp ... result temp.copy() temp None # 显式释放 return result另外如果节点里创建了 GPU 资源比如纹理、缓冲区一定要在节点销毁的时候释放。moderngl 的资源对象有release()方法记得调用。5.4 跨平台差异Windows、macOS、Linux 上的表现对比我在三个平台上都跑过类似的管线差异主要体现在 OpenGL 版本和窗口管理上。Windows 上OpenGL 的驱动支持最好一般都能跑到 4.6。窗口管理用 moderngl-window 的默认后端就行不需要额外配置。macOS 上OpenGL 最高只到 4.1而且苹果已经标记为废弃。如果用到 4.2 以上的特性会直接报错。另外macOS 的 Retina 屏幕会导致窗口尺寸和实际像素尺寸不一致需要手动处理pixel_ratio。Linux 上OpenGL 的版本取决于显卡驱动。NVIDIA 的闭源驱动一般支持到 4.6开源的 Mesa 驱动可能只到 4.5。窗口管理用 GLFW 后端比较稳但需要确保安装了libglfw3。提示如果你要在多个平台上发布建议把 OpenGL 版本锁定在 3.3 核心模式。这个版本在所有平台上都支持而且功能足够用。着色器里不要用layout(location ...)这种 4.1 以上才支持的语法改用glBindAttribLocation。5.5 常见问题速查表问题可能原因解决方法程序启动就崩溃缺少 OpenGL 上下文检查显卡驱动确保支持 OpenGL 3.3参数调节没反应imgui 回调没绑定检查imgui.slider_float的返回值是否写回了节点图像颜色不对色彩空间不匹配检查是否做了 sRGB 到线性空间的转换音频和图像不同步时间基准不一致用同一个时钟源驱动音频和图像的帧号导出序列丢帧文件写入阻塞用异步写入或者先写内存再批量保存6. 扩展方向从“能跑”到“好用”的几个思路6.1 节点库的模块化把常用功能封装成可复用的节点当你写了十几个节点之后会发现有些节点的逻辑很相似。比如“模糊”节点和“锐化”节点都是卷积操作只是卷积核不同。这时候可以把它们抽象成一个“卷积”节点卷积核作为参数传入。模块化的另一个好处是你可以把节点库单独打包分享给别人。别人只需要安装你的节点库就能在自己的管线里使用这些节点。我一般会把节点库放在一个单独的目录里每个节点一个文件文件名就是节点名。然后写一个__init__.py自动扫描目录注册所有节点。6.2 预设系统把调好的参数保存下来调参是一件很耗时的事情。有时候你调出一个很好的效果但过几天就忘了参数是多少。这时候预设系统就很有用。预设系统很简单把当前所有节点的参数序列化成一个 JSON 文件保存下来。下次打开的时候读取 JSON把参数写回节点。我一般会在预设文件里加上时间戳和备注方便以后查找。{ name: audio_distort_v1, timestamp: 2025-01-15T10:30:00, nodes: { distort: { strength: 20.0, frequency: 0.01 } } }6.3 批处理模式从交互式到自动化的过渡交互式调参适合探索但如果你要处理一百张图片一张一张调就太慢了。这时候需要批处理模式。批处理模式的核心是把管线里的交互式参数替换成从配置文件读取的参数然后循环处理输入文件。我一般会写一个batch_process.py脚本读取一个 YAML 配置文件然后调用管线里的节点批量处理。input_dir: ./inputs output_dir: ./outputs audio_file: ./audio.wav params: strength: 20.0 frequency: 0.01这个脚本不需要 GUI可以在服务器上跑。处理速度比交互式快很多因为不需要每帧渲染。6.4 与其他工具的对接导出成标准格式“artcraft”的最终输出不一定是图片或者视频。有时候你需要把数据导出成其他工具能读的格式。比如导出成 OBJ 给三维软件用导出成 SVG 给矢量软件用导出成 CSV 给数据分析工具用。我一般会在输出层加一个“格式转换”节点根据文件扩展名自动选择导出格式。比如.obj走 OBJ 导出器.svg走 SVG 导出器.csv走 CSV 导出器。这样你不需要改管线只需要改输出文件名。6.5 社区化思路把节点分享出去如果你觉得自己的节点写得不错可以考虑分享出去。分享的方式有很多种可以发到代码托管平台可以打包成 Python 包也可以写一篇教程把节点的实现思路讲清楚。我个人的经验是分享节点的时候一定要附上示例数据和预期结果。别人拿到你的节点最想知道的是“这个节点能做什么”。如果只有代码没有示例别人很难判断这个节点是否适合他的场景。最后再分享一个小技巧如果你在调参的时候发现某个参数怎么调都不对不妨把参数范围扩大十倍试试。我遇到过好几次参数范围设得太窄最佳值在范围之外怎么调都调不到。把范围扩大之后很快就找到了合适的值。这个技巧在调噪声频率、扭曲强度、色彩映射曲线的时候特别有用。