Rust图像处理第13节-任意角度旋转:旋转矩阵 + 双线性插值 + 自动补白边

🦀 Rust + WASM 实战系列 第 13 篇 阅读时间:约 10 分钟 | 实战可运行

📌 写在前面

上一篇做的 90° 旋转只动"整数坐标",是特例

这一篇进入任意角度(30°、45°、任意小数)。三个新问题一下子全冒出来:

  1. 小数坐标:旋转 30° 后,原图 (0, 0) 落在 (0.866, 0.5),怎么取色?
  2. 越界 :旋转后的图比原图大,空白处填什么颜色?
  3. 反向映射:原图旋转后可能落在画布外,要从画布反推原图坐标。

这一篇把这三个问题一次解决,旋转矩阵正式登场


🚀 TL;DR

关键点 一句话
旋转矩阵 R= cos⁡θ −sin⁡θ sin⁡θ cos⁡θ R = \begin{bmatrix}\cos\theta & -\sin\theta \\ \sin\theta & \cos\theta\end{bmatrix} R=cosθsinθ−sinθcosθ
反向映射 反向时用 RTR^T RT(正交矩阵的逆 = 转置)
画布尺寸 $w' =
小数坐标 双线性插值 = 4 像素加权平均
越界处理 填白色 (255, 255, 255)

核心理念:和上一篇一样的"反向映射",只是这次矩阵登场 + 加了插值。


📖 目录

  1. [为什么 90° 是特例?](#为什么 90° 是特例? "#%E4%B8%80%E4%B8%BA%E4%BB%80%E4%B9%88-90-%E6%98%AF%E7%89%B9%E4%BE%8B")
  2. 旋转矩阵:从公式到代码
  3. [新画布尺寸:4 顶点包围盒](#新画布尺寸:4 顶点包围盒 "#%E4%B8%89%E6%96%B0%E7%94%BB%E5%B8%83%E5%B0%BA%E5%AF%B84-%E9%A1%B6%E7%82%B9%E5%8C%85%E5%9B%B4%E7%9B%92")
  4. 反向映射:以中心为锚点
  5. [插值:最近邻 vs 双线性](#插值:最近邻 vs 双线性 "#%E4%BA%94%E6%8F%92%E5%80%BC%E6%9C%80%E8%BF%91%E9%82%BB-vs-%E5%8F%8C%E7%BA%BF%E6%80%A7")
  6. 关键代码
  7. 前端效果展示
  8. 性能对比
  9. 踩坑提醒
  10. 下篇预告

一、为什么 90° 是特例?

维度 90° 旋转 任意角度旋转
坐标 整数 小数(含三角函数值)
采样 直接拷贝 需要插值
越界 不存在(4 顶点仍在边界) 必然越界
画布尺寸 互换(h × w) 需要重新计算
工具 一个if + 减法 旋转矩阵 + 插值

90° 是特例的关键:三角函数值都是 0 / ±1 ,所以坐标永远是整数;而任意角度的 sin⁡θ\sin\theta sinθ / cos⁡θ\cos\theta cosθ 是小数。


二、旋转矩阵:从公式到代码

2D 旋转矩阵

R(θ)= cos⁡θ −sin⁡θ sin⁡θ cos⁡θ R(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix} R(θ)=cosθsinθ−sinθcosθ

把点 (x,y)(x, y) (x,y) 绕原点旋转 θ\theta θ(逆时针为正):
x′ y′ =R(θ)⋅ x y = xcos⁡θ−ysin⁡θ xsin⁡θ+ycos⁡θ \begin{bmatrix} x' \\ y' \end{bmatrix} = R(\theta) \cdot \begin{bmatrix} x \\ y \end{bmatrix} = \begin{bmatrix} x\cos\theta - y\sin\theta \\ x\sin\theta + y\cos\theta \end{bmatrix} x′y′=R(θ)⋅xy=xcosθ−ysinθxsinθ+ycosθ

⚠️ 数学上逆时针为正 ,但图像坐标 y 轴向下,所以很多库(如 CSS、PIL)定义为顺时针为正 。本篇用 JS 习惯:顺时针为正

在 nalgebra 里

rust 复制代码
use nalgebra::{Matrix2, Vector2};

let theta = angle_deg.to_radians();   // 度数 → 弧度
let c = theta.cos();
let s = theta.sin();
let r = Matrix2::new(c, -s, s, c);    // [[cos, -sin], [sin, cos]]

// 应用:把向量 (dx, dy) 旋转
let v = Vector2::new(dx, dy);
let rotated = r * v;                  // Matrix2 × Vector2 = Vector2

nalgebra 的 Matrix2 * Vector2 自动用 SIMD 优化,速度和手写循环一样。


三、新画布尺寸:4 顶点包围盒

旋转后图片会"撑大"------必须用 4 个顶点旋转后的最大包围盒

arduino 复制代码
原图 4 个顶点(相对中心):
A = (-w/2, -h/2)    B = ( w/2, -h/2)
D = (-w/2,  h/2)    C = ( w/2,  h/2)

旋转 θ 后(举例 θ=30°):
        A' = (-w/2 cos + h/2 sin, -w/2 sin - h/2 cos)
            B' = ( w/2 cos + h/2 sin,  w/2 sin - h/2 cos)
            ...

公式怎么来的?(max − min)

关键认知w' 不是神秘公式,而是"4 个旋转后顶点的 x 坐标最大跨度":
w′=max⁡(xA′,xB′,xC′,xD′)−min⁡(xA′,xB′,xC′,xD′)w' = \max(x'_A, x'_B, x'_C, x'_D) - \min(x'_A, x'_B, x'_C, x'_D) w′=max(xA′,xB′,xC′,xD′)−min(xA′,xB′,xC′,xD′)
h′=max⁡(yA′,yB′,yC′,yD′)−min⁡(yA′,yB′,yC′,yD′)h' = \max(y'_A, y'_B, y'_C, y'_D) - \min(y'_A, y'_B, y'_C, y'_D) h′=max(yA′,yB′,yC′,yD′)−min(yA′,yB′,yC′,yD′)

先看 x 坐标(假设 θ 在 0°~90° 之间,cos θ > 0,sin θ > 0):

顶点 旋转后的 x'
A (-w/2, -h/2) −w2cos⁡θ+h2sin⁡θ -\frac{w}{2}\cos\theta + \frac{h}{2}\sin\theta −2wcosθ+2hsinθ
B ( w/2, -h/2) +w2cos⁡θ+h2sin⁡θ +\frac{w}{2}\cos\theta + \frac{h}{2}\sin\theta +2wcosθ+2hsinθ ← 最大
C ( w/2, h/2) +w2cos⁡θ−h2sin⁡θ +\frac{w}{2}\cos\theta - \frac{h}{2}\sin\theta +2wcosθ−2hsinθ
D (-w/2, h/2) −w2cos⁡θ−h2sin⁡θ -\frac{w}{2}\cos\theta - \frac{h}{2}\sin\theta −2wcosθ−2hsinθ ← 最小
arduino 复制代码
x 坐标轴上 4 个点的位置:

←D═══════════A═════════C═══════════B→
               ↑ 最左 D                    ↑ 最右 B
       -w/2 cos - h/2 sin          +w/2 cos + h/2 sin

w' = 最右 − 最左
w′ = (w2cos⁡θ+h2sin⁡θ) − (−w2cos⁡θ−h2sin⁡θ) =wcos⁡θ+hsin⁡θ \begin{aligned} w' &= \left(\frac{w}{2}\cos\theta + \frac{h}{2}\sin\theta\right) - \left(-\frac{w}{2}\cos\theta - \frac{h}{2}\sin\theta\right) \\ &= w\cos\theta + h\sin\theta \end{aligned} w′=(2wcosθ+2hsinθ)−(−2wcosθ−2hsinθ)=wcosθ+hsinθ

y 坐标同理(假设 0°~90°):

顶点 旋转后的 y'
A −w2sin⁡θ−h2cos⁡θ -\frac{w}{2}\sin\theta - \frac{h}{2}\cos\theta −2wsinθ−2hcosθ ← 最小
B +w2sin⁡θ−h2cos⁡θ +\frac{w}{2}\sin\theta - \frac{h}{2}\cos\theta +2wsinθ−2hcosθ
C +w2sin⁡θ+h2cos⁡θ +\frac{w}{2}\sin\theta + \frac{h}{2}\cos\theta +2wsinθ+2hcosθ ← 最大
D −w2sin⁡θ+h2cos⁡θ -\frac{w}{2}\sin\theta + \frac{h}{2}\cos\theta −2wsinθ+2hcosθ

h′=wsin⁡θ+hcos⁡θh' = w\sin\theta + h\cos\theta h′=wsinθ+hcosθ

处理所有角度(加绝对值)

上面的推导假设 cos θ > 0 且 sin θ > 0(即 θ 在 0°~90°)。

θ 在其他区间时,最大最小会换位置,但差值仍然是同样的表达式统一用绝对值

关键公式(取最大绝对值):
w′ =∣wcos⁡θ∣+∣hsin⁡θ∣ h′ =∣wsin⁡θ∣+∣hcos⁡θ∣ \begin{aligned} w' &= |w\cos\theta| + |h\sin\theta| \\ h' &= |w\sin\theta| + |h\cos\theta| \end{aligned} w′h′=∣wcosθ∣+∣hsinθ∣=∣wsinθ∣+∣hcosθ∣

3 个特殊角度验证

θ w' h' 物理意义
w h 原图不动 ✓
90° h w 宽高互换 ✓
45°(正方形) w2 w\sqrt{2} w2 w2 w\sqrt{2} w2 对角线长度(包围盒 = 正方形的对角线)✓

直觉:旋转 45° 时画布变 2 \sqrt{2} 2 倍;旋转 90° 时画布变 h × w(退化为上一篇的结论)。

rust 复制代码
let new_w = ((w as f64) * c.abs() + (h as f64) * s.abs()).round() as usize;
let new_h = ((w as f64) * s.abs() + (h as f64) * c.abs()).round() as usize;

四、反向映射:以中心为锚点

为什么要求"逆"?(搞清 M / M⁻¹ 的方向)

我们做图像变换的流程:

scss 复制代码
正向(概念上):src(原图) ──── M ────→ dst(变换后)
                原图像素        变换       目标像素

实现时不能正向遍历------之前 §六"坐标映射的几种思路"讲过:

  • 反向遍历:对每个 dst 像素求"应该来自 src 哪里"------无空洞
  • ❌ 正向遍历:可能有空洞 / 漏点

所以需要 M−1M^{-1} M−1

方向 公式 用什么
正向:src → dst dst=M⋅src\text{dst} = M \cdot \text{src} dst=M⋅src MM M
反向:dst → src src=M−1⋅dst\text{src} = M^{-1} \cdot \text{dst} src=M−1⋅dst M−1M^{-1} M−1 ← 这里求逆

旋转为什么能用 RTR^T RT 代替 R−1R^{-1} R−1?

旋转矩阵是正交矩阵,所以:
R−1=RT(正交矩阵的特殊性质) R^{-1} = R^T \quad \text{(正交矩阵的特殊性质)} R−1=RT(正交矩阵的特殊性质)

两者完全等价 ------ RTR^T RT 就是 R−1R^{-1} R−1,只是计算更简单:

操作 复杂度
r.transpose() O(1)(2×2 矩阵直接交换对角元素)
r.try_inverse() O(n³)(高斯消元 / 伴随矩阵)

一张图把因果链串起来

ini 复制代码
我们想做的事:反向映射(对每个 dst 找 src)
        ↓
需要 M⁻¹
        ↓
旋转矩阵是正交矩阵
        ↓
M⁻¹ = M^T(转置就行,不用真求逆)
        ↓
代码:let r_inv = r.transpose();

几何直觉验证

把点正向转 30°,再反向转 30° = 回到原位:

rust 复制代码
let v = Vector2::new(1.0, 0.0);   // (1, 0)
let v_rotated = r * v;             // 旋转 30° → (0.866, 0.5)
let v_back = r_inv * v_rotated;    // 反向 30° → (1, 0) ✓

数学推导

旋转围绕图片中心,所以映射要分三步:

r 复制代码
① dst 相对新画布中心 → ② 应用 R^T → ③ 相对原图中心 → 加上 w/2, h/2

数学推导

正向旋转(围绕图片中心 (w/2,h/2)(w/2, h/2) (w/2,h/2)):
x′ y′ =R x−w/2 y−h/2 + w/2 h/2 \begin{bmatrix} x' \\ y' \end{bmatrix} = R \begin{bmatrix} x - w/2 \\ y - h/2 \end{bmatrix} + \begin{bmatrix} w/2 \\ h/2 \end{bmatrix} x′y′=Rx−w/2y−h/2+w/2h/2

反向映射(已知 dst,求 src):
x−w/2 y−h/2 =RT x′−w′/2 y′−h′/2 \begin{bmatrix} x - w/2 \\ y - h/2 \end{bmatrix} = R^T \begin{bmatrix} x' - w'/2 \\ y' - h'/2 \end{bmatrix} x−w/2y−h/2=RTx′−w′/2y′−h′/2
srcx=R00T(x′−w′/2)+R01T(y′−h′/2)+w/2 \text{src}x = R^T{00}(x'-w'/2) + R^T_{01}(y'-h'/2) + w/2 srcx=R00T(x′−w′/2)+R01T(y′−h′/2)+w/2

💡 为什么是 RTR^T RT? 旋转矩阵是正交矩阵 (行向量两两正交 + 单位长度),所以它的逆 = 自己的转置。算转置比算逆简单一万倍------这是正交矩阵的核心优势。

代码

rust 复制代码
let r_inv = r.transpose();   // [[cos, sin], [-sin, cos]]

for y in 0..new_h {
    for x in 0..new_w {
        // ① dst 相对新画布中心
        let dx = x as f64 - nw2;
        let dy = y as f64 - nh2;

        // ② 应用 R^T
        let src_delta = r_inv * Vector2::new(dx, dy);

        // ③ 加上原图中心,得到 src 坐标
        let src_x = src_delta.x + w2;
        let src_y = src_delta.y + h2;
    
        // src_x, src_y 现在是小数(甚至可能是负数)
        // 接下来:越界?插值?
    }
}

五、插值:最近邻 vs 双线性

问题:小数坐标怎么取色?

假设 src_x = 3.7, src_y = 5.2第 (3.7, 5.2) 个像素的颜色是什么?

方法 1:最近邻(nearest)

直接四舍五入

rust 复制代码
let sx = src_x.round() as usize;   // 3.7 → 4
let sy = src_y.round() as usize;   // 5.2 → 5

结果:从 P(4, 5) 取色。

  • ✅ 1 行代码,极快
  • ❌ 30° 以上明显锯齿("马赛克感")

方法 2:双线性(bilinear)

周围 4 像素按距离加权平均

text 复制代码
P(x0, y0) ── fx ── P(x1, y0)
   │          ·          │
   fy        ·dst        │
   │          ·          │
P(x0, y1) ── fx ── P(x1, y1)

x0 = floor(src_x) = 3,   fx = 0.7
y0 = floor(src_y) = 5,   fy = 0.2
x1 = 4
y1 = 6

公式
color=(1−fx)(1−fy)P00+fx(1−fy)P10+(1−fx)fyP01+fxfyP11\text{color} = (1-f_x)(1-f_y)P_{00} + f_x(1-f_y)P_{10} + (1-f_x)f_y P_{01} + f_x f_y P_{11} color=(1−fx)(1−fy)P00+fx(1−fy)P10+(1−fx)fyP01+fxfyP11

代码

rust 复制代码
let x0 = src_x.floor() as usize;
let y0 = src_y.floor() as usize;
let x1 = (x0 + 1).min(w - 1);     // 边界 clamp
let y1 = (y0 + 1).min(h - 1);
let fx = src_x - x0 as f64;
let let fy = src_y - y0 as f64;

let w00 = (1.0 - fx) * (1.0 - fy);  // P00 的权重
let w01 = fx * (1.0 - fy);          // P10 的权重
let w10 = (1.0 - fx) * fy;          // P01 的权重
let w11 = fx * fy;                  // P11 的权重

for c in 0..4 {  // RGBA 4 通道独立计算
    let v = pixels[i00 + c] as f64 * w00
          + pixels[i01 + c] as f64 * w01
          + pixels[i10 + c] as f64 * w10
          + pixels[i11 + c] as f64 * w11;
    result[dst_idx + c] = v.round().clamp(0.0, 255.0) as u8;
}
  • ✅ 平滑无锯齿,工业标准
  • ❌ 4 倍计算量(4 个像素 × 4 通道)

直观对比

ini 复制代码
原图一角(4 个像素):
  P00 = 白  P10 = 黑
  P01 = 黑  P11 = 白

src 落在 (0.5, 0.5) 正中间:

  nearest → 任取一个(白或黑,锯齿)
  bilinear → 4 像素平均 = 灰(平滑)

六、关键代码

完整 rotate 函数

rust 复制代码
#[wasm_bindgen]
pub fn rotate(
    pixels: &[u8],
    width: u32,
    height: u32,
    angle_deg: f64,
    method: &str,
) -> Vec<u8> {
    let w = width as usize;
    let h = height as usize;

    // 1. 度数 → 弧度 + 三角函数
    let theta = angle_deg.to_radians();
    let c = theta.cos();
    let s = theta.sin();

    // 2. 旋转矩阵 R
    let r = Matrix2::new(c, -s, s, c);

    // 3. 新画布尺寸
    let new_w = ((w as f64) * c.abs() + (h as f64) * s.abs()).round() as usize;
    let new_h = ((w as f64) * s.abs() + (h as f64) * c.abs()).round() as usize;

    // 4. 中心点
    let w2 = w as f64 / 2.0;
    let h2 = h as f64 / 2.0;
    let nw2 = new_w as f64 / 2.0;
    let nh2 = new_h as f64 / 2.0;

    // 5. R^T
    let r_inv = r.transpose();

    let pixel_len = new_w * new_h * 4;
    let mut result = vec![0u8; pixel_len + 8];

    for y in 0..new_h {
        for x in 0..new_w {
            // 反向映射
            let dx = x as f64 - nw2;
            let dy = y as f64 - nh2;
            let src_delta = r_inv * Vector2::new(dx, dy);
            let src_x = src_delta.x + w2;
            let src_y = src_delta.y + h2;

            let dst_idx = (y * new_w + x) * 4;

            // 越界 → 白色
            if src_x < 0.0 || src_x >= w as f64 - 1.0
                || src_y < 0.0 || src_y >= h as f64 - 1.0
            {
                result[dst_idx]     = 255;
                result[dst_idx + 1] = 255;
                result[dst_idx + 2] = 255;
                result[dst_idx + 3] = 255;
                continue;
            }

            // 在边界内 → 插值
            if method == "bilinear" {
                sample_bilinear(pixels, w, h, src_x, src_y, &mut result, dst_idx);
            } else {
                sample_nearest(pixels, w, h, src_x, src_y, &mut result, dst_idx);
            }
        }
    }

    // 末尾 8 字节写新尺寸
    result[pixel_len..pixel_len + 4].copy_from_slice(&(new_w as u32).to_le_bytes());
    result[pixel_len + 4..pixel_len + 8].copy_from_slice(&(new_h as u32).to_le_bytes());

    result
}

七、前端效果展示


八、性能对比

测试条件:1024×768 原图,旋转 30°

插值方式 单次耗时 视觉效果
nearest ~15 ms 30°+ 锯齿明显
bilinear ~45 ms 平滑无锯齿

3 倍速度差,几乎所有"现代图像处理库"默认都用双线性------因为人眼对锯齿极度敏感。

如果性能敏感(如实时视频),可以考虑:

  • 三次样条 / 双三次插值(更平滑但更慢,~5x)
  • SIMD 优化:手写 AVX2 / NEON,但代码复杂度爆炸
  • 分块 + 多线程:把图像分成 4 块并行处理(WASM threads)

九、踩坑提醒

1. 边界判断 >= w - 1.0 而非 >= w

rust 复制代码
// 错
if src_x >= w as f64 { ... }  // src_x = w - 0.5 时仍合法(要插值)

// 对
if src_x >= w as f64 - 1.0 { ... }  // 因为双线性要取 x0 + 1

原因:双线性插值要取 (x0, x0 + 1) 两个像素,所以 src_x = w - 1 是合法的 (x0 = w-1, x1 = w-1,clamp 一下),但 src_x = w - 0.5 也是合法的。

2. 双线性 x1 = (x0 + 1).min(w - 1) 必加

rust 复制代码
let x1 = (x0 + 1).min(w - 1);  // 防止 x0 = w - 1 时 x1 = w 越界

3. 角度单位:度 vs 弧度

  • 人类:用度(30°、90°)
  • math.cos / math.sin:用弧度(π/6、π/2)
rust 复制代码
let theta = angle_deg.to_radians();   // 度 → 弧度

忘了一步,旋转结果完全错乱(角度变成"几十度"或"几度",因为 2π2\pi 2π 和 360° 差了 57 倍)。

4. y 轴方向

  • 数学:y 向上
  • 图像:y 向下
  • 结果:数学上的"逆时针正方向"在图像里变成"顺时针"

这就是为什么很多图像库的 rotate(positive_angle) 是顺时针的。本篇用 JS 习惯:顺时针为正

5. 旋转中心不一定是图片中心

本篇默认是图片中心。如果想围绕其他点旋转(如 (0, 0) 左上角),反向映射公式会不同:
src=RT(dst−c)+c\text{src} = R^T (\text{dst} - c) + c src=RT(dst−c)+c

其中 cc c 是旋转中心。


十、下篇预告

任务 14:缩放 + 斜切

旋转搞定了,"线性几何变换三件套"还差两件

  • 缩放 (放大 / 缩小): S= sx 0 0 sy S = \begin{bmatrix} s_x & 0 \\ 0 & s_y \end{bmatrix} S=sx00sy
  • 斜切 (shear): H= 1 k 0 1 H = \begin{bmatrix} 1 & k \\ 0 & 1 \end{bmatrix} H=10k1

这两件比旋转简单(不用三角函数),但有个新概念------缩放比例不是 1 时也要插值(和旋转一模一样的问题)。

下篇还有个杀手锏:把旋转 + 缩放 + 平移组合成一个矩阵:
T⋅R⋅S= cos⁡θ⋅sx −sin⁡θ⋅sy tx sin⁡θ⋅sx cos⁡θ⋅sy ty 0 0 1 T \cdot R \cdot S = \begin{bmatrix} \cos\theta \cdot s_x & -\sin\theta \cdot s_y & t_x \\ \sin\theta \cdot s_x & \cos\theta \cdot s_y & t_y \\ 0 & 0 & 1 \end{bmatrix} T⋅R⋅S= cosθ⋅sxsinθ⋅sx0−sinθ⋅sycosθ⋅sy0txty1

这就是 3×3 齐次坐标矩阵------后面 16 篇(到第 5 部分的 PCA、回归)都会用到它。


🎁 写在最后

任务 12 是"特例",任务 13 才是"通用"。

任务 矩阵 插值 越界处理 中心
12 不用 不用 不会发生 N/A
13 Matrix2 nearest / bilinear 填白边 图片中心
14 Matrix3 (齐次) bilinear 填白边 可配置

关键认知:这一篇把"反向映射 + 插值"这套打法建立起来了。后面所有几何变换(缩放 / 斜切 / 鱼眼 / 透视)都长一个样:

  1. 算出反向映射函数(矩阵 / 非线性公式)
  2. 对每个 dst 像素,反推 src 坐标
  3. 越界 → 填背景色;边界内 → 插值取色

线代工具箱正式登场


📦 项目地址pixel-math-wasm 🦀 Rust + WebAssembly 实战系列


🏷️ 标签#Rust #WebAssembly #图像处理 #几何变换 #旋转矩阵 #双线性插值 #nalgebra #算法

相关推荐
SmalBox4 小时前
【节点】[IrisUVLocation节点]原理解析与实际应用
unity3d·游戏开发·图形学
梦帮科技5 小时前
GRAVIS v5.5:向硬核桌面端进化与云端多节点容灾部署实践
windows·架构·rust·自动化·区块链·智能合约·数字货币
梦醒沉醉6 小时前
2、Rust程序设计语言——常见编程概念
rust
程序员爱钓鱼7 小时前
Rust 开发环境安装(Windows|macOS|Linux)
后端·rust
花褪残红青杏小17 小时前
Rust图像处理第12节-图片翻转与旋转:坐标重映射几何变换
rust·webassembly·图形学
CodeStats1 天前
【编程语言】深度梳理C/C++、Java、Python、Go、Rust的区别
java·linux·c语言·c++·python·rust·go
程序员爱钓鱼1 天前
为什么学习 Rust?Rust 能做什么?
后端·rust
SmalBox1 天前
【节点】[IrisOutOfBoundColorClamp节点]原理解析与实际应用
unity3d·游戏开发·图形学
米沙AI1 天前
Rust001--08字符串高级操作
rust