如何在C++中用cv::Mat高效实现3维cv::Mat嵌入4维cv::Mat?
在C++中用cv::Mat高效实现3维卷积核转4维深度可分离卷积核逻辑
我明白你要做的事——把对应depthwise卷积的3维cv::Mat核,转换成标准4维卷积核的形式,就像PyTorch里那段代码实现的:每个通道的核只作用于自身通道,所以4维核里只有(i,i,:,:)的位置是原3维核的切片,其他位置保持初始值。
先说说你之前尝试遇到的问题:
- 尝试1:
kernel4d.at<float>(i,i)只传了两个索引,OpenCV的at方法对于4维Mat会默认把后面的维度索引设为0,所以你只修改了(i,i,0,0)这个单个像素,不是整个切片。 - 尝试2:OpenCV的
at模板函数本身不支持直接传4个索引参数(最多支持3个,对应3维Mat),所以这种写法编译器就会报错,更别说执行了。
下面给你两种高效的实现方式,分别兼顾可读性和性能:
方法1:用子Mat视图拷贝(可读性优先)
利用OpenCV的operator()提取高维Mat的子视图,然后直接拷贝整个切片,代码直观,不用手动写多层循环:
#include <opencv2/opencv.hpp> int main() { int channels = 3; int kernel_size = 5; // 对应你示例中的5x5核,可替换为3x3等 // 初始化3维卷积核:维度顺序 {channels, kernel_size, kernel_size} int size3d[] = {channels, kernel_size, kernel_size}; cv::Mat kernel3d = cv::Mat(3, size3d, CV_32F, cv::Scalar(0)); // 给3维核赋值测试(示例数据) kernel3d.at<cv::Vec<float, 1>>(cv::Vec<int, 3>(0, 1, 2)) = 1.0f; kernel3d.at<cv::Vec<float, 1>>(cv::Vec<int, 3>(1, 3, 4)) = 2.0f; kernel3d.at<cv::Vec<float, 1>>(cv::Vec<int, 3>(2, 0, 0)) = 3.0f; // 初始化4维卷积核:维度顺序 {channels, channels, kernel_size, kernel_size},初始值设为1 int size4d[] = {channels, channels, kernel_size, kernel_size}; cv::Mat kernel4d = cv::Mat(4, size4d, CV_32F, cv::Scalar(1)); // 核心赋值逻辑 for (int i = 0; i < channels; ++i) { // 提取4维核中第i个输出通道、第i个输入通道的完整切片 cv::Mat sub4d = kernel4d( cv::Range(i, i+1), cv::Range(i, i+1), cv::Range::all(), cv::Range::all() ); // 提取3维核中第i个通道的完整切片 cv::Mat sub3d = kernel3d( cv::Range(i, i+1), cv::Range::all(), cv::Range::all() ); // 将3维子切片拷贝到4维子切片中(reshape统一维度后拷贝) sub3d.copyTo(sub4d.reshape(2)); } // 验证结果(可选) float val = kernel4d.at<cv::Vec<float,1>>(cv::Vec<int,4>(0,0,1,2)); std::cout << "Expected 1.0, got: " << val << std::endl; return 0; }
方法2:直接内存拷贝(性能优先)
如果你的场景对性能要求极高,直接操作内存块是最快的方式,前提是确保Mat是连续存储的(初始化的Mat默认都是连续的,子Mat需要额外判断):
#include <opencv2/opencv.hpp> #include <cstring> int main() { int channels = 3; int kernel_size = 5; int size3d[] = {channels, kernel_size, kernel_size}; cv::Mat kernel3d = cv::Mat(3, size3d, CV_32F, cv::Scalar(0)); kernel3d.at<cv::Vec<float,1>>(cv::Vec<int,3>(0,1,2)) = 1.0f; kernel3d.at<cv::Vec<float,1>>(cv::Vec<int,3>(1,3,4)) = 2.0f; kernel3d.at<cv::Vec<float,1>>(cv::Vec<int,3>(2,0,0)) = 3.0f; int size4d[] = {channels, channels, kernel_size, kernel_size}; cv::Mat kernel4d = cv::Mat(4, size4d, CV_32F, cv::Scalar(1)); // 确保Mat是连续存储的,否则需要先clone成连续的 CV_Assert(kernel3d.isContinuous() && kernel4d.isContinuous()); float* ptr4d = kernel4d.ptr<float>(); float* ptr3d = kernel3d.ptr<float>(); int slice_size = kernel_size * kernel_size; // 每个3x3/5x5切片的元素个数 int channel_block_size = channels * slice_size; // 4维核中单个输出通道对应的所有输入通道的总元素数 for (int i = 0; i < channels; ++i) { // 计算4维核中(i,i,:,:)切片的起始地址 float* dest = ptr4d + i * channel_block_size + i * slice_size; // 计算3维核中(i,:,:)切片的起始地址 float* src = ptr3d + i * slice_size; // 直接拷贝整个切片的内存 memcpy(dest, src, slice_size * sizeof(float)); } // 验证 float val = kernel4d.at<cv::Vec<float,1>>(cv::Vec<int,4>(1,1,3,4)); std::cout << "Expected 2.0, got: " << val << std::endl; return 0; }
补充:关于之前尝试2的修正(不推荐,仅作参考)
如果你一定要用at方法访问4维Mat,需要传递一个cv::Vec<int,4>类型的索引,但这种三层循环的方式效率很低,不建议在实际代码中使用:
for (int i = 0; i < channels; ++i) { cv::Vec<int,4> idx4d; cv::Vec<int,3> idx3d; idx4d[0] = i; idx4d[1] = i; idx3d[0] = i; for (int k = 0; k < kernel_size; ++k) { idx4d[2] = k; idx3d[1] = k; for (int l = 0; l < kernel_size; ++l) { idx4d[3] = l; idx3d[2] = l; kernel4d.at<cv::Vec<float,1>>(idx4d) = kernel3d.at<cv::Vec<float,1>>(idx3d); } } }
内容的提问来源于stack exchange,提问作者Julz




