Initial Linux Qt OpenCV project

This commit is contained in:
luochen570
2026-05-29 22:29:30 +08:00
commit 81d586c032
22 changed files with 1748 additions and 0 deletions

47
.gitignore vendored Normal file
View 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
View 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
View 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/`
- 所有脚本应避免执行破坏性系统操作

View 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);
};

View 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
View 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
View 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
View 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;
};

View 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
View 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
View 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
View 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
View 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
View 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();
}

View 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 上执行构建脚本生成。

View 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 "构建完成。"

View 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不要装到系统目录。"

View 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
View 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
View 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"

View 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"

View 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"