Initial Linux Qt OpenCV project
This commit is contained in:
47
.gitignore
vendored
Normal file
47
.gitignore
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# 构建输出
|
||||
/build*/
|
||||
/dist*/
|
||||
|
||||
# 项目内二进制依赖与下载缓存
|
||||
/tools/linux-build-env/
|
||||
/tools/micromamba/
|
||||
/tools/cmake-linux
|
||||
/tools/ninja-linux
|
||||
/tools/compiler-linux
|
||||
/tools/python-bin
|
||||
/tools/python-venv
|
||||
/third_party/qt-linux
|
||||
/third_party/opencv-linux
|
||||
/downloads/
|
||||
|
||||
# 历史 Windows 二进制依赖,体积很大,不纳入 Git
|
||||
/third_party/opencv-windows
|
||||
/third_party/opencv-windows*/
|
||||
/third_party/qt-windows
|
||||
/third_party/qt-windows-msvc
|
||||
/third_party/qt-windows*/
|
||||
/third_party/qt-tools/
|
||||
/tools/cmake-windows/
|
||||
/tools/cmake-current-windows
|
||||
|
||||
# 日志和临时文件
|
||||
*.log
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
.DS_Store
|
||||
|
||||
# CMake / Ninja
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
cmake_install.cmake
|
||||
install_manifest.txt
|
||||
.ninja_*
|
||||
build.ninja
|
||||
compile_commands.json
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.user
|
||||
*.pro.user
|
||||
58
CMakeLists.txt
Normal file
58
CMakeLists.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project(chengnan VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC OFF)
|
||||
|
||||
# 本项目目标为 Linux 本地构建。Windows 工具链文件仅作为历史参考,不参与构建。
|
||||
if (WIN32)
|
||||
message(FATAL_ERROR "chengnan 当前按 README 要求只支持 Linux 构建,请不要使用 Windows 编译工具链。")
|
||||
endif()
|
||||
|
||||
find_package(Qt6 QUIET COMPONENTS Widgets Concurrent)
|
||||
if (NOT Qt6_FOUND)
|
||||
find_package(Qt5 REQUIRED COMPONENTS Widgets Concurrent)
|
||||
endif()
|
||||
|
||||
find_package(OpenCV REQUIRED COMPONENTS core imgproc imgcodecs)
|
||||
|
||||
set(PROJECT_SOURCES
|
||||
app/src/main.cpp
|
||||
app/src/MainWindow.cpp
|
||||
app/src/ImageView.cpp
|
||||
app/src/ImageConverter.cpp
|
||||
app/src/ImageProcessor.cpp
|
||||
app/src/ImageWorker.cpp
|
||||
|
||||
app/include/MainWindow.h
|
||||
app/include/ImageView.h
|
||||
app/include/ImageConverter.h
|
||||
app/include/ImageProcessor.h
|
||||
app/include/ImageWorker.h
|
||||
)
|
||||
|
||||
if (Qt6_FOUND)
|
||||
qt_add_executable(chengnan ${PROJECT_SOURCES})
|
||||
target_link_libraries(chengnan PRIVATE Qt6::Widgets Qt6::Concurrent ${OpenCV_LIBS})
|
||||
else()
|
||||
add_executable(chengnan ${PROJECT_SOURCES})
|
||||
target_link_libraries(chengnan PRIVATE Qt5::Widgets Qt5::Concurrent ${OpenCV_LIBS})
|
||||
endif()
|
||||
|
||||
target_include_directories(chengnan PRIVATE
|
||||
${OpenCV_INCLUDE_DIRS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/app/include
|
||||
)
|
||||
|
||||
target_compile_definitions(chengnan PRIVATE
|
||||
QT_NO_KEYWORDS
|
||||
CHENGNAN_APP_VERSION="${PROJECT_VERSION}"
|
||||
)
|
||||
|
||||
target_compile_options(chengnan PRIVATE -Wall -Wextra -Wpedantic)
|
||||
|
||||
install(TARGETS chengnan RUNTIME DESTINATION bin)
|
||||
407
README.md
Normal file
407
README.md
Normal file
@@ -0,0 +1,407 @@
|
||||
# chengnan
|
||||
|
||||
## 项目名称
|
||||
|
||||
chengnan:基于 Qt 与 OpenCV 的图像显示与处理系统
|
||||
|
||||
## 当前实现状态
|
||||
|
||||
本项目已初始化为一个 Qt Widgets + OpenCV + CMake 的 C++ 图像处理应用。当前构建策略已调整为:**在 Linux 中编译,不使用 Windows 编译工具链;所有构建项目所需工具和依赖均采用项目目录内的二进制安装方式,不使用源码编译安装依赖。**
|
||||
|
||||
已实现功能:
|
||||
|
||||
- 打开图片
|
||||
- 保存图片
|
||||
- 另存图片
|
||||
- 使用 QWidget 体系显示图像,不使用 `cv::imshow`
|
||||
- 左右对比显示处理前 / 处理后图像
|
||||
- 图像视图支持鼠标滚轮缩放、双击适配窗口、拖拽平移视图
|
||||
- 图像处理功能:
|
||||
- 放大 / 缩小
|
||||
- 平移
|
||||
- 指定角度旋转
|
||||
- 阈值分割 + 轮廓目标提取
|
||||
- 基于 HSV 均值的简单规则分类
|
||||
- 使用 `QThread` + 信号槽执行 OpenCV 处理任务,避免阻塞 UI
|
||||
|
||||
## Linux 二进制构建原则
|
||||
|
||||
本项目后续构建以 Linux 环境为准,要求如下:
|
||||
|
||||
- 在 Linux 中编译本项目
|
||||
- 不使用 Windows 编译工具链
|
||||
- 不使用 `cl.exe`、`windeployqt.exe`、Windows `cmake.exe`、Windows `ninja.exe` 构建本项目
|
||||
- 不使用当前系统上的 GCC / G++ 作为项目构建工具链
|
||||
- 不全局安装 Qt、OpenCV、CMake、Ninja、Python 包等依赖
|
||||
- 不污染系统 `/usr`、`/usr/local`、系统 Python site-packages 或全局环境变量
|
||||
- 所有构建项目所需工具和依赖必须放在项目目录内
|
||||
- 所有构建项目所需依赖必须优先使用官方或可信来源的二进制包
|
||||
- 不使用源码编译安装 Qt / OpenCV 等依赖
|
||||
- Python 使用项目目录内虚拟环境,不使用 uv 安装或管理环境
|
||||
- 构建脚本只能临时设置当前进程环境变量,不能修改系统环境变量
|
||||
|
||||
推荐目录约定:
|
||||
|
||||
```text
|
||||
chengnan/
|
||||
├── tools/
|
||||
│ ├── cmake-linux/ # Linux 版 CMake 二进制
|
||||
│ ├── ninja-linux/ # Linux 版 Ninja 二进制
|
||||
│ ├── compiler-linux/ # Linux 版二进制编译器工具链,如 LLVM/Clang
|
||||
│ ├── python-bin/ # Python 二进制包
|
||||
│ └── python-venv/ # Python 虚拟环境
|
||||
├── third_party/
|
||||
│ ├── qt-linux/ # Linux 版 Qt 二进制包
|
||||
│ └── opencv-linux/ # Linux 版 OpenCV 二进制包
|
||||
└── build-linux/ # Linux 构建输出目录
|
||||
```
|
||||
|
||||
> 注意:如果 OpenCV 没有适配当前 Linux 发行版的官方二进制包,不应退回源码编译安装。应改为寻找可信的预编译包、容器化二进制 SDK、发行版无污染解包方案,或更换依赖来源。
|
||||
|
||||
## 已清理的历史依赖
|
||||
|
||||
之前为了尝试构建曾产生过源码编译或非目标平台相关内容,目前已删除:
|
||||
|
||||
```text
|
||||
third_party/opencv-src
|
||||
third_party/opencv-build
|
||||
third_party/opencv
|
||||
third_party/qt-sdk
|
||||
third_party/qt
|
||||
third_party/opengl-stub
|
||||
tools/uv
|
||||
tools/pyenv
|
||||
tools/cmake
|
||||
tools/cmake.tar.gz
|
||||
```
|
||||
|
||||
这些内容被删除的原因:
|
||||
|
||||
- `opencv-src` / `opencv-build` / `opencv` 属于源码编译安装路径,不符合“只使用二进制安装”的要求
|
||||
- `qt-sdk` / `qt` 是 Linux 本地尝试阶段的旧 Qt 目录,需要按新的 Linux 二进制依赖规范重新放置
|
||||
- `opengl-stub` 是临时构建验证用内容,不属于正式项目依赖
|
||||
- `tools/uv` / `tools/pyenv` 是 uv 相关环境,不符合“Python 使用虚拟环境,不使用 uv 安装”的要求
|
||||
- `tools/cmake` 是旧 Linux CMake 工具目录,后续应按 `tools/cmake-linux/` 重新放置二进制工具
|
||||
|
||||
## 当前项目内已有的二进制依赖记录
|
||||
|
||||
当前目录中仍保留过 Windows 方案准备的二进制包和脚本,作为历史记录和可选参考,但**后续 Linux 构建不应使用这些 Windows 工具链**:
|
||||
|
||||
```text
|
||||
third_party/qt-windows-msvc-sdk/
|
||||
third_party/qt-windows-msvc
|
||||
third_party/opencv-windows-extract/
|
||||
third_party/opencv-windows
|
||||
third_party/qt-tools/
|
||||
tools/cmake-windows/
|
||||
tools/cmake-current-windows
|
||||
tools/python-bin/
|
||||
scripts/build-windows.ps1
|
||||
scripts/setup-python-venv-windows.ps1
|
||||
DEPENDENCIES.md
|
||||
```
|
||||
|
||||
已验证这些文件曾存在:
|
||||
|
||||
```text
|
||||
third_party/qt-windows-msvc/bin/qmake.exe
|
||||
third_party/qt-windows-msvc/bin/windeployqt.exe
|
||||
third_party/opencv-windows/x64/vc16/lib/OpenCVConfig.cmake
|
||||
third_party/opencv-windows/x64/vc16/lib/opencv_world4100.lib
|
||||
third_party/opencv-windows/x64/vc16/bin/opencv_world4100.dll
|
||||
third_party/qt-tools/Tools/Ninja/ninja.exe
|
||||
tools/cmake-current-windows/bin/cmake.exe
|
||||
tools/python-bin/python.exe
|
||||
```
|
||||
|
||||
这些 Windows 二进制内容不满足“在 Linux 里面编译,不使用 Windows 编译工具链”的新要求,因此 Linux 构建流程不应引用它们。
|
||||
|
||||
## Linux 构建目标
|
||||
|
||||
后续应补充或替换为 Linux 二进制依赖:
|
||||
|
||||
- Linux 版 CMake 二进制包
|
||||
- Linux 版 Ninja 二进制包
|
||||
- Linux 版二进制 C++ 编译器工具链,例如 LLVM/Clang 预编译包
|
||||
- Linux 版 Qt 二进制包
|
||||
- Linux 版 OpenCV 二进制包
|
||||
- 项目内 Python 二进制包和虚拟环境
|
||||
|
||||
构建脚本建议命名为:
|
||||
|
||||
```text
|
||||
scripts/setup-python-venv-linux.sh
|
||||
scripts/build-linux-binary.sh
|
||||
```
|
||||
|
||||
示例构建命令应类似:
|
||||
|
||||
```bash
|
||||
./scripts/build-linux-binary.sh
|
||||
```
|
||||
|
||||
该脚本应只使用项目内路径,例如:
|
||||
|
||||
```bash
|
||||
export PATH="$PWD/tools/cmake-linux/bin:$PWD/tools/ninja-linux:$PWD/tools/compiler-linux/bin:$PATH"
|
||||
export CMAKE_PREFIX_PATH="$PWD/third_party/qt-linux;$PWD/third_party/opencv-linux"
|
||||
```
|
||||
|
||||
但脚本不得写入系统环境,也不得调用系统 GCC/G++ 作为项目编译器。
|
||||
|
||||
## 项目目标
|
||||
|
||||
使用 Qt 设计图形化界面,结合 OpenCV 实现图像的读取、显示、保存、基础几何处理,以及分类或目标提取功能。程序需要支持处理前后图像对比显示,并在图像处理流程中尽量使用 Qt 线程,避免阻塞界面。
|
||||
|
||||
## 技术要求
|
||||
|
||||
### 1. 开发框架
|
||||
|
||||
- GUI 框架:Qt(建议 Qt 6.x,使用 Qt Widgets)
|
||||
- 图像处理库:OpenCV
|
||||
- 构建系统:CMake
|
||||
- 编程语言:C++17 或更高版本
|
||||
- 图像显示:必须使用 Qt 的 QWidget / QLabel / QGraphicsView 等控件显示图像
|
||||
- 禁止使用:`cv::imshow`、`cv::waitKey` 等 OpenCV 窗口显示方式
|
||||
|
||||
### 2. 界面功能要求
|
||||
|
||||
界面由 Qt 设计实现,至少应包含以下基础功能:
|
||||
|
||||
- 打开图片
|
||||
- 保存图片
|
||||
- 另存图片
|
||||
- 显示原始图片
|
||||
- 显示处理后的图片
|
||||
- 支持处理前、处理后图像的对比观看
|
||||
- 显示当前操作状态或处理结果提示
|
||||
|
||||
建议界面布局:
|
||||
|
||||
- 左侧显示原始图片
|
||||
- 右侧显示处理后图片
|
||||
- 顶部或侧边栏放置操作按钮
|
||||
- 底部状态栏显示当前文件路径、处理状态、图片尺寸等信息
|
||||
|
||||
### 3. 图像基础处理要求
|
||||
|
||||
图片处理应包含以下简单操作:
|
||||
|
||||
- 缩放
|
||||
- 放大
|
||||
- 缩小
|
||||
- 按比例缩放
|
||||
- 平移
|
||||
- 向上、下、左、右移动图像显示区域或图像内容
|
||||
- 旋转
|
||||
- 顺时针旋转
|
||||
- 逆时针旋转
|
||||
- 指定角度旋转
|
||||
|
||||
可选增强功能:
|
||||
|
||||
- 灰度化
|
||||
- 二值化
|
||||
- 亮度 / 对比度调节
|
||||
- 滤波 / 模糊
|
||||
- 边缘检测
|
||||
- 撤销 / 重做
|
||||
|
||||
### 4. 分类或目标提取功能要求
|
||||
|
||||
程序应至少实现以下两项功能之一,两者皆有最佳:
|
||||
|
||||
#### 方案 A:图像分类
|
||||
|
||||
可实现简单图像分类,例如:
|
||||
|
||||
- 根据颜色占比进行分类
|
||||
- 根据亮度、纹理、边缘数量进行规则分类
|
||||
- 使用 OpenCV DNN 加载轻量模型进行分类
|
||||
|
||||
分类结果应显示在界面中,例如:
|
||||
|
||||
- 图片类型
|
||||
- 置信度或规则判断依据
|
||||
- 分类耗时
|
||||
|
||||
#### 方案 B:目标提取
|
||||
|
||||
可实现目标提取,例如:
|
||||
|
||||
- 阈值分割
|
||||
- 颜色区域提取
|
||||
- 轮廓检测
|
||||
- 边缘检测
|
||||
- 前景目标提取
|
||||
|
||||
目标提取结果应显示在处理后图像窗口中,并可叠加以下信息:
|
||||
|
||||
- 目标轮廓
|
||||
- 外接矩形
|
||||
- 目标数量
|
||||
- 面积、中心点等简单特征
|
||||
|
||||
建议优先实现目标提取,再扩展简单分类功能。
|
||||
|
||||
### 5. 显示要求
|
||||
|
||||
- 必须具有图像显示功能
|
||||
- 必须显示处理前、处理后的图像对比
|
||||
- 显示控件必须基于 QWidget 体系
|
||||
- 不允许使用 OpenCV 的 `cv::imshow` 显示图片
|
||||
- OpenCV 的 `cv::Mat` 与 Qt 的 `QImage` / `QPixmap` 之间应封装转换函数
|
||||
|
||||
### 6. 线程要求
|
||||
|
||||
考虑图像处理使用 Qt 线程,避免大图像处理时卡住主界面。
|
||||
|
||||
建议设计:
|
||||
|
||||
- 主线程:负责 UI 显示、按钮响应、用户交互
|
||||
- 工作线程:负责 OpenCV 图像处理、分类、目标提取
|
||||
- 线程通信:使用 Qt 信号与槽
|
||||
- 禁止在工作线程中直接操作 QWidget 控件
|
||||
|
||||
建议类设计:
|
||||
|
||||
- `MainWindow`:主窗口,负责 UI 和用户操作
|
||||
- `ImageProcessor`:图像处理逻辑类
|
||||
- `ImageWorker`:运行在 QThread 中的图像处理工作对象
|
||||
- `ImageConverter`:负责 `cv::Mat` 与 `QImage` 转换
|
||||
|
||||
### 7. 环境与依赖安装要求
|
||||
|
||||
所有安装的依赖不得污染本地系统环境,必须采用项目文件夹内二进制安装或指定路径解压的方式。
|
||||
|
||||
要求:
|
||||
|
||||
- 在 Linux 中编译本项目
|
||||
- 不使用 Windows 编译工具链
|
||||
- 不使用系统 GCC / G++ 作为项目构建工具链
|
||||
- 不使用源码编译安装 Qt / OpenCV 等依赖
|
||||
- 不建议直接全局安装 Qt 或 OpenCV
|
||||
- 不建议污染系统 `/usr`、`/usr/local` 等全局路径
|
||||
- 建议将依赖安装到项目目录下,例如:
|
||||
- `chengnan/third_party/qt-linux/`
|
||||
- `chengnan/third_party/opencv-linux/`
|
||||
- `chengnan/tools/cmake-linux/`
|
||||
- `chengnan/tools/ninja-linux/`
|
||||
- `chengnan/tools/compiler-linux/`
|
||||
- `chengnan/tools/python-venv/`
|
||||
- CMake 通过 `CMAKE_PREFIX_PATH` 指向项目内依赖路径
|
||||
- 构建目录使用 `chengnan/build-linux/`
|
||||
- 可使用脚本统一配置环境变量,但只能影响当前进程,不修改系统环境变量
|
||||
|
||||
示例目录:
|
||||
|
||||
```text
|
||||
chengnan/
|
||||
├── README.md
|
||||
├── CMakeLists.txt
|
||||
├── .gitignore
|
||||
├── app/
|
||||
│ ├── include/
|
||||
│ │ ├── MainWindow.h
|
||||
│ │ ├── ImageView.h
|
||||
│ │ ├── ImageProcessor.h
|
||||
│ │ ├── ImageWorker.h
|
||||
│ │ └── ImageConverter.h
|
||||
│ └── src/
|
||||
│ ├── main.cpp
|
||||
│ ├── MainWindow.cpp
|
||||
│ ├── ImageView.cpp
|
||||
│ ├── ImageProcessor.cpp
|
||||
│ ├── ImageWorker.cpp
|
||||
│ └── ImageConverter.cpp
|
||||
├── docs/
|
||||
│ └── windows/ # 历史 Windows 依赖说明和脚本,仅作参考,不参与 Linux 构建
|
||||
├── scripts/
|
||||
│ ├── setup-linux-binary-deps.sh
|
||||
│ ├── setup-python-venv-linux.sh
|
||||
│ └── build-linux-binary.sh
|
||||
├── tools/
|
||||
│ ├── linux-build-env/ # 项目内 Linux 二进制构建环境,Git 忽略
|
||||
│ ├── cmake-linux -> linux-build-env
|
||||
│ ├── ninja-linux -> linux-build-env
|
||||
│ ├── compiler-linux -> linux-build-env
|
||||
│ ├── python-bin -> linux-build-env
|
||||
│ └── python-venv -> linux-build-env
|
||||
├── third_party/
|
||||
│ ├── qt-linux -> ../tools/linux-build-env
|
||||
│ └── opencv-linux -> ../tools/linux-build-env
|
||||
└── build-linux/ # Linux 构建输出目录,Git 忽略
|
||||
```
|
||||
|
||||
示例 CMake 配置方式应写入脚本中统一执行,不能依赖系统工具链:
|
||||
|
||||
```bash
|
||||
./scripts/setup-linux-binary-deps.sh
|
||||
./scripts/build-linux-binary.sh
|
||||
```
|
||||
|
||||
脚本会在项目目录内使用 Linux 二进制包创建隔离环境,并只在当前进程内设置环境变量。核心等价配置如下:
|
||||
|
||||
```bash
|
||||
export PATH="$PWD/tools/linux-build-env/bin:$PATH"
|
||||
export CMAKE_PREFIX_PATH="$PWD/tools/linux-build-env"
|
||||
export PKG_CONFIG_PATH="$PWD/tools/linux-build-env/lib/pkgconfig:$PWD/tools/linux-build-env/share/pkgconfig:$PKG_CONFIG_PATH"
|
||||
export LD_LIBRARY_PATH="$PWD/tools/linux-build-env/lib:$LD_LIBRARY_PATH"
|
||||
export CC="$PWD/tools/linux-build-env/bin/clang"
|
||||
export CXX="$PWD/tools/linux-build-env/bin/clang++"
|
||||
|
||||
cmake -S . -B build-linux \
|
||||
-G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH" \
|
||||
-DCMAKE_C_COMPILER="$CC" \
|
||||
-DCMAKE_CXX_COMPILER="$CXX"
|
||||
|
||||
cmake --build build-linux -j$(nproc)
|
||||
```
|
||||
|
||||
如需临时设置运行环境,建议写入脚本,不写入系统全局配置:
|
||||
|
||||
```bash
|
||||
export PATH="$PWD/third_party/qt-linux/bin:$PWD/tools/cmake-linux/bin:$PWD/tools/ninja-linux:$PWD/tools/compiler-linux/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="$PWD/third_party/qt-linux/lib:$PWD/third_party/opencv-linux/lib:$LD_LIBRARY_PATH"
|
||||
```
|
||||
|
||||
## 推荐实现步骤
|
||||
|
||||
1. 创建 Qt Widgets + CMake 项目结构
|
||||
2. 实现主窗口和基本按钮:打开、保存、另存
|
||||
3. 实现图片显示控件,完成原图和处理图对比显示
|
||||
4. 封装 `cv::Mat` 与 `QImage` 的转换工具
|
||||
5. 实现缩放、平移、旋转等基础处理
|
||||
6. 将图像处理逻辑迁移到 `QThread` 工作线程
|
||||
7. 实现目标提取功能,例如阈值分割 + 轮廓检测
|
||||
8. 可选实现简单分类功能
|
||||
9. 完成保存处理结果图片
|
||||
10. 编写 Linux 本地二进制工具链构建脚本,确保 Qt、OpenCV、CMake、Ninja、编译器等工具均放在项目目录内
|
||||
11. 测试大图处理时界面是否保持响应
|
||||
12. 完善 README、运行说明和截图
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 可以打开常见图片格式,例如 PNG、JPG、BMP
|
||||
- 可以保存当前处理后的图片
|
||||
- 可以另存为指定路径
|
||||
- 可以在同一界面中对比显示处理前和处理后图像
|
||||
- 支持缩放、平移、旋转操作
|
||||
- 至少实现分类或目标提取功能之一
|
||||
- 图像显示使用 QWidget 体系,不使用 `cv::imshow`
|
||||
- 图像处理任务不会明显阻塞 UI
|
||||
- 依赖安装和构建必须限制在项目目录内,不污染系统环境
|
||||
- 在 Linux 中编译,不使用 Windows 编译工具链
|
||||
- 所有工具和依赖使用二进制包或解压式安装,不使用源码编译安装
|
||||
|
||||
## 注意事项
|
||||
|
||||
- QWidget 只能在主线程中创建和更新
|
||||
- 工作线程只处理数据,不直接改 UI
|
||||
- 大图处理时应避免频繁深拷贝,可合理使用 `cv::Mat::clone()` 控制生命周期
|
||||
- `QImage` 与 `cv::Mat` 共享内存时要注意对象生命周期
|
||||
- 保存图片时应保存处理后的图像,而不是原图
|
||||
- 若使用 OpenCV DNN 模型,模型文件应放入项目目录,例如 `models/`
|
||||
- 所有脚本应避免执行破坏性系统操作
|
||||
11
app/include/ImageConverter.h
Normal file
11
app/include/ImageConverter.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <QImage>
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
class ImageConverter final
|
||||
{
|
||||
public:
|
||||
static QImage matToQImage(const cv::Mat &mat);
|
||||
static cv::Mat qImageToMat(const QImage &image);
|
||||
};
|
||||
23
app/include/ImageProcessor.h
Normal file
23
app/include/ImageProcessor.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QMetaType>
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
struct ProcessResult
|
||||
{
|
||||
cv::Mat image;
|
||||
QString description;
|
||||
};
|
||||
|
||||
class ImageProcessor final
|
||||
{
|
||||
public:
|
||||
static ProcessResult rotate(const cv::Mat &source, double angleDegrees);
|
||||
static ProcessResult scale(const cv::Mat &source, double factor);
|
||||
static ProcessResult translate(const cv::Mat &source, int dx, int dy);
|
||||
static ProcessResult extractTargets(const cv::Mat &source);
|
||||
static ProcessResult classifySimple(const cv::Mat &source);
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(ProcessResult)
|
||||
35
app/include/ImageView.h
Normal file
35
app/include/ImageView.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <QGraphicsView>
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
|
||||
class QGraphicsPixmapItem;
|
||||
class QGraphicsScene;
|
||||
class QWheelEvent;
|
||||
class QMouseEvent;
|
||||
|
||||
class ImageView final : public QGraphicsView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ImageView(QWidget *parent = nullptr);
|
||||
|
||||
void setImage(const QImage &image);
|
||||
void clearImage();
|
||||
void fitImageToView();
|
||||
bool hasImage() const;
|
||||
|
||||
protected:
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
void resetSceneRect();
|
||||
|
||||
QGraphicsScene *scene_ = nullptr;
|
||||
QGraphicsPixmapItem *pixmapItem_ = nullptr;
|
||||
QPixmap pixmap_;
|
||||
double zoomFactor_ = 1.0;
|
||||
};
|
||||
33
app/include/ImageWorker.h
Normal file
33
app/include/ImageWorker.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "ImageProcessor.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QMetaType>
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
Q_DECLARE_METATYPE(cv::Mat)
|
||||
|
||||
class ImageWorker final : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class Operation {
|
||||
Rotate,
|
||||
Scale,
|
||||
Translate,
|
||||
ExtractTargets,
|
||||
ClassifySimple
|
||||
};
|
||||
Q_ENUM(Operation)
|
||||
|
||||
public Q_SLOTS:
|
||||
void process(cv::Mat image, ImageWorker::Operation operation, double value, int dx, int dy);
|
||||
|
||||
Q_SIGNALS:
|
||||
void finished(ProcessResult result);
|
||||
void failed(QString message);
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(ImageWorker::Operation)
|
||||
72
app/include/MainWindow.h
Normal file
72
app/include/MainWindow.h
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include "ImageWorker.h"
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QThread>
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
class QAction;
|
||||
class QLabel;
|
||||
class QSpinBox;
|
||||
class QDoubleSpinBox;
|
||||
class QPushButton;
|
||||
class ImageView;
|
||||
|
||||
class MainWindow final : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
Q_SIGNALS:
|
||||
void processRequested(cv::Mat image, ImageWorker::Operation operation, double value, int dx, int dy);
|
||||
|
||||
private Q_SLOTS:
|
||||
void openImage();
|
||||
void saveImage();
|
||||
void saveImageAs();
|
||||
void resetToOriginal();
|
||||
void onProcessFinished(ProcessResult result);
|
||||
void onProcessFailed(const QString &message);
|
||||
void rotateLeft();
|
||||
void rotateRight();
|
||||
void scaleUp();
|
||||
void scaleDown();
|
||||
void translateUp();
|
||||
void translateDown();
|
||||
void translateLeft();
|
||||
void translateRight();
|
||||
void extractTargets();
|
||||
void classifyImage();
|
||||
|
||||
private:
|
||||
void buildUi();
|
||||
void setupWorkerThread();
|
||||
void updateViews();
|
||||
void setBusy(bool busy);
|
||||
bool ensureHasImage() const;
|
||||
void requestProcess(ImageWorker::Operation operation, double value = 0.0, int dx = 0, int dy = 0);
|
||||
bool writeCurrentImageTo(const QString &path);
|
||||
|
||||
ImageView *originalView_ = nullptr;
|
||||
ImageView *processedView_ = nullptr;
|
||||
QLabel *infoLabel_ = nullptr;
|
||||
QLabel *statusLabel_ = nullptr;
|
||||
QDoubleSpinBox *scaleSpin_ = nullptr;
|
||||
QDoubleSpinBox *angleSpin_ = nullptr;
|
||||
QSpinBox *translateSpin_ = nullptr;
|
||||
|
||||
QAction *saveAction_ = nullptr;
|
||||
QAction *saveAsAction_ = nullptr;
|
||||
|
||||
QThread workerThread_;
|
||||
ImageWorker *worker_ = nullptr;
|
||||
|
||||
cv::Mat originalImage_;
|
||||
cv::Mat processedImage_;
|
||||
QString currentPath_;
|
||||
bool busy_ = false;
|
||||
};
|
||||
47
app/src/ImageConverter.cpp
Normal file
47
app/src/ImageConverter.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#include "ImageConverter.h"
|
||||
|
||||
#include <opencv2/imgproc.hpp>
|
||||
|
||||
QImage ImageConverter::matToQImage(const cv::Mat &mat)
|
||||
{
|
||||
if (mat.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
switch (mat.type()) {
|
||||
case CV_8UC1: {
|
||||
QImage image(mat.data, mat.cols, mat.rows, static_cast<int>(mat.step), QImage::Format_Grayscale8);
|
||||
return image.copy();
|
||||
}
|
||||
case CV_8UC3: {
|
||||
cv::Mat rgb;
|
||||
cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB);
|
||||
QImage image(rgb.data, rgb.cols, rgb.rows, static_cast<int>(rgb.step), QImage::Format_RGB888);
|
||||
return image.copy();
|
||||
}
|
||||
case CV_8UC4: {
|
||||
QImage image(mat.data, mat.cols, mat.rows, static_cast<int>(mat.step), QImage::Format_ARGB32);
|
||||
return image.copy();
|
||||
}
|
||||
default: {
|
||||
cv::Mat converted;
|
||||
mat.convertTo(converted, CV_8U);
|
||||
return matToQImage(converted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cv::Mat ImageConverter::qImageToMat(const QImage &image)
|
||||
{
|
||||
if (image.isNull()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QImage converted = image.convertToFormat(QImage::Format_RGB888);
|
||||
cv::Mat rgb(converted.height(), converted.width(), CV_8UC3,
|
||||
const_cast<uchar *>(converted.bits()),
|
||||
static_cast<size_t>(converted.bytesPerLine()));
|
||||
cv::Mat bgr;
|
||||
cv::cvtColor(rgb, bgr, cv::COLOR_RGB2BGR);
|
||||
return bgr.clone();
|
||||
}
|
||||
149
app/src/ImageProcessor.cpp
Normal file
149
app/src/ImageProcessor.cpp
Normal file
@@ -0,0 +1,149 @@
|
||||
#include "ImageProcessor.h"
|
||||
|
||||
#include <opencv2/imgproc.hpp>
|
||||
#include <algorithm>
|
||||
|
||||
ProcessResult ImageProcessor::rotate(const cv::Mat &source, double angleDegrees)
|
||||
{
|
||||
if (source.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const cv::Point2f center(static_cast<float>(source.cols) / 2.0F, static_cast<float>(source.rows) / 2.0F);
|
||||
cv::Mat matrix = cv::getRotationMatrix2D(center, angleDegrees, 1.0);
|
||||
|
||||
const double absCos = std::abs(matrix.at<double>(0, 0));
|
||||
const double absSin = std::abs(matrix.at<double>(0, 1));
|
||||
const int boundW = static_cast<int>(source.rows * absSin + source.cols * absCos);
|
||||
const int boundH = static_cast<int>(source.rows * absCos + source.cols * absSin);
|
||||
|
||||
matrix.at<double>(0, 2) += boundW / 2.0 - center.x;
|
||||
matrix.at<double>(1, 2) += boundH / 2.0 - center.y;
|
||||
|
||||
cv::Mat output;
|
||||
cv::warpAffine(source, output, matrix, cv::Size(boundW, boundH), cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(30, 30, 30));
|
||||
return {output, QStringLiteral("旋转 %1° 完成").arg(angleDegrees)};
|
||||
}
|
||||
|
||||
ProcessResult ImageProcessor::scale(const cv::Mat &source, double factor)
|
||||
{
|
||||
if (source.empty() || factor <= 0.0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
cv::Mat output;
|
||||
cv::resize(source, output, cv::Size(), factor, factor, factor >= 1.0 ? cv::INTER_CUBIC : cv::INTER_AREA);
|
||||
return {output, QStringLiteral("缩放 %1 倍完成,尺寸:%2 × %3").arg(factor).arg(output.cols).arg(output.rows)};
|
||||
}
|
||||
|
||||
ProcessResult ImageProcessor::translate(const cv::Mat &source, int dx, int dy)
|
||||
{
|
||||
if (source.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const cv::Mat matrix = (cv::Mat_<double>(2, 3) << 1, 0, dx, 0, 1, dy);
|
||||
cv::Mat output;
|
||||
cv::warpAffine(source, output, matrix, source.size(), cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(30, 30, 30));
|
||||
return {output, QStringLiteral("平移完成:dx=%1, dy=%2").arg(dx).arg(dy)};
|
||||
}
|
||||
|
||||
ProcessResult ImageProcessor::extractTargets(const cv::Mat &source)
|
||||
{
|
||||
if (source.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
cv::Mat gray;
|
||||
if (source.channels() == 1) {
|
||||
gray = source.clone();
|
||||
} else {
|
||||
cv::cvtColor(source, gray, cv::COLOR_BGR2GRAY);
|
||||
}
|
||||
|
||||
cv::Mat blurred;
|
||||
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0);
|
||||
|
||||
cv::Mat binary;
|
||||
cv::threshold(blurred, binary, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
|
||||
|
||||
std::vector<std::vector<cv::Point>> contours;
|
||||
cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
|
||||
|
||||
cv::Mat output;
|
||||
if (source.channels() == 1) {
|
||||
cv::cvtColor(source, output, cv::COLOR_GRAY2BGR);
|
||||
} else {
|
||||
output = source.clone();
|
||||
}
|
||||
|
||||
int validCount = 0;
|
||||
double totalArea = 0.0;
|
||||
const double minArea = std::max(80.0, source.cols * source.rows * 0.0005);
|
||||
|
||||
for (const auto &contour : contours) {
|
||||
const double area = cv::contourArea(contour);
|
||||
if (area < minArea) {
|
||||
continue;
|
||||
}
|
||||
++validCount;
|
||||
totalArea += area;
|
||||
const cv::Rect box = cv::boundingRect(contour);
|
||||
cv::rectangle(output, box, cv::Scalar(0, 0, 255), 2);
|
||||
cv::drawContours(output, std::vector<std::vector<cv::Point>>{contour}, -1, cv::Scalar(0, 255, 0), 2);
|
||||
const cv::Moments moments = cv::moments(contour);
|
||||
if (moments.m00 != 0.0) {
|
||||
const int cx = static_cast<int>(moments.m10 / moments.m00);
|
||||
const int cy = static_cast<int>(moments.m01 / moments.m00);
|
||||
cv::circle(output, cv::Point(cx, cy), 4, cv::Scalar(255, 0, 0), -1);
|
||||
}
|
||||
}
|
||||
|
||||
return {output, QStringLiteral("目标提取完成:检测到 %1 个目标,总面积约 %2 像素")
|
||||
.arg(validCount)
|
||||
.arg(static_cast<int>(totalArea))};
|
||||
}
|
||||
|
||||
ProcessResult ImageProcessor::classifySimple(const cv::Mat &source)
|
||||
{
|
||||
if (source.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
cv::Mat hsv;
|
||||
if (source.channels() == 1) {
|
||||
cv::cvtColor(source, hsv, cv::COLOR_GRAY2BGR);
|
||||
cv::cvtColor(hsv, hsv, cv::COLOR_BGR2HSV);
|
||||
} else {
|
||||
cv::cvtColor(source, hsv, cv::COLOR_BGR2HSV);
|
||||
}
|
||||
|
||||
const cv::Scalar meanHsv = cv::mean(hsv);
|
||||
const double hue = meanHsv[0];
|
||||
const double saturation = meanHsv[1];
|
||||
const double value = meanHsv[2];
|
||||
|
||||
QString type;
|
||||
if (value < 70) {
|
||||
type = QStringLiteral("偏暗图像");
|
||||
} else if (saturation < 45) {
|
||||
type = QStringLiteral("低饱和 / 灰度感图像");
|
||||
} else if (hue < 15 || hue > 160) {
|
||||
type = QStringLiteral("红色系图像");
|
||||
} else if (hue < 45) {
|
||||
type = QStringLiteral("黄色 / 橙色系图像");
|
||||
} else if (hue < 85) {
|
||||
type = QStringLiteral("绿色系图像");
|
||||
} else if (hue < 130) {
|
||||
type = QStringLiteral("蓝色系图像");
|
||||
} else {
|
||||
type = QStringLiteral("紫色系图像");
|
||||
}
|
||||
|
||||
cv::Mat output = source.clone();
|
||||
return {output, QStringLiteral("简单分类:%1(平均 H=%2, S=%3, V=%4)")
|
||||
.arg(type)
|
||||
.arg(hue, 0, 'f', 1)
|
||||
.arg(saturation, 0, 'f', 1)
|
||||
.arg(value, 0, 'f', 1)};
|
||||
}
|
||||
89
app/src/ImageView.cpp
Normal file
89
app/src/ImageView.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
#include "ImageView.h"
|
||||
|
||||
#include <QGraphicsPixmapItem>
|
||||
#include <QGraphicsScene>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QScrollBar>
|
||||
#include <QWheelEvent>
|
||||
|
||||
ImageView::ImageView(QWidget *parent)
|
||||
: QGraphicsView(parent), scene_(new QGraphicsScene(this))
|
||||
{
|
||||
setScene(scene_);
|
||||
pixmapItem_ = scene_->addPixmap(QPixmap());
|
||||
pixmapItem_->setTransformationMode(Qt::SmoothTransformation);
|
||||
|
||||
setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
|
||||
setDragMode(QGraphicsView::ScrollHandDrag);
|
||||
setViewportUpdateMode(QGraphicsView::SmartViewportUpdate);
|
||||
setBackgroundBrush(QColor(36, 36, 36));
|
||||
setFrameShape(QFrame::StyledPanel);
|
||||
setAlignment(Qt::AlignCenter);
|
||||
}
|
||||
|
||||
void ImageView::setImage(const QImage &image)
|
||||
{
|
||||
if (image.isNull()) {
|
||||
clearImage();
|
||||
return;
|
||||
}
|
||||
|
||||
pixmap_ = QPixmap::fromImage(image);
|
||||
pixmapItem_->setPixmap(pixmap_);
|
||||
resetSceneRect();
|
||||
fitImageToView();
|
||||
}
|
||||
|
||||
void ImageView::clearImage()
|
||||
{
|
||||
pixmap_ = QPixmap();
|
||||
pixmapItem_->setPixmap(QPixmap());
|
||||
scene_->setSceneRect(QRectF());
|
||||
resetTransform();
|
||||
zoomFactor_ = 1.0;
|
||||
}
|
||||
|
||||
void ImageView::fitImageToView()
|
||||
{
|
||||
if (!hasImage()) {
|
||||
return;
|
||||
}
|
||||
resetTransform();
|
||||
fitInView(pixmapItem_, Qt::KeepAspectRatio);
|
||||
zoomFactor_ = transform().m11();
|
||||
}
|
||||
|
||||
bool ImageView::hasImage() const
|
||||
{
|
||||
return !pixmap_.isNull();
|
||||
}
|
||||
|
||||
void ImageView::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
if (!hasImage()) {
|
||||
QGraphicsView::wheelEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||
const int delta = event->angleDelta().y();
|
||||
#else
|
||||
const int delta = event->delta();
|
||||
#endif
|
||||
const double factor = delta > 0 ? 1.15 : 1.0 / 1.15;
|
||||
zoomFactor_ *= factor;
|
||||
scale(factor, factor);
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void ImageView::mouseDoubleClickEvent(QMouseEvent *event)
|
||||
{
|
||||
Q_UNUSED(event)
|
||||
fitImageToView();
|
||||
}
|
||||
|
||||
void ImageView::resetSceneRect()
|
||||
{
|
||||
scene_->setSceneRect(pixmap_.rect());
|
||||
}
|
||||
35
app/src/ImageWorker.cpp
Normal file
35
app/src/ImageWorker.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#include "ImageWorker.h"
|
||||
|
||||
void ImageWorker::process(cv::Mat image, ImageWorker::Operation operation, double value, int dx, int dy)
|
||||
{
|
||||
if (image.empty()) {
|
||||
Q_EMIT failed(QStringLiteral("没有可处理的图片"));
|
||||
return;
|
||||
}
|
||||
|
||||
ProcessResult result;
|
||||
switch (operation) {
|
||||
case Operation::Rotate:
|
||||
result = ImageProcessor::rotate(image, value);
|
||||
break;
|
||||
case Operation::Scale:
|
||||
result = ImageProcessor::scale(image, value);
|
||||
break;
|
||||
case Operation::Translate:
|
||||
result = ImageProcessor::translate(image, dx, dy);
|
||||
break;
|
||||
case Operation::ExtractTargets:
|
||||
result = ImageProcessor::extractTargets(image);
|
||||
break;
|
||||
case Operation::ClassifySimple:
|
||||
result = ImageProcessor::classifySimple(image);
|
||||
break;
|
||||
}
|
||||
|
||||
if (result.image.empty()) {
|
||||
Q_EMIT failed(QStringLiteral("图像处理失败"));
|
||||
return;
|
||||
}
|
||||
|
||||
Q_EMIT finished(result);
|
||||
}
|
||||
369
app/src/MainWindow.cpp
Normal file
369
app/src/MainWindow.cpp
Normal file
@@ -0,0 +1,369 @@
|
||||
#include "MainWindow.h"
|
||||
|
||||
#include "ImageConverter.h"
|
||||
#include "ImageView.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QBoxLayout>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QMenuBar>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QSplitter>
|
||||
#include <QStatusBar>
|
||||
#include <QToolBar>
|
||||
#include <QWidget>
|
||||
#include <opencv2/imgcodecs.hpp>
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent)
|
||||
{
|
||||
qRegisterMetaType<ProcessResult>("ProcessResult");
|
||||
qRegisterMetaType<ImageWorker::Operation>("ImageWorker::Operation");
|
||||
qRegisterMetaType<cv::Mat>("cv::Mat");
|
||||
|
||||
buildUi();
|
||||
setupWorkerThread();
|
||||
setWindowTitle(QStringLiteral("chengnan - Qt/OpenCV 图像处理"));
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
workerThread_.quit();
|
||||
workerThread_.wait();
|
||||
}
|
||||
|
||||
void MainWindow::buildUi()
|
||||
{
|
||||
auto *openAction = new QAction(QStringLiteral("打开图片"), this);
|
||||
saveAction_ = new QAction(QStringLiteral("保存图片"), this);
|
||||
saveAsAction_ = new QAction(QStringLiteral("另存图片"), this);
|
||||
auto *resetAction = new QAction(QStringLiteral("还原原图"), this);
|
||||
auto *exitAction = new QAction(QStringLiteral("退出"), this);
|
||||
|
||||
connect(openAction, &QAction::triggered, this, &MainWindow::openImage);
|
||||
connect(saveAction_, &QAction::triggered, this, &MainWindow::saveImage);
|
||||
connect(saveAsAction_, &QAction::triggered, this, &MainWindow::saveImageAs);
|
||||
connect(resetAction, &QAction::triggered, this, &MainWindow::resetToOriginal);
|
||||
connect(exitAction, &QAction::triggered, qApp, &QApplication::quit);
|
||||
|
||||
auto *fileMenu = menuBar()->addMenu(QStringLiteral("文件"));
|
||||
fileMenu->addAction(openAction);
|
||||
fileMenu->addAction(saveAction_);
|
||||
fileMenu->addAction(saveAsAction_);
|
||||
fileMenu->addSeparator();
|
||||
fileMenu->addAction(exitAction);
|
||||
|
||||
auto *editMenu = menuBar()->addMenu(QStringLiteral("处理"));
|
||||
editMenu->addAction(resetAction);
|
||||
|
||||
auto *toolbar = addToolBar(QStringLiteral("主工具栏"));
|
||||
toolbar->addAction(openAction);
|
||||
toolbar->addAction(saveAction_);
|
||||
toolbar->addAction(saveAsAction_);
|
||||
toolbar->addAction(resetAction);
|
||||
|
||||
originalView_ = new ImageView(this);
|
||||
processedView_ = new ImageView(this);
|
||||
|
||||
auto *leftBox = new QGroupBox(QStringLiteral("处理前图像"), this);
|
||||
auto *leftLayout = new QVBoxLayout(leftBox);
|
||||
leftLayout->addWidget(originalView_);
|
||||
|
||||
auto *rightBox = new QGroupBox(QStringLiteral("处理后图像"), this);
|
||||
auto *rightLayout = new QVBoxLayout(rightBox);
|
||||
rightLayout->addWidget(processedView_);
|
||||
|
||||
auto *splitter = new QSplitter(Qt::Horizontal, this);
|
||||
splitter->addWidget(leftBox);
|
||||
splitter->addWidget(rightBox);
|
||||
splitter->setStretchFactor(0, 1);
|
||||
splitter->setStretchFactor(1, 1);
|
||||
|
||||
scaleSpin_ = new QDoubleSpinBox(this);
|
||||
scaleSpin_->setRange(0.1, 5.0);
|
||||
scaleSpin_->setSingleStep(0.1);
|
||||
scaleSpin_->setValue(1.25);
|
||||
scaleSpin_->setSuffix(QStringLiteral(" 倍"));
|
||||
|
||||
angleSpin_ = new QDoubleSpinBox(this);
|
||||
angleSpin_->setRange(-360.0, 360.0);
|
||||
angleSpin_->setSingleStep(5.0);
|
||||
angleSpin_->setValue(90.0);
|
||||
angleSpin_->setSuffix(QStringLiteral("°"));
|
||||
|
||||
translateSpin_ = new QSpinBox(this);
|
||||
translateSpin_->setRange(1, 1000);
|
||||
translateSpin_->setValue(40);
|
||||
translateSpin_->setSuffix(QStringLiteral(" px"));
|
||||
|
||||
auto *scaleUpButton = new QPushButton(QStringLiteral("放大"), this);
|
||||
auto *scaleDownButton = new QPushButton(QStringLiteral("缩小"), this);
|
||||
auto *rotateLeftButton = new QPushButton(QStringLiteral("逆时针旋转"), this);
|
||||
auto *rotateRightButton = new QPushButton(QStringLiteral("顺时针旋转"), this);
|
||||
auto *upButton = new QPushButton(QStringLiteral("上移"), this);
|
||||
auto *downButton = new QPushButton(QStringLiteral("下移"), this);
|
||||
auto *leftButton = new QPushButton(QStringLiteral("左移"), this);
|
||||
auto *rightButton = new QPushButton(QStringLiteral("右移"), this);
|
||||
auto *extractButton = new QPushButton(QStringLiteral("目标提取"), this);
|
||||
auto *classifyButton = new QPushButton(QStringLiteral("简单分类"), this);
|
||||
|
||||
connect(scaleUpButton, &QPushButton::clicked, this, &MainWindow::scaleUp);
|
||||
connect(scaleDownButton, &QPushButton::clicked, this, &MainWindow::scaleDown);
|
||||
connect(rotateLeftButton, &QPushButton::clicked, this, &MainWindow::rotateLeft);
|
||||
connect(rotateRightButton, &QPushButton::clicked, this, &MainWindow::rotateRight);
|
||||
connect(upButton, &QPushButton::clicked, this, &MainWindow::translateUp);
|
||||
connect(downButton, &QPushButton::clicked, this, &MainWindow::translateDown);
|
||||
connect(leftButton, &QPushButton::clicked, this, &MainWindow::translateLeft);
|
||||
connect(rightButton, &QPushButton::clicked, this, &MainWindow::translateRight);
|
||||
connect(extractButton, &QPushButton::clicked, this, &MainWindow::extractTargets);
|
||||
connect(classifyButton, &QPushButton::clicked, this, &MainWindow::classifyImage);
|
||||
|
||||
auto *controlBox = new QGroupBox(QStringLiteral("图像处理操作"), this);
|
||||
auto *controlLayout = new QHBoxLayout(controlBox);
|
||||
controlLayout->addWidget(new QLabel(QStringLiteral("缩放比例:"), this));
|
||||
controlLayout->addWidget(scaleSpin_);
|
||||
controlLayout->addWidget(scaleUpButton);
|
||||
controlLayout->addWidget(scaleDownButton);
|
||||
controlLayout->addSpacing(12);
|
||||
controlLayout->addWidget(new QLabel(QStringLiteral("旋转角度:"), this));
|
||||
controlLayout->addWidget(angleSpin_);
|
||||
controlLayout->addWidget(rotateLeftButton);
|
||||
controlLayout->addWidget(rotateRightButton);
|
||||
controlLayout->addSpacing(12);
|
||||
controlLayout->addWidget(new QLabel(QStringLiteral("平移距离:"), this));
|
||||
controlLayout->addWidget(translateSpin_);
|
||||
controlLayout->addWidget(upButton);
|
||||
controlLayout->addWidget(downButton);
|
||||
controlLayout->addWidget(leftButton);
|
||||
controlLayout->addWidget(rightButton);
|
||||
controlLayout->addSpacing(12);
|
||||
controlLayout->addWidget(extractButton);
|
||||
controlLayout->addWidget(classifyButton);
|
||||
controlLayout->addStretch(1);
|
||||
|
||||
infoLabel_ = new QLabel(QStringLiteral("请打开一张图片开始处理。提示:鼠标滚轮可缩放视图,双击图片可适配窗口。"), this);
|
||||
infoLabel_->setWordWrap(true);
|
||||
|
||||
auto *central = new QWidget(this);
|
||||
auto *mainLayout = new QVBoxLayout(central);
|
||||
mainLayout->addWidget(splitter, 1);
|
||||
mainLayout->addWidget(controlBox);
|
||||
mainLayout->addWidget(infoLabel_);
|
||||
setCentralWidget(central);
|
||||
|
||||
statusLabel_ = new QLabel(QStringLiteral("就绪"), this);
|
||||
statusBar()->addPermanentWidget(statusLabel_, 1);
|
||||
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
void MainWindow::setupWorkerThread()
|
||||
{
|
||||
worker_ = new ImageWorker();
|
||||
worker_->moveToThread(&workerThread_);
|
||||
|
||||
connect(&workerThread_, &QThread::finished, worker_, &QObject::deleteLater);
|
||||
connect(this, &MainWindow::processRequested, worker_, &ImageWorker::process, Qt::QueuedConnection);
|
||||
connect(worker_, &ImageWorker::finished, this, &MainWindow::onProcessFinished, Qt::QueuedConnection);
|
||||
connect(worker_, &ImageWorker::failed, this, &MainWindow::onProcessFailed, Qt::QueuedConnection);
|
||||
|
||||
workerThread_.start();
|
||||
}
|
||||
|
||||
void MainWindow::openImage()
|
||||
{
|
||||
const QString path = QFileDialog::getOpenFileName(
|
||||
this,
|
||||
QStringLiteral("打开图片"),
|
||||
QString(),
|
||||
QStringLiteral("图片文件 (*.png *.jpg *.jpeg *.bmp *.tif *.tiff);;所有文件 (*.*)"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string localPath = QFile::encodeName(path).toStdString();
|
||||
cv::Mat image = cv::imread(localPath, cv::IMREAD_COLOR);
|
||||
if (image.empty()) {
|
||||
QMessageBox::warning(this, QStringLiteral("打开失败"), QStringLiteral("无法读取图片:%1").arg(path));
|
||||
return;
|
||||
}
|
||||
|
||||
originalImage_ = image;
|
||||
processedImage_ = image.clone();
|
||||
currentPath_ = path;
|
||||
updateViews();
|
||||
infoLabel_->setText(QStringLiteral("已打开:%1,尺寸:%2 × %3")
|
||||
.arg(QFileInfo(path).fileName())
|
||||
.arg(image.cols)
|
||||
.arg(image.rows));
|
||||
statusLabel_->setText(QStringLiteral("已打开图片"));
|
||||
}
|
||||
|
||||
void MainWindow::saveImage()
|
||||
{
|
||||
if (!ensureHasImage()) {
|
||||
return;
|
||||
}
|
||||
if (currentPath_.isEmpty()) {
|
||||
saveImageAs();
|
||||
return;
|
||||
}
|
||||
writeCurrentImageTo(currentPath_);
|
||||
}
|
||||
|
||||
void MainWindow::saveImageAs()
|
||||
{
|
||||
if (!ensureHasImage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString path = QFileDialog::getSaveFileName(
|
||||
this,
|
||||
QStringLiteral("另存图片"),
|
||||
currentPath_.isEmpty() ? QStringLiteral("processed.png") : currentPath_,
|
||||
QStringLiteral("PNG 图片 (*.png);;JPEG 图片 (*.jpg *.jpeg);;BMP 图片 (*.bmp);;所有文件 (*.*)"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (writeCurrentImageTo(path)) {
|
||||
currentPath_ = path;
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::resetToOriginal()
|
||||
{
|
||||
if (!ensureHasImage()) {
|
||||
return;
|
||||
}
|
||||
processedImage_ = originalImage_.clone();
|
||||
updateViews();
|
||||
infoLabel_->setText(QStringLiteral("已还原为原始图片"));
|
||||
}
|
||||
|
||||
void MainWindow::onProcessFinished(ProcessResult result)
|
||||
{
|
||||
processedImage_ = result.image;
|
||||
updateViews();
|
||||
infoLabel_->setText(result.description);
|
||||
statusLabel_->setText(QStringLiteral("处理完成"));
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
void MainWindow::onProcessFailed(const QString &message)
|
||||
{
|
||||
QMessageBox::warning(this, QStringLiteral("处理失败"), message);
|
||||
statusLabel_->setText(QStringLiteral("处理失败"));
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
void MainWindow::rotateLeft()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::Rotate, -angleSpin_->value());
|
||||
}
|
||||
|
||||
void MainWindow::rotateRight()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::Rotate, angleSpin_->value());
|
||||
}
|
||||
|
||||
void MainWindow::scaleUp()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::Scale, scaleSpin_->value());
|
||||
}
|
||||
|
||||
void MainWindow::scaleDown()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::Scale, 1.0 / scaleSpin_->value());
|
||||
}
|
||||
|
||||
void MainWindow::translateUp()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::Translate, 0.0, 0, -translateSpin_->value());
|
||||
}
|
||||
|
||||
void MainWindow::translateDown()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::Translate, 0.0, 0, translateSpin_->value());
|
||||
}
|
||||
|
||||
void MainWindow::translateLeft()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::Translate, 0.0, -translateSpin_->value(), 0);
|
||||
}
|
||||
|
||||
void MainWindow::translateRight()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::Translate, 0.0, translateSpin_->value(), 0);
|
||||
}
|
||||
|
||||
void MainWindow::extractTargets()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::ExtractTargets);
|
||||
}
|
||||
|
||||
void MainWindow::classifyImage()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::ClassifySimple);
|
||||
}
|
||||
|
||||
void MainWindow::updateViews()
|
||||
{
|
||||
originalView_->setImage(ImageConverter::matToQImage(originalImage_));
|
||||
processedView_->setImage(ImageConverter::matToQImage(processedImage_));
|
||||
}
|
||||
|
||||
void MainWindow::setBusy(bool busy)
|
||||
{
|
||||
busy_ = busy;
|
||||
saveAction_->setEnabled(!busy && !processedImage_.empty());
|
||||
saveAsAction_->setEnabled(!busy && !processedImage_.empty());
|
||||
if (busy) {
|
||||
statusLabel_->setText(QStringLiteral("处理中..."));
|
||||
}
|
||||
}
|
||||
|
||||
bool MainWindow::ensureHasImage() const
|
||||
{
|
||||
if (processedImage_.empty()) {
|
||||
QMessageBox::information(const_cast<MainWindow *>(this), QStringLiteral("提示"), QStringLiteral("请先打开图片。"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MainWindow::requestProcess(ImageWorker::Operation operation, double value, int dx, int dy)
|
||||
{
|
||||
if (busy_) {
|
||||
return;
|
||||
}
|
||||
if (!ensureHasImage()) {
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
Q_EMIT processRequested(processedImage_.clone(), operation, value, dx, dy);
|
||||
}
|
||||
|
||||
bool MainWindow::writeCurrentImageTo(const QString &path)
|
||||
{
|
||||
if (processedImage_.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string localPath = QFile::encodeName(path).toStdString();
|
||||
if (!cv::imwrite(localPath, processedImage_)) {
|
||||
QMessageBox::warning(this, QStringLiteral("保存失败"), QStringLiteral("无法保存图片:%1").arg(path));
|
||||
return false;
|
||||
}
|
||||
|
||||
statusLabel_->setText(QStringLiteral("已保存图片"));
|
||||
infoLabel_->setText(QStringLiteral("已保存处理后图片:%1").arg(path));
|
||||
return true;
|
||||
}
|
||||
20
app/src/main.cpp
Normal file
20
app/src/main.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include <QApplication>
|
||||
#include <QMetaType>
|
||||
|
||||
#include "MainWindow.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
QApplication::setApplicationName("chengnan");
|
||||
QApplication::setOrganizationName("luochen570");
|
||||
QApplication::setApplicationVersion(CHENGNAN_APP_VERSION);
|
||||
|
||||
qRegisterMetaType<cv::Mat>("cv::Mat");
|
||||
|
||||
MainWindow window;
|
||||
window.resize(1280, 760);
|
||||
window.show();
|
||||
|
||||
return QApplication::exec();
|
||||
}
|
||||
144
docs/windows/DEPENDENCIES.md
Normal file
144
docs/windows/DEPENDENCIES.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# chengnan 本地二进制依赖说明
|
||||
|
||||
本项目原则:**构建项目所用依赖使用二进制包放在项目目录内,不在本机编译安装依赖,不使用系统 GCC 等编译工具链参与本项目构建。**
|
||||
|
||||
## 已删除的非目标依赖
|
||||
|
||||
已删除之前为 Linux 本地尝试构建而产生的内容:
|
||||
|
||||
- `third_party/opencv-src/`:OpenCV 源码
|
||||
- `third_party/opencv-build/`:OpenCV 编译目录
|
||||
- `third_party/opencv/`:由源码编译安装出的 Linux OpenCV
|
||||
- `third_party/qt-sdk/`:Linux Qt 包
|
||||
- `third_party/qt`:Linux Qt 软链接
|
||||
- `third_party/opengl-stub/`:Linux 构建验证用 stub
|
||||
- `tools/uv/`:uv 工具
|
||||
- `tools/pyenv/`:uv 创建的 Python 环境
|
||||
- `tools/cmake/`:Linux CMake 二进制
|
||||
|
||||
## 当前保留的 Windows 二进制依赖
|
||||
|
||||
### Qt 6.7.3 MSVC 2019 64-bit
|
||||
|
||||
```text
|
||||
third_party/qt-windows-msvc-sdk/6.7.3/msvc2019_64/
|
||||
third_party/qt-windows-msvc -> qt-windows-msvc-sdk/6.7.3/msvc2019_64
|
||||
```
|
||||
|
||||
关键文件:
|
||||
|
||||
```text
|
||||
third_party/qt-windows-msvc/bin/qmake.exe
|
||||
third_party/qt-windows-msvc/bin/windeployqt.exe
|
||||
```
|
||||
|
||||
### OpenCV 4.10.0 Windows 官方预编译包
|
||||
|
||||
```text
|
||||
third_party/opencv-windows-extract/opencv/build/
|
||||
third_party/opencv-windows -> opencv-windows-extract/opencv/build
|
||||
```
|
||||
|
||||
关键文件:
|
||||
|
||||
```text
|
||||
third_party/opencv-windows/x64/vc16/lib/OpenCVConfig.cmake
|
||||
third_party/opencv-windows/x64/vc16/lib/opencv_world4100.lib
|
||||
third_party/opencv-windows/x64/vc16/bin/opencv_world4100.dll
|
||||
```
|
||||
|
||||
### Ninja / MinGW 工具包
|
||||
|
||||
```text
|
||||
third_party/qt-tools/
|
||||
```
|
||||
|
||||
关键文件:
|
||||
|
||||
```text
|
||||
third_party/qt-tools/Tools/Ninja/ninja.exe
|
||||
third_party/qt-tools/Tools/mingw1120_64/bin/g++.exe
|
||||
```
|
||||
|
||||
> 注意:项目保留 MinGW 二进制只是备用。当前推荐 MSVC,因为 OpenCV 官方 Windows 二进制是 `vc16`,不能直接给 MinGW 链接。
|
||||
|
||||
### Windows CMake 二进制
|
||||
|
||||
```text
|
||||
tools/cmake-windows/cmake-3.31.6-windows-x86_64/
|
||||
tools/cmake-current-windows -> cmake-windows/cmake-3.31.6-windows-x86_64
|
||||
```
|
||||
|
||||
关键文件:
|
||||
|
||||
```text
|
||||
tools/cmake-current-windows/bin/cmake.exe
|
||||
```
|
||||
|
||||
### Windows Python 二进制与虚拟环境脚本
|
||||
|
||||
```text
|
||||
tools/python-bin/
|
||||
scripts/setup-python-venv-windows.ps1
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `tools/python-bin/` 使用 Python 官方 Windows embeddable zip 二进制包解压得到
|
||||
- 不使用 uv
|
||||
- 如果需要 Python 包,请在 Windows 上运行:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\setup-python-venv-windows.ps1
|
||||
```
|
||||
|
||||
该脚本会优先尝试创建:
|
||||
|
||||
```text
|
||||
tools/python-venv/
|
||||
```
|
||||
|
||||
如果 embeddable Python 不支持 `venv`,则会在 `tools/python-bin/` 内安装 pip/aqtinstall,仍然限制在项目目录内。
|
||||
|
||||
## Windows 推荐构建方式
|
||||
|
||||
由于当前 OpenCV 是官方 MSVC `vc16` 二进制包,推荐使用 MSVC 构建。
|
||||
|
||||
在 Windows 上打开:
|
||||
|
||||
```text
|
||||
x64 Native Tools Command Prompt for VS 2019/2022
|
||||
```
|
||||
|
||||
进入项目目录后运行:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\build-windows.ps1 -Toolchain msvc -Deploy
|
||||
```
|
||||
|
||||
脚本会使用项目内:
|
||||
|
||||
- `tools/cmake-current-windows/bin/cmake.exe`
|
||||
- `third_party/qt-tools/Tools/Ninja/ninja.exe`
|
||||
- `third_party/qt-windows-msvc/`
|
||||
- `third_party/opencv-windows/`
|
||||
|
||||
## 关于“不使用系统工具链”
|
||||
|
||||
当前已经避免使用 Linux 系统 GCC 编译本项目,也删除了通过系统 GCC 编译出来的 OpenCV。
|
||||
|
||||
但 MSVC 编译器 `cl.exe` 通常来自 Visual Studio Build Tools。微软没有像 Qt/OpenCV 一样提供适合随项目简单解压携带的独立 MSVC 工具链包。因此:
|
||||
|
||||
- 如果允许使用 Windows 已安装的 Visual Studio Build Tools:可直接构建
|
||||
- 如果要求连 MSVC 编译器也必须项目内携带:建议改用 MinGW,但需要额外准备 MinGW 版 OpenCV 二进制包
|
||||
|
||||
## 当前 Linux 服务器限制
|
||||
|
||||
当前服务器是 Linux,只能整理 Windows 二进制依赖和脚本,不能直接运行 Windows 的:
|
||||
|
||||
- `cmake.exe`
|
||||
- `ninja.exe`
|
||||
- `cl.exe`
|
||||
- `windeployqt.exe`
|
||||
|
||||
因此最终 `.exe` 需要在 Windows 上执行构建脚本生成。
|
||||
86
docs/windows/build-windows.ps1
Normal file
86
docs/windows/build-windows.ps1
Normal file
@@ -0,0 +1,86 @@
|
||||
param(
|
||||
[ValidateSet("msvc", "mingw")]
|
||||
[string]$Toolchain = "msvc",
|
||||
[string]$BuildDir = "$PSScriptRoot\..\build-windows",
|
||||
[switch]$Deploy
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$ProjectRoot = Resolve-Path "$PSScriptRoot\.."
|
||||
$BuildDir = [System.IO.Path]::GetFullPath($BuildDir)
|
||||
$ThirdParty = Join-Path $ProjectRoot "third_party"
|
||||
$Tools = Join-Path $ProjectRoot "tools"
|
||||
|
||||
$CMakeExe = Join-Path $Tools "cmake-current-windows\bin\cmake.exe"
|
||||
if (!(Test-Path $CMakeExe)) {
|
||||
$CMakeExe = Join-Path $Tools "cmake-windows\cmake-3.31.6-windows-x86_64\bin\cmake.exe"
|
||||
}
|
||||
if (!(Test-Path $CMakeExe)) {
|
||||
throw "未找到项目内 Windows CMake:$CMakeExe"
|
||||
}
|
||||
|
||||
$NinjaExe = Join-Path $ThirdParty "qt-tools\Tools\Ninja\ninja.exe"
|
||||
if (!(Test-Path $NinjaExe)) {
|
||||
$NinjaExe = Join-Path $ThirdParty "qt-tools\Tools\ninja\ninja.exe"
|
||||
}
|
||||
|
||||
if ($Toolchain -eq "msvc") {
|
||||
$QtPrefix = Join-Path $ThirdParty "qt-windows-msvc"
|
||||
$OpenCvPrefix = Join-Path $ThirdParty "opencv-windows\x64\vc16\lib"
|
||||
$OpenCvBin = Join-Path $ThirdParty "opencv-windows\x64\vc16\bin"
|
||||
$Generator = "Ninja"
|
||||
|
||||
if (!(Test-Path $QtPrefix)) { throw "未找到 Qt MSVC:$QtPrefix" }
|
||||
if (!(Test-Path $OpenCvPrefix)) { throw "未找到 OpenCV MSVC:$OpenCvPrefix" }
|
||||
if (!(Get-Command cl.exe -ErrorAction SilentlyContinue)) {
|
||||
throw "未检测到 cl.exe。请在 x64 Native Tools Command Prompt for VS 中运行本脚本,或先运行 vcvars64.bat。"
|
||||
}
|
||||
} else {
|
||||
$QtPrefix = Join-Path $ThirdParty "qt-windows"
|
||||
$OpenCvPrefix = Join-Path $ThirdParty "opencv-windows\x64\vc16\lib"
|
||||
$OpenCvBin = Join-Path $ThirdParty "opencv-windows\x64\vc16\bin"
|
||||
$MingwBin = Join-Path $ThirdParty "qt-tools\Tools\mingw1120_64\bin"
|
||||
$Generator = "Ninja"
|
||||
|
||||
if (!(Test-Path $QtPrefix)) { throw "未找到 Qt MinGW:$QtPrefix" }
|
||||
if (!(Test-Path $MingwBin)) { throw "未找到项目内 MinGW:$MingwBin" }
|
||||
Write-Warning "OpenCV 官方 Windows 预编译包是 MSVC vc16,不适配 MinGW 链接。若要 MinGW 构建,需要另备 MinGW 版 OpenCV 二进制包。"
|
||||
|
||||
$env:PATH = "$MingwBin;$env:PATH"
|
||||
$env:CC = Join-Path $MingwBin "gcc.exe"
|
||||
$env:CXX = Join-Path $MingwBin "g++.exe"
|
||||
}
|
||||
|
||||
if (!(Test-Path $NinjaExe)) {
|
||||
throw "未找到项目内 Ninja:$NinjaExe"
|
||||
}
|
||||
|
||||
$env:PATH = "$QtPrefix\bin;$OpenCvBin;$(Split-Path $NinjaExe);$env:PATH"
|
||||
$PrefixPath = "$QtPrefix;$OpenCvPrefix"
|
||||
|
||||
Write-Host "ProjectRoot: $ProjectRoot"
|
||||
Write-Host "Toolchain: $Toolchain"
|
||||
Write-Host "QtPrefix: $QtPrefix"
|
||||
Write-Host "OpenCV: $OpenCvPrefix"
|
||||
Write-Host "BuildDir: $BuildDir"
|
||||
Write-Host "Ninja: $NinjaExe"
|
||||
|
||||
& $CMakeExe -S $ProjectRoot -B $BuildDir -G $Generator `
|
||||
-DCMAKE_BUILD_TYPE=Release `
|
||||
-DCMAKE_MAKE_PROGRAM="$NinjaExe" `
|
||||
-DCMAKE_PREFIX_PATH="$PrefixPath"
|
||||
|
||||
& $CMakeExe --build $BuildDir --config Release
|
||||
|
||||
if ($Deploy) {
|
||||
$Exe = Join-Path $BuildDir "chengnan.exe"
|
||||
$DeployDir = Join-Path $BuildDir "deploy"
|
||||
New-Item -ItemType Directory -Force -Path $DeployDir | Out-Null
|
||||
Copy-Item $Exe $DeployDir -Force
|
||||
& (Join-Path $QtPrefix "bin\windeployqt.exe") --release --no-translations (Join-Path $DeployDir "chengnan.exe")
|
||||
Copy-Item (Join-Path $OpenCvBin "*.dll") $DeployDir -Force
|
||||
Write-Host "已部署到:$DeployDir"
|
||||
}
|
||||
|
||||
Write-Host "构建完成。"
|
||||
19
docs/windows/setup-deps-windows.ps1
Normal file
19
docs/windows/setup-deps-windows.ps1
Normal file
@@ -0,0 +1,19 @@
|
||||
param(
|
||||
[string]$InstallRoot = "$PSScriptRoot\..\third_party"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$InstallRoot = [System.IO.Path]::GetFullPath($InstallRoot)
|
||||
New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null
|
||||
|
||||
Write-Host "本脚本不会写入系统目录,只在项目 third_party 下准备依赖。"
|
||||
Write-Host "建议方式:"
|
||||
Write-Host "1. 安装 Qt Online Installer 时,将安装目录选择为:$InstallRoot\qt-sdk"
|
||||
Write-Host "2. 安装组件选择 Qt 6.x / MinGW 或 MSVC 版本。"
|
||||
Write-Host "3. 将实际 Qt 编译套件目录复制或软链接到:$InstallRoot\qt"
|
||||
Write-Host " 例如:$InstallRoot\qt-sdk\6.7.3\mingw_64 -> $InstallRoot\qt"
|
||||
Write-Host "4. 下载 OpenCV Windows 预编译包,解压到:$InstallRoot\opencv"
|
||||
Write-Host "5. 然后运行:powershell -ExecutionPolicy Bypass -File scripts\build-windows.ps1"
|
||||
Write-Host ""
|
||||
Write-Host "如使用 vcpkg,也请将 vcpkg clone 到项目 tools/vcpkg,不要装到系统目录。"
|
||||
29
docs/windows/setup-python-venv-windows.ps1
Normal file
29
docs/windows/setup-python-venv-windows.ps1
Normal file
@@ -0,0 +1,29 @@
|
||||
param(
|
||||
[string]$PythonBin = "$PSScriptRoot\..\tools\python-bin",
|
||||
[string]$VenvDir = "$PSScriptRoot\..\tools\python-venv"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$PythonBin = [System.IO.Path]::GetFullPath($PythonBin)
|
||||
$VenvDir = [System.IO.Path]::GetFullPath($VenvDir)
|
||||
$PythonExe = Join-Path $PythonBin "python.exe"
|
||||
|
||||
if (!(Test-Path $PythonExe)) {
|
||||
throw "未找到项目内 Python 二进制:$PythonExe"
|
||||
}
|
||||
|
||||
# Windows embeddable Python 不一定带 venv 模块。优先尝试 venv;失败则使用同目录作为隔离 Python 环境安装 pip。
|
||||
try {
|
||||
& $PythonExe -m venv $VenvDir
|
||||
$VenvPython = Join-Path $VenvDir "Scripts\python.exe"
|
||||
& $VenvPython -m pip install --upgrade pip setuptools wheel
|
||||
& $VenvPython -m pip install aqtinstall
|
||||
Write-Host "已创建虚拟环境:$VenvDir"
|
||||
} catch {
|
||||
Write-Warning "venv 创建失败,改用项目内 embeddable Python 自身作为隔离环境。"
|
||||
& $PythonExe (Join-Path $PythonBin "get-pip.py")
|
||||
& $PythonExe -m pip install --upgrade pip setuptools wheel
|
||||
& $PythonExe -m pip install aqtinstall
|
||||
Write-Host "已在项目内 Python 二进制目录安装 pip/aqtinstall:$PythonBin"
|
||||
}
|
||||
30
scripts/build-linux-binary.sh
Executable file
30
scripts/build-linux-binary.sh
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ENV_DIR="$ROOT_DIR/tools/linux-build-env"
|
||||
BUILD_DIR="$ROOT_DIR/build-linux"
|
||||
|
||||
if [[ ! -x "$ENV_DIR/bin/cmake" || ! -x "$ENV_DIR/bin/ninja" || ! -x "$ENV_DIR/bin/clang++" ]]; then
|
||||
echo "[build] 未发现项目内 Linux 二进制构建环境,先执行 setup-linux-binary-deps.sh"
|
||||
"$ROOT_DIR/scripts/setup-linux-binary-deps.sh"
|
||||
fi
|
||||
|
||||
export PATH="$ENV_DIR/bin:$PATH"
|
||||
export CMAKE_PREFIX_PATH="$ENV_DIR"
|
||||
export PKG_CONFIG_PATH="$ENV_DIR/lib/pkgconfig:$ENV_DIR/share/pkgconfig:${PKG_CONFIG_PATH:-}"
|
||||
export LD_LIBRARY_PATH="$ENV_DIR/lib:${LD_LIBRARY_PATH:-}"
|
||||
export CC="$ENV_DIR/bin/clang"
|
||||
export CXX="$ENV_DIR/bin/clang++"
|
||||
|
||||
cmake -S "$ROOT_DIR" -B "$BUILD_DIR" \
|
||||
-G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH" \
|
||||
-DCMAKE_C_COMPILER="$CC" \
|
||||
-DCMAKE_CXX_COMPILER="$CXX" \
|
||||
-DCMAKE_INSTALL_PREFIX="$ROOT_DIR/dist-linux"
|
||||
|
||||
cmake --build "$BUILD_DIR" --parallel "$(nproc)"
|
||||
|
||||
echo "[build] 构建完成:$BUILD_DIR/chengnan"
|
||||
5
scripts/build-linux-local.sh
Executable file
5
scripts/build-linux-local.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
exec "$ROOT_DIR/scripts/build-linux-binary.sh"
|
||||
35
scripts/setup-linux-binary-deps.sh
Executable file
35
scripts/setup-linux-binary-deps.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DOWNLOAD_DIR="$ROOT_DIR/downloads"
|
||||
TOOLS_DIR="$ROOT_DIR/tools"
|
||||
MICROMAMBA_DIR="$TOOLS_DIR/micromamba"
|
||||
MICROMAMBA_BIN="$MICROMAMBA_DIR/bin/micromamba"
|
||||
ENV_DIR="$TOOLS_DIR/linux-build-env"
|
||||
|
||||
mkdir -p "$DOWNLOAD_DIR" "$TOOLS_DIR" "$MICROMAMBA_DIR"
|
||||
|
||||
if [[ ! -x "$MICROMAMBA_BIN" ]]; then
|
||||
echo "[setup] 下载 Linux micromamba 二进制包..."
|
||||
ARCHIVE="$DOWNLOAD_DIR/micromamba-linux-64.tar.bz2"
|
||||
curl -L --fail -o "$ARCHIVE" "https://micro.mamba.pm/api/micromamba/linux-64/latest"
|
||||
tar -xjf "$ARCHIVE" -C "$MICROMAMBA_DIR" bin/micromamba
|
||||
fi
|
||||
|
||||
if [[ ! -x "$ENV_DIR/bin/cmake" || ! -x "$ENV_DIR/bin/ninja" || ! -x "$ENV_DIR/bin/clang++" ]]; then
|
||||
echo "[setup] 在项目目录内创建 Linux 二进制构建环境..."
|
||||
"$MICROMAMBA_BIN" create -y -p "$ENV_DIR" -c conda-forge \
|
||||
cmake ninja clangxx qt-main opencv pkg-config
|
||||
fi
|
||||
|
||||
# README 中约定的路径,使用软链接指向隔离环境,便于 CMake 查找。
|
||||
ln -sfn "linux-build-env" "$TOOLS_DIR/compiler-linux"
|
||||
ln -sfn "linux-build-env" "$TOOLS_DIR/cmake-linux"
|
||||
ln -sfn "linux-build-env" "$TOOLS_DIR/ninja-linux"
|
||||
ln -sfn "linux-build-env" "$TOOLS_DIR/python-bin"
|
||||
ln -sfn "linux-build-env" "$TOOLS_DIR/python-venv"
|
||||
ln -sfn "../tools/linux-build-env" "$ROOT_DIR/third_party/qt-linux"
|
||||
ln -sfn "../tools/linux-build-env" "$ROOT_DIR/third_party/opencv-linux"
|
||||
|
||||
echo "[setup] 完成。所有 Linux 构建工具位于:$ENV_DIR"
|
||||
5
scripts/setup-python-venv-linux.sh
Executable file
5
scripts/setup-python-venv-linux.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
exec "$ROOT_DIR/scripts/setup-linux-binary-deps.sh"
|
||||
Reference in New Issue
Block a user