commit 01be3d03c3a1fad7fcad9213fc02f9128780c67e Author: 1amiracle <2589935806@qq.com> Date: Thu Feb 12 16:52:14 2026 +0800 Final diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c68275 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# IDE 配置 +.vscode/ + +# 编译产物 +build/ +*.exe +*.dll +*.obj +*.a + +# CMake 缓存 +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +*.ninja +.ninja_deps +.ninja_log + +# 测试输出 +test_image/ + +# 系统文件 +.DS_Store +Thumbs.db +desktop.ini diff --git a/c/CMakeLists.txt b/c/CMakeLists.txt new file mode 100644 index 0000000..938a5a6 --- /dev/null +++ b/c/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 3.10) +project(SealProcessing) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(OpenCV REQUIRED) + +include_directories(${OpenCV_INCLUDE_DIRS}) +include_directories(${CMAKE_SOURCE_DIR}) +include_directories(${CMAKE_SOURCE_DIR}/extract_seal_main) +include_directories(${CMAKE_SOURCE_DIR}/remove_border) + +set(COMMON_SOURCES + common.cpp + image_utilities.cpp +) + +set(EXTRACT_SEAL_SOURCES + extract_seal_main/hsv_color.cpp + extract_seal_main/image_morphology.cpp + extract_seal_main/adjust_image.cpp + extract_seal_main/color_filter.cpp + extract_seal_main/extract_seal_main.cpp +) + +set(REMOVE_BORDER_SOURCES + remove_border/remove_border.cpp + remove_border/remove_border_main.cpp +) + +add_library(seal_lib STATIC + ${COMMON_SOURCES} + ${EXTRACT_SEAL_SOURCES} + ${REMOVE_BORDER_SOURCES} +) +target_link_libraries(seal_lib ${OpenCV_LIBS}) + +add_executable(test_extract_seal extract_seal_main/test_extract_seal.cpp) +target_link_libraries(test_extract_seal seal_lib ${OpenCV_LIBS}) + +add_executable(test_remove_border remove_border/test_remove_border.cpp) +target_link_libraries(test_remove_border seal_lib ${OpenCV_LIBS}) diff --git a/c/README.md b/c/README.md new file mode 100644 index 0000000..27cd5e5 --- /dev/null +++ b/c/README.md @@ -0,0 +1,219 @@ +# Seal Processing - 印章处理模块 + +印章检测与处理的 C++ 模块,包含印章提取和边框移除功能。 + +## 环境要求 + +- **CMake** >= 3.10 +- **MinGW-w64** (通过 MSYS2 安装) +- **OpenCV** >= 4.0 (通过 MSYS2 安装: `mingw-w64-x86_64-opencv`) + +### 安装依赖(MSYS2) + +```bash +pacman -S mingw-w64-x86_64-toolchain +pacman -S mingw-w64-x86_64-cmake +pacman -S mingw-w64-x86_64-opencv +``` + +--- + +## 项目结构 + +``` +Final project/ +├── image/ # 测试图片目录 +│ ├── 1.png +│ └── 2.png +│ +└── c/ # C++ 项目目录 + ├── CMakeLists.txt # CMake 配置文件 + ├── build.bat # 一键编译脚本 + ├── README.md # 说明文档 + │ + ├── common.cpp # 公共函数 + ├── common.h + ├── image_utilities.cpp # 图像工具 + ├── image_utilities.h + │ + ├── extract_seal_main/ # 印章提取模块 + │ ├── extract_seal_main.cpp + │ ├── extract_seal_main.h + │ ├── adjust_image.cpp + │ ├── adjust_image.h + │ ├── color_filter.cpp + │ ├── color_filter.h + │ ├── hsv_color.cpp + │ ├── hsv_color.h + │ ├── image_morphology.cpp + │ ├── image_morphology.h + │ └── test_extract_seal.cpp + │ + └── remove_border/ # 边框移除模块 + ├── remove_border.cpp + ├── remove_border.h + ├── remove_border_main.cpp + ├── remove_border_main.h + └── test_remove_border.cpp +``` + +**编译后生成的目录(不会被 Git 追踪):** +- `c/build/` - 编译输出目录,包含可执行文件 +- `c/extract_seal_main/test_image/` - 印章提取测试输出 +- `c/remove_border/test_image/` - 边框移除测试输出 + +--- + +## 编译 + +### 方法1:使用 build.bat(在 cmd.exe 中运行) + +在 **命令提示符 (cmd)** 中运行: + +```cmd +cd "c:\Users\用户名\Desktop\Final project\c" +build.bat +``` + +或在 **PowerShell** 中运行: + +```powershell +cd "c:\Users\用户名\Desktop\Final project\c" +cmd /c .\build.bat +``` + +### 方法2:手动 CMake 编译(推荐) + +```powershell +cd "c:\Users\用户名\Desktop\Final project\c" +mkdir build -ErrorAction SilentlyContinue +cd build +cmake .. -G "MinGW Makefiles" -DOpenCV_DIR="C:/msys64/mingw64/lib/cmake/opencv4" +cmake --build . +``` + +**注意:** 请将 `用户名` 替换为您的 Windows 用户名。 + +--- + +## 运行 test_extract_seal(印章提取) + +### 功能 +从图片中提取印章,输出裁剪后的印章图像。 + +### 运行命令 + +```powershell +cd "c:\Users\用户名\Desktop\Final project\c\build" +.\test_extract_seal.exe +``` + +### 程序会自动处理 + +**输入:** +- 从 `build/` 目录运行时,程序会扫描 `../../image/` 目录 +- 即 `Final project/image/` 目录下的图片 +- 支持格式:`.png`, `.jpg`, `.jpeg`, `.bmp` + +**输出:** +- 自动保存在 `../extract_seal_main/test_image/` 目录 +- 即 `Final project/c/extract_seal_main/test_image/` 目录 +- 每个印章生成 3 个文件: + - `seal_{图片名}_{序号}_cropped.png` - 原始裁剪区域 + - `seal_{图片名}_{序号}_ex_img.png` - 提取后的印章图像 + - `seal_{图片名}_{序号}_adj_img.png` - 校正后的印章图像 + +### 关于检测数据 + +程序内部已预设了 `1.png` 和 `2.png` 的印章检测数据(det)。如需使用其他图片,请参考"测试说明"章节。 + +--- + +## 运行 test_remove_border(边框移除) + +### 功能 +对提取的印章进行边框移除处理。 + +### 运行命令 + +```powershell +cd "c:\Users\用户名\Desktop\Final project\c\build" +.\test_remove_border.exe +``` + +--- + +## 测试说明 + +### 关于印章检测数据(det) + +目前 C++ 程序**不包含印章检测功能**(即原 Python 项目 seal_ocr 中的 detect 部分)。 + +程序中使用的印章边框数据(det)是通过运行**原 Python 项目**获得后,在 C++ 代码中进行了硬编码设定。 + +**如需使用自己的测试图片:** +1. 使用原 Python 项目 seal_ocr 运行图片,获取检测数据(det) +2. 修改 C++ 测试文件中硬编码的 det 数据 +3. 重新编译并运行程序 + +### 各测试程序的差异 + +| 测试程序 | 处理范围 | 检测数据 | 说明 | +|---------|---------|----------|------| +| `test_extract_seal.cpp` | 多张图片 | 预设 1.png, 2.png | 自动扫描 `image/` 目录,但仅对预设的图片有检测数据 | +| `test_remove_border.cpp` | 单张图片 | 预设 1.png | 仅处理 1.png,需要手动修改代码中的图片路径和 det 数据 | + +**注意:** 两个测试程序都预设了检测数据(det),如需处理其他图片,需要先使用原 Python 项目获取 det 数据,然后修改代码。 + +--- + +## 快速开始 + +```powershell +# 1. 进入项目目录 +cd "c:\Users\用户名\Desktop\Final project\c" + +# 2. 编译(使用 CMake) +mkdir build -ErrorAction SilentlyContinue +cd build +cmake .. -G "MinGW Makefiles" -DOpenCV_DIR="C:/msys64/mingw64/lib/cmake/opencv4" +cmake --build . + +# 3. 运行印章提取测试 +.\test_extract_seal.exe + +# 4. 运行边框移除测试 +.\test_remove_border.exe +``` + +**注意:** 请将 `用户名` 替换为您的 Windows 用户名。 + +--- + +## 常见问题 + +### 1. `cd` 命令报错:找不到路径 + +**原因:** 路径中有空格 `Final project` + +**解决:** 用引号包围路径 +```powershell +cd "c:\Users\用户名\Desktop\Final project\c" +``` + +### 2. CMake 找不到 OpenCV + +确保指定正确的 OpenCV 路径: +```powershell +cmake .. -G "MinGW Makefiles" -DOpenCV_DIR="C:/msys64/mingw64/lib/cmake/opencv4" +``` + +### 3. 编译失败 + +检查: +- MinGW-w64 是否已正确安装 +- 环境变量 PATH 是否包含 `C:\msys64\mingw64\bin` + +### 4. 运行时找不到 DLL + +将 `C:\msys64\mingw64\bin` 添加到系统 PATH,或复制相关 DLL 到可执行文件目录。 diff --git a/c/build.bat b/c/build.bat new file mode 100644 index 0000000..5b4ee69 --- /dev/null +++ b/c/build.bat @@ -0,0 +1,76 @@ +@echo off +chcp 65001>nul +setlocal + +set "PROJECT_DIR=%~dp0" +set "BUILD_DIR=%PROJECT_DIR%build" + +echo ======================================== +echo Building SealProcessing project with CMake +echo ======================================== +echo. + +if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%" + +cd /d "%BUILD_DIR%" + +echo [1/3] Configuring CMake... +rem 尝试多个可能的 OpenCV 路径 +set "OPENCV_PATHS=C:/msys64/mingw64/lib/cmake/opencv4;C:/mingw64/lib/cmake/opencv4;C:/opencv/build/x64/mingw/lib" + +set "CMAKE_COMMAND=cmake .. -G "MinGW Makefiles"" + +rem 优先尝试我们指定的路径,忽略环境变量 +set "FOUND_OPENCV=0" +echo Trying multiple OpenCV paths... +for %%p in (%OPENCV_PATHS%) do ( + if exist "%%p" ( + echo Found OpenCV at: %%p + set "CMAKE_COMMAND=%CMAKE_COMMAND% -DOpenCV_DIR="%%p"" + set "FOUND_OPENCV=1" + goto :cmake_config + ) +) + +rem 如果没有找到,尝试使用环境变量 +if %FOUND_OPENCV%==0 ( + if defined OpenCV_DIR ( + echo Warning: Using OpenCV_DIR from environment variable: %OpenCV_DIR% + echo This might not be compatible with MinGW. + set "CMAKE_COMMAND=%CMAKE_COMMAND% -DOpenCV_DIR="%OpenCV_DIR%"" + ) else ( + echo Warning: No OpenCV directory found. CMake will try to find it automatically. + ) +) + +:cmake_config +%CMAKE_COMMAND% +if errorlevel 1 ( + echo. + echo CMake configuration failed! + pause + exit /b 1 +) + +echo. +echo [2/3] Building project... +cmake --build . +if errorlevel 1 ( + echo. + echo Build failed! + pause + exit /b 1 +) + +echo. +echo ======================================== +echo Build successful! +echo ======================================== +echo Executables are in: %BUILD_DIR% +echo. +echo Available targets: +echo - test_extract_seal.exe +echo - test_remove_border.exe +echo. + +pause diff --git a/c/common.cpp b/c/common.cpp new file mode 100644 index 0000000..ed4f03f --- /dev/null +++ b/c/common.cpp @@ -0,0 +1,17 @@ +#include "common.h" + +ShapeInfo::ShapeInfo() + : type(""), minAreaRect(cv::Point2f(0, 0), cv::Size2f(0, 0), 0), implement(false) { + ignores = {"implement", "needInits", "ignores"}; + needInits = {}; +} + +ShapeInfo::ShapeInfo(const string& t, const cv::RotatedRect& rect, + const vector& pts, + const vector& ign, + const vector& ni, bool imp) + : type(t), minAreaRect(rect), points(pts), ignores(ign), needInits(ni), implement(imp) {} + +ShapeInfo ShapeInfo::copy() const { + return ShapeInfo(type, minAreaRect, points, ignores, needInits, implement); +} diff --git a/c/common.h b/c/common.h new file mode 100644 index 0000000..be8903c --- /dev/null +++ b/c/common.h @@ -0,0 +1,92 @@ +#ifndef COMMON_H +#define COMMON_H + +#include +#include +#include +#include + +using namespace std; + +struct ShapeInfo { + string type; + cv::RotatedRect minAreaRect; + vector points; + vector ignores; + vector needInits; + bool implement; + + ShapeInfo(); + ShapeInfo(const string& t, const cv::RotatedRect& rect, + const vector& pts, + const vector& ign, + const vector& ni, bool imp); + + ShapeInfo copy() const; +}; + +enum SHAPETYPE { + CIRCULAR = 0, + ELLIPTICAL = 1, + SQUARE = 2, + DIAMOND = 3, + TRIANGLE = 4 +}; + +inline const char* const SHAPETYPE_NAMES[] = {"circle", "ellipse", "square", "diamond", "triangle"}; + +struct SealInfo { + int x1; + int y1; + int x2; + int y2; + double conf; + int shape; + cv::Mat cropped_img; + cv::Mat extracted_img; + std::string color_name; + ShapeInfo shape_info; + int angle; + cv::Mat adj_img; + cv::Mat rm_bd_img; + std::vector txt_img; + std::vector rec_img; +}; + +namespace seal_data { + +class SealDataManager { +public: + static SealDataManager& getInstance() { + static SealDataManager instance; + return instance; + } + + void setSealSet(const std::vector& seal_set) { + this->seal_set = seal_set; + } + + std::vector& getSealSet() { + return seal_set; + } + + bool hasSealSet() const { + return !seal_set.empty(); + } + + void clearSealSet() { + seal_set.clear(); + } + +private: + SealDataManager() {} + ~SealDataManager() {} + SealDataManager(const SealDataManager&) = delete; + SealDataManager& operator=(const SealDataManager&) = delete; + + std::vector seal_set; +}; + +} // namespace seal_data + +#endif \ No newline at end of file diff --git a/c/extract_seal_main/adjust_image.cpp b/c/extract_seal_main/adjust_image.cpp new file mode 100644 index 0000000..b8e6864 --- /dev/null +++ b/c/extract_seal_main/adjust_image.cpp @@ -0,0 +1,142 @@ +#include "adjust_image.h" +#include "image_morphology.h" +#include +#include +#include + +using namespace std; + +static tuple adjust_image_impl(const cv::Mat& img, const ShapeInfo& shapeinfo, int rotate_range) { + int final_angle = 0; + cv::Mat final_img; + + int img_h = img.rows; + int img_w = img.cols; + string shapetype = shapeinfo.type; + cv::RotatedRect minAreaRect = shapeinfo.minAreaRect; + cv::Point2f center = minAreaRect.center; + cv::Size2f wh = minAreaRect.size; + double wh_ratio = max(wh.width / wh.height, wh.height / wh.width); + + cv::Mat gray; + cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY); + + cv::Mat binary; + cv::threshold(gray, binary, 200, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU); + + if (shapetype == "ellipse" || shapetype == "circle") { + binary = wipe_star(binary, minAreaRect); + } + binary = wipe_noise(binary); + + ShapeInfo new_shapeinfo = shapeinfo.copy(); + cv::Mat fck_gray = wipe_noise_gray(gray); + + if (shapetype == "ellipse" || shapetype == "circle") { + fck_gray = my_dilate(fck_gray, cv::Size(16, 16), 1, WHITE_BG); + } + + double min_cal = 1e10; + cv::Mat first_M; + int first_angle = 0; + + for (int angle = -rotate_range; angle < rotate_range; ++angle) { + cv::Mat M = cv::getRotationMatrix2D(center, angle, 1.0); + cv::Mat fck_gray1; + cv::warpAffine(fck_gray, fck_gray1, M, cv::Size(img_w, img_h), cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(255)); + + int center_x = static_cast(center.x); + center_x = min(max(1, center_x), img_w - 1); + + int left_w = center_x; + int right_start = (img_w % 2 == 0) ? center_x : (center_x + 1); + int right_w = img_w - right_start; + int min_w = min(left_w, right_w); + + cv::Mat left, right; + if (min_w > 0) { + left = fck_gray1(cv::Rect(0, 0, min_w, img_h)).clone(); + cv::Mat right_part = fck_gray1(cv::Rect(right_start, 0, min_w, img_h)); + cv::flip(right_part, right, 1); + } else { + left = cv::Mat::zeros(img_h, 1, fck_gray1.type()); + right = cv::Mat::zeros(img_h, 1, fck_gray1.type()); + } + + cv::Mat diff; + cv::absdiff(left, right, diff); + int cal = cv::countNonZero(diff); + + if (cal < min_cal) { + min_cal = cal; + first_M = M.clone(); + first_angle = angle; + } + } + + cv::Mat first_gray; + cv::warpAffine(gray, first_gray, first_M, cv::Size(img_w, img_h), cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(255)); + final_angle = first_angle; + + if (wh_ratio < 1.1 && abs(first_angle) > 45) { + cv::Mat binary2; + cv::threshold(first_gray, binary2, 200, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU); + + int x_range[2] = {static_cast(center.x - wh.width / 2 * 0.4), + static_cast(center.x + wh.width / 2 * 0.4)}; + int y_range[2] = {static_cast(center.y - wh.height / 2 * 0.85), + static_cast(center.y + wh.height / 2 * 0.85)}; + + int tx1 = max(0, x_range[0]); + int ty1 = max(0, y_range[0]); + int tx2 = min(static_cast(first_gray.cols), x_range[1]); + int ty2 = min(static_cast(first_gray.rows), y_range[1]); + + if (tx1 < tx2 && ty1 < ty2) { + int range_w = tx2 - tx1; + int range_h = ty2 - ty1; + if (range_w > 0 && range_h > 0) { + cv::Mat hahaha = binary2(cv::Rect(tx1, ty1, range_w, range_h)).clone(); + + int up_h = max(1, static_cast(hahaha.rows / 4.0)); + int down_y = static_cast(hahaha.rows / 4.0 * 3); + down_y = min(max(down_y, 0), hahaha.rows - up_h); + + if (up_h > 0 && down_y >= 0 && down_y + up_h <= hahaha.rows) { + cv::Mat up = hahaha(cv::Rect(0, 0, hahaha.cols, up_h)).clone(); + cv::Mat down = hahaha(cv::Rect(0, down_y, hahaha.cols, up_h)).clone(); + + int up_cnt = cv::countNonZero(up); + int down_cnt = cv::countNonZero(down); + double up_div_down = max(up_cnt, down_cnt) / max(1, min(up_cnt, down_cnt)); + + if (up_div_down > 2) { + if (up_cnt < down_cnt) { + final_angle = first_angle + 180; + } + } + } + } + } + } + + cv::Mat final_M = cv::getRotationMatrix2D(center, final_angle, 1); + cv::warpAffine(img, final_img, final_M, cv::Size(img_w, img_h), cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(255, 255, 255)); + + new_shapeinfo.minAreaRect = cv::RotatedRect(new_shapeinfo.minAreaRect.center, + new_shapeinfo.minAreaRect.size, 0); + + return make_tuple(final_img, final_angle, new_shapeinfo); +} + +tuple adjust_img(const cv::Mat& img, const ShapeInfo& shapeinfo) { + string shapetype = shapeinfo.type; + + if (shapetype == "ellipse" || shapetype == "circle") { + return adjust_image_impl(img, shapeinfo, 90); + } else if (shapetype == "square") { + return adjust_image_impl(img, shapeinfo, 30); + } else { + return make_tuple(img.clone(), 0, shapeinfo.copy()); + } +} diff --git a/c/extract_seal_main/adjust_image.h b/c/extract_seal_main/adjust_image.h new file mode 100644 index 0000000..43a8157 --- /dev/null +++ b/c/extract_seal_main/adjust_image.h @@ -0,0 +1,14 @@ +#ifndef ADJUST_IMAGE_H +#define ADJUST_IMAGE_H + +#include +#include +#include +#include +#include "../common.h" + +using namespace std; + +tuple adjust_img(const cv::Mat& img, const ShapeInfo& shapeinfo); + +#endif diff --git a/c/extract_seal_main/color_filter.cpp b/c/extract_seal_main/color_filter.cpp new file mode 100644 index 0000000..d0e6dbb --- /dev/null +++ b/c/extract_seal_main/color_filter.cpp @@ -0,0 +1,111 @@ +#include "color_filter.h" +#include "hsv_color.h" +#include "../image_utilities.h" +#include +#include +#include +#include + +using namespace std; + +pair filter_color(const cv::Mat& img, const vector>>& ranges, int fillval /* = 255 */) { + cv::Mat out = cv::Mat::ones(img.size(), img.type()) * fillval; + cv::Mat hsv; + cv::cvtColor(img, hsv, cv::COLOR_BGR2HSV); + + cv::Mat mask = cv::Mat::zeros(img.size(), CV_8UC1); + + for (const auto& range_pair : ranges) { + cv::Scalar lower(range_pair[0][0], range_pair[0][1], range_pair[0][2]); + cv::Scalar upper(range_pair[1][0], range_pair[1][1], range_pair[1][2]); + + cv::Mat temp_mask; + cv::inRange(hsv, lower, upper, temp_mask); + + mask += temp_mask; + } + + cv::Mat pos = mask == 255; + for (int y = 0; y < img.rows; y++) { + for (int x = 0; x < img.cols; x++) { + if (pos.at(y, x)) { + out.at(y, x) = img.at(y, x); + } + } + } + + return {out, mask}; +} + +pair color_fliter_process(const cv::Mat& img, int threshold) { + try { + cv::Mat grayscale; + cv::cvtColor(img, grayscale, cv::COLOR_BGR2GRAY); + + cv::Mat binary = block_threshold(grayscale); + cv::Mat mask = binary > 0; + + vector color_names = {"red"}; + vector color_values = {{0, 0, 255}}; + int best_color_index = 0; + double max_color_area = -1e10; + cv::Mat final_mask = mask.clone(); + + for (int i = 0; i < color_names.size(); i++) { + string color_name = color_names[i]; + vector>> ranges = get_color_range(color_name).check; + auto [_, color_mask] = filter_color(img, ranges); + cv::Mat color_threshold = color_mask == 255; + cv::Mat combined_mask; + cv::bitwise_and(color_threshold, mask, combined_mask); + double color_area = cv::countNonZero(combined_mask); + + if (color_area > max_color_area) { + best_color_index = i; + max_color_area = color_area; + + vector>> full_ranges = get_color_range(color_name).full; + vector>> strict_ranges = get_color_range(color_name).ranges2; + vector>> combined_ranges; + + for (int r = 0; r < full_ranges.size(); r++) { + vector> range_pair; + vector lower; + vector upper; + for (int k = 0; k < 3; k++) { + int lower_val = static_cast(full_ranges[r][0][k] * (100 - threshold)/100.0 + strict_ranges[r][0][k] * threshold/100.0); + int upper_val = static_cast(full_ranges[r][1][k] * (100 - threshold)/100.0 + strict_ranges[r][1][k] * threshold/100.0); + lower.push_back(lower_val); + upper.push_back(upper_val); + } + range_pair.push_back(lower); + range_pair.push_back(upper); + combined_ranges.push_back(range_pair); + } + + auto [__, final_threshold] = filter_color(img, combined_ranges); + final_mask = final_threshold == 255; + cv::bitwise_and(final_mask, mask, final_mask); + } + } + + cv::Mat output = cv::Mat(img.size(), img.type(), cv::Scalar(255, 255, 255)); + cv::Scalar color_value = color_values[best_color_index]; + + for (int y = 0; y < img.rows; y++) { + for (int x = 0; x < img.cols; x++) { + if (final_mask.at(y, x)) { + output.at(y, x) = cv::Vec3b(color_value[0], color_value[1], color_value[2]); + } + } + } + + return {output, color_names[best_color_index]}; + } catch (const exception& e) { + cout << "Exception in color_fliter_process: " << e.what() << endl; + return {img, "red"}; + } catch (...) { + cout << "Unknown exception in color_fliter_process" << endl; + return {img, "red"}; + } +} diff --git a/c/extract_seal_main/color_filter.h b/c/extract_seal_main/color_filter.h new file mode 100644 index 0000000..c9ec8da --- /dev/null +++ b/c/extract_seal_main/color_filter.h @@ -0,0 +1,16 @@ +#ifndef COLOR_FILTER_H +#define COLOR_FILTER_H + +#include +#include +#include +#include +#include "hsv_color.h" + +using namespace std; + +pair filter_color(const cv::Mat& img, const vector>>& ranges, int fillval = 255); + +pair color_fliter_process(const cv::Mat& img, int threshold); + +#endif diff --git a/c/extract_seal_main/extract_seal_main.cpp b/c/extract_seal_main/extract_seal_main.cpp new file mode 100644 index 0000000..e0d4a0e --- /dev/null +++ b/c/extract_seal_main/extract_seal_main.cpp @@ -0,0 +1,127 @@ +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace cv; + +#include +#include +#include "../common.h" +#include "color_filter.h" +#include "adjust_image.h" + +vector extract_seal_main(const Mat& img, const vector>& det) { + int i = 0; + vector seal_set; + for (const auto& seal : det) { + int x1 = int(seal[0]); + int y1 = int(seal[1]); + int x2 = int(seal[2]); + int y2 = int(seal[3]); + double conf = seal[4]; + if (conf < 0.6) { + continue; + } + int shape = int(seal[5]); + + int roi_x = max(0, x1); + int roi_y = max(0, y1); + int roi_width = min(x2 - x1, img.cols - roi_x); + int roi_height = min(y2 - y1, img.rows - roi_y); + + if (roi_width <= 0 || roi_height <= 0) { + cout << "Warning: Invalid ROI for detection box, skipping" << endl; + continue; + } + + Mat cropped_img = img(Rect(roi_x, roi_y, roi_width, roi_height)); + + auto [extracted_img, color_name] = color_fliter_process(cropped_img, 50); + + vector channels; + split(extracted_img, channels); + Mat red_channel = channels[0]; + + Scalar mean_val = mean(red_channel); + double red_average = mean_val[0]; + if (red_average > 240) { + continue; + } + + SealInfo seal_info; + seal_info.x1 = x1; + seal_info.y1 = y1; + seal_info.x2 = x2; + seal_info.y2 = y2; + seal_info.conf = conf; + seal_info.shape = shape; + seal_info.cropped_img = cropped_img; + seal_info.extracted_img = extracted_img; + seal_info.color_name = color_name; + + ShapeInfo si; + if (shape >= 0 && shape < 5) { + si.type = SHAPETYPE_NAMES[shape]; + } else { + si.type = "unknown"; + } + + int width = x2 - x1; + int height = y2 - y1; + si.minAreaRect = {{width / 2.0f, height / 2.0f}, {float(width), float(height)}, 0}; + + auto [adj_img, angle, new_si] = adjust_img(seal_info.extracted_img, ShapeInfo(si.type, si.minAreaRect, si.points, si.ignores, si.needInits, si.implement)); + seal_info.shape_info = ShapeInfo(new_si.type, new_si.minAreaRect, new_si.points, new_si.ignores, new_si.needInits, new_si.implement); + seal_info.angle = angle; + seal_info.adj_img = adj_img; + + seal_set.push_back(seal_info); + } + + // 存储到单例类中,实现数据共享 + seal_data::SealDataManager::getInstance().setSealSet(seal_set); + + return seal_set; +} + +// int main() { +// // TODO: 修改为你的图片路径 +// string img_path = "..\\..\\image\\2.png"; +// +// Mat img = imread(img_path); +// if (img.empty()) { +// cout << "Error: Cannot read image " << img_path << endl; +// return 1; +// } +// +// cout << "Image dimensions: " << img.cols << "x" << img.rows << endl; +// +// vector> det = {{256.0, 384.0, 471.0, 592.0, 0.980, 2}, +// {511.0, 423.0, 665.0, 581.0, 0.969, 2}}; +// +// cout << "Number of detection boxes: " << det.size() << endl; +// for (const auto& seal : det) { +// cout << "Detection box: x1=" << seal[0] << ", y1=" << seal[1] << ", x2=" << seal[2] << ", y2=" << seal[3] << endl; +// } +// +// vector seal_info = extract_seal_main(img, det); +// +// for (size_t i = 0; i < seal_info.size(); ++i) { +// // TODO: 修改为你的输出路径 +// string filename = "test_image\\extracted_seal_"; +// filename += to_string(i) + ".png"; +// imwrite(filename, seal_info[i].adj_img); +// cout << "Test seal " << i << " saved to: " << filename << endl; +// cout << " Shape: " << seal_info[i].shape_info.type << endl; +// cout << " Center: (" << seal_info[i].shape_info.minAreaRect.center.x << ", " << seal_info[i].shape_info.minAreaRect.center.y << ")" << endl; +// cout << " Size: (" << seal_info[i].shape_info.minAreaRect.size.width << ", " << seal_info[i].shape_info.minAreaRect.size.height << ")" << endl; +// cout << " Angle: " << seal_info[i].angle << endl; +// } +// +// return 0; +// } + diff --git a/c/extract_seal_main/extract_seal_main.h b/c/extract_seal_main/extract_seal_main.h new file mode 100644 index 0000000..8a83663 --- /dev/null +++ b/c/extract_seal_main/extract_seal_main.h @@ -0,0 +1,13 @@ +#ifndef EXTRACT_SEAL_MAIN_H +#define EXTRACT_SEAL_MAIN_H + +#include +#include + +using namespace std; + +struct SealInfo; + +vector extract_seal_main(const cv::Mat& img, const vector>& det); + +#endif diff --git a/c/extract_seal_main/hsv_color.cpp b/c/extract_seal_main/hsv_color.cpp new file mode 100644 index 0000000..e0072ca --- /dev/null +++ b/c/extract_seal_main/hsv_color.cpp @@ -0,0 +1,33 @@ +#include "hsv_color.h" +#include + +using namespace std; + +static const map COLOR_RANGES = { + {"red", { + {{{130, 50, 100}, {180, 255, 255}}, {{0, 50, 100}, {6, 255, 255}}}, + {{{130, 70, 100}, {180, 255, 255}}, {{0, 70, 100}, {12, 255, 255}}}, + {{{130, 43, 60}, {180, 255, 255}}, {{0, 43, 60}, {12, 255, 255}}}, + {{{130, 5, 5}, {180, 255, 255}}, {{0, 5, 5}, {25, 255, 255}}} + }}, + {"blue", { + {{{78, 50, 50}, {124, 255, 255}}}, + {{{78, 70, 50}, {124, 255, 255}}}, + {{{78, 43, 20}, {124, 255, 255}}}, + {{{78, 43, 0}, {124, 255, 255}}} + }}, + {"black", { + {{{0, 0, 0}, {180, 255, 43}}}, + {{{0, 0, 0}, {180, 255, 43}}}, + {{{0, 0, 0}, {180, 255, 43}}}, + {{{0, 0, 0}, {180, 255, 43}}} + }} +}; + +const ColorRange& get_color_range(const string& color) { + auto it = COLOR_RANGES.find(color); + if (it != COLOR_RANGES.end()) { + return it->second; + } + throw invalid_argument("Color not found: " + color); +} diff --git a/c/extract_seal_main/hsv_color.h b/c/extract_seal_main/hsv_color.h new file mode 100644 index 0000000..63c4773 --- /dev/null +++ b/c/extract_seal_main/hsv_color.h @@ -0,0 +1,20 @@ +#ifndef HSV_COLOR_H +#define HSV_COLOR_H + +#include +#include +#include +#include + +using namespace std; + +struct ColorRange { + vector>> ranges1; + vector>> ranges2; + vector>> check; + vector>> full; +}; + +const ColorRange& get_color_range(const string& color); + +#endif diff --git a/c/extract_seal_main/image_morphology.cpp b/c/extract_seal_main/image_morphology.cpp new file mode 100644 index 0000000..27433fd --- /dev/null +++ b/c/extract_seal_main/image_morphology.cpp @@ -0,0 +1,116 @@ +#include "image_morphology.h" +#include +#include +#include +#include + +using namespace std; + +const string WHITE_BG = "white"; +const string BLACK_BG = "black"; + +cv::Mat my_dilate(const cv::Mat& img, const cv::Size& ksize, int iter, const string& bg_type) { + assert(bg_type == WHITE_BG || bg_type == BLACK_BG); + + cv::Mat kernel(ksize.height, ksize.width, CV_8UC1, cv::Scalar(255)); + cv::Mat result; + + if (bg_type == BLACK_BG) { + cv::dilate(img, result, kernel, cv::Point(-1, -1), iter); + } else { + cv::erode(img, result, kernel, cv::Point(-1, -1), iter); + } + + return result; +} + +cv::Mat wipe_noise(const cv::Mat& img) { + cv::Mat result = img.clone(); + int num_objects; + cv::Mat labels, stats, centroids; + num_objects = cv::connectedComponentsWithStats(img, labels, stats, centroids, 8); + + int img_min = min(img.rows, img.cols); + double scale = pow(max(1.0, img_min / 300.0), 2); + int threshold_area = static_cast(10 * scale); + + for (int index = 0; index < num_objects; ++index) { + int area = stats.at(index, 4); + if (area < threshold_area) { + result.setTo(0, labels == index); + } + } + return result; +} + +cv::Mat wipe_noise_gray(const cv::Mat& img) { + cv::Mat result = img.clone(); + cv::Mat blur; + cv::GaussianBlur(img, blur, cv::Size(3, 3), 0); + + cv::Mat binary; + cv::threshold(blur, binary, 200, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU); + + int num_objects; + cv::Mat labels, stats, centroids; + num_objects = cv::connectedComponentsWithStats(binary, labels, stats, centroids, 8); + + int img_min = min(img.rows, img.cols); + double scale = pow(max(1.0, img_min / 300.0), 2); + int threshold_area = static_cast(10 * scale); + + for (int index = 0; index < num_objects; ++index) { + int area = stats.at(index, 4); + if (area < threshold_area) { + result.setTo(255, labels == index); + } + } + + return result; +} + +cv::Mat wipe_star(const cv::Mat& img, const cv::RotatedRect& minAreaRect) { + cv::Mat result = img.clone(); + cv::Point2f center = minAreaRect.center; + cv::Size2f wh = minAreaRect.size; + double r = max(wh.width, wh.height) / min(wh.width, wh.height); + int base_wh = static_cast(wh.height / 2 + wh.height / 20); + double _W = base_wh * r; + double _H = base_wh; + + int num_objects; + cv::Mat labels, stats, centroids; + num_objects = cv::connectedComponentsWithStats(img, labels, stats, centroids, 8); + + for (int index = 1; index < num_objects; ++index) { + int x = stats.at(index, 0); + int y = stats.at(index, 1); + int w = stats.at(index, 2); + int h = stats.at(index, 3); + cv::Point2f _center(x + w / 2.0, y + h / 2.0); + double center_dis = sqrt(pow(_center.x - center.x, 2) + pow(_center.y - center.y, 2)); + double hehe = min(w, h); + + if (hehe > max(_W, _H) * 0.25 && hehe < min(_W, _H) && center_dis < max(_W, _H) * 0.2) { + result.setTo(0, labels == index); + } + } + + if (r < 1.2) { + int y1 = static_cast(center.y - wh.height * 0.15); + int y2 = static_cast(center.y + wh.height * 0.15); + int x1 = static_cast(center.x - wh.width * 0.15); + int x2 = static_cast(center.x + wh.width * 0.15); + + int tx1 = max(0, x1); + int ty1 = max(0, y1); + int tx2 = min(static_cast(result.cols), x2); + int ty2 = min(static_cast(result.rows), y2); + + if (tx1 < tx2 && ty1 < ty2) { + result(cv::Rect(tx1, ty1, tx2 - tx1, ty2 - ty1)).setTo(0); + } + } + + return result; +} diff --git a/c/extract_seal_main/image_morphology.h b/c/extract_seal_main/image_morphology.h new file mode 100644 index 0000000..9ef4b90 --- /dev/null +++ b/c/extract_seal_main/image_morphology.h @@ -0,0 +1,20 @@ +#ifndef IMAGE_MORPHOLOGY_H +#define IMAGE_MORPHOLOGY_H + +#include +#include + +using namespace std; + +extern const string WHITE_BG; +extern const string BLACK_BG; + +cv::Mat my_dilate(const cv::Mat& img, const cv::Size& ksize = cv::Size(5, 5), int iter = 1, const string& bg_type = BLACK_BG); + +cv::Mat wipe_noise(const cv::Mat& img); + +cv::Mat wipe_noise_gray(const cv::Mat& img); + +cv::Mat wipe_star(const cv::Mat& img, const cv::RotatedRect& minAreaRect); + +#endif diff --git a/c/extract_seal_main/test_extract_seal.cpp b/c/extract_seal_main/test_extract_seal.cpp new file mode 100644 index 0000000..c1c15f9 --- /dev/null +++ b/c/extract_seal_main/test_extract_seal.cpp @@ -0,0 +1,145 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "../common.h" +#include "extract_seal_main.h" + +using namespace std; +namespace fs = std::filesystem; + +struct ImageTask { + string img_path; + vector> det; +}; + +int main() { + // TODO: 修改为你的图片目录路径 + // 示例: "c:\\Users\\用户名\\Desktop\\Final project\\image" 或使用相对路径 "..\\..\\image" + string image_dir = "..\\..\\image"; + vector tasks; + + cout << "Scanning image directory: " << image_dir << endl; + + try { + for (const auto& entry : fs::directory_iterator(image_dir)) { + if (entry.is_regular_file()) { + string ext = entry.path().extension().string(); + if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") { + ImageTask task; + task.img_path = entry.path().string(); + + string filename = entry.path().filename().string(); + if (filename == "2.png") { + task.det = { + {256.0, 384.0, 471.0, 592.0, 0.980, 2}, + {511.0, 423.0, 665.0, 581.0, 0.969, 2} + }; + } else if (filename == "1.png") { + task.det = { + {408.0, 32.0, 559.0, 186.0, 0.974, 0}, + {267.0, 61.0, 348.0, 143.0, 0.973, 2}, + {54.0, 36.0, 212.0, 147.0, 0.972, 1} + }; + } + + tasks.push_back(task); + cout << "Found image: " << filename << endl; + } + } + } + } catch (const fs::filesystem_error& e) { + cout << "Error scanning directory: " << e.what() << endl; + return 1; + } + + if (tasks.empty()) { + cout << "No image files found in directory." << endl; + return 1; + } + + cout << "\nTotal images found: " << tasks.size() << endl; + + for (size_t task_idx = 0; task_idx < tasks.size(); ++task_idx) { + const ImageTask& task = tasks[task_idx]; + string img_path = task.img_path; + vector> det = task.det; + + cout << "\n========================================" << endl; + cout << "Processing image " << task_idx + 1 << "/" << tasks.size() << endl; + cout << "File: " << fs::path(img_path).filename().string() << endl; + cout << "========================================" << endl; + + cv::Mat img = cv::imread(img_path); + if (img.empty()) { + cout << "Error: Cannot read image " << img_path << endl; + if (task_idx < tasks.size() - 1) { + cout << "Press Enter to continue to next image..." << endl; + cin.ignore(numeric_limits::max(), '\n'); + } + continue; + } + + cout << "Image dimensions: " << img.cols << "x" << img.rows << endl; + + cout << "Number of detection boxes: " << det.size() << endl; + for (size_t i = 0; i < det.size(); ++i) { + cout << "Detection box " << i+1 << ": x1=" << det[i][0] << ", y1=" << det[i][1] + << ", x2=" << det[i][2] << ", y2=" << det[i][3] + << ", conf=" << det[i][4] << ", shape=" << det[i][5] << endl; + } + + cout << "\nStart extracting seals..." << endl; + vector seal_set = extract_seal_main(img, det); + + cout << "Total extracted seals: " << seal_set.size() << endl << endl; + + // TODO: 修改为你的输出目录路径 + // 示例: "c:\\Users\\用户名\\Desktop\\Final project\\c\\extract_seal_main\\test_image" 或使用相对路径 "..\\extract_seal_main\\test_image" + string output_dir = "..\\extract_seal_main\\test_image"; + fs::create_directories(output_dir); + + for (size_t i = 0; i < seal_set.size(); ++i) { + cout << "========== Seal " << i << " ==========" << endl; + + string img_name = fs::path(img_path).stem().string(); + string cropped_filename = output_dir + "\\seal_" + img_name + "_" + to_string(i) + "_cropped.png"; + string ex_filename = output_dir + "\\seal_" + img_name + "_" + to_string(i) + "_ex_img.png"; + string adj_filename = output_dir + "\\seal_" + img_name + "_" + to_string(i) + "_adj_img.png"; + + cv::imwrite(cropped_filename, seal_set[i].cropped_img); + cv::imwrite(ex_filename, seal_set[i].extracted_img); + cv::imwrite(adj_filename, seal_set[i].adj_img); + + cout << "Saved files:" << endl; + cout << " " << cropped_filename << " (cropped_img)" << endl; + cout << " " << ex_filename << " (extracted_img)" << endl; + cout << " " << adj_filename << " (adj_img)" << endl; + + cout << endl << "region: [" << seal_set[i].x1 << ", " << seal_set[i].y1 << ", " + << seal_set[i].x2 << ", " << seal_set[i].y2 << "]" << endl; + cout << "shape: " << seal_set[i].shape << endl; + cout << "color_name: " << seal_set[i].color_name << endl; + cout << "angle: " << seal_set[i].angle << endl; + cout << "shape_info.type: " << seal_set[i].shape_info.type << endl; + cout << "shape_info.minAreaRect: center=(" + << seal_set[i].shape_info.minAreaRect.center.x << ", " + << seal_set[i].shape_info.minAreaRect.center.y << "), size=(" + << seal_set[i].shape_info.minAreaRect.size.width << ", " + << seal_set[i].shape_info.minAreaRect.size.height << ")" << endl; + cout << endl; + } + + if (task_idx < tasks.size() - 1) { + cout << "Press Enter to process next image..." << endl; + cin.ignore(numeric_limits::max(), '\n'); + } + } + + cout << "\nAll images processed!" << endl; + return 0; +} diff --git a/c/image_utilities.cpp b/c/image_utilities.cpp new file mode 100644 index 0000000..f048e41 --- /dev/null +++ b/c/image_utilities.cpp @@ -0,0 +1,100 @@ +#include "image_utilities.h" +#include +#include +#include +#include +#include + +using namespace std; + +vector mrect2box(const cv::RotatedRect& mrect) { + cv::Point2f boxPoints[4]; + mrect.points(boxPoints); + + vector result; + for (int i = 0; i < 4; ++i) { + result.emplace_back(cv::Point(static_cast(boxPoints[i].x), static_cast(boxPoints[i].y))); + } + + return result; +} + +cv::Mat block_threshold(const cv::Mat& gray) { + try { + int height = gray.rows; + int width = gray.cols; + + vector divisions = {8, 13}; + vector block_sizes; + for (int div : divisions) { + int size = max(19, (height + width) / 2 / div); + if (size % 2 == 0) size++; + block_sizes.push_back(size); + } + + vector> block_strategies; + for (int size : block_sizes) { + block_strategies.push_back({size, 0}); + block_strategies.push_back({size, size / 2}); + } + + cv::Mat otsu; + double global_threshold = cv::threshold(gray, otsu, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU); + + cv::Mat adaptive_ref; + cv::adaptiveThreshold(gray, adaptive_ref, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY_INV, block_sizes[0], 20); + + cv::Mat result = cv::Mat::zeros(gray.size(), CV_8UC1); + + for (auto& strategy : block_strategies) { + int block_size = strategy.first; + int offset = strategy.second; + + cv::Mat current_result = cv::Mat::zeros(gray.size(), CV_8UC1); + + for (int y = offset; y < height; y += block_size) { + for (int x = offset; x < width; x += block_size) { + int end_y = min(y + block_size, height); + int end_x = min(x + block_size, width); + + cv::Mat block_roi = gray(cv::Rect(x, y, end_x - x, end_y - y)); + + cv::Scalar mean, stddev; + cv::meanStdDev(block_roi, mean, stddev); + double variance = stddev[0] * stddev[0]; + + cv::Mat block_result; + if (variance < 80) { + block_result = cv::Mat::zeros(block_roi.size(), CV_8UC1); + if (mean[0] < global_threshold) { + block_result = cv::Mat::ones(block_roi.size(), CV_8UC1) * 255; + } + } else { + cv::threshold(block_roi, block_result, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU); + + cv::Scalar ref_mean, block_mean; + cv::Mat adaptive_block = adaptive_ref(cv::Rect(x, y, end_x - x, end_y - y)); + cv::meanStdDev(adaptive_block, ref_mean, cv::noArray()); + cv::meanStdDev(block_result, block_mean, cv::noArray()); + + if (abs(ref_mean[0] - block_mean[0]) > 200) { + block_result = 255 - block_result; + } + } + + block_result.copyTo(current_result(cv::Rect(x, y, end_x - x, end_y - y))); + } + } + + cv::bitwise_or(result, current_result, result); + } + + return result; + } catch (const exception& e) { + cout << "Exception in block_threshold: " << e.what() << endl; + return cv::Mat(); + } catch (...) { + cout << "Unknown exception in block_threshold" << endl; + return cv::Mat(); + } +} diff --git a/c/image_utilities.h b/c/image_utilities.h new file mode 100644 index 0000000..4318886 --- /dev/null +++ b/c/image_utilities.h @@ -0,0 +1,13 @@ +#ifndef IMAGE_UTILITIES_H +#define IMAGE_UTILITIES_H + +#include +#include + +using namespace std; + +cv::Mat block_threshold(const cv::Mat& gray); + +vector mrect2box(const cv::RotatedRect& mrect); + +#endif diff --git a/c/remove_border/remove_border.cpp b/c/remove_border/remove_border.cpp new file mode 100644 index 0000000..e690883 --- /dev/null +++ b/c/remove_border/remove_border.cpp @@ -0,0 +1,315 @@ +#include "remove_border.h" +#include +#include "../common.h" +#include "../image_utilities.h" + +using namespace std; + +/** + * 根据边框内边缘获取去除边框所用掩膜 + * + * args: + * imgshape: 图片宽高信息 + * shapeType: 印章形状信息 + * filter_border: 边框内边缘 + * returns: + * filterMask: 除边框外的印章掩码 + */ +cv::Mat get_filterBorderMask(const cv::Size& imgshape, const string& shapeType, const cv::RotatedRect& filter_border) { + // 创建全黑图像作为基础 + cv::Mat empty = cv::Mat::zeros(imgshape, CV_8UC1); + + if (shapeType == "ellipse" || shapeType == "circle") { + // 绘制填充的椭圆 + cv::ellipse(empty, filter_border, cv::Scalar(255), -1); + } else { + vector filter_border_box = mrect2box(filter_border); + vector> contours; + contours.push_back(filter_border_box); + cv::drawContours(empty, contours, -1, cv::Scalar(255), -1); + } + + // 创建掩码:绘制区域为255,其他区域为0 + cv::Mat filterMask; + cv::compare(empty, 255, filterMask, cv::CMP_EQ); + + return filterMask; +} + +/** + * 枚举offsets,找到最合适的边界 + * + * args: + * img: 图 + * shapeType: 形状类型 + * mrect: 一个最小包围矩形 + * offsets: 平移变换组合 + * inverse: True以背景点数量最大时为最佳,False以前景点数量最大时为最佳 + * bg_val: 背景值 + * returns: + * best_mrect: 最佳边界,是一个最小包围矩形 + */ +cv::RotatedRect _find_bestfit(const cv::Mat& img, const string& shapeType, const cv::RotatedRect& mrect, + const vector& offsets, bool inverse, int bg_val) { + // 如果offsets为空,返回原始mrect + if (offsets.empty()) { + return mrect; + } + + // 提取mrect的参数 + cv::Point2f center = mrect.center; + cv::Size2f wh = mrect.size; + float angle = mrect.angle; + + // 初始化最佳分数和最佳mrect + float best_score = 0; + cv::RotatedRect best_mrect = mrect; + const int color = 255; + + // 遍历offsets中的每个变换参数 + for (const auto& offset : offsets) { + try { + float tx = offset.tx; + float ty = offset.ty; + float ws = offset.ws; + float hs = offset.hs; + float ta = offset.ta; + + // 创建全黑图像 + cv::Mat empty = cv::Mat::zeros(img.size(), CV_8UC1); + + // 计算新的mrect + cv::Point2f new_center(center.x + tx, center.y + ty); + // 确保宽高为正数 + float new_width = wh.width + ws; + float new_height = wh.height + hs; + if (new_width <= 0 || new_height <= 0) { + continue; + } + cv::Size2f new_wh(new_width, new_height); + float new_angle = angle + ta; + cv::RotatedRect new_mrect(new_center, new_wh, new_angle); + + // 绘制轮廓并计算内部面积 + float innerArea = 0; + const int BS = 1; // 线宽,与Python版本保持一致 + if (shapeType == "ellipse" || shapeType == "circle") { + // 使用线宽 1 绘制边界线,与Python版本保持一致 + cv::ellipse(empty, new_mrect, color, BS); + innerArea = (new_wh.width * new_wh.height / 4) * M_PI; + } else { + vector new_mrectbox = mrect2box(new_mrect); + vector> contours; + contours.push_back(new_mrectbox); + cv::drawContours(empty, contours, -1, color, BS); + innerArea = new_wh.width * new_wh.height; + } + + // 创建轮廓掩码 + cv::Mat cntMask;//得到黑底白色轮廓(线宽1),未进行内部填充 + cv::compare(empty, color, cntMask, cv::CMP_EQ); + + // 原始逐像素遍历代码(已注释,保留用于参考) + // int cntArea = 0; + // for (int i = 0; i < img.rows; ++i) { + // for (int j = 0; j < img.cols; ++j) { + // if (cntMask.at(i, j)) { + // if (!inverse) { + // if (img.at(i, j) != bg_val) { + // cntArea++; + // } + // } else { + // if (img.at(i, j) == bg_val) { + // cntArea++; + // } + // } + // } + // } + // } + + // 使用OpenCV向量化操作计算轮廓区域的像素数(性能优化) + cv::Mat condMask;//提取二值化图img的背景像素,白底黑字(印章边框+文字) + if (!inverse) { + cv::compare(img, bg_val, condMask, cv::CMP_NE);//黑底图走这一句 + } else { + cv::compare(img, bg_val, condMask, cv::CMP_EQ);//白底图走这一句,把二值图的白色背景提取出来。 + } + cv::Mat resultMask; + cv::bitwise_and(cntMask, condMask, resultMask);//对黑底轮廓图和白底二值图进行按位取与运算,得到重合都是白色的掩膜。 + int cntArea = cv::countNonZero(resultMask);//统计掩膜中的非零元素即255,因为初始化阶段已把最佳边框赋值为最小外接矩形,因此不存在会有最佳边框在印章外的情况。 + //通过offset数组设定了一个范围(不太可能无限缩小到内部文字圈)对最小外接矩形进行了不断缩减试探寻求最佳边框,通过找寻背景像素(白)最多的点得到最佳边框。因为文字区域和边框区域为黑,背景为白,在规定的范围内最多白背景的地方为最佳边框。 + + // 更新最佳分数和最佳mrect + float score = cntArea; + if (best_score < score) { + best_score = score; + best_mrect = new_mrect; + } + } catch (const exception& e) { + cout << "处理偏移时出错: " << e.what() << endl; + continue; + } catch (...) { + cout << "处理偏移时出错: 未知异常" << endl; + continue; + } + } + + return best_mrect; +} + +/** + * 获取去除边框所用掩膜 + * + * args: + * img: 图 + * shapeinfo: 形状信息 + * bg_val: 图片背景色 + * returns: + * filterMask: 用来去掉边框的掩膜mask + * new_shapeinfo: 新形状信息 + * inner_border: 边框内边缘 + */ +cv::Mat _getFilterMask(const cv::Mat& img, const ShapeInfo& shapeinfo, cv::RotatedRect& inner_border, int bg_val) { + try { + // 确保输入是灰度图 + if (img.empty()) { + cout << "错误: 输入图像为空" << endl; + return cv::Mat(); + } + if (img.channels() != 1) { + cout << "错误: 输入必须是灰度图" << endl; + return cv::Mat(); + } + + // 获取形状类型 + string shapeType = shapeinfo.type; + if (shapeType.empty()) { + shapeType = "unknown"; + } + + // 对图像进行阈值处理,生成二值图像 + cv::Mat binary; + // 使用THRESH_BINARY,与Python版本保持一致 + cv::threshold(img, binary, 200, 255, cv::THRESH_BINARY | cv::THRESH_OTSU); + + // 获取最佳边框 + cv::RotatedRect best_border = shapeinfo.minAreaRect; + + // 计算缩进值列表 + const int BIG_SIZE = 365; + vector indents; + for (int indent = -18; indent > -44; indent -= 2) { + indents.push_back(indent); + } + + // 计算k值并扩展indents + float k = min(best_border.size.width, best_border.size.height) / static_cast(BIG_SIZE); + if (k > 1) { + int num_extra = static_cast(ceil(k * 5)); + for (int i = 0; i < num_extra; ++i) { + indents.push_back(-44 - 4 * i); + } + } + + // 生成offsets_1 + vector offsets_1; + for (int indent : indents) { + offsets_1.emplace_back(0, 0, indent, indent, 0); + } + + // 获取边框内边缘 + cv::RotatedRect filter_border = _find_bestfit(binary, shapeType, best_border, offsets_1, true, bg_val); + + // 获取去除边框所需要的掩膜 + cv::Mat filterMask = get_filterBorderMask(img.size(), shapeType, filter_border); + + // 设置inner_border + inner_border = filter_border; + + return filterMask; + } catch (const exception& e) { + cout << "_getFilterMask出错: " << e.what() << endl; + return cv::Mat(); + } catch (...) { + cout << "_getFilterMask出错: 未知异常" << endl; + return cv::Mat(); + } +} + +/** + * 去除边框 + * + * args: + * img: 图片 + * shapeinfo: 形状信息 + * bg_val: 图片的背景颜色,默认是白底黑字, 如果是黑底白字的二值图,请设置bg_val为0 + * returns: + * img1: 去掉边框后的图片 + * mask: 去掉边框所用掩膜 + * new_shapeinfo: 拟合得更好的形状 + * inner_border: 边框内边缘 + */ +cv::Mat remove_border(const cv::Mat& img, const ShapeInfo& shapeinfo, cv::Mat& mask, ShapeInfo& new_shapeinfo, cv::RotatedRect& inner_border, int bg_val) { + try { + // 确保输入是灰度图 + if (img.empty()) { + cout << "错误: 输入图像为空" << endl; + return cv::Mat(); + } + if (img.channels() != 1) { + cout << "错误: 输入必须是灰度图" << endl; + return cv::Mat(); + } + + // 调用_getFilterMask获取掩膜、新形状信息和边框内边缘 + mask = _getFilterMask(img, shapeinfo, inner_border, bg_val); + + // 检查mask是否为空 + if (mask.empty()) { + cout << "警告: 生成的mask为空,返回原始图像" << endl; + // 返回原始图像的副本 + new_shapeinfo = shapeinfo; + return img.clone(); + } + + // 确保mask与img尺寸相同 + if (mask.size() != img.size()) { + cout << "警告: mask尺寸与图像尺寸不同,返回原始图像" << endl; + new_shapeinfo = shapeinfo; + return img.clone(); + } + + // 复制图片 + cv::Mat img1 = img.clone(); + + // 应用掩膜:将非mask区域设置为bg_val + // 原始逐像素遍历代码(已注释,保留用于参考) + // for (int i = 0; i < img1.rows; ++i) { + // for (int j = 0; j < img1.cols; ++j) { + // if (!mask.at(i, j)) { + // img1.at(i, j) = static_cast(bg_val); + // } + // } + // } + // 使用OpenCV向量化操作(性能优化) + cv::Mat invertedMask; + cv::bitwise_not(mask, invertedMask); + img1.setTo(bg_val, invertedMask); + + // 使用计算出的inner_border更新new_shapeinfo + new_shapeinfo = shapeinfo; + new_shapeinfo.minAreaRect = inner_border; + + return img1; + } catch (const exception& e) { + cout << "remove_border出错: " << e.what() << endl; + // 发生异常时返回原始图像的副本 + new_shapeinfo = shapeinfo; + return img.clone(); + } catch (...) { + cout << "remove_border出错: 未知异常" << endl; + // 发生未知异常时返回原始图像的副本 + new_shapeinfo = shapeinfo; + return img.clone(); + } +} \ No newline at end of file diff --git a/c/remove_border/remove_border.h b/c/remove_border/remove_border.h new file mode 100644 index 0000000..95fe36d --- /dev/null +++ b/c/remove_border/remove_border.h @@ -0,0 +1,84 @@ +#ifndef REMOVE_BORDER_H +#define REMOVE_BORDER_H + +#include +#include +#include +#include "../common.h" +#include "../image_utilities.h" + +using namespace std; + +/** + * 偏移参数结构体 + * 对应Python中的offset元组(tx, ty, ws, hs, ta) + */ +struct Offset { + float tx; // x方向平移 + float ty; // y方向平移 + float ws; // 宽度缩放 + float hs; // 高度缩放 + float ta; // 角度调整 + + Offset(float tx = 0.0f, float ty = 0.0f, float ws = 0.0f, float hs = 0.0f, float ta = 0.0f) + : tx(tx), ty(ty), ws(ws), hs(hs), ta(ta) {} +}; + +/** + * 根据边框内边缘获取去除边框所用掩膜 + * + * args: + * imgshape: 图片宽高信息 + * shapeType: 印章形状信息 + * filter_border: 边框内边缘 + * returns: + * filterMask: 除边框外的印章掩码 + */ +cv::Mat get_filterBorderMask(const cv::Size& imgshape, const string& shapeType, const cv::RotatedRect& filter_border); + +/** + * 枚举offsets,找到最合适的边界 + * + * args: + * img: 图 + * shapeType: 形状类型 + * mrect: 一个最小包围矩形 + * offsets: 平移变换组合 + * inverse: True以背景点数量最大时为最佳,False以前景点数量最大时为最佳 + * bg_val: 背景值 + * returns: + * best_mrect: 最佳边界,是一个最小包围矩形 + */ +cv::RotatedRect _find_bestfit(const cv::Mat& img, const string& shapeType, const cv::RotatedRect& mrect, + const vector& offsets, bool inverse = false, int bg_val = 255); + +/** + * 获取去除边框所用掩膜 + * + * args: + * img: 图 + * shapeinfo: 形状信息 + * bg_val: 图片背景色 + * returns: + * filterMask: 用来去掉边框的掩膜mask + * new_shapeinfo: 新形状信息 + * inner_border: 边框内边缘 + */ +cv::Mat _getFilterMask(const cv::Mat& img, const ShapeInfo& shapeinfo, cv::RotatedRect& inner_border, int bg_val = 255); + +/** + * 去除边框 + * + * args: + * img: 图片 + * shapeinfo: 形状信息 + * bg_val: 图片的背景颜色,默认是白底黑字, 如果是黑底白字的二值图,请设置bg_val为0 + * returns: + * img1: 去掉边框后的图片 + * mask: 去掉边框所用掩膜 + * new_shapeinfo: 拟合得更好的形状 + * inner_border: 边框内边缘 + */ +cv::Mat remove_border(const cv::Mat& img, const ShapeInfo& shapeinfo, cv::Mat& mask, ShapeInfo& new_shapeinfo, cv::RotatedRect& inner_border, int bg_val = 255); + +#endif // REMOVE_BORDER_H \ No newline at end of file diff --git a/c/remove_border/remove_border_main.cpp b/c/remove_border/remove_border_main.cpp new file mode 100644 index 0000000..f7086bf --- /dev/null +++ b/c/remove_border/remove_border_main.cpp @@ -0,0 +1,45 @@ +#include "remove_border_main.h" +#include "remove_border.h" +#include + +using namespace std; + +void remove_border_main(vector& seal_set, bool is_debug) { + int i = 0; + + for (auto& seal : seal_set) { + if (seal.adj_img.empty()) { + cout << "警告: seal.adj_img为空,跳过处理" << endl; + continue; + } + + cv::Mat gray; + cv::cvtColor(seal.adj_img, gray, cv::COLOR_BGR2GRAY); + + cv::Mat mask; + ShapeInfo new_shapeinfo; + cv::RotatedRect inner_border; + + try { + cv::Mat rm_bd_img = remove_border(gray, seal.shape_info, mask, new_shapeinfo, inner_border, 255); + + seal.rm_bd_img = rm_bd_img; + seal.shape_info = new_shapeinfo; + + if (is_debug) { + string window_name = "extracted_img_" + to_string(i); + cout << "Debug: Processed seal " << i << " (" << window_name << ")" << endl; + } + } catch (const exception& e) { + cout << "处理印章时出错: " << e.what() << endl; + } catch (...) { + cout << "处理印章时出错: 未知异常" << endl; + } + + i++; + } + + if (is_debug) { + cout << "Debug: Processing completed" << endl; + } +} diff --git a/c/remove_border/remove_border_main.h b/c/remove_border/remove_border_main.h new file mode 100644 index 0000000..8c71c6e --- /dev/null +++ b/c/remove_border/remove_border_main.h @@ -0,0 +1,10 @@ +#ifndef REMOVE_BORDER_MAIN_H +#define REMOVE_BORDER_MAIN_H + +#include "../common.h" + +using namespace std; + +void remove_border_main(vector& seal_set, bool is_debug = false); + +#endif diff --git a/c/remove_border/test_remove_border.cpp b/c/remove_border/test_remove_border.cpp new file mode 100644 index 0000000..9d69767 --- /dev/null +++ b/c/remove_border/test_remove_border.cpp @@ -0,0 +1,100 @@ +#include +#include +#include +#include +#include +#include "../extract_seal_main/extract_seal_main.h" +#include "remove_border.h" +#include "remove_border_main.h" + +using namespace std; +namespace fs = std::filesystem; + +void process_seal_remove_border() { + // TODO: 修改为你的测试图片路径 + // 示例: "c:\\Users\\用户名\\Desktop\\Final project\\image\\1.png" 或使用相对路径 "..\\..\\image\\1.png" + string img_path = "..\\..\\image\\1.png"; + + cout << "Image path: " << img_path << endl; + + ifstream file(img_path); + if (!file.good()) { + cout << "Error: Image file does not exist!" << endl; + return; + } + file.close(); + + cv::Mat img = cv::imread(img_path); + if (img.empty()) { + cout << "Error: Cannot read image!" << endl; + return; + } + + cout << "Image loaded: " << img.cols << "x" << img.rows << endl; + + vector> det = { + {408.0, 32.0, 559.0, 186.0, 0.974, 0}, + {267.0, 61.0, 348.0, 143.0, 0.973, 2}, + {54.0, 36.0, 212.0, 147.0, 0.972, 1} + }; + + cout << "Number of detection boxes: " << det.size() << endl; + + cout << "Extracting seals..." << endl; + try { + vector seal_set = extract_seal_main(img, det); + + cout << "Total extracted seals: " << seal_set.size() << endl; + + if (seal_set.empty()) { + cout << "Error: No seals extracted!" << endl; + return; + } + + for (size_t i = 0; i < seal_set.size(); ++i) { + cout << "Seal " << i << ":" << endl; + cout << " Shape: " << seal_set[i].shape_info.type << endl; + cout << " Adjusted image size: " << seal_set[i].adj_img.cols << "x" << seal_set[i].adj_img.rows << endl; + } + + cout << endl << "Removing borders..." << endl; + remove_border_main(seal_set, false); + + string output_dir = "..\\remove_border\\test_image"; + fs::create_directories(output_dir); + + cout << endl << "========== Results after remove_border ==========" << endl; + for (size_t i = 0; i < seal_set.size(); ++i) { + cout << endl << "========== Seal " << i << " ==========" << endl; + + if (seal_set[i].rm_bd_img.empty()) { + cout << " Warning: rm_bd_img is empty!" << endl; + continue; + } + + string save_path = output_dir + "\\seal_" + to_string(i) + "_rm_bd_img.png"; + cv::imwrite(save_path, seal_set[i].rm_bd_img); + cout << "rm_bd_img saved: " << save_path << endl; + cout << "rm_bd_img size: " << seal_set[i].rm_bd_img.cols << "x" << seal_set[i].rm_bd_img.rows << endl; + + cout << "shape_info.type: " << seal_set[i].shape_info.type << endl; + cout << "shape_info.minAreaRect: center=(" + << seal_set[i].shape_info.minAreaRect.center.x << ", " + << seal_set[i].shape_info.minAreaRect.center.y << "), size=(" + << seal_set[i].shape_info.minAreaRect.size.width << ", " + << seal_set[i].shape_info.minAreaRect.size.height << "), angle=" + << seal_set[i].shape_info.minAreaRect.angle << endl; + } + + cout << endl << "Done!" << endl; + } catch (const exception& e) { + cout << "Error: " << e.what() << endl; + } catch (...) { + cout << "Error: Unknown exception occurred!" << endl; + } +} + +int main() { + process_seal_remove_border(); + return 0; +} diff --git a/image/1.png b/image/1.png new file mode 100644 index 0000000..3fdf557 Binary files /dev/null and b/image/1.png differ diff --git a/image/2.png b/image/2.png new file mode 100644 index 0000000..8a744de Binary files /dev/null and b/image/2.png differ