44 · 视觉感知:OpenCV 图像处理与人脸提取
🔗 知识图谱导航:阅读本文前,建议先回顾《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) 的阈值分别改成 80、150,重新运行 --mode numpy,观察二值化图像里的白色区域如何变化。
Don’t wait for next time, do it in the next moment.