《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解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐