第四周第五天——重构成类

为什么要重构成类

前几天你的 main.cpp 里可能已经有:

复制代码
读图
resize
BGR → HSV
阈值分割
形态学
轮廓提取
面积过滤
画框
统计
保存

如果全部堆在 main.cpp:

复制代码
main.cpp
├── 图像路径
├── 一堆 cv::Mat
├── HSV 参数
├── kernel
├── contours
├── minArea
├── rectangle
└── imwrite

代码虽然能跑,但以后会越来越难维护。

重构后:

复制代码
main.cpp
→ 只负责"做什么"

ImageProcessor.cpp
→ 负责"怎么做"

这就是今天最重要的变化。

最终目录结构

建议:

复制代码
CropImageProcessor/
├── CMakeLists.txt
├── include/
│   └── ImageProcessor.h
├── src/
│   ├── ImageProcessor.cpp
│   └── main.cpp
├── images/
│   └── test.jpg
└── output/

三个代码文件职责分别是:

复制代码
ImageProcessor.h
→ 类的声明

ImageProcessor.cpp
→ 类的实现

main.cpp
→ 程序流程调度
类整体设计
复制代码
class ImageProcessor {
public:
    explicit ImageProcessor(
        const std::string& imagePath
    );

    bool load();

    void resize(double scale);

    void convertToHSV();

    void segmentGreen(
        const cv::Scalar& lower,
        const cv::Scalar& upper
    );

    void morphology(
        int kernelSize
    );

    void extractContours(
        double minArea
    );

    void drawResult();

    bool saveResult(
        const std::string& outputPath
    ) const;

private:
    std::string imagePath_;

    cv::Mat image_;
    cv::Mat resizedImage_;
    cv::Mat hsvImage_;
    cv::Mat mask_;
    cv::Mat cleanMask_;
    cv::Mat resultImage_;

    std::vector<
        std::vector<cv::Point>
    > validContours_;
};

你可以先把它分成两部分:

复制代码
public
→ 外界允许调用的功能

private
→ 类内部自己维护的数据
public 是"这个类能做什么"

比如:

复制代码
bool load();

表示:

ImageProcessor 有"加载图像"的能力。

复制代码
void resize(double scale);

表示:

它可以缩放图像。

复制代码
void segmentGreen(...);

表示:

它可以做绿色阈值分割。

所以 public 很像这个类对外暴露的一套:

API。

以后别人用你的类,只需要看这些函数。

private 是"这个对象内部保存什么"

例如:

复制代码
std::string imagePath_;

保存图像路径。

复制代码
cv::Mat image_;

保存原图。

复制代码
cv::Mat resizedImage_;

保存缩放结果。

复制代码
cv::Mat mask_;

保存阈值分割后的 mask。

这就是:

把原来散落在 main() 里的变量,变成对象自己的状态。

为什么成员变量后面加 _

例如:

复制代码
imagePath_
image_
mask_

最后这个 _ 不是 C++ 语法要求。

只是很常见的一种命名习惯:

表示"这是成员变量"。

比如构造函数里:

复制代码
imagePath_(imagePath)

你就能很容易区分:

复制代码
imagePath_
→ 成员变量

imagePath
→ 函数参数
构造函数

你的:

复制代码
explicit ImageProcessor(
    const std::string& imagePath
);

表示:

创建 ImageProcessor 对象时,需要给它一条图像路径。

比如:

复制代码
ImageProcessor processor(
    "../images/test.jpg"
);

这时候构造函数被自动调用。

实现可以写:

复制代码
ImageProcessor::ImageProcessor(
    const std::string& imagePath
)
    : imagePath_(imagePath)
{
}

这里:

复制代码
: imagePath_(imagePath)

就是你第二周学过的:

成员初始化列表。

为什么参数是 const std::string&
复制代码
const std::string& imagePath

你之前已经学过。

拆开:

复制代码
std::string
→ 字符串

&
→ 引用,不复制整份字符串

const
→ 函数不能通过这个参数修改原字符串

所以这特别适合:

只读取一个较大的对象参数。

同理后面的:

复制代码
const std::string& outputPath

也是同样思路。

explicit 是什么

这个是今天可以顺便认识的新东西。

复制代码
explicit ImageProcessor(
    const std::string& imagePath
);

explicit 可以先理解成:

不允许编译器偷偷把一个 string 自动转换成 ImageProcessor。

比如没有 explicit 时,某些单参数构造函数可能允许隐式转换。

你现在不用深究。

只记:

单参数构造函数经常建议写 explicit,避免意外隐式转换。

这是一个很好的现代 C++ 习惯。

load() 为什么返回 bool
复制代码
bool load();

因为读图可能成功,也可能失败。

实现:

复制代码
bool ImageProcessor::load() {
    image_ = cv::imread(imagePath_);

    if (image_.empty()) {
        std::cerr
            << "Failed to load image: "
            << imagePath_
            << '\n';

        return false;
    }

    return true;
}

于是 main.cpp:

复制代码
if (!processor.load()) {
    return -1;
}

逻辑非常自然:

复制代码
加载成功
→ 继续

加载失败
→ 结束程序
resize() 怎么设计

你现在写:

复制代码
void resize(double scale);

这个设计比固定:

复制代码
cv::Size(640, 480)

更灵活。

例如:

复制代码
processor.resize(0.25);

表示:

宽和高都缩小到原来的 25%。

实现可以:

复制代码
void ImageProcessor::resize(double scale) {
    cv::resize(
        image_,
        resizedImage_,
        cv::Size(),
        scale,
        scale
    );
}

这里:

复制代码
fx = scale
fy = scale

所以:

复制代码
原图 4000 × 3000

scale = 0.25

→ 1000 × 750
对象内部状态在逐步变化

调用:

复制代码
processor.load();

之后:

复制代码
image_

有数据。

调用:

复制代码
processor.resize(0.25);

之后:

复制代码
resizedImage_

有数据。

调用:

复制代码
processor.convertToHSV();

之后:

复制代码
hsvImage_

有数据。

所以整个对象内部其实在保存一条处理流水线:

复制代码
image_
↓
resizedImage_
↓
hsvImage_
↓
mask_
↓
cleanMask_
↓
validContours_
↓
resultImage_

这也是类重构之后很自然的一点。

convertToHSV()

声明:

复制代码
void convertToHSV();

实现:

复制代码
void ImageProcessor::convertToHSV() {
    cv::cvtColor(
        resizedImage_,
        hsvImage_,
        cv::COLOR_BGR2HSV
    );
}

这里你会发现:

不需要再传 resizedImage。

因为它已经是当前对象的成员变量。

这就是类的一个直接好处:

相关数据和相关操作绑在一起。

segmentGreen()

声明:

复制代码
void segmentGreen(
    const cv::Scalar& lower,
    const cv::Scalar& upper
);

实现:

复制代码
void ImageProcessor::segmentGreen(
    const cv::Scalar& lower,
    const cv::Scalar& upper
) {
    cv::inRange(
        hsvImage_,
        lower,
        upper,
        mask_
    );
}

注意:

复制代码
const cv::Scalar& lower

和:

复制代码
const cv::Scalar& upper

又是在复习:

不复制 + 不修改。

为什么 lower/upper 不存成成员变量

你现在这种设计:

复制代码
segmentGreen(lower, upper);

表示:

HSV 阈值由调用者每次传进来。

这很好。

因为这样:

复制代码
processor.segmentGreen(
    cv::Scalar(35, 40, 40),
    cv::Scalar(85, 255, 255)
);

以后也可以换:

复制代码
processor.segmentGreen(
    anotherLower,
    anotherUpper
);

不用改类内部代码。

morphology()

声明:

复制代码
void morphology(int kernelSize);

实现思路:

复制代码
void ImageProcessor::morphology(
    int kernelSize
) {
    cv::Mat kernel =
        cv::getStructuringElement(
            cv::MORPH_RECT,
            cv::Size(
                kernelSize,
                kernelSize
            )
        );

    cv::Mat opened;

    cv::morphologyEx(
        mask_,
        opened,
        cv::MORPH_OPEN,
        kernel
    );

    cv::morphologyEx(
        opened,
        cleanMask_,
        cv::MORPH_CLOSE,
        kernel
    );
}

流程:

复制代码
mask_
↓ OPEN
opened
↓ CLOSE
cleanMask_
为什么 opened 可以是局部变量

注意:

复制代码
cv::Mat opened;

没有放进:

复制代码
private:

成员变量里。

因为它只在:

复制代码
morphology()

这个函数执行过程中临时使用。

函数结束以后就不需要了。

所以:

复制代码
长期需要保存
→ 成员变量

只在一个函数里临时使用
→ 局部变量

这个工程习惯非常重要。

extractContours()

声明:

复制代码
void extractContours(
    double minArea
);

这一步建议不要直接把所有 contours 保存成成员变量。

可以这样:

复制代码
void ImageProcessor::extractContours(
    double minArea
) {
    std::vector<
        std::vector<cv::Point>
    > contours;

    cv::findContours(
        cleanMask_,
        contours,
        cv::RETR_EXTERNAL,
        cv::CHAIN_APPROX_SIMPLE
    );

    validContours_.clear();

    for (const auto& contour : contours) {
        double area =
            cv::contourArea(contour);

        if (area < minArea) {
            continue;
        }

        validContours_.push_back(
            contour
        );
    }
}

这里很值得你理解。

contours 和 validContours_ 的区别
复制代码
contours
→ findContours 找到的所有轮廓
→ 临时使用

validContours_
→ 经过面积过滤后的轮廓
→ 后面 drawResult() 还需要

所以:

复制代码
contours

是局部变量。

而:

复制代码
validContours_

是成员变量。

这就是在真正练习:

哪些数据应该属于对象状态,哪些只是临时中间量。

为什么先 validContours_.clear()
复制代码
validContours_.clear();

因为假设你以后又调用一次:

复制代码
processor.extractContours(200);

如果不清空,

之前:

复制代码
minArea = 100

留下来的轮廓还会存在。

然后新的轮廓又追加进去。

结果会重复。

所以每次重新提取前:

复制代码
validContours_.clear();

很合理。

drawResult()

声明:

复制代码
void drawResult();

实现可以:

复制代码
void ImageProcessor::drawResult() {
    resultImage_ =
        resizedImage_.clone();

    for (
        const auto& contour :
        validContours_
    ) {
        cv::Rect box =
            cv::boundingRect(contour);

        cv::rectangle(
            resultImage_,
            box,
            cv::Scalar(0, 0, 255),
            2
        );
    }
}

这里:

复制代码
resultImage_ =
    resizedImage_.clone();

表示:

每次绘制都从一张干净的 resized 图开始。

这是比较稳妥的设计。

怎么统计植株区域数量

其实你现在已经有:

复制代码
validContours_

那么有效区域数量天然就是:

复制代码
validContours_.size()

所以不一定还需要额外:

复制代码
int plantCount_;

例如在:

复制代码
drawResult()

里:

复制代码
std::cout
    << "Detected plant regions: "
    << validContours_.size()
    << '\n';

就可以。

这也是一个很好的设计意识:

如果一个信息能够从已有数据直接算出来,就不一定要额外保存一份状态。

saveResult() 为什么是 const

声明:

复制代码
bool saveResult(
    const std::string& outputPath
) const;

注意最后:

复制代码
const

不是修饰 outputPath。

前面的:

复制代码
const std::string&

才是修饰参数。

最后这个:

复制代码
) const

是在说:

这个成员函数承诺不修改当前 ImageProcessor 对象的状态。

两个 const 一定分开理解
复制代码
bool saveResult(
    const std::string& outputPath
) const;

第一个:

复制代码
const std::string& outputPath

表示:

不能通过 outputPath 修改外面的字符串。

第二个:

复制代码
) const

表示:

saveResult() 不应该修改 image_、mask_、validContours_ 等成员状态。

这是两个完全不同的 const。

saveResult() 实现

可以:

复制代码
bool ImageProcessor::saveResult(
    const std::string& outputPath
) const {
    if (resultImage_.empty()) {
        std::cerr
            << "Result image is empty\n";

        return false;
    }

    return cv::imwrite(
        outputPath,
        resultImage_
    );
}

逻辑:

复制代码
结果图为空
→ 保存失败

有结果图
→ imwrite
→ 返回成功/失败
为什么 imwrite 不算修改对象

它虽然会:

向磁盘写一个文件

但它没有修改:

复制代码
ImageProcessor 对象内部成员状态

所以:

复制代码
saveResult(...) const

是合理的。

这里顺便建立一个概念:

const 成员函数限制的是对象本身的逻辑状态,不是说函数完全不能产生任何外部行为。

ImageProcessor.h 可以这样组织

头文件大概是:

复制代码
#pragma once

#include <opencv2/opencv.hpp>
#include <string>
#include <vector>

class ImageProcessor {
public:
    explicit ImageProcessor(
        const std::string& imagePath
    );

    bool load();

    void resize(double scale);

    void convertToHSV();

    void segmentGreen(
        const cv::Scalar& lower,
        const cv::Scalar& upper
    );

    void morphology(
        int kernelSize
    );

    void extractContours(
        double minArea
    );

    void drawResult();

    bool saveResult(
        const std::string& outputPath
    ) const;

private:
    std::string imagePath_;

    cv::Mat image_;
    cv::Mat resizedImage_;
    cv::Mat hsvImage_;
    cv::Mat mask_;
    cv::Mat cleanMask_;
    cv::Mat resultImage_;

    std::vector<
        std::vector<cv::Point>
    > validContours_;
};
.h 里为什么要 include 这些东西

因为你的类声明直接用了:

复制代码
std::string

所以需要:

复制代码
#include <string>

直接用了:

复制代码
std::vector

所以需要:

复制代码
#include <vector>

直接用了:

复制代码
cv::Mat
cv::Scalar
cv::Point

所以需要 OpenCV 相关头文件。

你可以先继续:

复制代码
#include <opencv2/opencv.hpp>

最简单。

ImageProcessor.cpp 第一行

通常:

复制代码
#include "ImageProcessor.h"

然后:

复制代码
#include <iostream>

后面实现:

复制代码
ImageProcessor::ImageProcessor(...)

bool ImageProcessor::load()

void ImageProcessor::resize(...)

等等。

所以:

复制代码
.h
→ 告诉别人类有什么

.cpp
→ 真正实现这些成员函数

你第三周学的"声明/实现分离"现在完全用上了。

main.cpp 应该明显变短

现在:

复制代码
#include "ImageProcessor.h"

int main() {
    ImageProcessor processor(
        "../images/test.jpg"
    );

    if (!processor.load()) {
        return -1;
    }

    processor.resize(0.25);

    processor.convertToHSV();

    processor.segmentGreen(
        cv::Scalar(35, 40, 40),
        cv::Scalar(85, 255, 255)
    );

    processor.morphology(5);

    processor.extractContours(100);

    processor.drawResult();

    if (!processor.saveResult(
        "../output/result.jpg"
    )) {
        return -1;
    }

    return 0;
}

现在看 main(),你几乎不用看内部算法,就能读懂:

复制代码
加载
↓
缩放
↓
HSV
↓
绿色分割
↓
形态学
↓
轮廓
↓
画框
↓
保存

这就是重构的价值。

好的 main() 应该像"流程说明书"

你现在这个:

复制代码
processor.load();
processor.resize(...);
processor.convertToHSV();
processor.segmentGreen(...);
processor.morphology(...);
processor.extractContours(...);
processor.drawResult();
processor.saveResult(...);

实际上非常接近:

一份可执行的流程图。

这比原来 main.cpp 里混着几十行 OpenCV API 更容易维护。

CMake 也要更新

目录:

复制代码
include/ImageProcessor.h
src/ImageProcessor.cpp
src/main.cpp

那么:

复制代码
cmake_minimum_required(VERSION 3.16)

project(CropImageProcessor)

set(CMAKE_CXX_STANDARD 17)

find_package(OpenCV REQUIRED)

add_executable(
    app
    src/main.cpp
    src/ImageProcessor.cpp
)

target_include_directories(
    app
    PRIVATE
    include
    ${OpenCV_INCLUDE_DIRS}
)

target_link_libraries(
    app
    PRIVATE
    ${OpenCV_LIBS}
)

这里又把第三周内容完整串起来:

复制代码
add_executable
→ 哪些 cpp 参与构建

target_include_directories
→ ImageProcessor.h 去哪里找

target_link_libraries
→ OpenCV 库怎么链接
今天你还要理解"顺序依赖"

你的类现在实际上要求:

复制代码
load
↓
resize
↓
convertToHSV
↓
segmentGreen
↓
morphology
↓
extractContours
↓
drawResult
↓
saveResult

如果直接:

复制代码
processor.convertToHSV();

但还没:

复制代码
processor.resize(...)

那:

复制代码
resizedImage_

可能为空。

所以第一版可以要求调用者:

按正确顺序调用。

以后更工程化时,可以增加:

复制代码
empty() 检查
异常
状态验证

但今天不用过度设计。

可以适当加入防御性检查

例如:

复制代码
void ImageProcessor::convertToHSV() {
    if (resizedImage_.empty()) {
        std::cerr
            << "Resized image is empty\n";
        return;
    }

    cv::cvtColor(
        resizedImage_,
        hsvImage_,
        cv::COLOR_BGR2HSV
    );
}

这样调用顺序错了,不至于完全摸不着头脑。

不过今天不要每个函数写得特别复杂。

重点还是:

学会类重构。

为什么不用一个 process() 全做完

你完全可以写:

复制代码
processor.process();

然后类内部一次性:

复制代码
resize
HSV
segment
morphology
contours
draw

但对学习阶段不建议。

因为拆成:

复制代码
resize()
convertToHSV()
segmentGreen()
...

有几个好处:

复制代码
每一步职责清楚
方便单独调试
方便换参数
方便保存中间结果
方便以后扩展

这就是所谓:

单一职责思维。

以后换分割策略就很容易

现在:

复制代码
segmentGreen(...)

以后可能加:

复制代码
segmentByExG(...)

或者:

复制代码
segmentByModel(...)

其他流程:

复制代码
morphology
extractContours
drawResult

可以继续复用。

这就是类和模块化开始产生价值的地方。

这个类目前还有一个小设计点

你写:

复制代码
void extractContours(double minArea);

这个函数同时做了:

复制代码
轮廓提取
+
面积过滤

第一版完全没问题。

如果以后项目复杂,可以拆成:

复制代码
extractContours();
filterContours(minArea);

但现在没必要。

不要为了"看起来专业"把类切得过碎。

"数据归谁"

重构前:

复制代码
main.cpp

image
resized
hsv
mask
cleanMask
result
contours

全部是散落变量。

重构后:

复制代码
ImageProcessor 对象

image_
resizedImage_
hsvImage_
mask_
cleanMask_
resultImage_
validContours_

这些数据都有了明确"主人":

ImageProcessor 对象。

这其实就是面向对象里很重要的一件事:

把数据和操作这些数据的方法放在一起。

和 RAII 思维有联系

你的:

复制代码
cv::Mat
std::vector
std::string

本身都属于会自动管理资源的对象。

当:

复制代码
ImageProcessor processor(...)

离开作用域时,

它的成员:

复制代码
string
Mat
vector

也会依次自动析构。

所以你完全不需要:

复制代码
delete image_;
delete mask_;

这又和第二周学的:

RAII

连起来了。

特别留意一种错误

比如 .h:

复制代码
void resize(double scale);

但是 .cpp 写成:

复制代码
void ImageProcessor::resize(int scale)

这不是同一个函数。

可能最终报:

复制代码
undefined reference

或者出现其他声明/定义不匹配问题。

所以一定注意:

复制代码
返回类型
函数名
参数类型
const

声明和实现都要对应。

尤其:

复制代码
saveResult(...) const

实现时也必须:

复制代码
bool ImageProcessor::saveResult(
    const std::string& outputPath
) const

最后的 const 不能漏。

相关推荐
zhippt_10141 小时前
教师做课件太耗时?AI生成PPT工具与图转PPT实测梳理
人工智能·html·powerpoint
环境栈笔记1 小时前
指纹浏览器安卓怎么用?从运行位置到功能验收,先把三种方案分清
前端·人工智能·后端·自动化
赛博山人1 小时前
DeepSeek发布Super-agent
人工智能
凉茶钱1 小时前
多态:从虚函数语法到虚表寻址,一次讲透动态绑定
开发语言·c++·多态
Navigator_Z1 小时前
LeetCode //C - 1311. Get Watched Videos by Your Friends
c语言·算法·leetcode
鬼手点金1 小时前
提示注入:上下文安全的核心威胁
人工智能·安全·microsoft·机器学习·缓存·prompt
山川绿水1 小时前
DeepSeek-DSH安装报错[deploy] 命令失败:npx -y pnpm install --prefer-offline
人工智能·网络安全
ZhengEnCi1 小时前
脉冲神经网络是什么:从LIF神经元到第三代神经网络的能耗优势
人工智能·神经网络·机器学习
小虎AI生活1 小时前
腾讯 Buddy One 上手:跨应用 AI 协同的配置步骤与安全红线(附派活验收清单)
人工智能·超级个体·一人公司·青玥ai