Skip to content

第25章 案例2:图像处理模块

图像处理是计算机视觉和机器学习应用的核心。本章通过一个完整的图像处理模块封装案例,展示如何将 C++ 图像处理库高效暴露给 Python,特别关注内存管理和零拷贝策略。

  1. 避免内存拷贝 - 每个拷贝都是性能损失
  2. 格式自动转换 - 透明的 BGR↔RGB 转换
  3. 原地操作 - 减少内存分配开销
  4. 批处理支持 - 利用 SIMD 和多核
┌─────────────────────────────────────────────────────────────┐
│ 图像格式对比 │
├───────────────┬────────┬────────┬──────────────────────────┤
│ 格式 │ 通道数 │ 字节/px │ 用途 │
├───────────────┼────────┼────────┼──────────────────────────┤
│ 灰度 (GRAY) │ 1 │ 1 │ 文档扫描, 工业检测 │
│ RGB │ 3 │ 3 │ 屏幕显示, 照片 │
│ RGBA │ 4 │ 4 │ 图形设计, Web │
│ BGR │ 3 │ 3 │ OpenCV默认格式 │
│ HSV │ 3 │ 3 │ 色彩分割, 跟踪 │
│ YUV │ 3 │ 3/2 │ 视频压缩, 电视 │
└───────────────┴────────┴────────┴──────────────────────────┘
image_formats.h
#pragma once
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
#include <opencv2/opencv.hpp>
namespace py = pybind11;
enum class ImageFormat {
GRAY,
RGB,
RGBA,
BGR
};
class Image {
public:
Image() : mat_() {}
// 从 NumPy 数组构造(零拷贝)
static Image from_numpy(const py::array_t<uint8_t>& arr) {
auto buf = arr.request();
cv::Mat mat;
switch (buf.ndim) {
case 2: // 灰度图 [H, W]
mat = cv::Mat(buf.shape[0], buf.shape[1],
CV_8UC1, buf.ptr);
break;
case 3: // 彩色图 [H, W, C]
if (buf.shape[2] == 1) {
mat = cv::Mat(buf.shape[0], buf.shape[1],
CV_8UC1, buf.ptr);
} else if (buf.shape[2] == 3) {
// RGB/BGR 转换
cv::Mat temp(buf.shape[0], buf.shape[1],
CV_8UC3, buf.ptr);
// 默认认为是 RGB,转换为 BGR
cv::cvtColor(temp, mat, cv::COLOR_RGB2BGR);
} else if (buf.shape[2] == 4) {
cv::Mat temp(buf.shape[0], buf.shape[1],
CV_8UC4, buf.ptr);
cv::cvtColor(temp, mat, cv::COLOR_RGBA2BGRA);
}
break;
default:
throw py::value_error("Unsupported image dimensions");
}
return Image(mat);
}
// 转换为 NumPy 数组(零拷贝视图)
py::array_t<uint8_t> to_numpy() const {
// OpenCV BGR -> NumPy RGB
cv::Mat rgb;
cv::cvtColor(mat_, rgb, cv::COLOR_BGR2RGB);
py::array_t<uint8_t> arr(
{rgb.rows, rgb.cols, rgb.channels()},
{static_cast<size_t>(rgb.step1()), rgb.cols * rgb.channels(), 1},
rgb.data
);
return arr;
}
int height() const { return mat_.rows; }
int width() const { return mat_.cols; }
int channels() const { return mat_.channels(); }
cv::Mat& mat() { return mat_; }
private:
explicit Image(cv::Mat mat) : mat_(std::move(mat)) {}
cv::Mat mat_;
};
format_utils.cpp
#include <pybind11/pybind11.h>
#include <opencv2/opencv.hpp>
namespace py = pybind11;
cv::Mat convert_to_rgb(const cv::Mat& img) {
cv::Mat rgb;
switch (img.channels()) {
case 1:
cv::cvtColor(img, rgb, cv::COLOR_GRAY2RGB);
break;
case 3:
cv::cvtColor(img, rgb, cv::COLOR_BGR2RGB);
break;
case 4:
cv::cvtColor(img, rgb, cv::COLOR_BGRA2RGBA);
break;
default:
throw py::value_error("Invalid channel count");
}
return rgb;
}
cv::Mat convert_to_grayscale(const cv::Mat& img) {
cv::Mat gray;
switch (img.channels()) {
case 1:
gray = img;
break;
case 3:
cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
break;
case 4:
cv::cvtColor(img, gray, cv::COLOR_BGRA2GRAY);
break;
default:
throw py::value_error("Invalid channel count");
}
return gray;
}
filters.cpp
#include <pybind11/pybind11.h>
#include <opencv2/opencv.hpp>
namespace py = pybind11;
// 高斯模糊 - 边缘平滑
py::array_t<uint8_t> gaussian_blur(const py::array_t<uint8_t>& input,
int kernel_size, double sigma) {
auto buf = input.request();
cv::Mat img(buf.shape[0], buf.shape[1],
CV_8UC(buf.shape[2]), buf.ptr);
cv::Mat blurred;
cv::GaussianBlur(img, blurred,
cv::Size(kernel_size, kernel_size),
sigma, sigma);
return py::array_t<uint8_t>(
{blurred.rows, blurred.cols, blurred.channels()},
{blurred.step1(), blurred.cols * blurred.channels(), 1},
blurred.data
);
}
// 锐化 - 增强细节
py::array_t<uint8_t> sharpen(const py::array_t<uint8_t>& input) {
auto buf = input.request();
cv::Mat img(buf.shape[0], buf.shape[1],
CV_8UC(buf.shape[2]), buf.ptr);
// 锐化核
cv::Mat kernel = (cv::Mat_<float>(3, 3) <<
0, -1, 0,
-1, 5, -1,
0, -1, 0);
cv::Mat sharpened;
cv::filter2D(img, sharpened, -1, kernel);
return py::array_t<uint8_t>(
{sharpened.rows, sharpened.cols, sharpened.channels()},
{sharpened.step1(), sharpened.cols * sharpened.channels(), 1},
sharpened.data
);
}
// 边缘检测 - Sobel
py::array_t<uint8_t> sobel_edge(const py::array_t<uint8_t>& input) {
auto buf = input.request();
// 转换为灰度
cv::Mat gray;
if (buf.ndim == 3 && buf.shape[2] == 3) {
cv::Mat img(buf.shape[0], buf.shape[1], CV_8UC3, buf.ptr);
cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
} else {
gray = cv::Mat(buf.shape[0], buf.shape[1], CV_8UC1, buf.ptr);
}
// Sobel 导数
cv::Mat grad_x, grad_y;
cv::Sobel(gray, grad_x, CV_16S, 1, 0, 3);
cv::Sobel(gray, grad_y, CV_16S, 0, 1, 3);
// 合并梯度
cv::Mat abs_grad_x, abs_grad_y, edge;
cv::convertScaleAbs(grad_x, abs_grad_x);
cv::convertScaleAbs(grad_y, abs_grad_y);
cv::addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, edge);
return py::array_t<uint8_t>(
{edge.rows, edge.cols}, {edge.step1(), 1}, edge.data
);
}
morphology.cpp
#include <opencv2/opencv.hpp>
cv::Mat morph_operation(const cv::Mat& img,
const std::string& op,
const cv::Mat& kernel) {
cv::Mat result;
if (op == "erode") {
cv::erode(img, result, kernel);
} else if (op == "dilate") {
cv::dilate(img, result, kernel);
} else if (op == "open") {
cv::morphologyEx(img, result, cv::MORPH_OPEN, kernel);
} else if (op == "close") {
cv::morphologyEx(img, result, cv::MORPH_CLOSE, kernel);
} else if (op == "gradient") {
cv::morphologyEx(img, result, cv::MORPH_GRADIENT, kernel);
} else {
throw std::runtime_error("Unknown operation: " + op);
}
return result;
}
// 创建标准结构元素
cv::Mat create_kernel(const std::string& shape, int size) {
if (shape == "rect") {
return cv::getStructuringElement(cv::MORPH_RECT,
cv::Size(size, size));
} else if (shape == "ellipse") {
return cv::getStructuringElement(cv::MORPH_ELLIPSE,
cv::Size(size, size));
} else if (shape == "cross") {
return cv::getStructuringElement(cv::MORPH_CROSS,
cv::Size(size, size));
}
throw std::runtime_error("Unknown shape: " + shape);
}
batch_processing.cpp
#include <pybind11/pybind11.h>
#include <vector>
#include "image_formats.h"
namespace py = pybind11;
// 批量高斯模糊
py::list batch_gaussian_blur(const py::list& images,
int kernel_size,
double sigma) {
py::list results;
for (py::handle item : images) {
py::array_t<uint8_t> arr = py::cast<py::array_t<uint8_t>>(item);
results.append(gaussian_blur(arr, kernel_size, sigma));
}
return results;
}
// 批量转换格式
py::list batch_convert_color(const py::list& images,
const std::string& target_format) {
py::list results;
for (py::handle item : images) {
py::array_t<uint8_t> arr = py::cast<py::array_t<uint8_t>>(item);
Image img = Image::from_numpy(arr);
cv::Mat converted;
if (target_format == "rgb") {
converted = convert_to_rgb(img.mat());
} else if (target_format == "gray") {
converted = convert_to_grayscale(img.mat());
}
results.append(Image(converted).to_numpy());
}
return results;
}
// 并行批处理 - 使用 TBB 加速
py::list batch_parallel_process(
const py::list& images,
const std::function<py::array_t<uint8_t>(const py::array_t<uint8_t>&)>& func
) {
std::vector<py::array_t<uint8_t>> inputs;
for (py::handle item : images) {
inputs.push_back(py::cast<py::array_t<uint8_t>>(item));
}
std::vector<py::array_t<uint8_t>> outputs(inputs.size());
// 并行处理
#pragma omp parallel for
for (size_t i = 0; i < inputs.size(); ++i) {
outputs[i] = func(inputs[i]);
}
py::list results;
for (auto& out : outputs) {
results.append(out);
}
return results;
}
import numpy as np
from image_wrapper import (
gaussian_blur, sharpen, sobel_edge,
batch_gaussian_blur, morph_operation
)
def process_pipeline(images, kernel_size=5):
"""完整的图像处理流水线"""
results = []
for img in images:
# 模糊去噪
blurred = gaussian_blur(img, kernel_size, 1.5)
# 边缘检测
edges = sobel_edge(blurred)
# 锐化增强
sharpened = sharpen(edges)
results.append(sharpened)
return results
if __name__ == "__main__":
# 加载图像
img = np.random.randint(0, 256, (480, 640, 3), dtype=np.uint8)
# 单图处理
result = gaussian_blur(img, 5, 1.0)
print(f"Original: {img.shape}, Result: {result.shape}")
# 批处理
images = [img, img.copy(), img.copy()]
results = batch_gaussian_blur(images, 3, 1.0)
print(f"Batch processed {len(results)} images")
opencv_numpy.cpp
#include <pybind11/pybind11.h>
#include <opencv2/opencv.hpp>
namespace py = pybind11;
// 核心转换函数 - 零拷贝
py::array_t<uint8_t> mat_to_numpy(const cv::Mat& mat) {
// 确保是连续内存
cv::Mat cont = mat.isContinuous() ? mat : mat.clone();
return py::array_t<uint8_t>(
{cont.rows, cont.cols, cont.channels()},
{cont.step.p[0], cont.step.p[1], cont.elemSize()},
cont.data
);
}
cv::Mat numpy_to_mat(const py::array_t<uint8_t>& arr) {
auto buf = arr.request();
int type;
switch (buf.ndim) {
case 2:
type = CV_8UC1;
break;
case 3:
type = CV_8UC(buf.shape[2]);
break;
default:
throw py::value_error("Unsupported array dimensions");
}
return cv::Mat(buf.shape[0], buf.shape[1], type, buf.ptr);
}
advanced_ops.cpp
#include <pybind11/pybind11.h>
#include <opencv2/opencv.hpp>
namespace py = pybind11;
// 直方图均衡化 - 增强对比度
py::array_t<uint8_t> histogram_equalization(const py::array_t<uint8_t>& input) {
auto buf = input.request();
cv::Mat gray;
if (buf.ndim == 3 && buf.shape[2] == 3) {
cv::Mat img(buf.shape[0], buf.shape[1], CV_8UC3, buf.ptr);
cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
} else {
gray = cv::Mat(buf.shape[0], buf.shape[1], CV_8UC1, buf.ptr);
}
cv::Mat equalized;
cv::equalizeHist(gray, equalized);
return py::array_t<uint8_t>(
{equalized.rows, equalized.cols}, {equalized.step1()}, equalized.data
);
}
// 阈值分割 - Otsu 方法
py::tuple otsu_threshold(const py::array_t<uint8_t>& input) {
auto buf = input.request();
cv::Mat gray;
if (buf.ndim == 3 && buf.shape[2] == 3) {
cv::Mat img(buf.shape[0], buf.shape[1], CV_8UC3, buf.ptr);
cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
} else {
gray = cv::Mat(buf.shape[0], buf.shape[1], CV_8UC1, buf.ptr);
}
double thresh_value = cv::threshold(gray, gray, 0, 255,
cv::THRESH_BINARY | cv::THRESH_OTSU);
return py::make_tuple(
py::array_t<uint8_t>({gray.rows, gray.cols}, {gray.step1()}, gray.data),
py::float_(thresh_value)
);
}
// 轮廓检测
py::list find_contours(const py::array_t<uint8_t>& input) {
auto buf = input.request();
cv::Mat img(buf.shape[0], buf.shape[1], CV_8UC1, buf.ptr);
std::vector<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> hierarchy;
cv::findContours(img, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
py::list results;
for (const auto& contour : contours) {
py::list points;
for (const auto& pt : contour) {
points.append(py::make_tuple(pt.x, pt.y));
}
results.append(points);
}
return results;
}
memory_view.cpp
#include <pybind11/pybind11.h>
#include <opencv2/opencv.hpp>
namespace py = pybind11;
// 内存视图类 - 避免不必要的拷贝
class MemoryView {
public:
MemoryView() : data_(nullptr), size_(0), owns_data_(false) {}
MemoryView(uint8_t* data, size_t size, bool owns = false)
: data_(data), size_(size), owns_data_(owns) {}
~MemoryView() {
if (owns_data_ && data_) {
delete[] data_;
}
}
// 移动语义
MemoryView(MemoryView&& other) noexcept
: data_(other.data_), size_(other.size_), owns_data_(other.owns_data_) {
other.data_ = nullptr;
other.size_ = 0;
other.owns_data_ = false;
}
MemoryView& operator=(MemoryView&& other) noexcept {
if (this != &other) {
if (owns_data_ && data_) delete[] data_;
data_ = other.data_;
size_ = other.size_;
owns_data_ = other.owns_data_;
other.data_ = nullptr;
other.size_ = 0;
other.owns_data_ = false;
}
return *this;
}
uint8_t* data() { return data_; }
size_t size() const { return size_; }
private:
uint8_t* data_;
size_t size_;
bool owns_data_;
};
// 原地操作 - 减少内存分配
py::array_t<uint8_t> inplace_gaussian_blur(
py::array_t<uint8_t>& input,
int kernel_size,
double sigma) {
auto buf = input.request();
// 直接操作输入数据
cv::Mat img(buf.shape[0], buf.shape[1],
CV_8UC(buf.shape[2]), buf.ptr);
cv::GaussianBlur(img, img, cv::Size(kernel_size, kernel_size), sigma, sigma);
// 返回同一数组(原地操作)
return input;
}
memory_pool.cpp
#include <pybind11/pybind11.h>
#include <opencv2/opencv.hpp>
#include <memory>
#include <vector>
namespace py = pybind11;
// 预分配内存池 - 避免运行时分配
class MemoryPool {
public:
MemoryPool(size_t max_size) : max_size_(max_size) {
pool_.reserve(max_size);
for (size_t i = 0; i < max_size; ++i) {
pool_.push_back(new uint8_t[4096 * 4096 * 4]); // 64MB 块
}
}
~MemoryPool() {
for (auto* p : pool_) {
delete[] p;
}
}
uint8_t* acquire() {
if (pool_.empty()) {
return new uint8_t[4096 * 4096 * 4];
}
uint8_t* ptr = pool_.back();
pool_.pop_back();
return ptr;
}
void release(uint8_t* ptr) {
if (pool_.size() < max_size_) {
pool_.push_back(ptr);
} else {
delete[] ptr;
}
}
private:
std::vector<uint8_t*> pool_;
size_t max_size_;
};
// 批处理内存管理
py::list batch_with_memory_pool(
const py::list& images,
const std::function<py::array_t<uint8_t>(uint8_t*, size_t)>& process_func,
MemoryPool& pool) {
py::list results;
for (py::handle item : images) {
py::array_t<uint8_t> arr = py::cast<py::array_t<uint8_t>>(item);
auto buf = arr.request();
uint8_t* temp = pool.acquire();
size_t temp_size = buf.size;
// 复制数据到临时内存
std::memcpy(temp, buf.ptr, temp_size);
// 处理
py::array_t<uint8_t> result = process_func(temp, temp_size);
// 释放临时内存
pool.release(temp);
results.append(result);
}
return results;
}
import numpy as np
import pytest
from image_wrapper import (
gaussian_blur, sharpen, sobel_edge,
histogram_equalization, otsu_threshold
)
def test_gaussian_blur():
img = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8)
blurred = gaussian_blur(img, 5, 1.5)
assert blurred.shape == img.shape
# 模糊后变化应该平滑
assert np.std(blurred) <= np.std(img)
def test_sharpen():
img = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8)
sharpened = sharpen(img)
assert sharpened.shape == img.shape
def test_memory_efficiency():
"""验证避免内存拷贝"""
img = np.random.randint(0, 256, (1000, 1000, 3), dtype=np.uint8)
original_ptr = img.ctypes.data
# OpenCV 操作可能修改指针
processed = gaussian_blur(img, 3, 1.0)
# 验证数据连续性
assert processed.flags['C_CONTIGUOUS']
def test_batch_processing():
images = [np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8)
for _ in range(10)]
results = batch_gaussian_blur(images, 5, 1.5)
assert len(results) == len(images)
for r in results:
assert r.shape == images[0].shape
def test_format_conversion():
"""验证格式转换"""
# RGB -> 灰度
rgb_img = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8)
# 通过 OpenCV 处理会自动转换
from image_wrapper import Image
img = Image.from_numpy(rgb_img)
gray = img.to_grayscale()
assert gray.shape == (100, 100)

关键洞察:图像处理 pipeline 必须最小化内存拷贝。使用内存视图和原地操作可以显著降低内存带宽消耗。OpenCV 的 cv::Mat 可以通过正确的 stride 设置与 NumPy 数组共享内存。批处理时,利用 OpenMP 或 TBB 并行化可以充分利用多核,同时注意内存局部性。