OpenCV-Python——从读图到轮廓检测的实战路线




2021-09-02

blog_main_img

OpenCV-Python 的入口很轻:`import cv2`,读一张图,转个颜色,做个滤波,找个边缘,画个框。它不像深度学习框架那样一上来就要训练模型,而是更像一把图像处理工具刀,适合快速把图片和视频帧加工成你想要的形态。

这篇围绕几个高频动作来写:图片读写、颜色空间、裁剪缩放、阈值、滤波、边缘、轮廓、绘制标注、视频帧处理,再补几个能直接拼起来的小脚本。

安装先别装乱

常规桌面环境:

pip install opencv-python

如果你需要 contrib 模块:

pip install opencv-contrib-python

如果在服务器、Docker、无界面环境里跑,通常选 headless 包更干净:

pip install opencv-python-headless

注意:这些包都提供 cv2 命名空间,别在同一个环境里混装多个 OpenCV wheel。装乱了很容易出现导入冲突。

验证一下:

import cv2

print(cv2.__version__)

第一条流水线:读图、处理、保存

OpenCV 的图像处理基本都是围绕 NumPy 数组展开。图片读进来之后,本质上就是一个多维数组。

OpenCV 图像处理流水线

from pathlib import Path

import cv2


input_path = Path("input.jpg")
output_path = Path("output_gray.jpg")

image = cv2.imread(str(input_path))

if image is None:
    raise FileNotFoundError(f"image not found: {input_path}")

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv2.imwrite(str(output_path), gray)

这里有个必须养成的习惯:cv2.imread() 失败时不会抛异常,而是返回 None。所以读图后先判断,别直接往下处理。

BGR 和 RGB:颜色别一上来就翻车

OpenCV 默认读出来是 BGR,不是 RGB。

很多显示库和模型输入习惯 RGB。如果你用 Matplotlib 展示 OpenCV 读出来的图,却发现颜色怪怪的,大概率就是通道顺序没转。

OpenCV 颜色空间和阈值

import cv2
import matplotlib.pyplot as plt


image_bgr = cv2.imread("input.jpg")
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)

plt.imshow(image_rgb)
plt.axis("off")
plt.show()

常用颜色转换:

gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
hsv = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2HSV)

灰度适合阈值、边缘、轮廓。HSV 适合按颜色范围做筛选,比如找红色、绿色、蓝色物体。

裁剪、缩放、翻转

OpenCV 图片就是数组,所以裁剪非常直接。

image = cv2.imread("input.jpg")

# y1:y2, x1:x2
crop = image[80:320, 120:460]
cv2.imwrite("crop.jpg", crop)

缩放:

resized = cv2.resize(image, (640, 360), interpolation=cv2.INTER_AREA)

按比例缩放:

h, w = image.shape[:2]
scale = 0.5
resized = cv2.resize(
    image,
    (int(w * scale), int(h * scale)),
    interpolation=cv2.INTER_AREA,
)

翻转:

flip_horizontal = cv2.flip(image, 1)
flip_vertical = cv2.flip(image, 0)
flip_both = cv2.flip(image, -1)

几何操作看起来简单,但很多项目的问题都出在坐标顺序上。记住:数组切片是 y, x,而很多绘图函数用的是 (x, y)

旋转和仿射变换

绕中心旋转:

import cv2


image = cv2.imread("input.jpg")
h, w = image.shape[:2]
center = (w // 2, h // 2)

matrix = cv2.getRotationMatrix2D(center, 15, 1.0)
rotated = cv2.warpAffine(image, matrix, (w, h))

cv2.imwrite("rotated.jpg", rotated)

如果你想做 OCR 前处理、证件照矫正、票据方向修正,仿射和透视变换会非常常用。

一个透视变换骨架:

import numpy as np


src = np.float32([
    [120, 80],
    [520, 100],
    [560, 420],
    [100, 400],
])

dst = np.float32([
    [0, 0],
    [500, 0],
    [500, 320],
    [0, 320],
])

matrix = cv2.getPerspectiveTransform(src, dst)
warped = cv2.warpPerspective(image, matrix, (500, 320))

滤波:先把噪声压一压

边缘、轮廓、阈值都怕噪声。滤波不是万能,但经常是必要前置步骤。

均值滤波:

blur = cv2.blur(image, (5, 5))

高斯滤波:

blur = cv2.GaussianBlur(image, (5, 5), 0)

中值滤波:

median = cv2.medianBlur(image, 5)

一般经验:

  • 普通平滑可以用高斯滤波
  • 椒盐噪声可以试中值滤波
  • 核越大,画面越糊,细节越容易没掉

不要为了“干净”把图片糊成一片。图像处理经常是在噪声和细节之间取平衡。

阈值:把灰度图切成前景和背景

固定阈值:

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

_, binary = cv2.threshold(
    gray,
    127,
    255,
    cv2.THRESH_BINARY,
)

自适应阈值:

binary = cv2.adaptiveThreshold(
    gray,
    255,
    cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
    cv2.THRESH_BINARY,
    31,
    5,
)

Otsu 阈值:

_, binary = cv2.threshold(
    gray,
    0,
    255,
    cv2.THRESH_BINARY + cv2.THRESH_OTSU,
)

固定阈值适合光照稳定的图片。光照不均时,自适应阈值通常更顺手。

形态学:让 mask 更像 mask

阈值得到的二值图经常有小洞、小碎点。形态学操作可以修一修。

import numpy as np


kernel = np.ones((5, 5), dtype=np.uint8)

opened = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)

开运算适合去掉小白点。

闭运算适合填掉小黑洞。

膨胀和腐蚀:

dilated = cv2.dilate(binary, kernel, iterations=1)
eroded = cv2.erode(binary, kernel, iterations=1)

做轮廓前,形态学处理经常能让结果稳定不少。

Canny 边缘检测

Canny 是很常见的边缘检测方法。

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
edges = cv2.Canny(blur, 80, 160)

cv2.imwrite("edges.jpg", edges)

阈值怎么选?没有一个永远正确的值。可以先从较宽松的范围试起,再根据边缘断裂或噪点情况微调。

找轮廓:从边缘变成目标框

轮廓检测常见流程:

灰度 -> 滤波 -> 阈值或边缘 -> findContours -> 筛选 -> 绘制

OpenCV 视频帧和轮廓检测

import cv2


image = cv2.imread("input.jpg")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)

_, binary = cv2.threshold(
    blur,
    0,
    255,
    cv2.THRESH_BINARY + cv2.THRESH_OTSU,
)

contours, hierarchy = cv2.findContours(
    binary,
    cv2.RETR_EXTERNAL,
    cv2.CHAIN_APPROX_SIMPLE,
)

output = image.copy()

for contour in contours:
    area = cv2.contourArea(contour)
    if area < 300:
        continue

    x, y, w, h = cv2.boundingRect(contour)
    cv2.rectangle(output, (x, y), (x + w, y + h), (0, 255, 0), 2)
    cv2.putText(
        output,
        f"{int(area)}",
        (x, max(20, y - 8)),
        cv2.FONT_HERSHEY_SIMPLEX,
        0.6,
        (0, 255, 0),
        2,
    )

cv2.imwrite("contours.jpg", output)

cv2.RETR_EXTERNAL 只取最外层轮廓。很多目标检测前处理场景,这样就够了。

cv2.CHAIN_APPROX_SIMPLE 会压缩轮廓点,减少冗余。

按颜色抠目标:HSV 比 RGB 好调

如果目标颜色明确,比如找蓝色工牌、绿色按钮、红色标记,可以用 HSV 范围。

import numpy as np


image = cv2.imread("input.jpg")
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)

lower = np.array([35, 60, 60])
upper = np.array([85, 255, 255])

mask = cv2.inRange(hsv, lower, upper)

kernel = np.ones((5, 5), dtype=np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)

result = cv2.bitwise_and(image, image, mask=mask)
cv2.imwrite("green_mask.jpg", result)

HSV 的好处是把色相、饱和度、亮度拆开了,比直接在 BGR 里调范围更直观。

绘制标注:线、框、文字

OpenCV 画图函数很适合做结果可视化。

canvas = image.copy()

cv2.line(canvas, (30, 30), (300, 30), (255, 0, 0), 3)
cv2.rectangle(canvas, (60, 80), (260, 220), (0, 255, 0), 2)
cv2.circle(canvas, (360, 160), 48, (0, 0, 255), 3)

cv2.putText(
    canvas,
    "OpenCV",
    (60, 270),
    cv2.FONT_HERSHEY_SIMPLEX,
    1.0,
    (0, 255, 255),
    2,
)

颜色仍然是 BGR,比如 (0, 255, 0) 是绿色。

如果要在图片上写中文,cv2.putText 不太合适。可以借助 PIL:

from PIL import Image, ImageDraw, ImageFont
import cv2
import numpy as np


image_bgr = cv2.imread("input.jpg")
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)

pil_image = Image.fromarray(image_rgb)
draw = ImageDraw.Draw(pil_image)
font = ImageFont.truetype("PingFang.ttc", 32)
draw.text((40, 40), "检测结果", fill=(255, 0, 0), font=font)

output_bgr = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
cv2.imwrite("text_cn.jpg", output_bgr)

字体文件路径要按自己的系统调整。

视频文件处理:一帧一帧套图像流程

OpenCV 处理视频,本质上是不断读取帧,再把每一帧当图片处理。

import cv2


cap = cv2.VideoCapture("input.mp4")

if not cap.isOpened():
    raise RuntimeError("video open failed")

while True:
    ok, frame = cap.read()
    if not ok:
        break

    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    edges = cv2.Canny(gray, 80, 160)

    cv2.imshow("edges", edges)

    if cv2.waitKey(1) & 0xFF == ord("q"):
        break

cap.release()
cv2.destroyAllWindows()

如果是服务器环境,没有窗口能力,就不要用 imshow。可以把结果保存成图片、视频,或者交给后续程序处理。

保存视频:

cap = cv2.VideoCapture("input.mp4")

if not cap.isOpened():
    raise RuntimeError("video open failed")

width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS) or 25

writer = cv2.VideoWriter(
    "output.mp4",
    cv2.VideoWriter_fourcc(*"mp4v"),
    fps,
    (width, height),
)

while True:
    ok, frame = cap.read()
    if not ok:
        break

    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    edges = cv2.Canny(gray, 80, 160)
    edges_bgr = cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR)
    writer.write(edges_bgr)

cap.release()
writer.release()

摄像头读取

摄像头通常用设备编号打开。

cap = cv2.VideoCapture(0)

if not cap.isOpened():
    raise RuntimeError("camera open failed")

while True:
    ok, frame = cap.read()
    if not ok:
        break

    cv2.imshow("camera", frame)

    if cv2.waitKey(1) & 0xFF == ord("q"):
        break

cap.release()
cv2.destroyAllWindows()

如果打不开摄像头,先排查权限、设备编号、系统隐私设置和后端编码库。

小项目:图片里找最大目标框

这个小脚本适合用来理解“灰度、滤波、阈值、轮廓、框选”的组合。

import cv2


def find_largest_object(image_path: str, output_path: str) -> None:
    image = cv2.imread(image_path)
    if image is None:
        raise FileNotFoundError(image_path)

    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    blur = cv2.GaussianBlur(gray, (5, 5), 0)

    _, binary = cv2.threshold(
        blur,
        0,
        255,
        cv2.THRESH_BINARY + cv2.THRESH_OTSU,
    )

    contours, _ = cv2.findContours(
        binary,
        cv2.RETR_EXTERNAL,
        cv2.CHAIN_APPROX_SIMPLE,
    )

    if not contours:
        cv2.imwrite(output_path, image)
        return

    largest = max(contours, key=cv2.contourArea)
    x, y, w, h = cv2.boundingRect(largest)

    output = image.copy()
    cv2.rectangle(output, (x, y), (x + w, y + h), (0, 255, 0), 3)
    cv2.imwrite(output_path, output)


find_largest_object("input.jpg", "largest.jpg")

这个方法适合背景较干净、主体和背景差异明显的图片。如果场景复杂,就要换更细的分割策略,或者接入模型。

小项目:批量生成缩略图

文件夹里一批图片,统一缩放并保存。

from pathlib import Path

import cv2


def resize_keep_ratio(image, max_width: int):
    h, w = image.shape[:2]

    if w <= max_width:
        return image

    scale = max_width / w
    target_size = (max_width, int(h * scale))
    return cv2.resize(image, target_size, interpolation=cv2.INTER_AREA)


def build_thumbnails(input_dir: str, output_dir: str, max_width: int = 480):
    src_dir = Path(input_dir)
    dst_dir = Path(output_dir)
    dst_dir.mkdir(parents=True, exist_ok=True)

    for path in src_dir.glob("*"):
        if path.suffix.lower() not in {".jpg", ".jpeg", ".png", ".webp"}:
            continue

        image = cv2.imread(str(path))
        if image is None:
            continue

        thumbnail = resize_keep_ratio(image, max_width)
        cv2.imwrite(str(dst_dir / path.name), thumbnail)


build_thumbnails("images", "thumbs")

这个脚本看起来普通,但在后台管理、素材处理、数据集预处理里很实用。

小项目:视频里找绿色区域

把 HSV 抠色、轮廓和视频帧结合起来:

import cv2
import numpy as np


cap = cv2.VideoCapture("input.mp4")

if not cap.isOpened():
    raise RuntimeError("video open failed")

while True:
    ok, frame = cap.read()
    if not ok:
        break

    hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(
        hsv,
        np.array([35, 60, 60]),
        np.array([85, 255, 255]),
    )

    contours, _ = cv2.findContours(
        mask,
        cv2.RETR_EXTERNAL,
        cv2.CHAIN_APPROX_SIMPLE,
    )

    for contour in contours:
        area = cv2.contourArea(contour)
        if area < 500:
            continue

        x, y, w, h = cv2.boundingRect(contour)
        cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)

    cv2.imshow("green detector", frame)

    if cv2.waitKey(1) & 0xFF == ord("q"):
        break

cap.release()
cv2.destroyAllWindows()

这个版本很基础,但已经能跑通“颜色筛选 + 轮廓框选”的完整链路。

写 OpenCV 脚本的几个习惯

读图后检查 None

视频读取后检查 cap.isOpened()

坐标顺序分清:数组切片是 image[y1:y2, x1:x2],绘图点是 (x, y)

颜色顺序分清:OpenCV 常用 BGR,很多库常用 RGB。

处理前保留原图副本:

output = image.copy()

调参时把中间结果保存出来:

cv2.imwrite("debug_gray.jpg", gray)
cv2.imwrite("debug_binary.jpg", binary)
cv2.imwrite("debug_edges.jpg", edges)

图像处理不要只看最终结果。中间结果能帮你快速判断问题出在颜色、阈值、滤波还是轮廓筛选。