python的机器视觉技术与应用工业场景模拟第二十二篇:同时读取两张图片,分窗口并列展示,适合后期对比实验效果。
《Python 机器视觉技术与应用》第二十二篇:同时读取两张图片,分窗口并列展示,适合后期对比实验效果
声明:本篇全部为课程离线仿真代码,不能直接用于真实视觉检测系统,规避工程误导风险。
环境版本:本篇验证环境为
"opencv-python>=4.8.0",所有数值结果基于该版本复现。
1. 实际工业应用场景
周四上午,轴承滚子外径检测工位。视觉工程师小郑接到一个新需求:工艺部门正在评估两种光源方案——同轴环形光和背光,要求对比同一颗滚子在两种光照下的边缘提取效果。他把两套图像分别读进来,用
"cv2.imshow" 弹了两个窗口,名字都叫
"img"。他拖动窗口比对了五分钟,越看越糊涂:"到底哪张是同轴光、哪张是背光?两个窗口叠在一起,我分不清谁是谁。"更麻烦的是,他后面还要把原图、灰度图、边缘图三组数据两两对比,六张图同时弹出来,屏幕瞬间变成了一锅粥。
老师傅老周端着保温杯走过来:"你这代码在实验室跑 Demo 没问题,丢到产线上就是事故。工业视觉调试最常见的操作就是 A/B 对比——不同光源、不同曝光、不同算法参数,工程师需要把两张图放在同一个参照系里看:同样的缩放比例、同样的坐标轴、同样的亮度范围。如果两个窗口各自独立、位置由操作系统随意摆放,人眼会被窗口大小、位置、亮度自适应等因素干扰,看到的'差异'可能根本不是光源造成的,而是显示方式造成的。你得分窗口展示,用
"cv2.moveWindow" 把两个窗口摆到屏幕左右两侧,再用 matplotlib 拼成一张对比大图,让差异自己跳出来。"小郑调出参数表:200 万像素全局快门 CMOS,分辨率 1600×1200,像元尺寸 4.5 μm,镜头焦距 25 mm,光圈 F4,工作距离 300 mm。按小孔成像模型,传感器尺寸 7.2 mm×5.4 mm,视野约 86.4 mm×64.8 mm。同轴环形 LED 光源照度约 15000 lux,背光 LED 光源照度约 20000 lux。被测圆柱滚子直径 10 mm,要求检出最小 0.05 mm 的直径偏差。用本篇参数复算像素当量:水平方向 86.4 mm ÷ 1600 px = 0.054 mm/px,垂直方向 64.8 mm ÷ 1200 px = 0.054 mm/px,两个方向自洽;0.05 mm 偏差对应约 0.93 像素,这意味着后续尺寸测量必须达到亚像素精度。"先离线仿真,用程序生成两张不同光照条件的测试图——同轴光保留表面纹理和色块,背光模拟真实剪影成像(亮背景、暗工件),同时读取后分窗口展示,用
"moveWindow" 把窗口摆到屏幕左右两侧,再用 matplotlib 拼成一张对比大图,把对比实验的流程跑通。"
⚠️ 声明:本代码仅用于课程离线仿真,运行在普通 PC 上。不包含任何真实工业相机的 SDK、GigE 或 USB3 Vision 驱动。不可直接用于产线视觉系统。
效率说明:开篇场景为 1600×1200 的图像,但本篇为演示效率,仿真使用 400×300 的程序生成图像代替真实照片。相机内参、光源参数、零件参数在本篇仅用于打印说明,不参与成像计算——仿真图像由程序内置生成器直接合成。这些参数为后续篇章的标定与测量做铺垫,请勿误认为仿真结果与焦距、光圈等物理量存在数值关联。本篇以仿真图像替代实拍图,专注演示同时读取两张图片并分窗口展示的完整流程。
2. 痛点分析
课程模块 本篇对应的工程痛点
第1章 数字图像基础 → 多图像读取与显示 同时读取两张图时,默认窗口名冲突、窗口位置由系统随意摆放,导致无法区分图像来源与处理阶段
第1章 数字图像基础 → 可视化与对比实验 多窗口独立弹出不便于并排对比,缺少统一的画布拼接方案
第1章 数字图像基础 → 文件系统与批量处理 批量读取多张图时,中文路径、文件顺序跨 10 边界等问题需要统一处理
本篇代码解决 L1 离线仿真校核层。输入:程序自动生成的两张仿真图片(同轴光、背光条件)。处理:同时读取、分窗口展示(含屏幕坐标定位)、画布拼接、属性对比。输出:4 张图表 + 单元测试。
不覆盖:工业相机 SDK 实时取流、GigE Vision 协议、USB3 Vision 协议、硬件触发采集、亚像素边缘检测算法本身。
3. 核心逻辑讲解
物理直觉
工业视觉的对比实验本质上是在控制变量——除了光源不同,其他一切条件都必须相同。工程师需要把两张图放在同一个参照系里看:同样的缩放比例、同样的坐标轴、同样的亮度范围。如果两个窗口各自独立弹在屏幕中央叠在一起,人眼会被窗口位置、亮度自适应等因素干扰,看到的"差异"可能根本不是光源造成的,而是显示方式造成的。
同时读取两张图片并分窗口展示,核心是把"两张独立的图"变成"一组配对的数据"。每张图都有自己的身份标签(光源类型、分辨率、像素统计),配对后还有联合分析(差异图、直方图叠加、灰度剖面并排)。这样工程师看到的就不是两张孤立的像素矩阵,而是一组可量化的对比实验报告。
业务逻辑 → 代码映射
输入:两张仿真图片文件路径(同轴光、背光条件)
↓
【步骤1:程序自动生成两张不同光照条件的测试图片并保存】
同轴光:蓝色渐变背景 + 高对比度色块,模拟表面纹理可见
背光:亮灰色背景 + 中心暗色剪影,模拟真实背光成像(亮背景、暗工件)
↓
【步骤2:同时读取两张图片(独立校验,非短路)】
np.fromfile → cv2.imdecode → 判空 → 维度校验 → 类型校验
两张图各 try 一遍,汇总 (success, result) 报告,一张失败不影响另一张的错误定位
↓
【步骤3:OpenCV 分窗口展示(真并列)】
两个独立窗口,窗口名包含光源类型和分辨率信息
用 cv2.moveWindow 将窗口摆到屏幕左右两侧(x=100 和 x=500)
↓
【步骤4:matplotlib 画布拼接对比】
原图并排 / 灰度图并排 / 直方图叠加 / 绝对差异热力图
↓
【步骤5:输出 4 张图表 + 单元测试】
关键公式文字翻译
像素当量换算公式:
"像素当量 = 视野尺寸 ÷ 像素数"。每一项物理含义:视野尺寸是相机能拍到的实际物理范围(单位 mm),像素数是对应方向上的图像像素总量。两者相除得到每个像素代表的物理长度。对于本篇参数,水平方向 86.4 mm ÷ 1600 px = 0.054 mm/px,垂直方向 64.8 mm ÷ 1200 px = 0.054 mm/px,两个方向一致,说明像素是正方形的。0.05 mm 的直径偏差对应约 0.93 像素,这意味着后续外径测量算法必须达到亚像素精度。
绝对差异图公式:
"Diff = |Image_A - Image_B|"。每一项物理含义:对两张图对应位置的像素值逐像素做差的绝对值。结果图中接近 0 的像素表示两张图在该位置几乎一致,值越大表示差异越显著。差异图把"肉眼难以察觉的微小差异"放大成人眼可以直接看到的亮度信号,是工业视觉对比实验中最直观的量化工具。计算时必须使用
"cv2.absdiff" 而非 numpy 减法,因为 uint8 类型下
"a - b" 在
"a < b" 时会回绕到 255,产生错误结果。
4. OOP 代码实现
技术栈
限定:numpy, opencv-python (cv2>=4.8.0), matplotlib。不引入重型框架。
项目结构树
dual_image_read/
├── run_sim.py # 主入口(完整可运行)
├── requirements.txt # 依赖版本声明
├── vision/ # 视觉模块
│ ├── __init__.py
│ ├── models.py # dataclass 数据模型(相机/光源/零件参数)
│ ├── common.py # 公共工具(图像校验、属性提取)
│ ├── exceptions.py # 自定义异常体系
│ ├── image_generator.py # 两种光照条件的仿真图片生成
│ ├── image_reader.py # 图片读取器(支持独立校验的批量读取)
│ ├── dual_display.py # 双图分窗口展示(含 moveWindow 真并列)
│ ├── visualize.py # matplotlib 4 张对比图表
│ └── metrics.py # 属性校验指标(含 absdiff 差异图)
├── tests/
│ ├── __init__.py
│ └── test_integration.py # 3 类 11 个测试(完整源码)
├── data/
│ ├── coaxial_light.png # 同轴光仿真图片
│ └── backlight.png # 背光仿真图片(剪影成像)
└── results/ # 4 张图表输出目录
核心源码
"vision/models.py"
"""
Data models for dual image read simulation.
# OFFLINE SIMULATION ONLY — not for production vision systems.
"""
from dataclasses import dataclass
from typing import Tuple
@dataclass
class CameraIntrinsics:
"""
工业相机内参(本篇仅用于打印说明,不参与成像计算)。
resolution 统一约定为 (width, height),单位为像素。
"""
resolution: Tuple[int, int] # (width, height) 像素
pixel_size_um: float = 4.5 # 像元尺寸 μm
focal_length_mm: float = 25.0 # 焦距 mm
def pixel_equivalent_mm_per_px(self, working_distance_mm: float) -> float:
"""
计算像素当量(mm/px)。
使用水平方向计算:像素当量 = 传感器宽度 × (工作距离/焦距) ÷ 水平像素数。
传感器宽度 = 像元尺寸(μm→mm) × 水平像素数;
视野宽度 = 传感器宽度 × (工作距离 / 焦距)。
"""
if working_distance_mm <= 0:
raise ValueError("working_distance_mm must be positive")
sensor_w_mm = self.pixel_size_um * 1e-3 * self.resolution[0]
fov_w_mm = sensor_w_mm * (working_distance_mm / self.focal_length_mm)
return fov_w_mm / self.resolution[0]
def __post_init__(self):
w, h = self.resolution
if w <= 0 or h <= 0:
raise ValueError(f"Resolution must be positive, got {self.resolution}")
if self.pixel_size_um <= 0:
raise ValueError("pixel_size_um must be positive")
if self.focal_length_mm <= 0:
raise ValueError("focal_length_mm must be positive")
@dataclass
class LightingParameters:
"""光源参数(本篇仅用于打印说明,不参与成像计算)。"""
coaxial_lux: float = 15000.0 # 同轴环形光源照度 lux
backlight_lux: float = 20000.0 # 背光源照度 lux
position: str = "coaxial / backlight"
def __post_init__(self):
if self.coaxial_lux <= 0 or self.backlight_lux <= 0:
raise ValueError("illuminance values must be positive")
@dataclass
class PartParameters:
"""被测零件参数(本篇仅用于打印说明,不参与成像计算)。"""
diameter_mm: float = 10.0 # 滚子直径 mm
min_deviation_mm: float = 0.05 # 最小检出偏差 mm
def __post_init__(self):
if self.diameter_mm <= 0:
raise ValueError("diameter_mm must be positive")
if self.min_deviation_mm <= 0:
raise ValueError("min_deviation_mm must be positive")
"vision/exceptions.py"
"""
Custom exception hierarchy for image reading errors.
分层异常体系,让每一类读取失败都有独立的异常类型。
# OFFLINE SIMULATION ONLY — not for production vision systems.
"""
class ImageReadError(Exception):
"""图像读取异常的基类。"""
pass
class FileNotFoundReadError(ImageReadError):
"""文件不存在。"""
pass
class EmptyFileReadError(ImageReadError):
"""文件为空(0 字节)。"""
pass
class FileIOReadError(ImageReadError):
"""
文件 I/O 错误(权限不足、磁盘故障、文件被锁等)。
与 FileNotFoundReadError 区分,避免把权限问题误报为文件不存在。
"""
pass
class DecodeFailedReadError(ImageReadError):
"""文件字节流无法被解码为图像(损坏或格式不支持)。"""
pass
class InvalidShapeReadError(ImageReadError):
"""图像维度或通道数不符合预期(如非 3 通道彩色图)。"""
pass
class InvalidDtypeReadError(ImageReadError):
"""图像数据类型不符合预期(如非 uint8)。"""
pass
"vision/common.py"
"""
Common utilities shared across the series.
统一图像校验、属性提取等公共逻辑。
# OFFLINE SIMULATION ONLY — not for production vision systems.
"""
import os
import numpy as np
from vision.exceptions import InvalidShapeReadError, InvalidDtypeReadError
def validate_color_image(image: np.ndarray, name: str = "image"):
"""
统一彩色图像校验:类型、维度、通道数、数据类型。
分层抛出语义明确的异常,防止 None 或不合法数组往下传递。
"""
if not isinstance(image, np.ndarray):
raise TypeError(f"{name} must be numpy array, got {type(image).__name__}")
if image.ndim != 3 or image.shape[2] != 3:
raise InvalidShapeReadError(
f"{name} must be 3-channel color (H,W,3), got shape {image.shape}"
)
if image.dtype != np.uint8:
raise InvalidDtypeReadError(
f"{name} must be uint8, got {image.dtype}"
)
def validate_same_shape(img_a: np.ndarray, img_b: np.ndarray):
"""
校验两张图尺寸一致,对比实验要求相同视野和分辨率。
尺寸不同直接抛出 ValueError,不让错误静默传播。
"""
if img_a.shape != img_b.shape:
raise ValueError(
f"Images must have same shape for comparison, "
f"got {img_a.shape} vs {img_b.shape}"
)
def ensure_dir(path: str):
"""
安全创建目录。
对纯文件名(dirname 为空)不做任何操作,避免 os.makedirs('') 崩溃。
"""
d = os.path.dirname(path)
if d:
os.makedirs(d, exist_ok=True)
def extract_pixel_properties(img: np.ndarray) -> dict:
"""
提取图像的基础像素属性。
统一处理彩色(3维)图像,无冗余分支。
"""
validate_color_image(img, "img")
h, w, c = img.shape
return {
'ndim': img.ndim,
'height': h,
'width': w,
'channels': c,
'shape': img.shape,
'total_pixels': h * w,
'dtype': str(img.dtype),
'memory_bytes': img.nbytes,
'strides': img.strides,
}
"vision/image_generator.py"
"""
Simulated test image generator for dual-light conditions.
生成两种光照条件的仿真彩色测试图片。
同轴光:蓝色渐变背景 + 高对比度色块,模拟表面纹理可见。
背光:亮灰色背景 + 中心暗色剪影,模拟真实背光成像(亮背景、暗工件)。
使用 numpy 向量化生成,避免逐列 Python 循环。
# OFFLINE SIMULATION ONLY — not for production vision systems.
"""
import os
import cv2
import numpy as np
from vision.common import ensure_dir
class TestImageGenerator:
"""
生成两种光照条件的仿真测试图片。
同轴光保留表面纹理和色块细节,背光模拟真实剪影成像。
"""
def __init__(self, height: int = 300, width: int = 400):
if height <= 0 or width <= 0:
raise ValueError("height and width must be positive")
self.height = height
self.width = width
# 工件剪影的椭圆参数(位于图像中央)
self._center = (width // 2, height // 2)
self._axes = (width // 4, height // 3)
def generate_coaxial(self) -> np.ndarray:
"""
生成同轴光条件下的测试图片(BGR 格式)。
蓝色渐变背景模拟同轴环形光在金属表面的反射,
高对比度红/绿/蓝/白色块模拟滚子表面的不同反射区域。
表面纹理可见,用于验证光照均匀性和颜色还原。
"""
img = np.zeros((self.height, self.width, 3), dtype=np.uint8)
# 蓝色渐变背景(B 通道从 50 到 150)
b_gradient = np.round(
np.linspace(50, 150, self.width)
).astype(np.uint8)[None, :]
img[:, :, 0] = b_gradient
img[:, :, 1] = 30
img[:, :, 2] = 30
# 红色色块 (B=0, G=0, R=255)
img[50:150, 50:150] = [0, 0, 255]
# 绿色色块 (B=0, G=255, R=0)
img[50:150, 200:300] = [0, 255, 0]
# 蓝色彩块 (B=255, G=0, R=0)
img[200:300, 50:150] = [255, 0, 0]
# 白色区域 (B=255, G=255, R=255)
img[200:300, 200:300] = [255, 255, 255]
return img
def generate_backlight(self) -> np.ndarray:
"""
生成背光条件下的测试图片(BGR 格式)。
模拟真实背光成像:亮灰色均匀背景(背光源透过),
中心暗色椭圆剪影模拟不透明的被测滚子遮挡光线。
这是背光条件的物理本质——工件呈暗剪影、背景亮,
而非简单的"光照不足导致色块变暗"。
"""
# 亮灰色均匀背景(背光源)
img = np.full((self.height, self.width, 3), 200, dtype=np.uint8)
# 在中心绘制暗色椭圆剪影(模拟滚子遮挡背光)
# 使用 cv2.ellipse 填充,颜色为暗灰接近黑色
cv2.ellipse(
img,
self._center, # 中心坐标
self._axes, # 长轴、短轴
0, # 旋转角度
0, # 起始角度
360, # 终止角度
(40, 40, 40), # 暗色 BGR
-1 # 填充
)
return img
def save_coaxial(self, filepath: str) -> str:
"""
保存同轴光仿真图片。
使用 imencode+tofile 兜底中文路径,与读取侧对称处理。
"""
ensure_dir(filepath)
img = self.generate_coaxial()
_, buf = cv2.imencode('.png', img)
buf.tofile(filepath)
return filepath
def save_backlight(self, filepath: str) -> str:
"""
保存背光仿真图片。
使用 imencode+tofile 兜底中文路径,与读取侧对称处理。
"""
ensure_dir(filepath)
img = self.generate_backlight()
_, buf = cv2.imencode('.png', img)
buf.tofile(filepath)
return filepath
"vision/image_reader.py"
"""
Image reader with robust error handling.
封装图片读取逻辑,使用 np.fromfile + cv2.imdecode 兜底中文路径。
# OFFLINE SIMULATION ONLY — not for production vision systems.
"""
import os
import cv2
import numpy as np
from vision.common import validate_color_image
from vision.exceptions import (
FileNotFoundReadError,
EmptyFileReadError,
FileIOReadError,
DecodeFailedReadError,
)
class ImageReader:
"""
图片读取器。
支持单张读取和双图独立校验读取。
双图读取时两张图各 try 一遍,汇总报告,
一张失败不影响另一张的错误定位(非短路求值)。
"""
def read(self, filepath: str) -> np.ndarray:
"""
读取单张彩色图片。
分层校验:文件存在 → I/O 成功 → 非空 → 解码成功 → 维度/类型合法。
"""
if not isinstance(filepath, str):
raise TypeError("filepath must be string")
if not os.path.isfile(filepath):
raise FileNotFoundReadError(f"File not found: {filepath}")
try:
file_bytes = np.fromfile(filepath, dtype=np.uint8)
except OSError as e:
raise FileIOReadError(
f"I/O error reading file (permission denied, disk error, etc.): {filepath}"
) from e
if file_bytes.size == 0:
raise EmptyFileReadError(f"File is empty (0 bytes): {filepath}")
img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
if img is None:
raise DecodeFailedReadError(
f"Failed to decode image (corrupted or unsupported format): {filepath}"
)
validate_color_image(img, "decoded image")
return img
def try_read(self, filepath: str) -> tuple:
"""
尝试读取单张图片,返回 (success: bool, result: np.ndarray | str)。
成功返回 (True, img),失败返回 (False, error_message)。
仅捕获 ImageReadError,不吞掉 KeyboardInterrupt 等系统信号
或调用方自身的编程错误(如 TypeError),保证异常语义清晰。
"""
try:
img = self.read(filepath)
return True, img
except ImageReadError as e:
return False, str(e)
def read_dual_independent(self, path_a: str, path_b: str) -> dict:
"""
独立校验读取两张图片,返回结果字典。
两张图各 try 一遍,汇总 (success, result) 报告——
即使 path_a 失败,path_b 仍会被尝试,实现真正的"独立校验"。
调用方根据返回的字典决定后续流程。
"""
result_a = self.try_read(path_a)
result_b = self.try_read(path_b)
return {
'path_a': path_a,
'result_a': result_a,
'path_b': path_b,
'result_b': result_b,
}
"vision/dual_display.py"
"""
OpenCV dual-window display with side-by-side positioning.
分窗口展示两张图片,用 cv2.moveWindow 将窗口摆到屏幕左右两侧,
实现真正的并列展示而非依赖操作系统随意摆放。
# OFFLINE SIMULATION ONLY — not for production vision systems.
"""
import cv2
import numpy as np
from vision.common import validate_color_image, validate_same_shape
class DualDisplay:
"""
双图分窗口展示器。
两个窗口名包含光源类型和分辨率信息,
用 cv2.moveWindow 指定屏幕坐标,确保窗口并排不重叠。
"""
def __init__(self, window_name_a: str, window_name_b: str,
window_width: int = 800, window_height: int = 600,
screen_x_a: int = 100, screen_x_b: int = 500):
if not window_name_a or not window_name_b:
raise ValueError("window names must be non-empty")
if window_width <= 0 or window_height <= 0:
raise ValueError("window dimensions must be positive")
self.window_name_a = window_name_a
self.window_name_b = window_name_b
self.window_width = window_width
self.window_height = window_height
self.screen_x_a = screen_x_a
self.screen_x_b = screen_x_b
def show_side_by_side(self, img_a: np.ndarray, img_b: np.ndarray,
wait_ms: int = 3000):
"""
分两个窗口并列展示两张图片。
用 cv2.moveWindow 将窗口摆到屏幕左右两侧,
确保不重叠——这才是真正的"并列",而非依赖系统默认位置。
"""
validate_color_image(img_a, "img_a")
validate_color_image(img_b, "img_b")
validate_same_shape(img_a, img_b)
# 创建窗口并指定大小
cv2.namedWindow(self.window_name_a, cv2.WINDOW_NORMAL)
cv2.namedWindow(self.window_name_b, cv2.WINDOW_NORMAL)
cv2.resizeWindow(self.window_name_a, self.window_width, self.window_height)
cv2.resizeWindow(self.window_name_b, self.window_width, self.window_height)
# 关键:用 moveWindow 指定屏幕坐标,实现真正的并列
# 窗口 A 放在左侧,窗口 B 放在右侧(x 坐标错开)
cv2.moveWindow(self.window_name_a, self.screen_x_a, 100)
cv2.moveWindow(self.window_name_b, self.screen_x_b, 100)
cv2.imshow(self.window_name_a, img_a)
cv2.imshow(self.window_name_b, img_b)
cv2.waitKey(wait_ms)
def close_all(self):
"""关闭所有 OpenCV 窗口,确保资源释放。"""
cv2.destroyAllWindows()
"vision/metrics.py"
"""
Pixel property validation metrics for dual image comparison.
校验两张图像的像素属性自洽性,计算绝对差异图。
# OFFLINE SIMULATION ONLY — not for production vision systems.
"""
import numpy as np
from vision.common import validate_color_image, validate_same_shape
class DualImageMetrics:
"""
双图对比校验指标。
验证两张图在尺寸、内存、灰度统计上的一致性,
并提供差异量化方法。
"""
def __init__(self, img_a: np.ndarray, img_b: np.ndarray):
validate_color_image(img_a, "img_a")
validate_color_image(img_b, "img_b")
self.img_a = img_a
self.img_b = img_b
def validate_same_shape(self) -> bool:
"""两张图必须具有相同尺寸,对比实验的基本要求。"""
return self.img_a.shape == self.img_b.shape
def validate_same_memory(self) -> bool:
"""相同尺寸和类型的图像内存占用必须一致。"""
return self.img_a.nbytes == self.img_b.nbytes
def compute_abs_diff(self) -> np.ndarray:
"""
计算两张图的绝对差异图(uint8 安全)。
使用 cv2.absdiff 而非 numpy 减法,因为 uint8 下
a - b 在 a < b 时会回绕到 255,产生错误结果。
cv2.absdiff 内部处理溢出,保证结果正确。
"""
# 先转灰度再算差异,得到单通道差异图
gray_a = cv2.cvtColor(self.img_a, cv2.COLOR_BGR2GRAY)
gray_b = cv2.cvtColor(self.img_b, cv2.COLOR_BGR2GRAY)
return cv2.absdiff(gray_a, gray_b)
def report(self) -> dict:
"""返回双图对比的综合校验报告。"""
return {
'shape_a': self.img_a.shape,
'shape_b': self.img_b.shape,
'same_shape': self.validate_same_shape(),
'memory_a': self.img_a.nbytes,
'memory_b': self.img_b.nbytes,
'same_memory': self.validate_same_memory(),
'dtype_a': str(self.img_a.dtype),
'dtype_b': str(self.img_b.dtype),
}
"vision/visualize.py"
"""
4 visualization charts for dual image comparison.
# OFFLINE SIMULATION ONLY — not for production vision systems.
"""
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
from vision.common import validate_color_image, validate_same_shape
# 本篇图表统一使用英文标题与标签,规避无 GUI 环境下中文字体缺失导致的渲染问题
plt.rcParams['font.sans-serif'] = ['DejaVu Sans', 'Noto Sans', 'Arial']
plt.rcParams['axes.unicode_minus'] = False
class DualImageVisualizer:
"""生成 4 张双图对比分析图表。"""
def __init__(self, results_dir="results"):
self.rd = results_dir
os.makedirs(results_dir, exist_ok=True)
def plot_original_side_by_side(self, img_a: np.ndarray, img_b: np.ndarray,
label_a: str = "Image A",
label_b: str = "Image B"):
"""
图1:原图并排对比(RGB)。
验证两张图读取后 BGR→RGB 颜色转换正确,
并排展示让差异一目了然。
"""
validate_color_image(img_a, "img_a")
validate_color_image(img_b, "img_b")
rgb_a = cv2.cvtColor(img_a, cv2.COLOR_BGR2RGB)
rgb_b = cv2.cvtColor(img_b, cv2.COLOR_BGR2RGB)
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].imshow(rgb_a)
axes[0].set_title(f'{label_a}\n{rgb_a.shape[1]}x{rgb_a.shape[0]}', fontsize=10)
axes[0].set_xlabel('Width (pixels)')
axes[0].set_ylabel('Height (pixels)')
axes[0].grid(False)
axes[1].imshow(rgb_b)
axes[1].set_title(f'{label_b}\n{rgb_b.shape[1]}x{rgb_b.shape[0]}', fontsiz=10)
axes[1].set_xlabel('Width (pixels)')
axes[1].set_ylabel('Height (pixels)')
axes[1].grid(False)
plt.tight_layout()
path = os.path.join(self.rd, "01_original_side_by_side.png")
plt.savefig(path, dpi=150)
plt.close()
return path
def plot_grayscale_co
利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)