引言
在掌握了 EGE 相机模块的基础使用方法后(参见 camera_base – 相机基础示例),我们可以进一步探索相机图像的实时处理和特效应用。camera_wave.cpp 演示了如何在相机采集的实时图像上实现交互式的水波变形特效,展示了 EGE 相机模块在图像处理领域的强大能力。
本文将深入解析 camera_wave.cpp 的实现原理,包括物理模拟、纹理映射、性能优化等核心技术。建议在阅读本文前先学习 camera_base,了解相机模块的基本使用方法。
一、功能概述
1.1 主要特性
camera_wave.cpp 在 camera_base 的基础上,添加了以下高级特性:
- 实时水波模拟:基于物理模拟的弹性网格变形
- 交互式控制:通过鼠标拖拽实时改变网格形状
- 可调参数:通过键盘动态调整弹性强度
- 高性能渲染:使用纹理映射和三角形光栅化
- 完整功能保留:继承 camera_base 的所有设备管理功能
1.2 交互操作
除了 camera_base 的所有键盘操作外,camera_wave 还支持:
| 操作 | 功能 |
|---|---|
| 鼠标左键拖拽 | 拖动网格点,产生波纹效果 |
| + 或 = 键 | 增加弹性强度(增强波纹效果) |
| – 或 _ 键 | 减少弹性强度(减弱波纹效果) |
1.3 视觉效果
当鼠标按下并拖动时,相机画面会像水面一样产生波纹,并在松开鼠标后逐渐恢复平静。整个过程基于真实的物理模拟,波纹的传播、衰减都符合弹性力学规律。
二、核心原理
2.1 网格物理模拟
水波特效的核心是一个基于弹性力学的网格系统。网格由 M \times N 个点组成(在 camera_wave 中默认为 80×60),每个点具有以下属性:
\text{Point}_i = (x_i, y_i, dx_i, dy_i, u_i, v_i)
其中:
– (x_i, y_i) 是点的屏幕坐标(归一化到 [0, 1] 范围)
– (dx_i, dy_i) 是点的速度
– (u_i, v_i) 是该点对应的纹理坐标
网格的运动遵循简化的弹性方程。对于每个内部点 (i, j),其加速度由四个相邻点(上下左右)的位置决定:
a_x = (x_{i-1,j} + x_{i+1,j} + x_{i,j-1} + x_{i,j+1}) - 4x_{i,j}
a_y = (y_{i-1,j} + y_{i+1,j} + y_{i,j-1} + y_{i,j+1}) - 4y_{i,j}
这是二维拉普拉斯算子 \nabla^2 的离散形式,它计算了四个方向邻居对当前点的拉力总和。当相邻点离得越远,拉力就越大。
2.2 能量损耗模拟
为了使波纹逐渐衰减而不是无限振荡,程序引入了能量损耗机制。当加速度方向与速度方向相反(即正在减速)时,加大阻尼效果:
|
1 2 3 4 5 6 7 8 |
// 模拟能量损失 if (std::signbit(dx) != std::signbit(m_vec[m_index][h].dx)) { dx *= 1.0f + m_intensity; } if (std::signbit(dy) != std::signbit(m_vec[m_index][h].dy)) { dy *= 1.0f + m_intensity; } |
这种设计使得波纹能够快速收敛到静止状态。m_intensity 参数控制弹性强度,取值范围为 0.001 到 0.3:
– 较小的值:波纹传播缓慢,衰减快
– 较大的值:波纹传播快速,持续时间长
2.3 纹理映射与三角形光栅化
网格中每个四边形单元被分解为两个三角形,然后使用纹理映射进行渲染:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 将四边形分解为两个三角形 for (int i = 1; i != m_height; ++i) { const int k1 = (i - 1) * m_width; const int k2 = i * m_width; for (int j = 1; j != m_width; ++j) { const int p1 = k1 + j - 1; // 左上 const int p2 = k1 + j; // 右上 const int p3 = k2 + j - 1; // 左下 const int p4 = k2 + j; // 右下 fillTriangle(v[p1], v[p2], v[p3]); // 上三角形 fillTriangle(v[p3], v[p2], v[p4]); // 下三角形 } } |
每个三角形使用透视正确的纹理插值进行填充。对于三角形内部的每个像素,根据重心坐标计算对应的纹理坐标 (u, v),然后从相机帧图像中采样颜色。这样即使网格变形,纹理也能正确映射。
三、Net 类详解
3.1 类结构
Net 类封装了整个网格系统的实现:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
class Net { public: Net() : m_index(0), m_intensity(0.2f), m_lastIndex(-1) {} // 初始化网格(w x h 分辨率) bool initNet(int w, int h, PIMAGE inputTexture, PIMAGE outputTarget); // 设置输入纹理(相机帧) void setTextureImage(PIMAGE texture); // 设置输出目标 void setOutputTarget(PIMAGE target); // 更新网格物理状态 void update(); // 捕获网格点(鼠标按下时) void catchPoint(float x, float y); // 释放网格点(鼠标松开时) void releasePoint(); // 渲染网格 void drawNet(); // 调整弹性强度 void intensityInc(float f); void intensityDec(float f); float getIntensity(); private: std::vector<Point> m_vec[2]; // 双缓冲点数组 std::vector<Point> m_pointCache; // 渲染缓存 int m_index; // 当前缓冲区索引 int m_width, m_height; // 网格尺寸 float m_intensity; // 弹性强度 int m_lastIndex; // 当前捕获的点索引 PIMAGE m_texture; // 输入纹理(相机帧) PIMAGE m_outputTarget; // 输出目标 // 三角形填充函数 template <class Type> void fillTriangle(const Type& v0, const Type& v1, const Type& v2); }; |
3.2 网格初始化
网格初始化时,将所有点均匀分布在 [0, 1]×[0, 1] 的归一化坐标空间中:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
bool Net::initNet(int w, int h, PIMAGE inputTexture, PIMAGE outputTarget) { if (w < 2 || h < 2) { return false; } m_width = w; m_height = h; m_vec[0].resize(w * h); m_vec[1].resize(w * h); float widthStep = 1.0f / (w - 1); float heightStep = 1.0f / (h - 1); for (int i = 0; i < h; ++i) { const float heightI = i * heightStep; int index = w * i; for (int j = 0; j < w; ++j) { const float widthJ = j * widthStep; // 初始位置和纹理坐标相同,速度为0 m_vec[0][index] = Point(widthJ, heightI, widthJ, heightI); m_vec[1][index] = Point(widthJ, heightI, widthJ, heightI); ++index; } } return true; } |
技术要点:
– 使用双缓冲数组 m_vec[0] 和 m_vec[1] 交替读写
– 归一化坐标便于适配不同分辨率的窗口
– 初始状态下,所有点静止且均匀分布
3.3 网格更新算法
网格的更新使用双缓冲技术,避免读写冲突:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
void Net::update() { const float widthStep = 1.0f / (m_width - 1.0f); const float heightStep = 1.0f / (m_height - 1.0f); int index = (m_index + 1) % 2; // 切换到另一个缓冲区 // 只更新内部点,边界保持固定 for (int i = 1; i < m_height - 1; ++i) { const int k = m_width * i; for (int j = 1; j < m_width - 1; ++j) { const int h = k + j; float dx, dy; // 计算水平方向的加速度(左右邻居的拉力) dx = (m_vec[m_index][h - 1].x + m_vec[m_index][h + 1].x - m_vec[m_index][h].x * 2.0f); dy = (m_vec[m_index][h - 1].y + m_vec[m_index][h + 1].y - m_vec[m_index][h].y * 2.0f); // 计算垂直方向的加速度(上下邻居的拉力) dx += (m_vec[m_index][h - m_width].x + m_vec[m_index][h + m_width].x - m_vec[m_index][h].x * 2.0f); dy += (m_vec[m_index][h - m_width].y + m_vec[m_index][h + m_width].y - m_vec[m_index][h].y * 2.0f); // 模拟能量损失 if (std::signbit(dx) != std::signbit(m_vec[m_index][h].dx)) { dx *= 1.0f + m_intensity; } if (std::signbit(dy) != std::signbit(m_vec[m_index][h].dy)) { dy *= 1.0f + m_intensity; } // 更新速度:v' = v + a * intensity m_vec[m_index][h].dx += dx * m_intensity; m_vec[m_index][h].dy += dy * m_intensity; m_vec[index][h].dx = m_vec[m_index][h].dx; m_vec[index][h].dy = m_vec[m_index][h].dy; // 更新位置:p' = p + v' m_vec[index][h].x = m_vec[m_index][h].x + m_vec[index][h].dx; m_vec[index][h].y = m_vec[m_index][h].y + m_vec[index][h].dy; } } m_index = index; // 切换缓冲区 } |
技术要点:
– 双缓冲避免在更新过程中读写同一个数组
– 边界点不更新,保持网格形状稳定
– 加速度由相邻点的位置差决定(拉普拉斯算子)
– 能量损耗机制确保波纹逐渐衰减
3.4 鼠标交互
当用户按下鼠标左键时,程序会找到最接近鼠标的网格点并将其固定在鼠标位置:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
void Net::catchPoint(float x, float y) { int index; if (m_lastIndex < 0) { // 首次捕获,找到最近的点(使用曼哈顿距离) float mdis = 1e9f; for (int i = 1; i < m_height - 1; ++i) { const int k = m_width * i; for (int j = 1; j < m_width - 1; ++j) { const int h = k + j; const float dis = fabsf(x - m_vec[m_index][h].x) + fabsf(y - m_vec[m_index][h].y); if (dis < mdis) { index = h; mdis = dis; } } } m_lastIndex = index; } else { index = m_lastIndex; } // 固定该点的位置并清零速度 m_vec[0][index].x = x; m_vec[0][index].y = y; m_vec[1][index].x = x; m_vec[1][index].y = y; m_vec[0][index].dx = 0.0f; m_vec[0][index].dy = 0.0f; m_vec[1][index].dx = 0.0f; m_vec[1][index].dy = 0.0f; } void Net::releasePoint() { m_lastIndex = -1; // 松开鼠标,释放捕获的点 } |
技术要点:
– 首次按下时查找最近的网格点
– 后续拖动直接使用已记录的点索引,避免重复查找
– 将该点固定在鼠标位置,速度清零
– 松开鼠标后,该点恢复参与物理模拟
3.5 三角形光栅化
三角形填充是整个渲染过程的核心。Net 类将每个四边形分解为两个三角形,然后对每个三角形进行纹理映射填充:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
template <class Type> inline void Net::fillTriangle(const Type& v0, const Type& v1, const Type& v2) { if (v0.y == v2.y) { _fillSimpleTriangle(v0, v1, v2); } else if (v1.y == v2.y) { _fillSimpleTriangle(v1, v0, v2); } else if (v0.y == v1.y) { _fillSimpleTriangle(v0, v2, v1); } else { _fillNormalTriangle(v0, v1, v2); } } |
三角形有两种情况:
1. 平底/平顶三角形:有两个点的 y 坐标相同,直接填充
2. 普通三角形:将其分解为一个平底和一个平顶三角形
对于简单三角形(平底或平顶),逐行扫描填充:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
template <class Type> inline void Net::_fillSimpleTriangle(const Type& vv0, const Type& v1, const Type& vv2) { assert(vv0.y == vv2.y); const Type& v0 = (vv0.x < vv2.x) ? vv0 : vv2; const Type& v2 = (vv0.x < vv2.x) ? vv2 : vv0; float h = v1.y - v0.y; if (fabs(h) < 1e-6f) { return; // 退化为线段,跳过 } // 计算左右边界的斜率 float dL = (v1.x - v0.x) / h; // 左边界 x 增量 float dR = (v1.x - v2.x) / h; // 右边界 x 增量 // 计算纹理坐标的插值增量 float dUL = (v1.u - v0.u) / h; float dVL = (v1.v - v0.v) / h; float dUR = (v1.u - v2.u) / h; float dVR = (v1.v - v2.v) / h; float xL = v0.x, xR = v2.x; float uL = v0.u, vL = v0.v; float uR = v2.u, vR = v2.v; const color_t* data = m_textureData; color_t* outputBuffer = (color_t*)getbuffer(m_outputTarget); // 逐行扫描填充 for (int i = v0.y; i < v1.y; ++i) { float len = xR - xL; if (fabs(len) < 1e-6f) { continue; // 该行长度为0,跳过 } // 逐列填充 for (int j = xL; j < xR; ++j) { float percent = (j - xL) / len; float u = uL + (uR - uL) * percent; float v = vL + (vR - vL) * percent; // 边界检查 if (u < 0 || v < 0 || u > 1 || v > 1 || i < 0 || j < 0 || i >= m_outputHeight || j >= m_outputWidth) { continue; } // 从纹理中采样 int ww = u * (m_texWidth - 1); int hh = v * (m_texHeight - 1); int index = ww + hh * m_texWidth; int outputIndex = j + i * m_outputWidth; outputBuffer[outputIndex] = data[index]; } // 更新下一行的边界和纹理坐标 xL += dL; xR += dR; uL += dUL; vL += dVL; uR += dUR; vR += dVR; } } |
技术要点:
– 逐行扫描,从三角形顶部到底部
– 每行计算左右边界,然后逐列填充
– 使用线性插值计算纹理坐标
– 添加除零检查和边界检查,确保数值稳定性
四、主循环实现
camera_wave 的主循环整合了相机采集、网格更新和交互控制:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
Net net; PIMAGE target = newimage(getwidth(), getheight()); net.initNet(80, 60, nullptr, target); // 80x60 网格 // 主循环 for (; camera.isStarted() && is_run(); delay_fps(60)) { cleardevice(); // 获取相机帧(非阻塞模式) auto newFrame = camera.grabFrame(0); if (newFrame) { frame = newFrame; net.setTextureImage(frame->getImage()); } // 鼠标交互 if (keystate(key_mouse_l)) { int x, y; mousepos(&x, &y); net.catchPoint(x / (float)getwidth(), y / (float)getheight()); } else { net.releasePoint(); } // 键盘控制 if (kbhit()) { auto keyMsg = getkey(); switch (keyMsg.key) { case '+': case '=': net.intensityInc(0.005f); // 增加弹性 break; case '-': case '_': net.intensityDec(0.005f); // 减少弹性 break; // ... 其他按键处理(相机切换、分辨率切换等) } flushkey(); } // 更新并渲染网格 net.drawNet(); net.update(); putimage(0, 0, target); // 显示信息界面 displayInfo(net.getIntensity(), deviceNames, currentDeviceIndex, resolutions, currentResolutionIndex); } |
技术要点:
– 使用 grabFrame(0) 非阻塞模式,立即返回最新帧或 nullptr
– 鼠标坐标归一化到 [0, 1] 范围,便于与网格坐标匹配
– 先渲染网格 (drawNet),再更新物理状态 (update)
– 鼠标按下时显示网格线,帮助理解变形过程
五、技术要点与优化
5.1 性能优化
- 双缓冲技术:使用两个点数组交替读写,避免数据竞争
- 非阻塞帧获取:
grabFrame(0)立即返回,避免等待造成的卡顿 - 缓存变换结果:将归一化坐标转换为屏幕坐标后缓存在
m_pointCache中 - 边界固定:网格边界点保持不变,减少 20% 以上的计算量
- 合理的网格分辨率:80×60 的网格在性能和效果之间取得平衡
5.2 数值稳定性
- 除零保护:在所有除法运算前检查分母是否接近零
- 边界检查:绘制前检查坐标是否在有效范围内
- 浮点比较:使用
fabs(x) < 1e-6f判断浮点数是否接近零 - 退化三角形处理:当三角形面积为零时跳过渲染,避免无效计算
5.3 用户体验
- 实时反馈:在屏幕上显示当前弹性强度
- 网格可视化:鼠标按下时显示网格线(黄色),帮助理解变形
- 参数调节:通过
+/-键微调弹性参数(每次 0.005) - 信息丰富:显示操作提示、相机设备列表、分辨率列表、作者信息
六、扩展思路
基于 camera_wave 的架构,您可以实现更多有趣的特效:
6.1 彩色滤镜
在纹理采样时修改颜色通道:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 采样后添加滤镜效果 color_t color = data[index]; int r = EGEGET_R(color); int g = EGEGET_G(color); int b = EGEGET_B(color); // 例如:黑白滤镜 int gray = (r + g + b) / 3; outputBuffer[outputIndex] = EGERGB(gray, gray, gray); // 例如:暖色调滤镜 outputBuffer[outputIndex] = EGERGB( std::min(r + 30, 255), g, std::max(b - 30, 0) ); |
6.2 时间延迟效果
保存历史帧,实现时间回溯:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
std::deque<std::shared_ptr<CameraFrame>> historyFrames; const int MAX_HISTORY = 30; // 保存 30 帧历史 // 每帧保存 historyFrames.push_back(frame); if (historyFrames.size() > MAX_HISTORY) { historyFrames.pop_front(); } // 使用延迟 N 帧的图像作为纹理 int delay = 15; if (historyFrames.size() > delay) { net.setTextureImage(historyFrames[historyFrames.size() - delay]->getImage()); } |
6.3 边缘检测
对相机图像进行实时边缘检测:
|
1 2 3 4 5 6 7 8 9 10 11 |
// Sobel 边缘检测 PIMAGE edgeImage = newimage(width, height); for (int i = 1; i < height - 1; ++i) { for (int j = 1; j < width - 1; ++j) { // 计算梯度... int gx = ..., gy = ...; int magnitude = sqrt(gx*gx + gy*gy); putpixel(j, i, magnitude > threshold ? WHITE : BLACK, edgeImage); } } net.setTextureImage(edgeImage); |
6.4 多重网格叠加
使用多个不同参数的网格叠加,产生更复杂的效果:
|
1 2 3 4 5 6 7 8 9 10 11 |
Net net1, net2; net1.initNet(40, 30, frame->getImage(), target1); net1.setIntensity(0.1f); net2.initNet(20, 15, frame->getImage(), target2); net2.setIntensity(0.3f); // 混合渲染 net1.drawNet(); net2.drawNet(); alphaBlend(target1, target2, finalTarget, 0.5f); |
七、常见问题解答
Q1: 为什么我的水波效果看起来不自然?
A: 可以尝试调整以下参数:
– 降低弹性强度(按 - 键)
– 减小网格分辨率(如 40×30)
– 检查窗口大小是否与相机分辨率匹配
Q2: 如何提高渲染性能?
A: 性能优化建议:
– 降低网格分辨率(如从 80×60 改为 60×45)
– 降低相机分辨率(如使用 640×480)
– 使用 Release 模式编译(而非 Debug)
– 确保使用了 INIT_RENDERMANUAL 标志
Q3: 网格变形后如何复位?
A: 可以添加一个复位功能:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
void Net::reset() { for (int i = 0; i < m_height; ++i) { for (int j = 0; j < m_width; ++j) { int index = i * m_width + j; float x = j / (float)(m_width - 1); float y = i / (float)(m_height - 1); m_vec[0][index] = Point(x, y, x, y); m_vec[1][index] = Point(x, y, x, y); } } } |
Q4: 可以改变网格的形状吗?
A: 可以。修改 initNet 函数中的初始位置计算。例如,创建圆形网格:
|
1 2 3 4 5 6 |
float centerX = 0.5f, centerY = 0.5f, radius = 0.4f; float angle = (j / (float)w) * 2 * PI; float r = (i / (float)h) * radius; float x = centerX + r * cos(angle); float y = centerY + r * sin(angle); m_vec[0][index] = Point(x, y, widthJ, heightI); |
八、总结
camera_wave 展示了如何在 EGE 相机模块的基础上实现复杂的图像处理和视觉特效:
- ✅ 基于物理的网格模拟
- ✅ 实时纹理映射和三角形光栅化
- ✅ 交互式鼠标控制
- ✅ 高性能渲染优化
- ✅ 数值稳定性保障
通过学习这个示例,您可以:
– 理解弹性网格的物理原理
– 掌握纹理映射和三角形光栅化技术
– 学习如何优化实时图像处理程序
– 创造自己的相机特效应用
参考资源
- 基础教程:camera_base – 相机基础示例
- EGE 官方网站:http://xege.org
- EGE GitHub 仓库:https://github.com/x-ege/xege
- API 文档:参见 EGE 安装包或源码仓库的
man/api/目录 - 更多示例:参见 EGE 安装包或源码仓库的
demo/目录
完整代码
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 |
/** * @file camera_wave.cpp * @author wysaid (this@xege.org) * @brief 一个使用 ege 内置的相机模块打开相机的例子 * @date 2025-05-18 * */ #define SHOW_CONSOLE 1 #ifndef _CRT_SECURE_NO_WARNINGS #define _CRT_SECURE_NO_WARNINGS 1 #endif #ifndef NOMINMAX #define NOMINMAX 1 #endif #include <cstdio> #include <graphics.h> #include <ege/camera_capture.h> #include <memory> #include <vector> #include <string> #include <cassert> #include <cmath> #include <algorithm> // 文本本地化宏定义 #ifdef _MSC_VER // MSVC编译器使用中文文案 #define TEXT_WINDOW_TITLE "EGE 相机水波特效演示 - 作者: wysaid - 2025" #define TEXT_ERROR_NO_CAMERA "此演示需要相机设备才能运行。\n请连接相机设备后重试。" #define TEXT_ERROR_NO_CAMERA_FEATURE \ "当前 ege 版本不支持相机功能,请使用支持 C++17 的编译器。 如果是 MSVC 编译器,请使用 MSVC 2022 或更高版本。" #define TEXT_ERROR_EXIT_HINT "按任意键退出。" #define TEXT_ERROR_NO_DEVICE "未找到相机设备!" #define TEXT_ERROR_OPEN_FAILED "打开相机设备失败!" #define TEXT_ERROR_GRAB_FAILED "获取帧数据失败!" #define TEXT_CAMERA_CLOSED "相机设备已关闭!" #define TEXT_CAMERA_DEVICE "相机设备: %s" #define TEXT_CPP11_REQUIRED "需要 C++11 或更高版本。" #define TEXT_INTENSITY_RULE "拖拽变形网格。弹性强度: %g" #define TEXT_INFO_MSG "按 '+' 或 '-' 调整弹性, ↑/↓ 切换分辨率。作者: wysaid: http://xege.org" #define TEXT_CAMERA_LIST_TITLE "可用相机设备:" #define TEXT_CAMERA_LIST_ITEM " [%d] %s" #define TEXT_CAMERA_SWITCH "按空格键切换相机,或按数字键选择 | 当前: [%d] %s" #define TEXT_SWITCHING_CAMERA "正在切换到相机 %d..." #define TEXT_RESOLUTION_LIST_TITLE "支持的分辨率:" #define TEXT_RESOLUTION_ITEM " %dx%d" #define TEXT_RESOLUTION_CURRENT " <-当前" #define TEXT_SWITCHING_RESOLUTION "正在切换到分辨率 %dx%d..." #define TEXT_WINDOW_RESIZED "窗口大小已调整为 %dx%d" #else // 非MSVC编译器使用英文文案 #define TEXT_WINDOW_TITLE "EGE camera wave By wysaid - 2025" #define TEXT_ERROR_NO_CAMERA "This demo requires a camera device to run.\nPlease connect a camera and try again." #define TEXT_ERROR_NO_CAMERA_FEATURE \ "The current version of EGE does not support camera features. Please use a compiler that supports C++17. If using MSVC, please use MSVC 2022 or later." #define TEXT_ERROR_EXIT_HINT "Press any key to exit." #define TEXT_ERROR_NO_DEVICE "No camera device found!!" #define TEXT_ERROR_OPEN_FAILED "Failed to open camera device!!" #define TEXT_ERROR_GRAB_FAILED "Failed to grab frame!!" #define TEXT_CAMERA_CLOSED "Camera device closed!!" #define TEXT_CAMERA_DEVICE "Camera device: %s" #define TEXT_CPP11_REQUIRED "C++11 or higher is required." #define TEXT_INTENSITY_RULE "Drag to deform mesh. Intensity: %g" #define TEXT_INFO_MSG "Press '+'/'-' for elasticity, UP/DOWN for resolution. By wysaid: http://xege.org" #define TEXT_CAMERA_LIST_TITLE "Available cameras:" #define TEXT_CAMERA_LIST_ITEM " [%d] %s" #define TEXT_CAMERA_SWITCH "Press SPACE to switch camera, or press number key | Current: [%d] %s" #define TEXT_SWITCHING_CAMERA "Switching to camera %d..." #define TEXT_RESOLUTION_LIST_TITLE "Supported resolutions:" #define TEXT_RESOLUTION_ITEM " %dx%d" #define TEXT_RESOLUTION_CURRENT " <-Current" #define TEXT_SWITCHING_RESOLUTION "Switching to resolution %dx%d..." #define TEXT_WINDOW_RESIZED "Window resized to %dx%d" #endif // 判断一下 C++ 版本, 低于 C++11 的编译器不支持 #if __cplusplus < 201103L #pragma message("C++11 or higher is required.") int main() { fputs(TEXT_CPP11_REQUIRED, stderr); return 0; } #else #define WINDOW_WIDTH 1280 #define WINDOW_HEIGHT 720 // 窗口尺寸限制 #define MIN_LONG_EDGE 640 // 窗口长边最小值 #define MAX_LONG_EDGE 1920 // 窗口长边最大值 /// 结合水波荡漾的 Demo, 给一个相机的版本 struct Point { Point() : x(0), y(0), dx(0), dy(0) {} Point(float _x, float _y, float _u, float _v) : x(_x), y(_y), dx(0), dy(0), u(_u), v(_v) {} float x, y; float dx, dy; float u, v; }; void my_line(int* data, int width, int height, int pnt1x, int pnt1y, int pnt2x, int pnt2y, int color) { int dx = pnt2x - pnt1x; int dy = pnt2y - pnt1y; float x = pnt1x; float y = pnt1y; int step = 0; if (abs(dx) > abs(dy)) { step = abs(dx); } else { step = abs(dy); } // 修复:添加除零检查 if (step == 0) { // 起点和终点相同,直接绘制单点 if (pnt1x >= 0 && pnt1y >= 0 && pnt1x < width && pnt1y < height) { data[pnt1x + pnt1y * width] = color; } return; } float xStep = dx / (float)step; float yStep = dy / (float)step; for (int i = 0; i <= step; i++) { // 修复:应该包含终点,使用 <= if (x >= 0 && y >= 0 && x < width && y < height) { // 修复:边界检查 data[(int)x + (int)y * width] = color; } x += xStep; y += yStep; } } class Net { public: Net() : m_index(0), m_intensity(0.2f), m_lastIndex(-1) {} ~Net() {} void setTextureImage(PIMAGE texture) { m_texture = texture; m_texWidth = getwidth(texture); m_texHeight = getheight(texture); m_textureData = (color_t*)getbuffer(m_texture); } void setOutputTarget(PIMAGE target) { m_outputTarget = target; m_outputWidth = getwidth(target); m_outputHeight = getheight(target); } // 网格的分辨率 w x h bool initNet(int w, int h, PIMAGE inputTexture, PIMAGE outputTarget) { if (w < 2 || h < 2) { return false; } if (inputTexture) { setTextureImage(inputTexture); } if (outputTarget) { setOutputTarget(outputTarget); } m_width = w; m_height = h; m_vec[0].resize(w * h); m_vec[1].resize(w * h); float widthStep = 1.0f / (w - 1); float heightStep = 1.0f / (h - 1); for (int i = 0; i != h; ++i) { const float heightI = i * heightStep; int index = w * i; for (int j = 0; j != w; ++j) { const float widthJ = j * widthStep; m_vec[0][index] = Point(widthJ, heightI, widthJ, heightI); m_vec[1][index] = Point(widthJ, heightI, widthJ, heightI); ++index; } } return true; } void update() { const float widthStep = 1.0f / (m_width - 1.0f); const float heightStep = 1.0f / (m_height - 1.0f); int index = (m_index + 1) % 2; for (int i = 1; i < m_height - 1; ++i) { const int k = m_width * i; for (int j = 1; j < m_width - 1; ++j) { const int h = k + j; float dx, dy; dx = (m_vec[m_index][h - 1].x + m_vec[m_index][h + 1].x - m_vec[m_index][h].x * 2.0f); dy = (m_vec[m_index][h - 1].y + m_vec[m_index][h + 1].y - m_vec[m_index][h].y * 2.0f); dx += (m_vec[m_index][h - m_width].x + m_vec[m_index][h + m_width].x - m_vec[m_index][h].x * 2.0f); dy += (m_vec[m_index][h - m_width].y + m_vec[m_index][h + m_width].y - m_vec[m_index][h].y * 2.0f); // 模拟能量损失, 当加速度方向与速度方向相反时,加快减速 if (std::signbit(dx) != std::signbit(m_vec[m_index][h].dx)) { dx *= 1.0f + m_intensity; } if (std::signbit(dy) != std::signbit(m_vec[m_index][h].dy)) { dy *= 1.0f + m_intensity; } m_vec[m_index][h].dx += dx * m_intensity; m_vec[m_index][h].dy += dy * m_intensity; m_vec[index][h].dx = m_vec[m_index][h].dx; m_vec[index][h].dy = m_vec[m_index][h].dy; m_vec[index][h].x = m_vec[m_index][h].x + m_vec[index][h].dx; m_vec[index][h].y = m_vec[m_index][h].y + m_vec[index][h].dy; } } m_index = index; } void catchPoint(float x, float y) { int index; if (m_lastIndex < 0) { float mdis = 1e9f; for (int i = 1; i < m_height - 1; ++i) { const int k = m_width * i; for (int j = 1; j < m_width - 1; ++j) { const int h = k + j; const float dis = fabsf(x - m_vec[m_index][h].x) + fabsf(y - m_vec[m_index][h].y); if (dis < mdis) { index = h; mdis = dis; } } } m_lastIndex = index; } else { index = m_lastIndex; } m_vec[0][index].x = x; m_vec[0][index].y = y; m_vec[1][index].x = x; m_vec[1][index].y = y; m_vec[0][index].dx = 0.0f; m_vec[0][index].dy = 0.0f; m_vec[1][index].dx = 0.0f; m_vec[1][index].dy = 0.0f; } void releasePoint() { m_lastIndex = -1; } template <class Type> inline void fillTriangle(const Type& v0, const Type& v1, const Type& v2) { if (v0.y == v2.y) { _fillSimpleTriangle(v0, v1, v2); } else if (v1.y == v2.y) { _fillSimpleTriangle(v1, v0, v2); } else if (v0.y == v1.y) { _fillSimpleTriangle(v0, v2, v1); } else { _fillNormalTriangle(v0, v1, v2); } } template <class Type> inline void _fillSimpleTriangle(const Type& vv0, const Type& v1, const Type& vv2) { assert(vv0.y == vv2.y); bool isOK = (vv0.x < vv2.x); const Type& v0 = isOK ? vv0 : vv2; const Type& v2 = isOK ? vv2 : vv0; float h = v1.y - v0.y; // 修复:添加除零检查 if (fabs(h) < 1e-6f) { return; // 三角形退化为线段,跳过 } float dL = (v1.x - v0.x) / h; float dR = (v1.x - v2.x) / h; float dUL = (v1.u - v0.u) / h; float dUR = (v1.u - v2.u) / h; float dVL = (v1.v - v0.v) / h; float dVR = (v1.v - v2.v) / h; float xL = v0.x, xR = v2.x; float uL = v0.u, uR = v2.u; float vL = v0.v, vR = v2.v; const color_t* data = m_textureData; color_t* outputBuffer = (color_t*)getbuffer(m_outputTarget); if (v0.y < v1.y) { for (int i = v0.y; i < v1.y; ++i) { float len = xR - xL; float uLen = uR - uL; float vLen = vR - vL; // 修复:添加除零检查 if (fabs(len) > 1e-6f) { for (int j = xL; j < xR; ++j) { float percent = (j - xL) / len; float u = uL + uLen * percent; float v = vL + vLen * percent; if (u < 0 || v < 0 || u > 1 || v > 1 || i < 0 || j < 0 || i >= m_outputHeight || j >= m_outputWidth) { continue; } // 修复:添加纹理边界检查 int ww = u * (m_texWidth - 1); int hh = v * (m_texHeight - 1); int index = ww + hh * m_texWidth; int outputIndex = j + i * m_outputWidth; outputBuffer[outputIndex] = data[index]; } } xL += dL; xR += dR; uL += dUL; uR += dUR; vL += dVL; vR += dVR; } } else { for (int i = v0.y; i > v1.y; --i) { float len = xR - xL; float uLen = uR - uL; float vLen = vR - vL; // 修复:添加除零检查 if (fabs(len) > 1e-6f) { for (int j = xL; j < xR; ++j) { float percent = (j - xL) / len; float u = uL + uLen * percent; float v = vL + vLen * percent; if (u < 0 || v < 0 || u > 1 || v > 1 || i < 0 || j < 0 || i >= m_outputHeight || j >= m_outputWidth) { continue; } int ww = u * (m_texWidth - 1); int hh = v * (m_texHeight - 1); int index = ww + hh * m_texWidth; outputBuffer[j + i * m_outputWidth] = data[index]; } } xL -= dL; xR -= dR; uL -= dUL; uR -= dUR; vL -= dVL; vR -= dVR; } } } template <class Type> inline void _fillNormalTriangle(const Type& v0, const Type& v1, const Type& v2) { const Type* pnts[] = {&v0, &v1, &v2}; if ((*pnts[0]).y > (*pnts[1]).y) { std::swap(pnts[0], pnts[1]); } if ((*pnts[0]).y > (*pnts[2]).y) { std::swap(pnts[0], pnts[2]); } if ((*pnts[1]).y > (*pnts[2]).y) { std::swap(pnts[1], pnts[2]); } const Type &vv0 = *pnts[0], &vv1 = *pnts[1], &vv2 = *pnts[2]; // 修复:添加除零检查 float heightDiff = vv2.y - vv0.y; if (fabs(heightDiff) < 1e-6f) { return; // 三角形退化,跳过 } Type newPoint; float percent = (vv1.y - vv0.y) / heightDiff; newPoint.x = floorf(vv0.x + (vv2.x - vv0.x) * percent); newPoint.y = vv1.y; newPoint.u = vv0.u + (vv2.u - vv0.u) * percent; newPoint.v = vv0.v + (vv2.v - vv0.v) * percent; _fillSimpleTriangle(newPoint, vv0, vv1); _fillSimpleTriangle(newPoint, vv2, vv1); } void drawNet() { std::vector<Point>& vec = m_vec[m_index]; int sz = vec.size(); int i; // i变量前置, 方便vc6.0 编译 m_pointCache.resize(sz); #if _MSC_VER < 1600 // 兼容vc6.0 Point* v = &m_pointCache[0]; memcpy(v, &vec[0], sz * sizeof(vec[0])); #else Point* v = m_pointCache.data(); memcpy(v, vec.data(), sz * sizeof(vec[0])); #endif for (i = 0; i != sz; ++i) { v[i].x = floorf(v[i].x * m_outputWidth); v[i].y = floorf(v[i].y * m_outputHeight); } for (i = 1; i != m_height; ++i) { const int k1 = (i - 1) * m_width; const int k2 = i * m_width; for (int j = 1; j != m_width; ++j) { const int p1 = k1 + j - 1; const int p2 = k1 + j; const int p3 = k2 + j - 1; const int p4 = k2 + j; fillTriangle(v[p1], v[p2], v[p3]); fillTriangle(v[p3], v[p2], v[p4]); } } color_t* outputBuffer = (color_t*)getbuffer(m_outputTarget); if (m_lastIndex > 0) { for (i = 0; i != m_height; ++i) { const int k = i * m_width; for (int j = 1; j != m_width; ++j) { const int h = k + j; // line(v[h - 1].x, v[h - 1].y, v[h].x, v[h].y, m_outputTarget); my_line((int*)outputBuffer, m_outputWidth, m_outputHeight, v[h - 1].x, v[h - 1].y, v[h].x, v[h].y, 0x00ffff00); } } for (i = 0; i != m_width; ++i) { for (int j = 1; j != m_height; ++j) { const int h2 = j * m_width + i; const int h1 = (j - 1) * m_width + i; // line(v[h1].x, v[h1].y, v[h2].x, v[h2].y, m_outputTarget); my_line((int*)outputBuffer, m_outputWidth, m_outputHeight, v[h1].x, v[h1].y, v[h2].x, v[h2].y, 0x00ffff00); } } } } void intensityInc(float f) { m_intensity += f; if (m_intensity > 0.3f) { m_intensity = 0.3f; } } void intensityDec(float f) { m_intensity -= f; if (m_intensity < 0.001f) { m_intensity = 0.001f; } } float getIntensity() { return m_intensity; } private: std::vector<Point> m_vec[2]; std::vector<Point> m_pointCache; int m_index; int m_width, m_height; float m_intensity; int m_lastIndex; PIMAGE m_texture; int m_texWidth, m_texHeight; color_t* m_textureData; PIMAGE m_outputTarget; int m_outputWidth, m_outputHeight; }; // 根据相机分辨率调整窗口大小,保持比例一致 // 返回 true 表示窗口大小发生了变化 bool adjustWindowToCamera(int cameraWidth, int cameraHeight, PIMAGE& target, Net& net) { int windowWidth = getwidth(); int windowHeight = getheight(); // 计算相机的长边和短边 int cameraLongEdge = (std::max)(cameraWidth, cameraHeight); int cameraShortEdge = (std::min)(cameraWidth, cameraHeight); float cameraRatio = (float)cameraWidth / cameraHeight; // 获取屏幕可用区域(考虑任务栏) RECT workArea; SystemParametersInfo(SPI_GETWORKAREA, 0, &workArea, 0); int screenAvailWidth = workArea.right - workArea.left; int screenAvailHeight = workArea.bottom - workArea.top; // 留一点边距,避免窗口贴边 screenAvailWidth -= 20; screenAvailHeight -= 40; // 计算目标窗口长边 int targetLongEdge = cameraLongEdge; // 限制1:长边不能小于 MIN_LONG_EDGE if (targetLongEdge < MIN_LONG_EDGE) { targetLongEdge = MIN_LONG_EDGE; } // 限制2:长边不能大于 MAX_LONG_EDGE if (targetLongEdge > MAX_LONG_EDGE) { targetLongEdge = MAX_LONG_EDGE; } // 根据长边和比例计算窗口尺寸 int newWidth, newHeight; if (cameraWidth >= cameraHeight) { // 横向视频,宽度是长边 newWidth = targetLongEdge; newHeight = (int)(newWidth / cameraRatio); } else { // 纵向视频,高度是长边 newHeight = targetLongEdge; newWidth = (int)(newHeight * cameraRatio); } // 限制3:不能超过屏幕可用区域 if (newWidth > screenAvailWidth) { float scale = (float)screenAvailWidth / newWidth; newWidth = screenAvailWidth; newHeight = (int)(newHeight * scale); } if (newHeight > screenAvailHeight) { float scale = (float)screenAvailHeight / newHeight; newHeight = screenAvailHeight; newWidth = (int)(newWidth * scale); } // 如果新尺寸与当前尺寸相同,无需调整 if (newWidth == windowWidth && newHeight == windowHeight) { return false; } // 调用 initgraph 调整窗口大小 (无需 closegraph) initgraph(newWidth, newHeight, INIT_RENDERMANUAL); setcaption(TEXT_WINDOW_TITLE); setbkmode(TRANSPARENT); // 重新创建目标图像并重新初始化网格 delimage(target); target = newimage(newWidth, newHeight); net.setOutputTarget(target); printf(TEXT_WINDOW_RESIZED, newWidth, newHeight); printf("\n"); return true; } void showErrorWindow() { settarget(nullptr); setbkcolor(BLACK); cleardevice(); setcolor(RED); if (hasCameraCaptureModule()) { outtextrect(0, 0, getwidth(), getheight(), TEXT_ERROR_NO_CAMERA); } else { outtextrect(0, 0, getwidth(), getheight(), TEXT_ERROR_NO_CAMERA_FEATURE); } outtextxy(10, 30, TEXT_ERROR_EXIT_HINT); getch(); closegraph(); } // 切换相机设备的辅助函数 bool switchCamera(ege::CameraCapture& camera, int deviceIndex, int deviceCount, int resWidth = WINDOW_WIDTH, int resHeight = WINDOW_HEIGHT) { if (deviceIndex < 0 || deviceIndex >= deviceCount) { return false; } // 先关闭当前相机 if (camera.isStarted()) { camera.close(); } // 设置相机分辨率 camera.setFrameSize(resWidth, resHeight); camera.setFrameRate(30); // 打开新相机 if (!camera.open(deviceIndex)) { return false; } camera.start(); return true; } // 分辨率信息结构体 struct ResolutionItem { int width; int height; }; // 获取并保存分辨率列表 std::vector<ResolutionItem> getResolutionList(ege::CameraCapture& camera) { std::vector<ResolutionItem> resolutions; auto resList = camera.getDeviceSupportedResolutions(); if (resList.count > 0) { for (int i = 0; i < resList.count; ++i) { resolutions.push_back({resList.info[i].width, resList.info[i].height}); } } return resolutions; } // 根据当前帧分辨率找到对应的分辨率索引 int findCurrentResolutionIndex(const std::vector<ResolutionItem>& resolutions, int width, int height) { for (size_t i = 0; i < resolutions.size(); ++i) { if (resolutions[i].width == width && resolutions[i].height == height) { return static_cast<int>(i); } } return 0; // 如果找不到,返回第一个 } int main() { /// 在相机的高吞吐场景下, 不设置 RENDERMANUAL 会出现闪屏. initgraph(WINDOW_WIDTH, WINDOW_HEIGHT, INIT_RENDERMANUAL); setcaption(TEXT_WINDOW_TITLE); Net net; ege::CameraCapture camera; PIMAGE target = newimage(getwidth(), getheight()); char msgBuffer[1024]; char intensityBuffer[256]; setbkmode(TRANSPARENT); setcolor(YELLOW, target); sprintf(intensityBuffer, TEXT_INTENSITY_RULE, net.getIntensity()); net.initNet(80, 60, nullptr, target); // 0: 不输出日志, 1: 输出警告日志, 2: 输出常规信息, 3: 输出调试信息, 超过 3 等同于 3. ege::enableCameraModuleLog(2); // 保存设备信息 std::vector<std::string> deviceNames; int deviceCount = 0; int currentDeviceIndex = 0; // 分辨率相关 std::vector<ResolutionItem> resolutions; int currentResolutionIndex = 0; { /// 获取所有相机设备的名称 auto devices = camera.findDeviceNames(); if (devices.count == 0) { fputs(TEXT_ERROR_NO_DEVICE, stderr); showErrorWindow(); return -1; } deviceCount = devices.count; for (int i = 0; i < devices.count; ++i) { deviceNames.push_back(devices.info[i].name); printf(TEXT_CAMERA_DEVICE, devices.info[i].name); printf("\n"); } } // 打开第一个相机设备 if (!switchCamera(camera, 0, deviceCount)) { fputs(TEXT_ERROR_OPEN_FAILED, stderr); return -1; } // 获取分辨率列表 resolutions = getResolutionList(camera); std::shared_ptr<CameraFrame> frame; { // 尝试获取一帧数据, 最多等五秒, 如果失败, 直接退出. frame = camera.grabFrame(5000); if (!frame) { fputs(TEXT_ERROR_GRAB_FAILED, stderr); camera.close(); return -1; } net.setTextureImage(frame->getImage()); } for (; camera.isStarted() && is_run(); delay_fps(60)) { cleardevice(); // 0 表示不等待, 直接返回, 要处理一下返回值为空的情况. auto newFrame = camera.grabFrame(0); if (newFrame) { // 使用 shared_ptr,无需手动释放旧帧 frame = newFrame; net.setTextureImage(frame->getImage()); } if (!frame) { fputs(TEXT_ERROR_GRAB_FAILED, stderr); break; } if (keystate(key_mouse_l)) { int x, y; mousepos(&x, &y); net.catchPoint(x / (float)getwidth(), y / (float)getheight()); } else { net.releasePoint(); } if (kbhit()) { auto keyMsg = getkey(); int key = keyMsg.key; int newDeviceIndex = -1; int newResolutionIndex = -1; switch (key) { case '+': case '=': net.intensityInc(0.005f); sprintf(intensityBuffer, TEXT_INTENSITY_RULE, net.getIntensity()); break; case '-': case '_': net.intensityDec(0.005f); sprintf(intensityBuffer, TEXT_INTENSITY_RULE, net.getIntensity()); break; case key_up: // 上箭头键:切换到上一个分辨率 if (!resolutions.empty()) { newResolutionIndex = (currentResolutionIndex - 1 + static_cast<int>(resolutions.size())) % static_cast<int>(resolutions.size()); } break; case key_down: // 下箭头键:切换到下一个分辨率 if (!resolutions.empty()) { newResolutionIndex = (currentResolutionIndex + 1) % static_cast<int>(resolutions.size()); } break; case ' ': // 空格键:切换到下一个相机 newDeviceIndex = (currentDeviceIndex + 1) % deviceCount; break; case key_esc: exit(0); default: // 数字键:切换到指定相机 if (key >= '0' && key <= '9') { int requestedIndex = key - '0'; if (requestedIndex < deviceCount) { newDeviceIndex = requestedIndex; } } break; } // 切换相机设备 if (newDeviceIndex >= 0 && newDeviceIndex != currentDeviceIndex) { printf(TEXT_SWITCHING_CAMERA, newDeviceIndex); printf("\n"); // 使用 shared_ptr,释放引用即可 frame.reset(); if (switchCamera(camera, newDeviceIndex, deviceCount)) { currentDeviceIndex = newDeviceIndex; // 获取新相机的分辨率列表 resolutions = getResolutionList(camera); currentResolutionIndex = 0; // 获取新相机的第一帧 frame = camera.grabFrame(5000); if (frame) { net.setTextureImage(frame->getImage()); } } } // 切换分辨率 if (newResolutionIndex >= 0 && newResolutionIndex != currentResolutionIndex && !resolutions.empty()) { int newWidth = resolutions[newResolutionIndex].width; int newHeight = resolutions[newResolutionIndex].height; printf(TEXT_SWITCHING_RESOLUTION, newWidth, newHeight); printf("\n"); frame.reset(); if (switchCamera(camera, currentDeviceIndex, deviceCount, newWidth, newHeight)) { currentResolutionIndex = newResolutionIndex; // 调整窗口大小以匹配相机分辨率比例 adjustWindowToCamera(newWidth, newHeight, target, net); // 获取新分辨率的第一帧 frame = camera.grabFrame(5000); if (frame) { net.setTextureImage(frame->getImage()); } } } flushkey(); } net.drawNet(); net.update(); putimage(0, 0, target); // 显示相机设备列表 int textY = 10; setcolor(0x00ff0000); // 红色 outtextxy(10, textY, TEXT_INFO_MSG); textY += 20; outtextxy(10, textY, intensityBuffer); textY += 25; // 显示设备列表标题 setcolor(YELLOW); outtextxy(10, textY, TEXT_CAMERA_LIST_TITLE); textY += 18; // 显示每个设备 for (int i = 0; i < deviceCount; ++i) { if (i == currentDeviceIndex) { setcolor(LIGHTGREEN); } else { setcolor(WHITE); } sprintf(msgBuffer, TEXT_CAMERA_LIST_ITEM, i, deviceNames[i].c_str()); outtextxy(10, textY, msgBuffer); textY += 16; } // 如果有多个相机,显示切换提示 if (deviceCount > 1) { textY += 5; setcolor(CYAN); sprintf(msgBuffer, TEXT_CAMERA_SWITCH, currentDeviceIndex, deviceNames[currentDeviceIndex].c_str()); outtextxy(10, textY, msgBuffer); textY += 20; } // 显示分辨率列表 if (!resolutions.empty()) { textY += 10; setcolor(YELLOW); outtextxy(10, textY, TEXT_RESOLUTION_LIST_TITLE); textY += 18; // 获取当前帧的分辨率来更新索引 if (frame && frame->getImage()) { currentResolutionIndex = findCurrentResolutionIndex(resolutions, frame->getWidth(), frame->getHeight()); } // 最多显示6个分辨率,避免占用太多屏幕空间 int displayCount = std::min(static_cast<int>(resolutions.size()), 6); int startIndex = 0; // 如果分辨率列表很长,以当前选中的为中心显示 if (resolutions.size() > 6) { startIndex = std::max(0, currentResolutionIndex - 3); startIndex = std::min(startIndex, static_cast<int>(resolutions.size()) - 6); } for (int i = startIndex; i < startIndex + displayCount && i < static_cast<int>(resolutions.size()); ++i) { if (i == currentResolutionIndex) { setcolor(LIGHTGREEN); sprintf(msgBuffer, TEXT_RESOLUTION_ITEM TEXT_RESOLUTION_CURRENT, resolutions[i].width, resolutions[i].height); } else { setcolor(WHITE); sprintf(msgBuffer, TEXT_RESOLUTION_ITEM, resolutions[i].width, resolutions[i].height); } outtextxy(10, textY, msgBuffer); textY += 14; } } } fputs(TEXT_CAMERA_CLOSED, stderr); camera.close(); closegraph(); return 0; } #endif |

近期评论