python的机器视觉技术与应用工业场景模拟第十三篇:修复有轻微划痕的老旧图像,模糊淡化划痕瑕疵。
《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解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!
更多推荐
所有评论(0)