1. 项目概述为什么我们需要cvui在计算机视觉和图像处理项目中我们常常面临一个看似简单却颇为棘手的问题如何快速地为我们的算法或演示程序添加一个交互界面传统的解决方案比如集成Qt、MFC甚至WinForms往往意味着引入一个庞大、复杂的GUI框架需要处理窗口消息循环、事件回调、资源管理等一系列与核心视觉算法无关的繁琐工作。这不仅增加了项目的编译依赖和体积也让代码结构变得臃肿对于只想快速验证算法、调整参数或者做一个轻量级演示的开发者来说学习成本和开发效率都成了障碍。这时cvui的出现就像一场及时雨。它是一个基于OpenCV HighGUI模块构建的超轻量级、跨平台的C UI库。它的核心设计哲学是“在图像上直接绘制UI”。听起来很神奇其实原理很简单OpenCV的imshow函数可以实时显示一个Mat图像而cvui所做的就是在这个图像矩阵上通过画线、画矩形、填充颜色、写文字等基本的绘图操作“画”出按钮、滑块、文本框等UI组件。所有的交互逻辑如鼠标点击检测、滑块拖动都通过监听OpenCV的鼠标回调事件来实现并更新对应的UI状态和内部变量值。这意味着什么意味着你不需要任何额外的GUI库依赖只需要OpenCV。你的界面就是一张图你的UI状态就是一些变量你的交互就是处理鼠标事件。整个库只有一个头文件cvui.h直接包含进项目就能用真正做到即插即用。它特别适合以下几种场景算法参数实时调试比如调整Canny边缘检测的阈值、教学演示直观展示算法每一步的效果、小型工具开发如图片标注工具以及任何需要快速原型验证的场合。如果你厌倦了重型GUI框架的配置和编程模型想用几十行代码就为你的OpenCV程序加上可交互的控件那么cvui绝对值得你深入了解。2. cvui核心设计思路与架构解析2.1 基于“立即模式”GUI的渲染哲学cvui采用了一种称为“立即模式GUI”Immediate Mode GUI, IMGUI的设计范式。这与Qt、WinForms等传统的“保留模式GUI”Retained Mode GUI有根本区别。在保留模式中你首先需要创建UI组件如按钮对象设置其属性并将其添加到某个窗口或布局管理器中。框架会负责维护这些组件的状态、层级关系和生命周期并在收到重绘事件时自动渲染。这是一个“声明式”的过程。而立即模式GUI则截然不同。它没有持久的UI对象。每一帧你都需要在代码中显式地“描述”这一帧UI应该长什么样。例如你调用cvui::button(frame, x, y, “Click Me”)这个函数会做两件事1. 根据当前帧的鼠标状态判断这个区域是否被点击并返回一个布尔值2. 立即在frame图像的指定位置绘制出一个按钮的视觉外观。下一帧你需要再次调用这个函数才能让按钮继续存在并保持交互。UI的状态如按钮是否被按下是瞬时的由每帧的函数调用结果决定。这种模式完美契合了基于帧循环的视觉程序如视频处理、游戏。我们的OpenCV程序主循环通常是这样的while(true) { cap frame; process(frame); imshow(“result”, frame); waitKey(1); }。cvui的UI绘制和交互检测就自然地嵌入在这个循环中。每一帧我们清空上一帧的UI痕迹根据最新的逻辑状态和用户输入重新绘制全新的UI。这种方式的优点是极其简单直观状态管理轻量通常就是几个int、double变量并且渲染逻辑与业务逻辑紧密耦合没有复杂的对象生命周期和事件监听器需要管理。2.2 组件化与状态管理机制尽管是立即模式cvui仍然提供了组件化的抽象。它内置了丰富的组件按钮button、文本标签text、计数器counter、滑块trackbar、复选框checkbox、窗口window等。每个组件都是一个独立的函数接收画布Mat、位置、尺寸、标签、以及一个或多个指向基础数据类型int*,double*,bool*的指针。这些指针是cvui状态管理的核心。例如你有一个控制高斯模糊核大小的变量int kernelSize 5;。当你创建一个滑块cvui::trackbar(frame, 10, 10, 200, kernelSize, 1, 31);时你传入了kernelSize。在每一帧的渲染和交互处理中cvui会检查鼠标是否在拖动这个滑块。如果是它会直接修改kernelSize指向的内存地址的值。在你的业务逻辑中kernelSize的值已经是最新的了你可以立即使用它GaussianBlur(src, dst, Size(kernelSize, kernelSize), 0);。这种直接绑定变量的方式使得UI和程序逻辑的数据同步变得异常简单和高效无需任何手动的事件处理函数来同步数据。2.3 渲染管线与性能考量cvui的渲染管线非常直接。在每一帧开始时通常需要调用cvui::context()来创建一个UI上下文虽然简单使用时可省略。然后在调用任何组件函数前需要调用cvui::beginRow(),cvui::beginColumn()等布局函数来定义组件的排列方式这能极大简化手动计算每个组件位置的工作。之后依次调用各个组件函数。每个组件函数内部会交互检测根据组件区域和当前帧收集到的鼠标坐标、按键状态计算组件的交互状态如悬停、按下、激活。状态更新如果发生交互如点击按钮、拖动滑块则更新传入的指针所指向的变量值。视觉渲染根据组件的类型和当前交互状态在传入的Mat图像上调用OpenCV的绘图函数rectangle,line,putText,circle等绘制出组件。 所有组件调用完毕后最后需要调用cvui::imshow()来替代OpenCV原生的imshow()。这个函数会先更新内部维护的鼠标状态然后再显示图像并处理waitKey确保UI交互的响应性。从性能角度看cvui的渲染是CPU密集型的因为每一帧都在进行像素级的绘图操作。对于分辨率很高如4K的图像或者非常复杂的UI可能会成为性能瓶颈。但在典型的计算机视觉演示中窗口尺寸在1280x720以下UI组件数量在几十个以内其开销是完全可以接受的甚至难以察觉。它的优势在于极低的内存占用和启动开销以及无与伦比的易用性。3. 环境搭建与第一个cvui程序3.1 依赖准备与项目配置使用cvui的唯一硬性依赖就是OpenCV。你需要一个正确安装并配置好的OpenCV开发环境。这里以Windows系统、Visual Studio 2022和OpenCV 4.8为例简述配置过程其他平台Linux/macOS和编译器CMake原理相通。首先从OpenCV官网下载预编译的Windows版本或者从源码编译安装。假设你将OpenCV解压到D:\opencv。接着获取cvui你只需要下载其GitHub仓库中的cvui.h和cvui.cpp两个文件实际上cvui.cpp只是一个提供了默认字体数据的文件核心功能都在头文件里。将这两个文件放入你的项目目录。在Visual Studio中创建一个新的空C控制台项目。然后进行关键配置包含目录在项目属性 - C/C - 常规 - 附加包含目录中添加OpenCV的头文件路径如D:\opencv\build\include。同时确保你的项目目录也在包含路径中或者将cvui.h放在你能直接#include的地方。库目录在链接器 - 常规 - 附加库目录中添加OpenCV的库文件路径如D:\opencv\build\x64\vc16\lib注意vc16对应VS2019/2022x64对应你的目标平台。附加依赖项在链接器 - 输入 - 附加依赖项中添加你需要链接的OpenCV库文件例如opencv_world480.lib对于Release模式和opencv_world480d.lib对于Debug模式。world模块包含了大多数常用功能简化链接。环境变量确保OpenCV的bin目录如D:\opencv\build\x64\vc16\bin被添加到系统的PATH环境变量中这样运行时才能找到对应的DLL文件。注意Debug和Release模式的配置包含目录、库目录通常相同但附加依赖项中的库文件名可能不同带d后缀的是Debug版。务必为两种配置分别设置否则在切换配置时会出现链接错误。3.2 “Hello, cvui!”创建第一个窗口与按钮让我们从一个最简单的例子开始创建一个显示按钮并响应点击的窗口。#include opencv2/opencv.hpp #define CVUI_IMPLEMENTATION // 必须在使用cvui的任何一个.cpp文件中定义一次 #include “cvui.h” int main() { // 创建一个800x600的黑色画布 cv::Mat frame cv::Mat(600, 800, CV_8UC3, cv::Scalar(50, 50, 50)); // 初始化cvui并告诉它创建哪个窗口 cvui::init(“Hello CVUI”); // 定义一个控制按钮状态的变量 bool isButtonClicked false; while (true) { // 每一帧开始先用背景色清空画布这里用灰色 frame cv::Scalar(50, 50, 50); // 开始一个UI渲染上下文在简单单窗口程序中可省略但建议保留 // cvui::context(“Hello CVUI”); // 在坐标(100, 100)的位置绘制一个按钮标签是“Click me!” // button()函数会返回当前帧这个按钮是否被按下 if (cvui::button(frame, 100, 100, “Click me!”)) { // 如果按钮在这一帧被点击了就翻转isButtonClicked的状态 isButtonClicked !isButtonClicked; } // 根据按钮状态在画布上显示不同的文本 if (isButtonClicked) { cvui::text(frame, 100, 150, “The button was clicked!”); } else { cvui::text(frame, 100, 150, “Waiting for a click...”); } // 使用cvui的imshow更新UI并显示参数30表示等待30ms相当于约33FPS cvui::imshow(“Hello CVUI”, frame); // 如果按下ESC键退出循环 if (cv::waitKey(30) 27) { break; } } return 0; }将这段代码编译运行你会看到一个灰色窗口中间有一个按钮。点击它下方的文字会在“The button was clicked!”和“Waiting for a click...”之间切换。这个简单的例子揭示了cvui的工作流在循环中清空画布 - 调用组件函数同时完成交互检测和绘制 - 用cvui::imshow显示并推进一帧。实操心得#define CVUI_IMPLEMENTATION这个宏定义至关重要它告诉编译器将cvui.h中的模板和实现代码实例化出来。你必须且只能在一个翻译单元即一个.cpp文件中定义它。如果项目有多个.cpp文件用到cvui只在其中一个里定义其他文件只包含头文件即可否则会导致重复定义链接错误。4. 核心组件详解与实战应用4.1 布局系统告别手动计算坐标在第一个例子中我们手动指定了按钮和文本的坐标(100, 100)和(100, 150)。当UI组件多起来时手动管理坐标将是噩梦。cvui提供了一个简洁的“行列”布局系统来解决这个问题。布局的核心是“行”Row和“列”Column。你可以想象一个隐式的光标cvui记录着这个光标当前的位置。当你调用cvui::beginRow()时你开始了一个新的行光标重置到行的起始位置。之后你添加的每一个组件按钮、文本等都会水平排列并且cvui会自动在组件之间添加间距默认为10像素同时更新光标位置。调用cvui::endRow()结束这一行光标会移动到下一行的起始位置。beginColumn()和endColumn()则用于垂直排列。cv::Mat frame cv::Mat(400, 600, CV_8UC3); cvui::init(“Layout Demo”); int value1 50, value2 100; double value3 0.5; bool checked false; while (true) { frame cv::Scalar(50, 50, 50); // 从画布的(20, 20)位置开始一个行布局 cvui::beginRow(frame, 20, 20); // 在这个行内依次添加组件它们会水平排列 cvui::text(“Value 1:”); cvui::trackbar(200, value1, 0, 100); // 一个宽度200的滑块 cvui::space(20); // 添加20像素的空白间距 cvui::text(“ “ std::to_string(value1)); cvui::endRow(); // 结束这一行 // 自动换到下一行因为beginRow会从上一行的底部开始 cvui::beginRow(frame); cvui::text(“Value 2:”); cvui::counter(value2); // 一个计数器组件 cvui::space(20); cvui::button(“Reset”, value2); // 点击按钮将value2重置为0 cvui::endRow(); // 再开始一个新行 cvui::beginRow(frame); cvui::checkbox(“Enable Feature”, checked); cvui::space(10); // 嵌套一个列布局 cvui::beginColumn(); cvui::button(“Option A”); cvui::button(“Option B”); cvui::endColumn(); cvui::endRow(); cvui::imshow(“Layout Demo”, frame); if (cv::waitKey(30) 27) break; }这个例子展示了如何用布局系统快速构建整齐的UI。cvui::space(width)用于在行内插入自定义宽度的空白。布局系统极大地提升了开发效率是使用cvui构建复杂界面的基石。4.2 丰富组件库及其参数解析cvui提供了足以满足轻量级需求的组件库。下面详细解析几个最常用的组件及其关键参数1. 按钮 (button):// 基础形式 if (cvui::button(frame, x, y, “Label”)) { /* 点击处理 */ } // 自定义宽高 if (cvui::button(frame, x, y, width, height, “Label”)) { ... } // 绑定一个变量点击时将其设为true常用于开关 bool btnState false; cvui::button(frame, x, y, “Toggle”, btnState);按钮是最简单的交互组件。其返回值是bool类型仅在鼠标在该帧按下并释放于按钮区域内时为true。自定义宽高允许你创建非标准尺寸的按钮。绑定bool变量的形式会让按钮表现为一个开关btnState会随点击在true/false间切换同时按钮的视觉外观如颜色也会反映这个状态。2. 滑块 (trackbar):// 整数滑块 int intValue 50; cvui::trackbar(frame, x, y, width, intValue, min, max); // 浮点数滑块 double doubleValue 0.7; cvui::trackbar(frame, x, y, width, doubleValue, min, max, decimals, format);滑块是参数调整的利器。width参数决定了滑块的视觉长度。min和max定义了取值范围。对于浮点滑块decimals控制显示的小数位数format是printf风格的格式字符串如”%.2f”。滑块内部会自动处理鼠标拖拽事件并更新绑定的变量值。3. 计数器 (counter):int count 0; double dCount 0.0; cvui::counter(frame, x, y, count, step, format); // 整数计数器 cvui::counter(frame, x, y, dCount, step, format); // 浮点计数器计数器显示一个带有“-”和“”按钮的数值控件。点击“-”或“”会以step为步长减小或增加绑定的变量值。format同样用于控制显示格式。它比滑块更节省空间适合调整离散值或较小范围的连续值。4. 复选框 (checkbox) 与单选框 (radiobutton):bool option1 false, option2 true; cvui::checkbox(frame, x, y, “Enable Option 1”, option1); cvui::checkbox(frame, x, y30, “Enable Option 2”, option2); int selectedOption 0; // 0代表第一个1代表第二个... cvui::text(frame, x, y60, “Choose one:”); cvui::radiobutton(frame, x100, y60, “Option A”, selectedOption, 0); cvui::radiobutton(frame, x100, y90, “Option B”, selectedOption, 1);复选框绑定一个bool变量。单选框则绑定一个int变量并通过最后一个参数id来标识自己是哪个选项。当点击一个单选框时cvui会将绑定的变量设置为对应的id值并自动将同组其他单选框置为未选中状态。5. 窗口 (window):cvui::window(frame, x, y, width, height, “Settings”); // 在窗口内绘制组件需要结合布局或手动偏移坐标 cvui::beginRow(frame, x10, y30); // 从窗口内部开始布局 cvui::text(“Inside window”); cvui::endRow();window组件会绘制一个带标题栏和边框的区域。它本身不包含布局管理你需要手动计算内部组件的坐标偏移或者更优雅地在调用beginRow/beginColumn时传入窗口内部的起始坐标。窗口可以拖动标题栏来移动这为组织UI提供了更多灵活性。4.3 实战构建一个图像处理参数调试面板让我们综合运用以上知识创建一个实用的图像处理演示程序。这个程序能实时调整Canny边缘检测和双边滤波的参数并观察效果。#include opencv2/opencv.hpp #define CVUI_IMPLEMENTATION #include “cvui.h” int main() { cv::VideoCapture cap(0); // 打开默认摄像头 if (!cap.isOpened()) return -1; cv::Mat frame, processedFrame, edges; cvui::init(“Image Processing Debugger”); // 定义控制参数 int cannyLowThresh 50; int cannyHighThresh 150; int bilateralD 5; double bilateralSigmaColor 50.0; double bilateralSigmaSpace 50.0; bool showEdges true; bool useBilateral false; while (true) { cap frame; if (frame.empty()) break; cv::Mat displayFrame frame.clone(); // 在显示帧的右侧留出400像素宽的区域作为UI面板 int uiWidth 400; cv::Mat uiPanel displayFrame(cv::Rect(displayFrame.cols - uiWidth, 0, uiWidth, displayFrame.rows)); uiPanel cv::Scalar(40, 40, 40); // 将面板背景设为深灰色 // 在UI面板区域开始绘制 cvui::context(uiPanel); // 指定当前上下文为uiPanel所有后续组件都画在这上面 int startX 20, startY 20; // 创建一个可拖动的设置窗口 cvui::window(uiPanel, startX, startY, uiWidth - 40, 300, “Processing Parameters”); // 在窗口内部使用布局 cvui::beginRow(uiPanel, startX 10, startY 30); cvui::beginColumn(200); // 限制列宽为200 cvui::text(“Canny Edge Detection”); cvui::trackbar(180, cannyLowThresh, 0, 255); cvui::text(“Low Thresh: ” std::to_string(cannyLowThresh)); cvui::trackbar(180, cannyHighThresh, 0, 255); cvui::text(“High Thresh: ” std::to_string(cannyHighThresh)); cvui::checkbox(“Show Edges”, showEdges); cvui::space(20); cvui::text(“Bilateral Filter”); cvui::checkbox(“Enable”, useBilateral); cvui::trackbar(180, bilateralD, 1, 20); cvui::text(“Diameter (d): ” std::to_string(bilateralD)); cvui::trackbar(180, bilateralSigmaColor, 1.0, 150.0, 1, “%.1f”); cvui::text(“SigmaColor: ” std::to_string(bilateralSigmaColor)); cvui::trackbar(180, bilateralSigmaSpace, 1.0, 150.0, 1, “%.1f”); cvui::text(“SigmaSpace: ” std::to_string(bilateralSigmaSpace)); cvui::endColumn(); cvui::endRow(); // 处理图像 processedFrame frame.clone(); if (useBilateral) { cv::bilateralFilter(frame, processedFrame, bilateralD, bilateralSigmaColor, bilateralSigmaSpace); } if (showEdges) { cv::Canny(processedFrame, edges, cannyLowThresh, cannyHighThresh); cv::cvtColor(edges, edges, cv::COLOR_GRAY2BGR); // 将边缘图白色边缘黑色背景以半透明方式叠加到原图 cv::addWeighted(processedFrame, 0.7, edges, 0.3, 0, processedFrame); } // 将处理后的图像显示在左侧主区域 cv::Mat mainArea displayFrame(cv::Rect(0, 0, displayFrame.cols - uiWidth, displayFrame.rows)); processedFrame.copyTo(mainArea); // 在UI面板下方显示状态信息 cvui::text(uiPanel, startX, startY 320, “Press ‘ESC’ to quit.”); cvui::printf(uiPanel, startX, startY 350, 0.4, 0xff0000, “FPS: %.2f”, cap.get(cv::CAP_PROP_FPS)); cvui::imshow(“Image Processing Debugger”, displayFrame); if (cv::waitKey(1) 27) break; } cap.release(); return 0; }这个程序创建了一个直观的调试界面。左侧是实时摄像头画面和处理结果右侧是一个可拖动的参数面板。通过滑动滑块和勾选复选框你可以实时看到Canny边缘检测阈值和双边滤波参数对图像的影响。cvui::context()函数在这里很关键它让我们可以将UI组件绘制在displayFrame的一个子区域uiPanel上而不是整个帧实现了画中画式的UI布局。5. 高级技巧、性能优化与问题排查5.1 自定义样式与主题cvui的默认样式是深灰色主题。但你可以通过修改一系列全局静态变量来自定义几乎所有视觉元素。这些变量通常在cvui命名空间内需要在初始化后、渲染循环前进行设置。cvui::init(“Styling Demo”); // 修改主题颜色 cvui::theme( cvui::themes::blue ); // 使用内置的蓝色主题 // 或者进行精细化的自定义 cvui::setTheme( cvui::themes::blue ); // 另一种设置方式 // 直接修改颜色常量RGB格式 cvui::DEFAULT_BACKGROUND_COLOR cv::Scalar(30, 30, 50); // 深蓝背景 cvui::DARK_BACKGROUND_COLOR cv::Scalar(20, 20, 40); cvui::LIGHT_BACKGROUND_COLOR cv::Scalar(60, 60, 80); cvui::TEXT_COLOR cv::Scalar(220, 220, 220); cvui::PRIMARY_COLOR cv::Scalar(0, 150, 255); // 主色调如按钮高亮 cvui::SECONDARY_COLOR cv::Scalar(255, 100, 100); // 次要色调 // 修改字体和间距 cvui::setFontScale(0.5); // 设置字体缩放因子 cvui::setPadding(5, 5); // 设置组件内部边距水平垂直 cvui::setSpacing(8); // 设置行内组件间距自定义样式可以让你的UI更贴合整体项目风格。建议在程序初始化阶段集中设置避免在渲染循环中修改以免影响性能。5.2 性能优化策略虽然cvui很轻量但在一些极端情况下如高分辨率、高帧率、大量组件仍可能遇到性能问题。以下是一些优化建议减少不必要的重绘cvui是立即模式每一帧都会重绘所有UI。确保你的主循环速率是合理的。对于非实时应用可以使用cv::waitKey(0)等待按键而不是以固定高帧率运行。限制UI区域如实战例子所示使用cvui::context()或手动指定绘制区域将UI限制在画布的一小部分而不是全屏绘制可以显著减少绘图操作。避免在循环中创建临时字符串例如cvui::text(“Value: ” std::to_string(var))会在每一帧构造新的字符串。如果var更新不频繁可以考虑在var变化时才更新一个持久的字符串变量然后显示这个变量。谨慎使用透明度和复杂图形cvui的绘图基于OpenCV的基本函数大量使用半透明叠加或复杂形状会影响性能。保持UI简洁。分离渲染与逻辑如果UI非常复杂可以考虑仅在UI状态发生变化或每隔几帧才调用cvui的渲染函数而业务逻辑照常运行。但这需要更精细的状态管理。5.3 常见问题与排查技巧实录在实际使用cvui的过程中你可能会遇到一些典型问题。这里记录一份速查表问题现象可能原因解决方案编译错误未定义的引用1. 忘记定义CVUI_IMPLEMENTATION。2. 在多个.cpp文件中定义了CVUI_IMPLEMENTATION。3. OpenCV库链接不正确。1. 确保在一个且仅一个.cpp文件中#define CVUI_IMPLEMENTATION。2. 检查项目属性确保OpenCV的包含目录、库目录和附加依赖项配置正确且Debug/Release模式匹配。运行时崩溃或黑窗口1. OpenCV的DLL路径未添加到系统PATH。2. 传入的Mat图像类型不是CV_8UC3或CV_8UC4。3. 组件坐标超出图像范围。1. 将OpenCV的bin目录加入PATH或直接将所需的DLL如opencv_world480.dll复制到可执行文件同级目录。2.cvui默认只在CV_8UC3BGR三通道或CV_8UC4BGRA四通道图像上工作。使用cvtColor转换或确保图像类型正确。3. 使用布局系统或仔细检查坐标计算。UI组件不显示或显示不全1. 在调用cvui组件函数后又用OpenCV绘图函数覆盖了UI区域。2. 未在每一帧开始时用背景色“清空”画布导致UI残影。3. 布局嵌套错误begin和end不匹配。1. 确保所有cvui绘制调用完成后再执行其他可能覆盖UI的绘图操作。2. 在循环开始处用frame cv::Scalar(...)或frame.setTo(...)清空画布。3. 检查每个beginRow/Column都有对应的endRow/Column且嵌套关系正确。使用缩进提高代码可读性。鼠标交互无响应1. 使用了OpenCV原生的imshow和waitKey而不是cvui::imshow。2. 窗口名称与cvui::init或cvui::context中指定的不一致。3. 有多个OpenCV窗口鼠标事件被错误窗口捕获。1.必须使用cvui::imshow(windowName, frame)来显示包含UI的窗口它内部会处理鼠标回调。2. 确保cvui::init()和cvui::imshow()的第一个参数窗口名完全一致。3. 对于多窗口应用确保每个窗口都有独立的上下文并使用对应的窗口名进行imshow。UI更新闪烁1. 在循环中创建了新的Mat对象而不是复用。2. 绘图顺序问题背景清空操作可能覆盖了已绘制的内容。1. 在循环外创建Mat在循环内使用setTo或赋值来更新内容避免频繁的内存分配。2. 确保绘图顺序是清空背景 - 绘制业务图像 - 绘制UI。踩坑心得cvui::imshow是关键中的关键。我曾经花了半小时排查为什么滑块拖不动最后发现是不小心写回了OpenCV的cv::imshow。cvui::imshow不仅显示图像还调用了cvui::update()后者负责处理所有窗口的鼠标事件。如果你需要同时显示多个不包含UI的视图对它们使用cv::imshow是可以的但至少要有一个包含UI的窗口使用cvui::imshow来驱动整个交互循环。最后再分享一个小技巧如果你觉得cvui默认的字体有点小或者不好看它可以替换自定义的字体位图。cvui使用一个内置的位图字体定义在cvui.cpp的unsigned char数组里。你可以修改这个数组需要一定的耐心和图形工具或者自己实现一个renderText函数来接入TrueType字体比如通过OpenCV的putText配合Hershey字体但这会失去cvui内置字体的一致性风格。对于大多数应用默认字体已经足够清晰和通用。