《Python 机器视觉技术与应用》第十三篇:修复有轻微划痕的老旧图像

 

声明:本篇全部为课程离线仿真代码,不能直接用于真实视觉检测系统,规避工程误导风险。

环境版本:本篇验证环境为 

"opencv-python>=4.8.0",所有数值结果基于该版本复现。

 

1. 实际工业应用场景

 

上午九点,汽车零部件厂的来料检验工位。工程师小陈从档案柜里抽出一张2015年拍摄的轴承密封圈表面照片——这是当年供应商首批试产件的视觉检测存档。现在客户要求追溯这批零件的表面缺陷,需要重新分析这张老照片。

 

照片表面横跨三道浅灰色划痕,宽度约2~4像素,灰度值比周围背景只高15~25。小陈把图像导入检测软件,阈值分割的结果里,划痕和密封圈表面的正常纹理混在一起,连边缘检测都分不清哪些是真实缺陷、哪些是划痕造成的伪边缘。他试着用高斯模糊,划痕倒是淡了,可密封圈上0.3 mm宽的微裂纹也跟着糊成一团。

 

"老照片的划痕不能靠模糊硬压。"老周把椅子滑过来,指着屏幕,"划痕是局部窄区域的灰度异常,和周围正常纹理在频域上不一样。先用形态学梯度把划痕的线状结构强化出来,再用图像修复算法,让算法沿着纹理方向'猜'出划痕下面原本应该是什么。模糊是均匀抹,修复是定向填。"

 

小陈调出参数表:200万像素全局快门CMOS,分辨率1600×1200,像元尺寸4.5 μm,镜头焦距25 mm,光圈F4,工作距离300 mm,视野范围约216 mm×162 mm。环形白色LED光源,照度约15000 lux,安装在镜头同轴位置。被测密封圈外径80 mm,内径60 mm,表面划痕宽度约0.05~0.1 mm,对应图像上2~4像素。要求检出最小0.1 mm宽的真实缺陷,划痕造成的伪缺陷误报率需控制在5%以下。

 

"先离线仿真一组带划痕的密封圈表面图像,验证修复算法能不能把划痕淡化的同时保住真实缺陷。"

 

⚠️ 声明:本代码仅用于课程离线仿真,运行在普通PC上。不包含任何真实工业相机的SDK、GigE或USB3 Vision驱动。不可直接用于产线视觉系统。

 

效率说明:开篇场景为1600×1200的图像,但本篇为演示效率,仿真使用400×300的模拟图像代替真实分辨率。相机内参、光源参数、零件参数在本篇仅用于打印说明,不参与成像计算——仿真图像由程序内置生成器直接合成。这些参数为后续篇章的标定与测量做铺垫,请勿误认为仿真结果与焦距、光圈等物理量存在数值关联。本篇以仿真图像替代实拍图,不涉及

"cv2.imread"读取外部图片。

 

2. 痛点分析

 

课程模块 本篇对应的工程痛点

第1章 数字图像基础 → 图像退化与噪声 老旧图像同时存在划痕(结构性退化)和高斯噪声(随机性退化),单一滤波无法兼顾

第2章 图像增强与滤波 → 形态学处理 划痕呈窄线状,需要形态学操作分离线状结构与面状纹理

第2章 图像增强与滤波 → 图像修复 划痕区域需要定向填充,而非均匀模糊,避免损伤真实缺陷边缘

 

本篇代码解决 L1 离线仿真校核层。输入:模拟生成的带划痕老旧灰度图像(含背景纹理、线状划痕、高斯噪声)。处理:形态学梯度提取划痕 → NL-means预去噪 → 基于TELEA算法的图像修复。输出:4张图表 + 单元测试。

 

不覆盖:基于深度学习的图像修复(如PatchMatch、扩散模型)、多尺度Retinex、频域带阻滤波。

 

3. 核心逻辑讲解

 

物理直觉

 

老旧图像上的划痕就像一张纸上被指甲划出的浅痕——它只改变了局部很少几个像素的灰度,但人眼和算法都会沿着这条线"脑补"出一条连续的边界。直接模糊相当于把整张纸揉皱再展平,划痕没了,纸上的字也糊了。

 

正确的思路分两步。第一步,把划痕"描"出来:划痕是又细又长的线,用形态学梯度(膨胀减腐蚀)可以把窄线状结构强化,因为膨胀会让线变粗、腐蚀会让线变细,两者相减刚好留下线本身。第二步,把描出来的区域"填"回去:OpenCV的

"cv2.inpaint"实现的是基于快速行进(FMM)的修复算法,其中TELEA(Telea算法)会从划痕边界开始,沿着等照度线方向向外传播已知区域的像素信息,用边界周围的加权平均值填充划痕区域。它不像高斯模糊那样无差别地抹,而是"看周围像什么就填什么",所以能保住真实的缺陷边缘。

 

需要注意的是,划痕本身在图像中表现为结构性的灰度异常,而非随机噪声。非局部均值(NL-means)的目标是去除随机噪声,对结构性的划痕响应有限。因此本篇将NL-means作为预去噪步骤,先降低背景噪声干扰,再使用

"cv2.inpaint"做真正的划痕填充修复。

 

业务逻辑 → 代码映射

 

输入:模拟带划痕老旧灰度图像(400×300,uint8)

  ↓

【步骤1:生成仿真图像(固定随机种子)】

  密封圈表面纹理(分形噪声模拟) + 3道线状划痕(numpy切片绘制)

  + 高斯噪声(模拟传感器噪声)

  ↓

【步骤2:形态学梯度提取划痕】

  3×3十字结构元 → 膨胀 - 腐蚀 → 阈值分割 → 开运算去噪 → 连通域长宽比筛选

  ↓

【步骤3:NL-means预去噪 + TELEA修复】

  先对整图做NL-means去噪 → 再用cv2.inpaint以掩码为输入做inpainting

  ↓

【步骤4:修复质量评估】

  划痕区域灰度差异、全图结构相似性(SSIM)、差分法噪声估计

  ↓

【步骤5:输出4张图表 + 单元测试】

 

关键公式文字翻译

 

形态学梯度:膨胀结果减去腐蚀结果。膨胀用结构元覆盖区域内的最大值替换每个像素,腐蚀用最小值替换,两者相减得到的是"局部灰度变化剧烈"的区域——正好是划痕这种窄线状边缘的响应。

 

连通域长宽比筛选:对每个二值连通域,计算其最小外接矩形的宽度和高度,取长边与短边的比值。划痕是细长结构,长宽比远大于1;而纹理噪声产生的孤立响应长宽比接近1。通过设定长宽比阈值(如>3),可以滤除大部分误检的纹理边缘。

 

TELEA修复权重:算法从划痕边界像素开始,按等照度线方向向外传播。每个已知像素对边界待修复像素的贡献权重与其距离(空间距离和灰度梯度距离)成反比,距离越近、灰度越相似的点权重越大。这保证了填充的像素与周围纹理在视觉上连续。

 

全图SSIM:结构相似性指数从亮度、对比度、结构三个方面衡量两幅图像的相似度,取值范围[-1,1],越接近1表示越相似。对全图(而非仅背景)计算SSIM,可以衡量修复对整幅图像结构信息的保持程度——如果仅对背景计算,由于背景像素未被修改,SSIM将恒为1,无法反映修复效果。

 

差分法噪声估计:用相邻列像素差值的绝对值的标准差除以√2来估计噪声。这天然消除了背景渐变对噪声估计的污染,与第十一篇、第十二篇的方法一致。

 

4. OOP代码实现

 

技术栈

 

限定:numpy, opencv-python (cv2>=4.8.0), matplotlib。不引入重型框架。

 

项目结构树

 

scratch_restoration/

├── run_sim.py # 主入口

├── requirements.txt # 依赖版本声明

├── vision/ # 视觉模块

│ ├── __init__.py

│ ├── models.py # dataclass数据模型

│ ├── common.py # 公共工具(图像校验)

│ ├── image_generator.py # 模拟带划痕老旧图像生成

│ ├── restoration.py # 形态学梯度提取 + NL-means + inpainting

│ ├── visualize.py # 4张图表

│ └── metrics.py # 修复质量指标

├── tests/

│ ├── __init__.py

│ └── test_integration.py # 3类10个测试

└── results/ # 4张图表输出目录

 

核心源码

 

"vision/models.py"

 

"""

Data models for scratch restoration simulation.

# OFFLINE SIMULATION ONLY — not for production vision systems.

"""

 

from dataclasses import dataclass

from typing import Tuple

 

 

@dataclass

class CameraIntrinsics:

    """工业相机内参(本篇仅用于打印说明,不参与成像计算)。"""

    resolution: Tuple[int, int] # (width, height) 像素

    pixel_size_um: float = 4.5 # 像元尺寸 μm

    focal_length_mm: float = 25.0 # 焦距 mm

 

    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:

    """光源参数(本篇仅用于打印说明,不参与成像计算)。"""

    illuminance_lux: float = 15000.0 # 环形光源照度 lux

    position: str = "coaxial" # 同轴安装

 

    def __post_init__(self):

        if self.illuminance_lux <= 0:

            raise ValueError("illuminance_lux must be positive")

 

 

@dataclass

class PartParameters:

    """被测零件参数(本篇仅用于打印说明,不参与成像计算)。"""

    outer_diameter_mm: float = 80.0 # 密封圈外径 mm

    inner_diameter_mm: float = 60.0 # 密封圈内径 mm

    min_defect_width_mm: float = 0.1 # 最小检出缺陷宽度 mm

    scratch_width_mm: float = 0.08 # 典型划痕宽度 mm

 

    def __post_init__(self):

        if self.outer_diameter_mm <= self.inner_diameter_mm:

            raise ValueError("outer_diameter must exceed inner_diameter")

        if self.min_defect_width_mm <= 0:

            raise ValueError("min_defect_width_mm must be positive")

        if self.scratch_width_mm <= 0:

            raise ValueError("scratch_width_mm must be positive")

 

 

@dataclass

class RestorationParameters:

    """修复算法参数。"""

    gradient_kernel_size: int = 3 # 形态学梯度核尺寸(奇数)

    scratch_threshold: int = 20 # 划痕掩码阈值(降低以匹配15~25的划痕增量)

    morph_open_kernel: int = 3 # 开运算核尺寸

    min_aspect_ratio: float = 3.0 # 连通域最小长宽比(滤除噪声点)

    nlm_h: float = 15.0 # NL-means滤波强度(浮点)

    nlm_template_window: int = 7 # NL-means模板窗口大小(奇数)

    nlm_search_window: int = 21 # NL-means搜索窗口大小(奇数)

    inpaint_radius: int = 3 # inpaint算法邻域半径

 

    def __post_init__(self):

        if self.gradient_kernel_size % 2 == 0 or self.gradient_kernel_size <= 0:

            raise ValueError("gradient_kernel_size must be positive odd integer")

        if self.scratch_threshold <= 0:

            raise ValueError("scratch_threshold must be positive")

        if self.morph_open_kernel % 2 == 0 or self.morph_open_kernel <= 0:

            raise ValueError("morph_open_kernel must be positive odd integer")

        if self.min_aspect_ratio <= 1.0:

            raise ValueError("min_aspect_ratio must exceed 1.0")

        if self.nlm_h <= 0:

            raise ValueError("nlm_h must be positive")

        if self.nlm_template_window % 2 == 0 or self.nlm_template_window <= 0:

            raise ValueError("nlm_template_window must be positive odd integer")

        if self.nlm_search_window % 2 == 0 or self.nlm_search_window <= 0:

            raise ValueError("nlm_search_window must be positive odd integer")

        if self.inpaint_radius <= 0:

            raise ValueError("inpaint_radius must be positive")

 

"vision/common.py"

 

"""

Common utilities shared across the series.

统一图像校验等公共逻辑。

# OFFLINE SIMULATION ONLY — not for production vision systems.

"""

 

import numpy as np

 

 

def validate_grayscale_image(image: np.ndarray, name: str = "image"):

    """

    统一灰度图像校验:类型、维度、值域。

    """

    if not isinstance(image, np.ndarray):

        raise TypeError(f"{name} must be numpy array")

    if image.ndim != 2:

        raise ValueError(f"{name} must be 2D grayscale, got {image.ndim}D")

    if image.dtype != np.uint8:

        raise ValueError(f"{name} must be uint8, got {image.dtype}")

 

"vision/image_generator.py"

 

"""

Simulated scratched vintage image generator.

生成模拟带划痕的老旧灰度图像:密封圈表面纹理 + 线状划痕 + 高斯噪声。

划痕使用numpy切片绘制,包含1像素和2~3像素宽两种,匹配场景参数。

# OFFLINE SIMULATION ONLY — not for production vision systems.

"""

 

import cv2

import numpy as np

 

 

class ScratchedImageGenerator:

    """

    模拟生成带划痕的老旧灰度图像。

    使用显式随机数生成器,避免依赖全局随机状态。

    """

    def __init__(self, height: int = 300, width: int = 400,

                 noise_std: float = 8.0,

                 rng: np.random.Generator = None):

        self.height = height

        self.width = width

        self.noise_std = noise_std

        self.rng = rng if rng is not None else np.random.default_rng()

 

    def _fractal_noise(self, shape: tuple, octaves: int = 4) -> np.ndarray:

        """

        生成分形噪声模拟密封圈表面纹理。

        多倍频叠加产生具有自然粗糙感的背景,避免纯高斯模糊造成的

        不真实均匀纹理。cv2.resize 使用双线性上采样产生平滑随机场。

        """

        noise = np.zeros(shape, dtype=np.float32)

        amplitude = 1.0

        total_amp = 0.0

 

        for octave in range(octaves):

            freq_h = 1 << octave

            freq_w = 1 << octave

            # 生成低频随机场并上采样到目标尺寸

            h = max(2, self.height // freq_h)

            w = max(2, self.width // freq_w)

            base = self.rng.standard_normal((h, w)).astype(np.float32)

            scaled = cv2.resize(base, (self.width, self.height),

                                interpolation=cv2.INTER_LINEAR)

            noise += amplitude * scaled

            total_amp += amplitude

            amplitude *= 0.5

 

        noise /= (total_amp + 1e-8)

        return noise

 

    def _draw_thick_line(self, img: np.ndarray,

                          x0: int, y0: int, x1: int, y1: int,

                          thickness: int, intensity: int):

        """

        用numpy切片绘制指定厚度的直线。

        通过遍历直线上的每个像素点及其邻域来模拟线宽,

        避免cv2.line在浮点图上的兼容性问题。

        """

        steps = max(abs(x1 - x0), abs(y1 - y0))

        if steps == 0:

            return

        x_step = (x1 - x0) / steps

        y_step = (y1 - y0) / steps

        half = thickness // 2

 

        for i in range(steps + 1):

            x = int(round(x0 + i * x_step))

            y = int(round(y0 + i * y_step))

            # 在(x,y)周围填充thickness×thickness的方形邻域

            for dy in range(-half, half + 1):

                for dx in range(-half, half + 1):

                    ny, nx = y + dy, x + dx

                    if 0 <= ny < self.height and 0 <= nx < self.width:

                        new_val = min(255, int(img[ny, nx]) + intensity)

                        img[ny, nx] = new_val

 

    def generate(self) -> np.ndarray:

        """

        生成模拟带划痕的老旧灰度图像。

        返回:uint8灰度图像,shape=(height, width),值0~255。

        """

        # 分形噪声作为密封圈表面纹理基底

        texture = self._fractal_noise((self.height, self.width))

        # 映射到灰度范围 60~180,模拟金属反光表面

        img = 120.0 + 60.0 * texture

        img = np.clip(img, 0, 255).astype(np.uint8)

 

        # 用numpy切片绘制3道线状划痕

        # 划痕1:水平短线(y=100, x=80~200),1像素宽,灰度增量+22

        img[100, 80:200] = np.clip(

            img[100, 80:200].astype(np.int16) + 22, 0, 255

        ).astype(np.uint8)

 

        # 划痕2:垂直短线(x=220, y=120~240),1像素宽,灰度增量+25

        img[120:240, 220] = np.clip(

            img[120:240, 220].astype(np.int16) + 25, 0, 255

        ).astype(np.uint8)

 

        # 划痕3:45°斜线(从(260,60)到(340,140)),2~3像素宽,灰度增量+20

        # 匹配场景中"划痕宽度0.05~0.1mm对应2~4像素"的参数

        self._draw_thick_line(img, 260, 60, 340, 140,

                               thickness=3, intensity=20)

 

        # 添加高斯噪声模拟传感器噪声

        noise = self.rng.normal(0, self.noise_std, img.shape)

        img = (img.astype(np.float32) + noise).clip(0, 255).astype(np.uint8)

 

        return img

 

"vision/restoration.py"

 

"""

Scratch restoration: morphological gradient + NL-means denoising + inpainting.

形态学梯度提取划痕,NL-means预去噪,TELEA算法修复。

# OFFLINE SIMULATION ONLY — not for production vision systems.

"""

 

import cv2

import numpy as np

 

from vision.models import RestorationParameters

from vision.common import validate_grayscale_image

 

 

class ScratchRestorer:

    """

    划痕修复算法。

    步骤:形态学梯度提取划痕掩码 → NL-means预去噪 → TELEA inpainting修复。

    """

    def __init__(self, params: RestorationParameters = None):

        self.params = params if params is not None else RestorationParameters()

 

    def extract_scratch_mask(self, image: np.ndarray) -> np.ndarray:

        """

        用形态学梯度提取划痕区域。

        包含阈值分割、开运算去噪、连通域长宽比筛选三个后处理步骤,

        减少纹理边缘的误检。

        """

        validate_grayscale_image(image, "image")

 

        ksize = self.params.gradient_kernel_size

        # 十字结构元对线状结构敏感,比矩形核更适合提取划痕

        kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (ksize, ksize))

 

        # 膨胀取局部最大值,腐蚀取局部最小值,相减得到梯度

        dilated = cv2.dilate(image, kernel, iterations=1)

        eroded = cv2.erode(image, kernel, iterations=1)

        gradient = cv2.subtract(dilated, eroded)

 

        # 阈值分割得到二值划痕掩码

        _, mask = cv2.threshold(

            gradient, self.params.scratch_threshold, 255, cv2.THRESH_BINARY

        )

 

        # 开运算去除小的孤立噪声点(这些点通常来自纹理而非划痕)

        open_ksize = self.params.morph_open_kernel

        open_kernel = cv2.getStructuringElement(

            cv2.MORPH_RECT, (open_ksize, open_ksize)

        )

        mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, open_kernel)

 

        # 连通域长宽比筛选:滤除近似方形的纹理噪声响应

        # 只保留细长结构(长宽比大于阈值的连通域)

        num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(

            mask, connectivity=8

        )

        filtered_mask = np.zeros_like(mask)

        for label in range(1, num_labels): # label 0 是背景

            # stats格式:[x, y, width, height, area]

            w = stats[label, cv2.CC_STAT_WIDTH]

            h = stats[label, cv2.CC_STAT_HEIGHT]

            if w == 0 or h == 0:

                continue

            aspect_ratio = max(w, h) / float(min(w, h))

            if aspect_ratio >= self.params.min_aspect_ratio:

                filtered_mask[labels == label] = 255

 

        return filtered_mask.astype(np.uint8)

 

    def restore(self, image: np.ndarray) -> np.ndarray:

        """

        执行划痕修复。

        1. NL-means预去噪:降低背景噪声对修复的干扰

        2. cv2.inpaint:以掩码为输入,使用TELEA算法做定向填充修复

        """

        validate_grayscale_image(image, "image")

 

        # 提取划痕掩码

        mask = self.extract_scratch_mask(image)

 

        # NL-means预去噪:降低背景噪声,使修复算法获得更干净的输入

        # 注意:NL-means不负责去除划痕,划痕是结构性信号,需要inpainting处理

        denoised = cv2.fastNlMeansDenoising(

            image,

            None,

            h=self.params.nlm_h,

            templateWindowSize=self.params.nlm_template_window,

            searchWindowSize=self.params.nlm_search_window

        )

 

        # TELEA inpainting:以掩码为输入,从边界开始沿等照度线方向填充

        # 这是真正的"定向填充"修复,而非均匀模糊

        restored = cv2.inpaint(

            denoised, mask,

            self.params.inpaint_radius,

            cv2.INPAINT_TELEA

        )

 

        return restored

 

"vision/metrics.py"

 

"""

Quality metrics for scratch restoration comparison.

评估划痕修复效果:修复差异、全图结构相似性、噪声抑制。

# OFFLINE SIMULATION ONLY — not for production vision systems.

"""

 

import cv2

import numpy as np

 

from vision.common import validate_grayscale_image

 

 

class RestorationMetrics:

    """

    修复质量对比指标。

    """

    def __init__(self, original: np.ndarray, restored: np.ndarray,

                 mask: np.ndarray):

        validate_grayscale_image(original, "original")

        validate_grayscale_image(restored, "restored")

        if mask.shape != original.shape:

            raise ValueError("mask must have same shape as original")

        if mask.dtype != np.uint8:

            raise ValueError("mask must be uint8")

        self.original = original

        self.restored = restored

        self.mask = mask

        # 二值掩码用于区域选择

        self.mask_binary = (mask > 0).astype(bool)

 

    def estimate_noise_std_diff(self, image: np.ndarray) -> float:

        """

        使用相邻像素差分法估计噪声标准差。

        std(diff) / sqrt(2) 天然消除缓变背景污染。

        """

        img_arr = np.asarray(image, dtype=np.float64)

        if img_arr.shape[1] < 2:

            return 0.0

        diff = img_arr[:, 1:] - img_arr[:, :-1]

        return float(np.std(diff) / np.sqrt(2.0))

 

    def scratch_region_difference(self) -> float:

        """

        划痕区域修复前后平均绝对差异。

        衡量修复算法对划痕区域的修改程度——差异越大说明修复越充分。

        """

        if np.sum(self.mask_binary) == 0:

            return 0.0

        diff = np.abs(

            self.restored[self.mask_binary].astype(np.float64) -

            self.original[self.mask_binary].astype(np.float64)

        )

        return float(np.mean(diff))

 

    def full_image_ssim(self) -> float:

        """

        全图结构相似性指数(SSIM)。

        对全图计算,衡量修复对整幅图像结构信息的保持程度。

        若仅对背景计算,由于背景像素未被修改,SSIM将恒为1,无法反映修复效果。

        """

        # 使用OpenCV的SSIM实现(需转换为浮点型)

        orig_f = self.original.astype(np.float64)

        rest_f = self.restored.astype(np.float64)

 

        # 计算均值和方差

        mu_x = cv2.GaussianBlur(orig_f, (11, 11), 1.5)

        mu_y = cv2.GaussianBlur(rest_f, (11, 11), 1.5)

 

        mu_x_sq = mu_x ** 2

        mu_y_sq = mu_y ** 2

        mu_xy = mu_x * mu_y

 

        sigma_x_sq = cv2.GaussianBlur(orig_f ** 2, (11, 11), 1.5) - mu_x_sq

        sigma_y_sq = cv2.GaussianBlur(rest_f ** 2, (11, 11), 1.5) - mu_y_sq

        sigma_xy = cv2.GaussianBlur(orig_f * rest_f, (11, 11), 1.5) - mu_xy

 

        # SSIM常数

        C1 = (0.01 * 255) ** 2

        C2 = (0.03 * 255) ** 2

 

        # SSIM公式

        numerator = (2 * mu_xy + C1) * (2 * sigma_xy + C2)

        denominator = (mu_x_sq + mu_y_sq + C1) * (sigma_x_sq + sigma_y_sq + C2)

 

        ssim_map = numerator / (denominator + 1e-12)

        return float(np.mean(ssim_map))

 

    def report(self) -> dict:

        """返回综合对比报告。"""

        return {

            'scratch_diff': self.scratch_region_difference(),

            'full_ssim': self.full_image_ssim(),

            'noise_original': self.estimate_noise_std_diff(self.original),

            'noise_restored': self.estimate_noise_std_diff(self.restored),

            'scratch_pixel_ratio': float(np.mean(self.mask_binary)),

        }

 

"vision/visualize.py"

 

"""

4 visualization charts for scratch restoration 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_grayscale_image

 

# 设置中文字体,Linux/Mac下若无SimHei会回退到Noto Sans CJK SC

plt.rcParams['font.sans-serif'] = ['SimHei', 'Noto Sans CJK SC', 'DejaVu Sans']

plt.rcParams['axes.unicode_minus'] = False

 

 

class RestorationVisualizer:

    """生成4张对比验证图表。"""

    def __init__(self, results_dir="results"):

        self.rd = results_dir

        os.makedirs(results_dir, exist_ok=True)

 

    def plot_image_comparison(self, original: np.ndarray,

                               restored: np.ndarray,

                               mask: np.ndarray):

        """

        图1:原图、划痕掩码、修复结果三图并排。

        直观对比修复效果。

        """

        for name, img in [("original", original), ("restored", restored)]:

            validate_grayscale_image(img, name)

        if mask.shape != original.shape:

            raise ValueError("mask must have same shape as original")

 

        fig, axes = plt.subplots(1, 3, figsize=(15, 5))

 

        titles = ['原始图像\n(带划痕)', '划痕掩码\n(形态学梯度+筛选)', '修复结果\n(NL-means+TELEA)']

        images = [original, mask, restored]

        cmaps = ['gray', 'gray', 'gray']

 

        for ax, img, title, cmap in zip(axes, images, titles, cmaps):

            ax.imshow(img, cmap=cmap)

            ax.set_title(f'{title}\n{img.shape[1]}×{img.shape[0]} 像素', fontsize=11)

            ax.set_xlabel('宽度 (像素)')

            ax.set_ylabel('高度 (像素)')

            ax.grid(False)

 

        plt.tight_layout()

        path = os.path.join(self.rd, "01_image_comparison.png")

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

    def plot_profile_line(self, original: np.ndarray,

                           restored: np.ndarray,

                           row: int = 100):

        """

        图2:水平灰度剖面对比(穿过划痕1)。

        量化展示划痕处灰度的变化。

        使用竖线标注划痕位置,避免带状标注的误导。

        """

        validate_grayscale_image(original, "original")

        validate_grayscale_image(restored, "restored")

 

        x = np.arange(original.shape[1])

        orig_profile = original[row, :]

        rest_profile = restored[row, :]

 

        fig, ax = plt.subplots(figsize=(10, 5))

        ax.plot(x, orig_profile, label='原始图像', color='red', alpha=0.7, linewidth=1.0)

        ax.plot(x, rest_profile, label='修复结果', color='blue', alpha=0.7, linewidth=1.0)

 

        # 用竖线标注划痕1的实际位置(1像素宽,位于x=80~200之间的第100行)

        ax.axvline(x=80, color='gray', linestyle='--', alpha=0.5, label='划痕1位置')

        ax.axvline(x=2

利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!

Logo

聚焦RDK与具身智能,汇集机器人开发教程、开源案例和技术交流

更多推荐