智能缩放(Seam Carving)
Rust + WebAssembly 用动态规划找出图中最不重要的像素缝,缩放时保持主体不变形
智能缩放
普通缩放(整体压扁或拉伸)
拖动「尺寸」滑块缩小或放大 | 切到「标记区域」后用 🖱️ / 👆 涂抹:绿色保护,红色优先删除(可以把物体从图中抹掉)
📖 原理说明
普通缩放把每个像素一视同仁,图片变窄时人物和建筑一起被压扁。Seam Carving(Avidan 与 Shamir,2007)换了个思路:每次从上到下找一条「最不重要」的像素缝,一行删一个像素,删掉它图片就窄了一列。天空、海面、沙滩这些平坦的区域会先被删掉,灯塔、帆船这些有明显边缘的物体保持原样。
🧮算法原理
e(x, y) = |∇xI|² + |∇yI|²能量:左右和上下相邻像素的颜色差,边缘处能量高
M(x, y) = e(x, y) + min(M(x−1, y−1), M(x, y−1), M(x+1, y−1))动态规划:到达每个像素的最小累计能量,从最后一行的最小值往回走就得到缝
- 缝
- 从第一行到最后一行、每行一个像素、相邻两行最多左右错开一格的路径,所以删掉之后每行都少一个像素,图片仍然是规整的矩形。所有这样的路径多达 3高度 条,动态规划只需把每个像素算一遍,就能找出累计能量最小的那条。
- 预先计算删除顺序
- 加载图片后连续删掉宽度一半的缝(640 像素宽就是 320 条),每删一条都重新找下一条,并给每个原始像素记下它在第几轮被删掉。之后拖动滑块到任意宽度,只要保留「第 k 轮以后才被删」的像素,瞬间就能得到结果,不必重新计算。「缝的位置」视图就是把前 k 轮的像素涂成红色。
- 放大
- 把图片变宽时,找出前 k 条最不重要的缝,在每条缝旁边插入一个取左右平均色的新像素。平坦区域被拉宽,物体依然保持原样。
- 保护与删除
- 涂成绿色的像素能量加上一个极大值,缝不会经过它们;涂成红色的像素能量减去一个极大值,缝会优先穿过它们。把一个物体涂红,刚好删掉所有红色像素所需的缝数就是它的宽度,页面会自动把尺寸设到这个值,物体就从画面中消失了。
- 只更新缝附近
- 删掉一条缝后,只有它两侧的像素换了邻居,所以只重算这些像素的能量;累计能量表则每次都要整张重算,这是主要开销。
🔄Rust 与 JavaScript 的分工
- JS示例图由 Canvas 程序化绘制,上传的图片先缩小到不超过 640×480。JS 把像素和涂抹标记传给
sc_load(像素, 宽, 高, 是否改变高度, 标记)。
- RustJS 每一帧调用
sc_carve(24),Rust 再找出并删除 24 条缝,记录每个像素的删除轮次,页面同步显示进度。改变高度时,Rust 先把图片转置,把行当成列来处理。
- JS拖动滑块时调用
sc_render(k)(k 为正时删缝,为负时插缝)取回结果,下方用浏览器的普通缩放画出同样尺寸的对比图。
⚡性能要点
- 实测(桌面 Chromium):640×400 的示例图算出全部 320 条缝约 250ms,平均每条不到 1ms(第一批约 2ms,图片越窄越快);之后拖动滑块,每次生成结果约 1.6ms,所以滑块跟手。
- Seam Carving 并不是万能的:画面里没有空白区域可删、或者缝不得不穿过有规则结构的物体(比如直线、人脸)时会出现扭曲;删除物体后,缝经过的地方偶尔能看到细小的接缝(示例中的海浪)。
源码crates/media/seam-carving/src/lib.rswww/seam-carving/index.js