文章

44 · 视觉感知:OpenCV 图像处理与人脸提取

#053 · 2026-04-17 · Python

🔗 知识图谱导航:阅读本文前,建议先回顾《31 · NumPy 实战》中的数组切片和矩阵运算;本文把这些数组知识用到图像上。OpenCV 只是工具库,图像处理的底层仍然是数组运算。

运行环境:基础演示只需要 pip install numpy--mode opencv 需要额外安装 opencv-python。本文不依赖外部图片,所有图像都由脚本自动生成。

痛点与架构:很多新手一学 OpenCV 就卡在“读图片、找素材、窗口显示”上。本文先把图像拆成最本质的 NumPy 数组,再手写模糊、边缘、二值化,最后再过渡到 OpenCV API。

图像处理先建立直觉

图像 = ndarray
灰度图: shape = (H, W)
彩色图: shape = (H, W, 3)
像素值: 0~255,越大越亮
处理图像 = 改变数组里的数字

极客解析:图像处理像修一张 Excel 表:每个单元格是一个像素。模糊是在算邻居平均,边缘检测是在找数字突变,二值化是在按阈值把格子分成黑白两类。

步步为营:核心逻辑自适应拆解

这一篇拆成 7 个台阶:造图、模糊、边缘、ASCII 可视化、完整 NumPy 流程、统计分析和 CLI 调度。每段都能独立运行,不需要外部图片。

Step 1:用 make_synthetic_image 把图像看成 ndarray

痛点与机制

图像不是魔法文件,在 Python 里就是 NumPy 数组。灰度图像像一张 Excel 表,每个格子是 0 到 255 的亮度值;0 接近黑色,255 接近白色。先自己造图,读者不用准备任何图片,也能理解像素。

核心源码(逐字来自文末完整源码)

def make_synthetic_image(h: int = 64, w: int = 64) -> np.ndarray:
    """生成合成测试图像(灰度,含几何形状)"""
    img = np.zeros((h, w), dtype=np.uint8)
    # 矩形
    img[10:30, 10:30] = 200
    # 圆形(近似)
    cy, cx, r = 45, 45, 12
    for y in range(h):
        for x in range(w):
            if (y - cy) ** 2 + (x - cx) ** 2 <= r ** 2:
                img[y, x] = 150
    # 渐变背景
    for i in range(h):
        img[i, :] = np.clip(img[i, :].astype(int) + i // 4, 0, 255).astype(np.uint8)
    return img

可运行演示(补齐 Mock 数据与 print 反馈)

import numpy as np

def make_synthetic_image(h: int = 64, w: int = 64) -> np.ndarray:
    """生成合成测试图像(灰度,含几何形状)"""
    img = np.zeros((h, w), dtype=np.uint8)
    # 矩形
    img[10:30, 10:30] = 200
    # 圆形(近似)
    cy, cx, r = 45, 45, 12
    for y in range(h):
        for x in range(w):
            if (y - cy) ** 2 + (x - cx) ** 2 <= r ** 2:
                img[y, x] = 150
    # 渐变背景
    for i in range(h):
        img[i, :] = np.clip(img[i, :].astype(int) + i // 4, 0, 255).astype(np.uint8)
    return img

img = make_synthetic_image(h=32, w=32)
print("图像 shape:", img.shape)
print("数据类型:", img.dtype)
print("像素范围:", int(img.min()), "~", int(img.max()))
print("左上角 5x5 像素:")
print(img[:5, :5])

Step 2:用 numpy_gaussian_blur 手写高斯模糊

痛点与机制

高斯模糊像“把画面揉软一点”:当前像素不再只看自己,而是参考周围 3x3 邻居。中心权重大,边缘权重小,所以噪声会被平均掉,图像标准差通常会下降。

核心源码(逐字来自文末完整源码)

def numpy_gaussian_blur(img: np.ndarray, sigma: float = 1.0) -> np.ndarray:
    """numpy 实现高斯模糊(3×3核)"""
    k = np.array([[1, 2, 1], [2, 4, 2], [1, 2, 1]], dtype=np.float32) / 16
    h, w = img.shape
    out = img.astype(np.float32).copy()
    for y in range(1, h - 1):
        for x in range(1, w - 1):
            patch = img[y-1:y+2, x-1:x+2].astype(np.float32)
            out[y, x] = np.sum(patch * k)
    return np.clip(out, 0, 255).astype(np.uint8)

可运行演示(补齐 Mock 数据与 print 反馈)

import numpy as np

def make_synthetic_image(h: int = 64, w: int = 64) -> np.ndarray:
    """生成合成测试图像(灰度,含几何形状)"""
    img = np.zeros((h, w), dtype=np.uint8)
    # 矩形
    img[10:30, 10:30] = 200
    # 圆形(近似)
    cy, cx, r = 45, 45, 12
    for y in range(h):
        for x in range(w):
            if (y - cy) ** 2 + (x - cx) ** 2 <= r ** 2:
                img[y, x] = 150
    # 渐变背景
    for i in range(h):
        img[i, :] = np.clip(img[i, :].astype(int) + i // 4, 0, 255).astype(np.uint8)
    return img

def numpy_gaussian_blur(img: np.ndarray, sigma: float = 1.0) -> np.ndarray:
    """numpy 实现高斯模糊(3×3核)"""
    k = np.array([[1, 2, 1], [2, 4, 2], [1, 2, 1]], dtype=np.float32) / 16
    h, w = img.shape
    out = img.astype(np.float32).copy()
    for y in range(1, h - 1):
        for x in range(1, w - 1):
            patch = img[y-1:y+2, x-1:x+2].astype(np.float32)
            out[y, x] = np.sum(patch * k)
    return np.clip(out, 0, 255).astype(np.uint8)

img = make_synthetic_image()
blurred = numpy_gaussian_blur(img)
print("原始标准差:", round(float(img.std()), 2))
print("模糊后标准差:", round(float(blurred.std()), 2))
print("中心像素变化:", int(img[20, 20]), "->", int(blurred[20, 20]))
print("高斯模糊本质:用周围像素的加权平均替换当前像素")

Step 3:用 numpy_sobel_edge 找出像素突变的边缘

痛点与机制

Sobel 边缘检测像找山坡最陡的地方。横向卷积核找左右变化,纵向卷积核找上下变化,两个方向合起来就是边缘强度。矩形边框、圆形轮廓都会变亮。

核心源码(逐字来自文末完整源码)

def numpy_sobel_edge(img: np.ndarray) -> np.ndarray:
    """numpy 实现 Sobel 边缘检测"""
    kx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32)
    ky = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32)
    h, w = img.shape
    gx = np.zeros_like(img, dtype=np.float32)
    gy = np.zeros_like(img, dtype=np.float32)
    for y in range(1, h - 1):
        for x in range(1, w - 1):
            patch = img[y-1:y+2, x-1:x+2].astype(np.float32)
            gx[y, x] = np.sum(patch * kx)
            gy[y, x] = np.sum(patch * ky)
    magnitude = np.sqrt(gx ** 2 + gy ** 2)
    return np.clip(magnitude, 0, 255).astype(np.uint8)

可运行演示(补齐 Mock 数据与 print 反馈)

import numpy as np

def make_synthetic_image(h: int = 64, w: int = 64) -> np.ndarray:
    """生成合成测试图像(灰度,含几何形状)"""
    img = np.zeros((h, w), dtype=np.uint8)
    # 矩形
    img[10:30, 10:30] = 200
    # 圆形(近似)
    cy, cx, r = 45, 45, 12
    for y in range(h):
        for x in range(w):
            if (y - cy) ** 2 + (x - cx) ** 2 <= r ** 2:
                img[y, x] = 150
    # 渐变背景
    for i in range(h):
        img[i, :] = np.clip(img[i, :].astype(int) + i // 4, 0, 255).astype(np.uint8)
    return img

def numpy_sobel_edge(img: np.ndarray) -> np.ndarray:
    """numpy 实现 Sobel 边缘检测"""
    kx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32)
    ky = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32)
    h, w = img.shape
    gx = np.zeros_like(img, dtype=np.float32)
    gy = np.zeros_like(img, dtype=np.float32)
    for y in range(1, h - 1):
        for x in range(1, w - 1):
            patch = img[y-1:y+2, x-1:x+2].astype(np.float32)
            gx[y, x] = np.sum(patch * kx)
            gy[y, x] = np.sum(patch * ky)
    magnitude = np.sqrt(gx ** 2 + gy ** 2)
    return np.clip(magnitude, 0, 255).astype(np.uint8)

img = make_synthetic_image()
edges = numpy_sobel_edge(img)
print("边缘图 shape:", edges.shape)
print("强边缘像素数(>80):", int((edges > 80).sum()))
print("边缘强度最大值:", int(edges.max()))
print("Sobel 会在像素突变的位置给出更大的梯度值")

Step 4:用 ascii_image 在终端直接看图

痛点与机制

ascii_image 把亮度映射成字符,暗处用空格和点,亮处用 %@。这像用文字拼图,让新手不用 GUI、不用 matplotlib,也能看到图像大概长什么样。

核心源码(逐字来自文末完整源码)

def ascii_image(img: np.ndarray, width: int = 48, height: int = 16,
                title: str = "") -> None:
    """将灰度图像渲染为ASCII字符"""
    chars = " .:-=+*#%@"
    h, w = img.shape
    if title:
        print(f"\n  {title}")
        print(f"  {'─'*width}")
    for row in range(height):
        line = "  "
        for col in range(width):
            y = int(row / height * h)
            x = int(col / width * w)
            val = img[y, x]
            line += chars[int(val / 256 * len(chars))]
        print(line)
    if title:
        print(f"  {'─'*width}")

可运行演示(补齐 Mock 数据与 print 反馈)

import numpy as np

def make_synthetic_image(h: int = 64, w: int = 64) -> np.ndarray:
    """生成合成测试图像(灰度,含几何形状)"""
    img = np.zeros((h, w), dtype=np.uint8)
    # 矩形
    img[10:30, 10:30] = 200
    # 圆形(近似)
    cy, cx, r = 45, 45, 12
    for y in range(h):
        for x in range(w):
            if (y - cy) ** 2 + (x - cx) ** 2 <= r ** 2:
                img[y, x] = 150
    # 渐变背景
    for i in range(h):
        img[i, :] = np.clip(img[i, :].astype(int) + i // 4, 0, 255).astype(np.uint8)
    return img

def ascii_image(img: np.ndarray, width: int = 48, height: int = 16,
                title: str = "") -> None:
    """将灰度图像渲染为ASCII字符"""
    chars = " .:-=+*#%@"
    h, w = img.shape
    if title:
        print(f"\n  {title}")
        print(f"  {'─'*width}")
    for row in range(height):
        line = "  "
        for col in range(width):
            y = int(row / height * h)
            x = int(col / width * w)
            val = img[y, x]
            line += chars[int(val / 256 * len(chars))]
        print(line)
    if title:
        print(f"  {'─'*width}")

img = make_synthetic_image(h=32, w=32)
ascii_image(img, width=32, height=12, title="合成图像 ASCII 预览")

Step 5:用 mode_numpy 跑完整零依赖图像处理流水线

痛点与机制

mode_numpy 串起生成图像、模糊、边缘检测、二值化和统计表。它是学习 OpenCV 前的“透明版”:每一步都能看到数组怎么变,避免一上来就背 API。

核心源码(逐字来自文末完整源码)

def mode_numpy() -> None:
    print(f"[{nexdo_time()}] 纯 numpy 图像处理演示(零依赖)")
    img = make_synthetic_image(64, 64)
    blurred = numpy_gaussian_blur(img)
    edges = numpy_sobel_edge(img)
    binary = numpy_threshold(img, 100)

    ascii_image(img,     title="原始合成图像(矩形+圆形+渐变)")
    ascii_image(blurred, title="高斯模糊后")
    ascii_image(edges,   title="Sobel 边缘检测")
    ascii_image(binary,  title="二值化(阈值=100)")

    rows = [
        ["原始图像",   f"{img.shape}",     f"{img.min()}", f"{img.max()}", f"{img.mean():.1f}"],
        ["高斯模糊",   f"{blurred.shape}", f"{blurred.min()}", f"{blurred.max()}", f"{blurred.mean():.1f}"],
        ["Sobel边缘",  f"{edges.shape}",   f"{edges.min()}", f"{edges.max()}", f"{edges.mean():.1f}"],
        ["二值化",     f"{binary.shape}",  f"{binary.min()}", f"{binary.max()}", f"{binary.mean():.1f}"],
    ]
    print_table(["处理步骤", "形状", "最小值", "最大值", "均值"], rows, "图像统计信息")

可运行演示(补齐 Mock 数据与 print 反馈)

import time
from typing import Tuple
import numpy as np

def nexdo_time() -> str:
    return time.strftime("%Y-%m-%d %H:%M:%S")

def print_table(headers: list, rows: list, title: str = "") -> None:
    if title:
        print(f"\n{'='*65}\n  {title}\n{'='*65}")
    col_widths = [max(len(str(h)), max((len(str(r[i])) for r in rows), default=0))
                  for i, h in enumerate(headers)]
    print(f"┌{'┬'.join('─'*(w+2) for w in col_widths)}┐")
    print(f"│{'│'.join(f' {str(h):<{w}} ' for h, w in zip(headers, col_widths))}│")
    print(f"├{'┼'.join('─'*(w+2) for w in col_widths)}┤")
    for row in rows:
        print(f"│{'│'.join(f' {str(v):<{w}} ' for v, w in zip(row, col_widths))}│")
    print(f"└{'┴'.join('─'*(w+2) for w in col_widths)}┘")

def make_synthetic_image(h: int = 64, w: int = 64) -> np.ndarray:
    """生成合成测试图像(灰度,含几何形状)"""
    img = np.zeros((h, w), dtype=np.uint8)
    # 矩形
    img[10:30, 10:30] = 200
    # 圆形(近似)
    cy, cx, r = 45, 45, 12
    for y in range(h):
        for x in range(w):
            if (y - cy) ** 2 + (x - cx) ** 2 <= r ** 2:
                img[y, x] = 150
    # 渐变背景
    for i in range(h):
        img[i, :] = np.clip(img[i, :].astype(int) + i // 4, 0, 255).astype(np.uint8)
    return img

def numpy_gaussian_blur(img: np.ndarray, sigma: float = 1.0) -> np.ndarray:
    """numpy 实现高斯模糊(3×3核)"""
    k = np.array([[1, 2, 1], [2, 4, 2], [1, 2, 1]], dtype=np.float32) / 16
    h, w = img.shape
    out = img.astype(np.float32).copy()
    for y in range(1, h - 1):
        for x in range(1, w - 1):
            patch = img[y-1:y+2, x-1:x+2].astype(np.float32)
            out[y, x] = np.sum(patch * k)
    return np.clip(out, 0, 255).astype(np.uint8)

def numpy_sobel_edge(img: np.ndarray) -> np.ndarray:
    """numpy 实现 Sobel 边缘检测"""
    kx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32)
    ky = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32)
    h, w = img.shape
    gx = np.zeros_like(img, dtype=np.float32)
    gy = np.zeros_like(img, dtype=np.float32)
    for y in range(1, h - 1):
        for x in range(1, w - 1):
            patch = img[y-1:y+2, x-1:x+2].astype(np.float32)
            gx[y, x] = np.sum(patch * kx)
            gy[y, x] = np.sum(patch * ky)
    magnitude = np.sqrt(gx ** 2 + gy ** 2)
    return np.clip(magnitude, 0, 255).astype(np.uint8)

def numpy_threshold(img: np.ndarray, thresh: int = 128) -> np.ndarray:
    """numpy 实现二值化"""
    return np.where(img >= thresh, 255, 0).astype(np.uint8)

def ascii_image(img: np.ndarray, width: int = 48, height: int = 16,
                title: str = "") -> None:
    """将灰度图像渲染为ASCII字符"""
    chars = " .:-=+*#%@"
    h, w = img.shape
    if title:
        print(f"\n  {title}")
        print(f"  {'─'*width}")
    for row in range(height):
        line = "  "
        for col in range(width):
            y = int(row / height * h)
            x = int(col / width * w)
            val = img[y, x]
            line += chars[int(val / 256 * len(chars))]
        print(line)
    if title:
        print(f"  {'─'*width}")

def mode_numpy() -> None:
    print(f"[{nexdo_time()}] 纯 numpy 图像处理演示(零依赖)")
    img = make_synthetic_image(64, 64)
    blurred = numpy_gaussian_blur(img)
    edges = numpy_sobel_edge(img)
    binary = numpy_threshold(img, 100)

    ascii_image(img,     title="原始合成图像(矩形+圆形+渐变)")
    ascii_image(blurred, title="高斯模糊后")
    ascii_image(edges,   title="Sobel 边缘检测")
    ascii_image(binary,  title="二值化(阈值=100)")

    rows = [
        ["原始图像",   f"{img.shape}",     f"{img.min()}", f"{img.max()}", f"{img.mean():.1f}"],
        ["高斯模糊",   f"{blurred.shape}", f"{blurred.min()}", f"{blurred.max()}", f"{blurred.mean():.1f}"],
        ["Sobel边缘",  f"{edges.shape}",   f"{edges.min()}", f"{edges.max()}", f"{edges.mean():.1f}"],
        ["二值化",     f"{binary.shape}",  f"{binary.min()}", f"{binary.max()}", f"{binary.mean():.1f}"],
    ]
    print_table(["处理步骤", "形状", "最小值", "最大值", "均值"], rows, "图像统计信息")

mode_numpy()

Step 6:用 mode_stats 读懂像素直方图和统计量

痛点与机制

图像统计像体检报告:均值看整体亮度,标准差看对比度,直方图看像素集中在哪些区间。很多图像增强、阈值分割、曝光判断,第一步都要看这些指标。

核心源码(逐字来自文末完整源码)

def mode_stats() -> None:
    """图像统计分析"""
    print(f"[{nexdo_time()}] 图像统计分析")
    img = make_synthetic_image(64, 64)

    # 直方图(ASCII)
    hist, _ = np.histogram(img.flatten(), bins=8, range=(0, 256))
    max_count = hist.max()
    print("\n  像素值分布直方图(8个区间)")
    print(f"  {'─'*50}")
    for i, count in enumerate(hist):
        bar_len = int(count / max_count * 40)
        low = i * 32
        high = (i + 1) * 32
        print(f"  [{low:3d}-{high:3d}] │{'█'*bar_len:<40} {count}")
    print(f"  {'─'*50}")

    rows = [
        ["均值",   f"{img.mean():.2f}"],
        ["标准差", f"{img.std():.2f}"],
        ["最小值", str(img.min())],
        ["最大值", str(img.max())],
        ["中位数", f"{np.median(img):.1f}"],
        ["非零像素", f"{np.count_nonzero(img)} / {img.size}"],
    ]
    print_table(["统计量", "值"], rows, "图像统计信息")

可运行演示(补齐 Mock 数据与 print 反馈)

import time
import numpy as np

def nexdo_time() -> str:
    return time.strftime("%Y-%m-%d %H:%M:%S")

def print_table(headers: list, rows: list, title: str = "") -> None:
    if title:
        print(f"\n{'='*65}\n  {title}\n{'='*65}")
    col_widths = [max(len(str(h)), max((len(str(r[i])) for r in rows), default=0))
                  for i, h in enumerate(headers)]
    print(f"┌{'┬'.join('─'*(w+2) for w in col_widths)}┐")
    print(f"│{'│'.join(f' {str(h):<{w}} ' for h, w in zip(headers, col_widths))}│")
    print(f"├{'┼'.join('─'*(w+2) for w in col_widths)}┤")
    for row in rows:
        print(f"│{'│'.join(f' {str(v):<{w}} ' for v, w in zip(row, col_widths))}│")
    print(f"└{'┴'.join('─'*(w+2) for w in col_widths)}┘")

def make_synthetic_image(h: int = 64, w: int = 64) -> np.ndarray:
    """生成合成测试图像(灰度,含几何形状)"""
    img = np.zeros((h, w), dtype=np.uint8)
    # 矩形
    img[10:30, 10:30] = 200
    # 圆形(近似)
    cy, cx, r = 45, 45, 12
    for y in range(h):
        for x in range(w):
            if (y - cy) ** 2 + (x - cx) ** 2 <= r ** 2:
                img[y, x] = 150
    # 渐变背景
    for i in range(h):
        img[i, :] = np.clip(img[i, :].astype(int) + i // 4, 0, 255).astype(np.uint8)
    return img

def mode_stats() -> None:
    """图像统计分析"""
    print(f"[{nexdo_time()}] 图像统计分析")
    img = make_synthetic_image(64, 64)

    # 直方图(ASCII)
    hist, _ = np.histogram(img.flatten(), bins=8, range=(0, 256))
    max_count = hist.max()
    print("\n  像素值分布直方图(8个区间)")
    print(f"  {'─'*50}")
    for i, count in enumerate(hist):
        bar_len = int(count / max_count * 40)
        low = i * 32
        high = (i + 1) * 32
        print(f"  [{low:3d}-{high:3d}] │{'█'*bar_len:<40} {count}")
    print(f"  {'─'*50}")

    rows = [
        ["均值",   f"{img.mean():.2f}"],
        ["标准差", f"{img.std():.2f}"],
        ["最小值", str(img.min())],
        ["最大值", str(img.max())],
        ["中位数", f"{np.median(img):.1f}"],
        ["非零像素", f"{np.count_nonzero(img)} / {img.size}"],
    ]
    print_table(["统计量", "值"], rows, "图像统计信息")

mode_stats()

Step 7:用 main 做 numpy/opencv/stats/all 命令调度

痛点与机制

main 是脚本遥控器。新手可以先跑 numpy 零依赖模式,再跑 stats 看统计,装好 OpenCV 后再跑 opencv。这样学习坡度更平,不会卡在安装或外部图片上。

核心源码(逐字来自文末完整源码)

def main() -> None:
    parser = argparse.ArgumentParser(description="OpenCV 图像处理演示")
    parser.add_argument("--mode", choices=["numpy", "opencv", "stats", "all"],
                        default="all")
    args = parser.parse_args()
    dispatch = {
        "numpy":  mode_numpy,
        "opencv": mode_opencv,
        "stats":  mode_stats,
        "all":    lambda: [mode_numpy(), mode_stats(), mode_opencv()],
    }
    dispatch[args.mode]()
    print(f"\n[{nexdo_time()}] 完成")

可运行演示(补齐 Mock 数据与 print 反馈)

import argparse
import sys


def mode_numpy() -> None:
    print("numpy:不用 OpenCV,也能理解像素、模糊、边缘、二值化")


def mode_opencv() -> None:
    print("opencv:安装 opencv-python 后,使用工业级 API 跑完整流程")


def mode_stats() -> None:
    print("stats:查看图像像素分布和统计指标")


def nexdo_time() -> str:
    return "2026-04-18 11:12:00"

def main() -> None:
    parser = argparse.ArgumentParser(description="OpenCV 图像处理演示")
    parser.add_argument("--mode", choices=["numpy", "opencv", "stats", "all"],
                        default="all")
    args = parser.parse_args()
    dispatch = {
        "numpy":  mode_numpy,
        "opencv": mode_opencv,
        "stats":  mode_stats,
        "all":    lambda: [mode_numpy(), mode_stats(), mode_opencv()],
    }
    dispatch[args.mode]()
    print(f"\n[{nexdo_time()}] 完成")

for mode in ["numpy", "stats", "opencv", "all"]:
    print(f"\n$ python3 44-opencv-vision.py --mode {mode}")
    sys.argv = ["prog", "--mode", mode]
    main()

极客实战:完整源码与运行

现在,把上面的积木拼起来,将以下完整代码放进你的编辑器。建议先跑 --mode numpy,确认零依赖流程;再跑 --mode stats 看像素分布;安装 OpenCV 后再跑 --mode opencv

#!/usr/bin/env python3
"""
44-opencv-vision.py
图像处理演示:numpy模拟图像 + 纯numpy实现核心算法(零依赖可运行部分)
配合 opencv-python 的完整流程演示

用法:
  python 44-opencv-vision.py --mode numpy    # 零依赖,numpy模拟
  python 44-opencv-vision.py --mode opencv   # 需要 pip install opencv-python
  python 44-opencv-vision.py --mode stats    # 图像统计分析
  python 44-opencv-vision.py --mode all
"""

import argparse
import time
from typing import Tuple

import numpy as np


def nexdo_time() -> str:
    return time.strftime("%Y-%m-%d %H:%M:%S")


def print_table(headers: list, rows: list, title: str = "") -> None:
    if title:
        print(f"\n{'='*65}\n  {title}\n{'='*65}")
    col_widths = [max(len(str(h)), max((len(str(r[i])) for r in rows), default=0))
                  for i, h in enumerate(headers)]
    print(f"┌{'┬'.join('─'*(w+2) for w in col_widths)}┐")
    print(f"│{'│'.join(f' {str(h):<{w}} ' for h, w in zip(headers, col_widths))}│")
    print(f"├{'┼'.join('─'*(w+2) for w in col_widths)}┤")
    for row in rows:
        print(f"│{'│'.join(f' {str(v):<{w}} ' for v, w in zip(row, col_widths))}│")
    print(f"└{'┴'.join('─'*(w+2) for w in col_widths)}┘")




def make_synthetic_image(h: int = 64, w: int = 64) -> np.ndarray:
    """生成合成测试图像(灰度,含几何形状)"""
    img = np.zeros((h, w), dtype=np.uint8)
    # 矩形
    img[10:30, 10:30] = 200
    # 圆形(近似)
    cy, cx, r = 45, 45, 12
    for y in range(h):
        for x in range(w):
            if (y - cy) ** 2 + (x - cx) ** 2 <= r ** 2:
                img[y, x] = 150
    # 渐变背景
    for i in range(h):
        img[i, :] = np.clip(img[i, :].astype(int) + i // 4, 0, 255).astype(np.uint8)
    return img


def numpy_gaussian_blur(img: np.ndarray, sigma: float = 1.0) -> np.ndarray:
    """numpy 实现高斯模糊(3×3核)"""
    k = np.array([[1, 2, 1], [2, 4, 2], [1, 2, 1]], dtype=np.float32) / 16
    h, w = img.shape
    out = img.astype(np.float32).copy()
    for y in range(1, h - 1):
        for x in range(1, w - 1):
            patch = img[y-1:y+2, x-1:x+2].astype(np.float32)
            out[y, x] = np.sum(patch * k)
    return np.clip(out, 0, 255).astype(np.uint8)


def numpy_sobel_edge(img: np.ndarray) -> np.ndarray:
    """numpy 实现 Sobel 边缘检测"""
    kx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32)
    ky = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32)
    h, w = img.shape
    gx = np.zeros_like(img, dtype=np.float32)
    gy = np.zeros_like(img, dtype=np.float32)
    for y in range(1, h - 1):
        for x in range(1, w - 1):
            patch = img[y-1:y+2, x-1:x+2].astype(np.float32)
            gx[y, x] = np.sum(patch * kx)
            gy[y, x] = np.sum(patch * ky)
    magnitude = np.sqrt(gx ** 2 + gy ** 2)
    return np.clip(magnitude, 0, 255).astype(np.uint8)


def numpy_threshold(img: np.ndarray, thresh: int = 128) -> np.ndarray:
    """numpy 实现二值化"""
    return np.where(img >= thresh, 255, 0).astype(np.uint8)


def ascii_image(img: np.ndarray, width: int = 48, height: int = 16,
                title: str = "") -> None:
    """将灰度图像渲染为ASCII字符"""
    chars = " .:-=+*#%@"
    h, w = img.shape
    if title:
        print(f"\n  {title}")
        print(f"  {'─'*width}")
    for row in range(height):
        line = "  "
        for col in range(width):
            y = int(row / height * h)
            x = int(col / width * w)
            val = img[y, x]
            line += chars[int(val / 256 * len(chars))]
        print(line)
    if title:
        print(f"  {'─'*width}")


def mode_numpy() -> None:
    print(f"[{nexdo_time()}] 纯 numpy 图像处理演示(零依赖)")
    img = make_synthetic_image(64, 64)
    blurred = numpy_gaussian_blur(img)
    edges = numpy_sobel_edge(img)
    binary = numpy_threshold(img, 100)

    ascii_image(img,     title="原始合成图像(矩形+圆形+渐变)")
    ascii_image(blurred, title="高斯模糊后")
    ascii_image(edges,   title="Sobel 边缘检测")
    ascii_image(binary,  title="二值化(阈值=100)")

    rows = [
        ["原始图像",   f"{img.shape}",     f"{img.min()}", f"{img.max()}", f"{img.mean():.1f}"],
        ["高斯模糊",   f"{blurred.shape}", f"{blurred.min()}", f"{blurred.max()}", f"{blurred.mean():.1f}"],
        ["Sobel边缘",  f"{edges.shape}",   f"{edges.min()}", f"{edges.max()}", f"{edges.mean():.1f}"],
        ["二值化",     f"{binary.shape}",  f"{binary.min()}", f"{binary.max()}", f"{binary.mean():.1f}"],
    ]
    print_table(["处理步骤", "形状", "最小值", "最大值", "均值"], rows, "图像统计信息")


def mode_opencv() -> None:
    """需要 pip install opencv-python"""
    print(f"[{nexdo_time()}] OpenCV 完整流程演示")
    try:
        import cv2
    except ImportError:
        print("  ⚠ 未安装 opencv-python,请执行: pip install opencv-python")
        print("  当前演示将使用 numpy 替代实现")
        mode_numpy()
        return

    # 用 numpy 生成合成图像(避免依赖外部文件)
    img_np = make_synthetic_image(128, 128)
    img_bgr = cv2.cvtColor(img_np, cv2.COLOR_GRAY2BGR)

    # 各种处理
    gray    = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)
    edges   = cv2.Canny(blurred, 50, 150)
    _, binary = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY)
    kernel  = np.ones((3, 3), np.uint8)
    dilated = cv2.dilate(edges, kernel, iterations=1)

    rows = [
        ["原始(BGR)",  str(img_bgr.shape), str(img_bgr.dtype)],
        ["灰度",       str(gray.shape),    str(gray.dtype)],
        ["高斯模糊",   str(blurred.shape), str(blurred.dtype)],
        ["Canny边缘",  str(edges.shape),   str(edges.dtype)],
        ["二值化",     str(binary.shape),  str(binary.dtype)],
        ["膨胀",       str(dilated.shape), str(dilated.dtype)],
    ]
    print_table(["处理步骤", "形状", "数据类型"], rows, "OpenCV 处理流程")
    print(f"\n  OpenCV 版本: {cv2.__version__}")
    print(f"  ✓ 所有处理步骤完成(图像来自 numpy 合成,无需外部文件)")

    # 人脸检测说明
    print("\n  人脸检测示例代码(需要真实图像):")
    print("  ┌─────────────────────────────────────────────────────┐")
    print("  │  cascade = cv2.CascadeClassifier(                   │")
    print("  │      cv2.data.haarcascades +                        │")
    print("  │      'haarcascade_frontalface_default.xml')         │")
    print("  │  faces = cascade.detectMultiScale(gray, 1.1, 4)    │")
    print("  │  for (x,y,w,h) in faces:                           │")
    print("  │      cv2.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)│")
    print("  └─────────────────────────────────────────────────────┘")


def mode_stats() -> None:
    """图像统计分析"""
    print(f"[{nexdo_time()}] 图像统计分析")
    img = make_synthetic_image(64, 64)

    # 直方图(ASCII)
    hist, _ = np.histogram(img.flatten(), bins=8, range=(0, 256))
    max_count = hist.max()
    print("\n  像素值分布直方图(8个区间)")
    print(f"  {'─'*50}")
    for i, count in enumerate(hist):
        bar_len = int(count / max_count * 40)
        low = i * 32
        high = (i + 1) * 32
        print(f"  [{low:3d}-{high:3d}] │{'█'*bar_len:<40} {count}")
    print(f"  {'─'*50}")

    rows = [
        ["均值",   f"{img.mean():.2f}"],
        ["标准差", f"{img.std():.2f}"],
        ["最小值", str(img.min())],
        ["最大值", str(img.max())],
        ["中位数", f"{np.median(img):.1f}"],
        ["非零像素", f"{np.count_nonzero(img)} / {img.size}"],
    ]
    print_table(["统计量", "值"], rows, "图像统计信息")


def main() -> None:
    parser = argparse.ArgumentParser(description="OpenCV 图像处理演示")
    parser.add_argument("--mode", choices=["numpy", "opencv", "stats", "all"],
                        default="all")
    args = parser.parse_args()
    dispatch = {
        "numpy":  mode_numpy,
        "opencv": mode_opencv,
        "stats":  mode_stats,
        "all":    lambda: [mode_numpy(), mode_stats(), mode_opencv()],
    }
    dispatch[args.mode]()
    print(f"\n[{nexdo_time()}] 完成")


if __name__ == "__main__":
    main()
$ python3 44-opencv-vision.py --mode stats
[2026-04-18 11:11:57] 图像统计分析

  像素值分布直方图(8个区间)
  ──────────────────────────────────────────────────
  [  0- 32] │████████████████████████████████████████ 3255
  [ 32- 64]0
  [ 64- 96]0
  [ 96-128]0
  [128-160] │█                                        95
  [160-192] │████                                     346
  [192-224] │████                                     400
  [224-256]0
  ──────────────────────────────────────────────────

=================================================================
  图像统计信息
=================================================================
┌──────┬─────────────┐
│ 统计量  │ 值           │
├──────┼─────────────┤
│ 均值   │ 43.18       │
│ 标准差  │ 71.19       │

$ python3 44-opencv-vision.py --mode numpy
[2026-04-18 11:11:57] 纯 numpy 图像处理演示(零依赖)

  原始合成图像(矩形+圆形+渐变)
  ────────────────────────────────────────────────
                                                  
                                                  
                                                  
          ###############                         
          ###############                         
          %%%%%%%%%%%%%%%                         
          %%%%%%%%%%%%%%%                         
          %%%%%%%%%%%%%%%                         
                                                  
                               ***********        
                             ***************      
                            *****************     
                            *****************     
                             ***************      

小结

模块 你要记住什么
make_synthetic_image 自己生成灰度图,避免外部图片依赖
numpy_gaussian_blur 用 3x3 加权平均让图像更平滑
numpy_sobel_edge 用梯度变化找边缘轮廓
ascii_image 在终端直接预览灰度图
mode_numpy 串起完整零依赖图像处理流程
mode_stats 用直方图和统计量理解像素分布
main --mode 分层运行 NumPy、统计和 OpenCV 演示

⏱ NexDo Time(5 分钟)

挑战:把 numpy_threshold(img, 100) 的阈值分别改成 80150,重新运行 --mode numpy,观察二值化图像里的白色区域如何变化。

Don’t wait for next time, do it in the next moment.