5.29-6.4解决问题归纳
1.CLion配置opencv
在opencv官网下载对应版本的opencv。
下载地址
计算机电脑内配置环境
验证是否安装成功
cmd终端输入以下命令成功,成功显示版本号则为成功。
python
import cv2
print( cv2.__version__ )
CLion编写CPP连接opencv
GPT建议文件夹目录为:
MyProject/
├── CMakeLists.txt # 顶层 CMake 构建文件
├── README.md # 项目说明文档
├── third_party/ # 第三方库(如 OpenCV 可通过 find_package,也可源码放此)
│ └── ...
├── include/ # 公共头文件(暴露给外部使用的 API 接口)
│ └── my_project/ # 命名空间子目录(避免污染)
│ └── my_header.h
├── src/ # 源代码
│ ├── main.cpp # 入口程序
│ └── my_module.cpp
├── lib/ # 项目生成的静态库或动态库(可选)
│ └── ...
├── bin/ # 可执行文件输出目录
│ └── ...
├── test/ # 单元测试(如使用 GoogleTest)
│ ├── CMakeLists.txt
│ └── my_module_test.cpp
├── tools/ # 工具程序或脚本(如数据生成、图像标注工具等)
│ └── data_preprocessor.cpp
├── data/ # 测试样例数据或图片(不编译)
│ └── image01.jpg
├── docs/ # 项目文档
│ └── design.md
└── config/ # 配置文件(如 YAML、JSON)└── default.yaml
本demo不涉及太多代码,文件夹目录为:
project/
├── bin/
├──imgs/
├── include/
│ └── feature_detection.h
├── src/
│ ├── feature_detection.cpp
│ └── main.cpp
├── CMakeLists.txt
编写CMakeLists文件
cmake_minimum_required(VERSION 3.10)
project(project) # 项目名称# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17) # C++ 标准版本# 查找 OpenCV
find_package(OpenCV REQUIRED) # 查找 OpenCV 库
include_directories(${OpenCV_INCLUDE_DIRS} # 包含 OpenCV 头文件目录${PROJECT_SOURCE_DIR}/include # 包含头文件目录
)# 设置源文件路径
file(GLOB SRC_FILES${PROJECT_SOURCE_DIR}/src/*.cpp
)# 添加可执行文件
add_executable(project ${SRC_FILES}) # 创建名为 project 的可执行文件,包含所有源文件# 链接 OpenCV 库
target_link_libraries(project ${OpenCV_LIBS}) # 链接 OpenCV 库到可执行文件
cmake_minimum_required(VERSION 3.10)
设置cmake最低版本要求project(project)
设置项目名称set(CMAKE_CXX_STANDARD 17)
设置C++标准版本find_package(OpenCV REQUIRED)
查找opencv库include_directories( ${OpenCV_INCLUDE_DIRS} # 包含 OpenCV 头文件目录 ${PROJECT_SOURCE_DIR}/include # 包含头文件目录 )
中include_directories
是用来添加头文件的,${OpenCV_INCLUDE_DIRS}
在配置的opencv环境中找opencv的头文件库,${PROJECT_SOURCE_DIR}/include
找项目根目录中叫include的子目录中的头文件。file(GLOB SRC_FILES ${PROJECT_SOURCE_DIR}/src/*.cpp )
设置指定目录下的所有cpp文件为SRC_FIES
add_executable(project ${SRC_FILES})
把项目里SRC_FILES
都添加为可执行文件。target_link_libraries
添加链接opencv库到可执行文件。
CLion编译器内配置
工具链选择MSVC,记得和opencv版本相匹配。
Cmake配置选项表内填入
-B build -DOpenCV_DIR=C:\opencv\build\x64\vc16\lib
-B build
:-B 表示指定构建输出目录,也就是构建了一个叫build的存放构建中间文件和生成文件的目录。-DOpenCV_DIR=
-D 表示定义一个变量,OpenCV_DIR 是 OpenCV 的 CMake 配置路径变量。
.h文件编写
#ifndef 头文件名的大写宏
#define 头文件名的大写宏// 头文件内容...#endif
当你在多个 .cpp
文件中包含同一个头文件(比如 feature_detection.h
)时,为了防止这个头文件内容被重复包含造成编译错误,就加上这种宏判断。
简单来说:防止重复加载头文件。
与program one区别
特性 | #ifndef/#define | #pragma once |
---|---|---|
机制 | 通过宏名判断是否重复 | 编译器自动确保只包含一次 |
可移植性 | 完全标准、跨平台 | 非标准,但主流编译器都支持 |
重复命名风险 | 可能有宏冲突 | 没有冲突 |
易写性 | 要手动命名宏 | 简洁一行 |
性能 | 稍慢(需宏判断) | 稍快(编译器优化) |
编码格式问题
当你编写好你的代码准备构建运行时会发现类似这样的报错。
warning C4819: 该文件包含不能在当前代码页(936)中表示的字符。请将该文件保存为 Unicode 格式以防止数据丢失
虽然是个 警告(warning) 而不是致命错误,但它 常常会导致 VS 编译器处理失败或生成乱码结果。这与文件的 编码格式 和当前系统的 代码页 设置有关。
解决办法:
要编译执行的文件添加编码BOM
报错原理:
用 CLion + CMake + MSVC (cl.exe) 编译 C++ 项目时,这其实是 Windows 编译器(cl.exe)在处理非 ASCII 字符时的一个“编码问题”。Windows 上的 MSVC 编译器 cl.exe 默认使用 系统的当前代码页(Code Page) 来读取和解释源文件内容。cl.exe 会默认把 .cpp 文件当成 GBK 编码来读取
BOM 是 UTF-8 文件的开头特殊标记:
EF BB BF
这 3 个字节告诉读取程序:这是一个 UTF-8 编码文件!当 MSVC(cl.exe)看到 BOM 时,它会自动切换到 UTF-8 解码模式,不再用默认的 GBK 来读文件。
DLL文件导出
想要把图像识别相关算法封装好让其他语言调用,其中一种方法就是生成动态链接库。与之对应的是静态链接库。
- 相同点:
项目 | 描述 |
---|---|
功能作用 | 都用于将通用代码封装起来,供多个程序共享或重用。 |
组织结构 | 都可以包含函数、类、数据等。 |
编译依赖 | 使用它们的程序在编译时都需要有对应的库头文件和接口声明。 |
目的 | 提高代码复用性、降低维护成本、模块化开发。 |
- 不同点:
比较项 | 静态链接库(.lib / .a) | 动态链接库(.dll / .so / .dylib) |
---|---|---|
链接时间 | 编译时链接(静态链接) | 程序运行时加载(动态链接) |
生成文件 | .lib (Windows),.a (Linux) | .dll (Windows),.so (Linux),.dylib (macOS) |
可执行文件大小 | 较大(库内容被复制到可执行文件) | 较小(库代码不嵌入可执行文件) |
运行依赖 | 无需依赖外部库文件 | 必须确保动态库存在于运行环境 |
更新维护 | 更新库需重新编译程序 | 只需替换 DLL,即可更新功能 |
内存使用 | 每个程序有自己的副本 | 多个程序可共享同一份 DLL |
调试复杂度 | 调试简单 | 相对复杂,特别是跨模块调试 |
跨平台移植 | 可控性较强 | 需要确保目标系统兼容动态库 |
.h文件接口
#ifdef _WIN32
#define DLL_EXPORT extern "C" __declspec(dllexport)
#else
#define DLL_EXPORT extern "C"
#endif
#ifdef _WIN32
检查当前编译环境是否是 Windows。__declspec(dllexport)
表示这个符号(函数、变量)需要被导出到 DLL 中,供其他程序调用。extern "C"
用于告诉编译器采用 C语言的方式导出符号名(不进行 C++ 名字重整 name mangling),这样 DLL 可以被 C/C++ 或其他语言调用,否则会乱码。
For a example:
DLL_EXPORT int detect_feature_points(const char* imagePath,PointResult** results,int* resultCount,const char* saveFolderPath = "result"
);
参数名 | 类型 | 含义 |
---|---|---|
const char* imagePath | 输入 | 图像文件路径,用于加载图像 |
PointResult** results | 输出 | 返回的特征点数组的指针(通过内存分配) |
int* resultCount | 输出 | 返回的特征点个数 |
const char* saveFolderPath = "result" | 可选输入 | 结果保存路径(默认是 “result” 文件夹) |
.cpp接口
在生产DLL文件时报错了,我就有了这个疑问,为什么.h里面写好接口,.cpp文件还要进一步加上导出关键字。
查阅相关资料了解到:.h
头文件中的 extern "C" __declspec(dllexport)
只是声明;.cpp
文件中如果定义函数时没有加上 extern "C"
,链接器可能无法识别并导出正确的符号。extern "C"
是“声明约定”不是“自动继承”,这只是告诉编译器:“这个函数应该用 C语言的方式导出名字”。这个声明不会影响 .cpp
文件中函数定义的 链接方式,它并不会自动“套用”到实现上。
因此在.cpp文件上也需要加上导出关键字:
extern "C" __declspec(dllexport)
int detect_feature_points(const char* imagePath, PointResult** results, int* resultCount, const char* saveFolderPath) {if (!imagePath || !results || !resultCount) return -1;try {FeatureDetector detector;std::vector<PointData> points = detector.detectContours(imagePath, saveFolderPath ? saveFolderPath : "result");if (points.empty()) {*resultCount = 0;*results = nullptr;return 0; // 无检测点也算成功}// 分配内存给 C#,使用 new[]PointResult* temp = new PointResult[points.size()];for (size_t i = 0; i < points.size(); ++i) {temp[i].name = _strdup(points[i].name.c_str()); // 分配 name 的内存temp[i].pixel_x = points[i].pixel.x;temp[i].pixel_y = points[i].pixel.y;temp[i].xz_x = points[i].xz.x;temp[i].xz_z = points[i].xz.y;}*results = temp;*resultCount = static_cast<int>(points.size());return 0;}catch (...) {return -2; // 其他错误}
}
CMakeLists文件重新编写
cmake_minimum_required(VERSION 3.10)
project(feature_detection_dll)
set(CMAKE_CXX_STANDARD 17)
# 查找 OpenCV
find_package(OpenCV REQUIRED)
# 头文件目录
include_directories(${PROJECT_SOURCE_DIR}/include${OpenCV_INCLUDE_DIRS}
)# 源码文件(生成 DLL)
file(GLOB DLL_SRC_FILES${PROJECT_SOURCE_DIR}/src/feature_detection.cpp
)
# 生成 DLL
add_library(feature_detection SHARED ${DLL_SRC_FILES})
target_link_libraries(feature_detection ${OpenCV_LIBS})
add_library(... SHARED ...)
创建共享库(DLL)。
C#调用
生成的DLL文件要放到C#项目的对应文件夹去调用。调试代码类似这种:
using System;
using System.Runtime.InteropServices;
class Program
{// C# 对应 C++ 的结构体[StructLayout(LayoutKind.Sequential)]public struct PointResult{public IntPtr name; // char*public int pixel_x;public int pixel_y;public double xz_x;public double xz_z;}// 将 char* 转为 C# stringstatic string PtrToString(IntPtr ptr) => Marshal.PtrToStringAnsi(ptr);// 导入 detect_feature_points[DllImport("feature_detection.dll", CallingConvention = CallingConvention.Cdecl)]public static extern int detect_feature_points(string imagePath,out IntPtr results,out int resultCount,string saveFolderPath);// 导入 free_feature_points[DllImport("feature_detection.dll", CallingConvention = CallingConvention.Cdecl)]public static extern void free_feature_points(IntPtr results);static void Main(){string imgPath = @"C:\\project\\PythonProject\\imgs\\AlgorithmData\\1.jpg";string savePath = @"C:\\project\\DLL_test\\test3\\imgs\\result";IntPtr resultPtr;int count;int status = detect_feature_points(imgPath, out resultPtr, out count, savePath);if (status != 0){Console.WriteLine($"DLL 调用失败,错误码: {status}");return;}Console.WriteLine($"成功检测到 {count} 个点:");int size = Marshal.SizeOf(typeof(PointResult));for (int i = 0; i < count; i++){IntPtr itemPtr = IntPtr.Add(resultPtr, i * size);PointResult point = Marshal.PtrToStructure<PointResult>(itemPtr);string name = PtrToString(point.name);Console.WriteLine($"{name}: 像素({point.pixel_x}, {point.pixel_y}), XZ({point.xz_x:F2}, {point.xz_z:F2})");}Console.WriteLine("\n按任意键退出...");Console.ReadKey();// 释放由 DLL 分配的内存free_feature_points(resultPtr);}
}
any cpu和x64的区别
第一次直接跑代码并没有成功,发现any cpu和x64是有区别的。Any CPU 是 .NET 平台的中立选项,但一旦调用了本地(如 C++)的 DLL,就必须保证 C# 和 DLL 是在相同位数平台(x86/x64)下运行,否则会报错。
而我们这种方式生成的一个 64 位 DLL。
CMake + OpenCV + MSVC x64 编译器
any cpu 虽然有x64 但visual stdio的项目属性里面会默认勾选首选32位。因此运行报错。
release和debug的区别
- Debug 模式 用于开发和调试,包含调试信息、运行较慢;
- Release 模式 用于发布或上线,优化性能,但不包含调试信息。
项目 | Debug 模式 | Release 模式 |
---|---|---|
目的 | 开发、调试、测试 | 发布、部署、生产环境 |
优化 | ❌ 不开启编译器优化(保留原始结构) | ✅ 开启各种性能优化 |
调试信息 | ✅ 含调试符号(.pdb 文件) | ❌ 默认不含调试符号(可以手动开启) |
运行速度 | 🐢 慢(未优化) | 🚀 快(优化代码执行) |
生成的文件大小 | 📦 较大(保留变量名、调试信息) | 💡 较小(去除无用信息) |
断点调试支持 | ✅ 支持断点、变量查看、单步运行 | ❌ 不支持或极其困难 |
安全检查 | ✅ 启用运行时断言、越界检测等 | ❌ 通常关闭这些检查以提升性能 |
可读性(反汇编) | ✅ 结构清晰,接近源代码 | ❌ 优化过度,难以跟踪 |