欢迎光临
我们一直在努力

图像运算、掩膜/ROI 与绘制交互

图像运算、掩膜/ROI 与绘制交互

目标:掌握「在图像上做代数/逻辑运算」和「局部处理」这两类日常操作,
会画标注、会做鼠标交互调试,并且知道 cv::putText 画不了中文这件事的真相。

写作基线:OpenCV 4.13.0。本篇的数值与配图均为本机实测。


一、算术运算:溢出不报错,这才是最危险的

1.1 一个问题

一张灰度图,你想让整体亮一点。最自然的写法是写个循环:

cv::Mat img = ...; // CV_8UC1,像素值 0-255

for (int r = 0; r < img.rows; ++r) {
uchar* p = img.ptr<uchar>(r);
for (int c = 0; c < img.cols; ++c) {
p[c] = p[c] + 100; // ← 这里有个严重的 bug
}
}

p[c] 是 unsigned char。当 p[c] 是 200 时会发生什么?

  • C++ 的整数提升规则把 p[c] 提升为 int,加法在 int 上算,得到 300
  • 把 300 赋回 unsigned char —— 这是模 256 的截断,得到 44
  • 这不是未定义行为,是标准明确规定的回绕。 也正因为它是"定义良好的",编译器和运行时都不会给你任何提示。

    于是你的图不是变亮了,而是超过 255 的地方全部变成黑色。

    ⚠️ 顺带说明:cv::Mat 之间的 operator+(走 MatExpr)是饱和的,因为它内部调用了 cv::add。
    所以「用运算符」在 C++ 里通常没问题。真正的危险都在手写循环里 —— 也就是上面这种写法。
    本文因此不讨论「该不该用运算符」,只讲循环里的溢出。

    1.2 实测对比

    在这里插入图片描述

    图 1:cv2.add 与 numpy 加法的区别(OpenCV 4.13.0 实测)。本机实测第 200 列像素:原图 200 | cv2.add 255 | numpy 44。

    看中间那张和下面那张的差别 —— 一个平滑地从白过渡到黑(正确),一个突然跳变成黑色(错误)。这个跳变就是溢出的痕迹。

    1.3 正确做法:用 OpenCV 的运算函数

    cv::Mat img = ...; // CV_8UC1

    cv::Mat brighter;
    cv::add(img, 100, brighter); // ✅ 超过 255 的部分饱和到 255

    // 或者用标量
    cv::add(img, cv::Scalar(100), brighter);

    // 也可以用带缩放的加法(常用于加权叠加)
    cv::addWeighted(a, 0.7, b, 0.3, 0.0, dst); // dst = 0.7*a + 0.3*b + 0

    cv::add 内部做了 saturate_cast —— 超出范围的值被截断到边界,而不是回绕。这就是这些函数存在的理由。

    1.4 运算函数对照表

    用途C++ 写法等价数学
    加 cv::add(a, b, dst) dst = a + b(饱和)
    加权和 cv::addWeighted(a, α, b, β, γ, dst) dst = αa + βb + γ
    减 cv::subtract(a, b, dst) dst = a – b(饱和到 0)
    乘 cv::multiply(a, b, dst) dst = a * b
    除 cv::divide(a, b, dst) dst = a / b
    带缩放 cv::divide(a, b, dst, scale) dst = scale * a / b
    绝对差 cv::absdiff(a, b, dst) dst = |a – b|
    取反 cv::bitwise_not(a, dst) dst = ~a
    逐元素最值 cv::max(a, b, dst) / cv::min(a, b, dst) 逐元素取最大/最小

    cv::absdiff 在缺陷检测里极其常用 —— 它是「两张图相减找差异」的标准工具,不会因为顺序不同而得到负值。

    1.5 手动 saturate_cast

    如果你确实要写循环,用 cv::saturate_cast<T>():

    for (int r = 0; r < img.rows; ++r) {
    uchar* p = img.ptr<uchar>(r);
    for (int c = 0; c < img.cols; ++c) {
    p[c] = cv::saturate_cast<uchar>(p[c] + 100); // ✅ 安全
    }
    }

    注意 p[c] + 100 这个表达式本身:p[c] 会先被提升为 int,加法在 int 上完成(不会溢出),然后 saturate_cast<uchar> 把它截断到 [0, 255]。

    但如果两个 uchar 相加再乘,中间结果可能超出 int —— 那种情况下要显式先转成 int 或 float 再算。


    二、掩膜与 ROI:局部处理的两条路

    2.1 别名的坑:cv::Rect 是 (x, y, w, h)

    cv::Mat roi = img(cv::Rect(10, 20, 30, 40));
    // x y w h

    不是 (x1, y1, x2, y2)。写成后者会取到一个位置和大小都不对的区域 —— 而且不报错。

    2.2 掩膜:按「像素条件」选区域

    cv::Mat gray, mask, cut;
    cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);

    // 阈值生成掩膜:亮于 200 的像素为 255,其余为 0
    cv::threshold(gray, mask, 200, 255, cv::THRESH_BINARY);

    // 用掩膜取出原图对应像素,其余置 0
    cv::bitwise_and(img, img, cut, mask);

    实测:本篇测试图中掩膜命中了 6625 / 95000 个像素(7.0%),即按 > 200 筛出的亮区。

    cv::bitwise_and(src, src, dst, mask) 这个「自己与自己做与运算」的写法看起来奇怪,但它是标准用法 —— 效果就是「mask 为 0 的地方输出 0,其余保留原值」。

    等价的写法是 img.copyTo(cut, mask) —— 注意 copyTo 带 mask 时,是「只复制 mask 非零处」,目标的其他位置保持原样(不是清零)。两者语义不同:

    cv::Mat dst1 = cv::Mat::zeros(img.size(), img.type());
    img.copyTo(dst1, mask); // mask=0 处保持 dst1 的原值(这里是 0)

    cv::Mat dst2;
    cv::bitwise_and(img, img, dst2, mask); // mask=0 处一定是 0

    如果目标图不是全 0 初始化的,这两个写法结果不同。 这是很隐蔽的一类 bug。

    2.3 ROI:按「矩形位置」选区域

    cv::Mat roi = img(cv::Rect(10, 20, 30, 40)); // 视图,共享数据
    roi = cv::Scalar(0, 0, 255); // 直接改到原图上

    或者用 copyTo 把一块贴回去:

    cv::Mat patch = MakePatch(); // 尺寸必须匹配目标 ROI
    patch.copyTo(img(cv::Rect(x, y, w, h)));

    2.4 水印:ROI + addWeighted

    这是 ROI 最典型的用法 —— 只需要处理一小块区域,就不要对整个图做运算:

    // 做一个半透明的 logo
    cv::Mat logo = cv::Mat::zeros(80, 160, CV_8UC3);
    cv::circle(logo, cv::Point(40, 40), 32, cv::Scalar(255, 255, 255), cv::FILLED);
    cv::putText(logo, "LOGO", cv::Point(52, 48),
    cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(255, 255, 255), 2);

    // 定位到右下角
    const int x = img.cols – 170;
    const int y = img.rows – 90;

    // 只对这块 ROI 做加权叠加
    cv::Mat roi = img(cv::Rect(x, y, logo.cols, logo.rows));
    cv::addWeighted(roi, 0.5, logo, 0.5, 0.0, roi);
    // ↑原图权重 ↑logo权重 ↑加到 ROI 上(视图,直接改原图)

    实测中这块 ROI 是 160×80 @ (210,160) —— 坐标是从图的实际尺寸算出来的,不是写死的。

    如果写成 cv::addWeighted(img, …, logo, …, dst),会因为尺寸不匹配直接报错。用水印就必须走 ROI。

    在这里插入图片描述

    图 2:水印叠加、掩膜抠图、ROI 局部替换(OpenCV 4.13.0 实测)。第一格是原图,第二格是右下水印的效果。

    实测数据:水印 ROI 尺寸 160×80 @ (210, 160);掩膜(灰度 > 200 的亮区)命中 6625 / 95000 像素(7.0%)。

    2.5 创建背景图

    cv::Mat canvas = cv::Mat::zeros(480, 640, CV_8UC3); // 黑底
    cv::Mat canvas2(480, 640, CV_8UC3, cv::Scalar(255, 255, 255)); // 白底

    cv::Mat::zeros 是静态方法,尺寸在前;cv::Mat(rows, cols, type, value) 是构造函数,尺寸也是 (行, 列) —— 注意和 cv::Rect 的 (x, y, w, h) 顺序不同,容易混。


    三、绘制:往图上画东西

    // 直线
    cv::line(img, cv::Point(10, 10), cv::Point(200, 200),
    cv::Scalar(0, 0, 255), 2, cv::LINE_AA);

    // 矩形
    cv::rectangle(img, cv::Rect(50, 50, 120, 80),
    cv::Scalar(0, 255, 0), 2);
    cv::rectangle(img, cv::Point(50, 50), cv::Point(170, 130),
    cv::Scalar(0, 255, 0), 2); // 也可以用两个对角点

    // 圆
    cv::circle(img, cv::Point(300, 200), 60,
    cv::Scalar(255, 0, 0), cv::FILLED); // FILLED 或 -1 表示实心

    // 椭圆
    cv::ellipse(img, cv::Point(400, 300), cv::Size(80, 50),
    30.0, // 旋转角度
    0, 360, // 起始/结束角度
    cv::Scalar(0, 255, 255), 2);

    // 多边形(折线)
    std::vector<cv::Point> pts;
    pts.push_back(cv::Point(100, 100));
    pts.push_back(cv::Point(200, 120));
    pts.push_back(cv::Point(150, 220));
    cv::polylines(img, pts, /*isClosed=*/true, cv::Scalar(255, 255, 0), 2);

    // 实心多边形
    std::vector<std::vector<cv::Point> > polys;
    polys.push_back(pts);
    cv::fillPoly(img, polys, cv::Scalar(255, 255, 0));

    // 文字(仅 ASCII,下一节详说)
    cv::putText(img, "OK", cv::Point(20, 40),
    cv::FONT_HERSHEY_SIMPLEX, 1.0, cv::Scalar(255, 255, 255),
    2, cv::LINE_AA);

    三个参数细节

    thickness 为负(或 cv::FILLED = -1)表示填充。

    lineType 用 cv::LINE_AA 开抗锯齿。 默认的 LINE_8 画斜线和圆会有明显锯齿。代价是慢一些 —— 批量绘制大量图元时可以考虑关掉。

    cv::polylines 和 cv::fillPoly 的参数形式不同:

    • polylines 接受 std::vector<Point>(一条折线)
    • fillPoly 接受 std::vector<std::vector<Point>>(多条多边形)

    传错了会编译失败,不是运行时问题。

    测量文字尺寸

    int baseline = 0;
    const cv::Size sz = cv::getTextSize("Result: PASS",
    cv::FONT_HERSHEY_SIMPLEX, 1.0, 2,
    &baseline);

    getTextSize 返回文字的包围盒,baseline 是基线到包围盒底部的距离。要在图上居中或者做背景框时会用到。

    注意它只对 ASCII 有意义 —— 同一个字符串如果含中文,putText 画出来的和 getTextSize 算出来的完全对不上(下一节)。


    四、cv::putText 画不了中文:真相与解法

    这是中文读者必然撞上、而英文教程完全不会提的问题。

    4.1 现象

    在这里插入图片描述

    图 3:cv::putText 画中文的真实结果(OpenCV 4.13.0 实测)。“检测结果:合格” 这 7 个字(UTF-8 共 21 字节)被画成了 21 个问号。

    同一个 putText 调用,画英文完全正常,画中文变成一问号串。

    实测数据:字符串 "检测结果:合格" 有 7 个字符,UTF-8 编码是 21 字节,画出来是 21 个问号。

    4.2 为什么

    cv::putText 硬编码使用 OpenCV 内置的 Hershey 矢量字体。这是 1967 年 Allen Hershey 为绘图仪设计的一套字体,OpenCV 内置了它的约 95 个 ASCII 字形。

    它里面一个 CJK 字形都没有。

    而 putText 遇到无法映射的字符时,会画一个 ?。由于它按字节遍历字符串,每个非 ASCII 的 UTF-8 字节都会变成一个 ? —— 这就是为什么 21 字节变成了 21 个问号(而不是 7 个)。

    4.3 两个常见的错误尝试

    错误尝试 1:换个 FONT_HERSHEY_* 变体。

    cv::putText(img, "检测结果", pt, cv::FONT_HERSHEY_DUPLEX, 1.0, color);
    // 结果一样是问号

    没用。 所有 FONT_HERSHEY_* 变体都是 Hershey 字体族的不同字重/风格,全都没有 CJK 字形。

    错误尝试 2:转换编码。

    cv::putText(img, "检测结果", pt, cv::FONT_HERSHEY_SIMPLEX, 1.0, color);
    // 换成 GBK、UTF-16……都一样是问号

    也没用。 问题不在编码,在光栅化器里根本没有那些字形。Python 侧用 u8"中文" 或 .encode('utf-8') 同样无效。

    顺便澄清一个流传很广的说法:即使你的 OpenCV 编译时开了 -DWITH_FREETYPE=ON,cv::putText 依然走 Hershey 路径。FreeType 支持并没有暴露给原生 putText API —— 它只给了 contrib 里的 cv::freetype 模块。很多中文教程在这一点上写错了。

    4.4 正确做法

    方案 A:contrib 的 freetype 模块(需要 contrib 和 FreeType 库)

    #include <opencv2/freetype.hpp>

    cv::Ptr<cv::freetype::FreeType2> ft2 = cv::freetype::createFreeType2();
    ft2->loadFontData("C:/Windows/Fonts/msyh.ttc", 0); // 微软雅黑

    ft2->putText(img, u8"检测结果:合格",
    cv::Point(10, 40),
    28, // 字号
    cv::Scalar(0, 0, 255),
    cv::FILLED, // 负值/填充表示实心字形
    cv::LINE_AA,
    true); // bottomLeftOrigin

    代价:需要自编译带 contrib 的 OpenCV(见 00-02 的 vcpkg 路线),部署时还要带上 FreeType 的 DLL。

    方案 B:在图上只写 ASCII,中文交给外部 UI(推荐用于生产)

    把中文放在窗口标题、控制台、或者宿主程序的界面元素里,图上只画英文和数字。

    对于 NX 插件这类已经有 UI 框架的场景,这是最省事的做法 —— 你本来就有 NXMessageBox,没必要非把中文画在图里。

    方案 C:用别的库渲染后再合成

    PIL(Python)、GDI/DirectWrite(Windows)、Qt 都可以。思路都是「先渲染成一张图,再把图贴到 cv::Mat 上」。本系列的配图就是这么做的 —— tools/figures/_common.py 里的 load_cjk_font() 加载微软雅黑,全部中文标注走 PIL。

    如果只是自己调试看效果,Python + PIL 是最快的:

    from PIL import Image, ImageDraw, ImageFont
    import numpy as np, cv2

    pil = Image.fromarray(cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB))
    ImageDraw.Draw(pil).text((10, 40), "检测结果:合格",
    font=ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 28),
    fill=(0, 0, 255))
    bgr = cv2.cvtColor(np.array(pil), cv2.COLOR_RGB2BGR)

    注意 ImageDraw.text 的坐标是左上角,而 cv::putText 默认是左下角(基线位置)。换算时容易差一个文字高度。


    五、鼠标交互

    调试时经常需要「点一下看看这个位置的像素值是多少」。

    struct MouseState {
    cv::Point pt;
    bool pressed;
    MouseState() : pt(–1, –1), pressed(false) {}
    };

    static MouseState g_state;
    static cv::Mat g_img;

    void OnMouse(int event, int x, int y, int flags, void* userdata)
    {
    MouseState* st = static_cast<MouseState*>(userdata);

    switch (event) {
    case cv::EVENT_LBUTTONDOWN:
    st->pressed = true;
    st->pt = cv::Point(x, y);
    std::cout << "按下 (" << x << ", " << y << ")" << std::endl;

    // 打印该点的像素值
    if (x >= 0 && x < g_img.cols && y >= 0 && y < g_img.rows) {
    const cv::Vec3b px = g_img.at<cv::Vec3b>(y, x);
    std::cout << " BGR = (" << static_cast<int>(px[0]) << ", "
    << static_cast<int>(px[1]) << ", "
    << static_cast<int>(px[2]) << ")" << std::endl;
    }
    break;

    case cv::EVENT_MOUSEMOVE:
    if (st->pressed) {
    st->pt = cv::Point(x, y);
    }
    break;

    case cv::EVENT_LBUTTONUP:
    st->pressed = false;
    std::cout << "释放 (" << x << ", " << y << ")" << std::endl;
    break;

    default:
    break; // 必须有 default
    }
    }

    int main()
    {
    g_img = cv::imread("test.png");
    if (g_img.empty()) { return –1; }

    cv::namedWindow("win");
    cv::setMouseCallback("win", OnMouse, &g_state);
    cv::imshow("win", g_img);
    cv::waitKey(0);
    return 0;
    }

    实时框选 ROI 的常见模式是:按下时记起点,拖动时用 imshow 刷一张带临时矩形的副本,松开时确定最终矩形。注意不要直接画在原图上 —— 每次刷新都会留下上一次的框。正确做法是保留一份原图副本,每次在副本上重新画:

    cv::Mat display = g_img.clone(); // 每次刷新都从这份干净的副本开始
    cv::rectangle(display, rect, cv::Scalar(0, 255, 0), 2);
    cv::imshow("win", display);


    六、这套方案什么时候会失败

    • 整数图像上做加减乘。 溢出会回绕且不报错。一律用 cv::add / cv::subtract / cv::multiply / cv::absdiff。
    • cv::Rect 超出图像边界。 OpenCV 会抛出异常(在 Release 下也可能只是裁剪)。构造前应该做范围检查,尤其是坐标来自外部输入时。
    • copyTo 带掩膜时目标图未初始化。 掩膜为 0 的位置保持目标的原值 —— 如果目标里有垃圾数据,结果里就有垃圾数据。用 bitwise_and 或者先把目标清零。
    • cv::putText 画任何非 ASCII 字符。 不只是中文,日文、韩文、俄文、甚至带重音的拉丁字母全都会变成问号。
    • getTextSize 和 putText 的坐标基准不一致。 putText 的 org 是文字左下角(基线),getTextSize 返回的矩形也是相对基线算的。想精确排版要自己减 sz.height。
    • 在 imshow 的原图上反复画标注。 标注会累积。始终保留一份干净的副本。
    • 用 ROI 视图做长时间存活的对象。 前面说过,ROI 持有父图的整块内存。

    七、Python 对照

    7.1 算术运算:Python 更容易写出错而不自知

    Python 侧的危险在于:numpy 的加法语法上完全合法,运算也照常返回,只是结果不是你要的。

    以下是在本机 NumPy 2.4.3 上的实测:

    import numpy as np, cv2

    img = np.array([[200]], dtype=np.uint8)

    print(img + 100) # [[ 44]] ← 回绕!不是 300
    print(img + np.uint8(100)) # [[ 44]] ← 回绕
    print(cv2.add(img, 100)) # [[255]] ← 饱和,正确

    三种写法里有两种给出的是错误结果,而且一个警告都不会有。

    关于 NumPy 的版本差异:NumPy 1.x 时代,uint8 数组 + Python int 会提升到能容纳结果的类型(得到 300);NumPy 2.0 起收紧了类型提升规则(NEP 50),Python 的 int 在运算中被视为"弱类型",结果会保持 uint8 —— 于是变成回绕。

    也就是说:同一行 Python 代码,在 NumPy 1.x 和 2.x 上结果不同。 这正是 5.0 迁移里「NumPy 2.x 支持」被列为一项变更的原因。

    所以不要靠记忆去区分哪种写法安全。 只要记住一条:

    图像像素运算一律用 cv2.add / cv2.subtract / cv2.multiply / cv2.absdiff,不要用 numpy 的 + – * /。

    多通道情况也一样(实测):

    c = np.array([[[200, 200, 200]]], dtype=np.uint8)
    c + np.uint8(100) # -> [[44, 44, 44]] 三个通道一起回绕
    cv2.add(c, 100) # -> [[255, 255, 255]] 三个通道一起饱和

    7.2 绘制函数的差异

    # C++: cv::rectangle(img, cv::Rect(50, 50, 120, 80), color, 2);
    # Python 用两个点:
    cv2.rectangle(img, (50, 50), (170, 130), (0, 255, 0), 2)
    # ↑pt1 ↑pt2 = pt1 + (w, h)

    # C++ 的 cv::Rect 在 Python 里没有直接对应,用 (x, y, w, h) 元组要自己转成两个点
    x, y, w, h = 50, 50, 120, 80
    cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)

    这是很容易出错的一处:C++ 习惯用 Rect,Python 的绘图函数收的是两个点。直接把 (50, 50, 120, 80) 传给 Python 的 rectangle 会被当成 pt1=(50,50), pt2=120 —— 参数个数不对,报错还好;要是参数个数刚好对上是更危险的。

    常用等价对照:

    功能C++Python
    矩形 cv::rectangle(img, cv::Rect(x,y,w,h), c, t) cv2.rectangle(img, (x,y), (x+w,y+h), c, t)
    圆 cv::circle(img, cv::Point(x,y), r, c, t) cv2.circle(img, (x,y), r, c, t)
    填充 cv::FILLED 或 -1 cv2.FILLED 或 -1

    (C++ 的 cv::Rect 在 Python 侧对应的是 cv2.boundingRect 等的返回值,是 (x, y, w, h) 元组,可以解包。)

    7.3 掩膜参数的位置

    # C++ 是位置参数:cv::bitwise_and(a, b, dst, mask)
    # Python 是关键字参数:
    cv2.bitwise_and(a, b, mask=mask)

    Python 侧 bitwise_and 必须用 mask= 关键字。 写成位置参数会报错。

    7.4 putText 的中文

    Python 侧完全一样画不了中文,原因相同(同一个 Hershey 字体)。备选方案:

    # 方案 1:PIL(推荐)
    from PIL import Image, ImageDraw, ImageFont
    pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    ImageDraw.Draw(pil).text((10, 40), "检测结果:合格",
    font=ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 28),
    fill=(0, 0, 255))
    img = cv2.cvtColor(np.array(pil), cv2.COLOR_RGB2BGR)

    # 方案 2:opencv-contrib-python 里的 freetype
    # (需要先 pip install opencv-contrib-python,
    # 注意不能和 opencv-python 同时装)

    # 方案 3:图上只写 ASCII,中文走窗口标题或外部 UI

    注意 PIL 的坐标是左上角,cv2.putText 是左下角基线 —— 两者换用时坐标要调整。


    八、自测

  • unsigned char 的值是 200,执行 p[c] = p[c] + 100; 之后 p[c] 是多少?为什么?
  • img.copyTo(dst, mask) 和 cv::bitwise_and(img, img, dst, mask) 在什么情况下结果不同?
  • 为什么 cv::putText 画中文会变成一串问号?换成 FONT_HERSHEY_DUPLEX 能解决吗?
  • cv::putText 的 org 参数指的是文字的哪个位置?
  • 答案

  • 44。200 + 100 = 300,unsigned char 只能存 0-255,300 溢出后回绕成 300 – 256 = 44。要避免就包一层 cv::saturate_cast<uchar>(…),或者直接用 cv::add。
  • 当 dst 不是全 0 初始化时。 copyTo 带掩膜是「只复制掩膜非零处,其余位置保持 dst 原值」;bitwise_and 是「掩膜为 0 处一定输出 0」。如果 dst 里有旧数据,两者结果不同。
  • 因为 putText 用的内置 Hershey 字体里根本没有 CJK 字形,它把每个非 ASCII 的 UTF-8 字节都画成一个 ?。换成任何 FONT_HERSHEY_* 变体都没用 —— 它们都是同一个字体族。
  • 文字的左下角(基线位置)。这也是为什么经常要把 y 坐标加上文字高度才能让文字贴合框的上沿。

  • 阶段 1 完成

    到这里,OpenCV 的基础部分就走完了。你现在应该能:

    • 理解 cv::Mat 的指针语义,知道什么时候会共享数据、什么时候需要 clone()
    • 在 Windows 上正确处理中文路径的图像读写
    • 用 HSV 做颜色筛选,用直方图判断图像质量
    • 写不会溢出的像素运算代码,用掩膜和 ROI 做局部处理
    • 知道 cv::putText 画不了中文,以及正确的替代路径
    赞(0)
    未经允许不得转载:171主机测评 » 图像运算、掩膜/ROI 与绘制交互
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址