透视形变(perspective distortion)

本文介绍了透视变形的概念,包括其特点及两种形式:扩展变形与压缩变形,并通过实例解释了距离如何影响变形的程度。

参考:
https://baike.baidu.com/item/%E9%80%8F%E8%A7%86%E7%95%B8%E5%8F%98/228760
https://zh.wikipedia.org/wiki/%E9%80%8F%E8%A7%86%E5%8F%98%E5%BD%A2#/media/File:Camera_focal_length_distance.gif
透视变形指的是一个物体及其周围区域与标准镜头中看到的相比完全不同,由于远近特征的相对比例变化,发生了弯曲或变形。
我们贴上维基的一幅动图直观感受下随着镜头逐渐拉远,物体的形变程序的变化。
这里写图片描述
你会看到的是,镜头逐渐拉远的时候,物体形变的程度越来越轻。
正如我们所能知道的,在同一幅图像中,远处的物体比相同大小的近处物体显得小。由于这一原因,平行的铁轨会随着我们向远处瞭望而显得越来越靠近,直至汇聚成一点。
透视还有另外一种表现,即物体越近,透视效果越强烈。这也和上面动图表达的思想是一样的,刚开始镜头很近时,正方体的一个角会在图像上显示为一个锐角。
具体地,如果有200名士兵排成一纵队正在行进。如果在距离前面士兵10英尺的地方观看或拍摄队伍(即近距离拍摄),那么前面的士兵就会显得比最后的士兵高大得多(景深形变的程度是不一样的,景深越深,这种变化的对比就比较强烈)。但是,如果在远离前面士兵100米的地方观看或拍摄同一支队伍,第一个和最后一个士兵之间的大小差异就不会显得那么大(这种变化的对比就比较小)。
这里写图片描述
也就是说,随着被摄体的越来越远,透视变形会变得越来越小;但却开始变得扁平。相距很远的两个被摄体却显得像一个在另一个之上似的。

透视形变的特点

通过上面的描述,我们应该能大致看出透视形变的特点。
1.被摄体越远,显得越小;
2.镜头离被摄体越远,被摄体外观上的大小变化越小。即镜头离被摄体越远,那么被摄体的形变就越小。

透视形变的形式

透视变形有两种形式:扩展变形(extension distortion)和压缩变形(compression distortion)。
扩展变形(广角失真)可以看作是用广角镜头(视角比标准镜头广)近拍得到的图像。离镜头近的物体与远处物体相比显得比正常尺寸大,而远处物体显得比正常尺寸小而且远——所以距离被扩展了。
压缩变形(长焦失真)可以看作是用长焦镜头(视角比标准镜头窄)在远处拍摄到的图像。物体无论远近看起来大小大致相同——较近的物体显得比正常尺寸小,而较远的物体显得比正常尺寸大,这样便无法区分远近物体的距离——所以距离被压缩了。
虽然上面的阐述引入了2个不同的镜头,好像透视形变的2种形式和特定的镜头有关。但实际上,要注意透视变形是由距离引起的,而非镜头——在同一距离,拍摄同一场景,无论用什么镜头,拍到的透视变形都是完全相同的

import cv2 import numpy as np import os import glob import sys import math # -------------------------- 1. 环境准备与参数设定 -------------------------- # 创建失真图保存目录 output_dir = "distorted_images" if not os.path.exists(output_dir): os.makedirs(output_dir) # 自定义SSIM计算函数(使用OpenCV实现) def compute_ssim(img1, img2, win_size=11, data_range=255): """ 使用OpenCV实现的结构相似性指数(SSIM)计算 替代scikit-image的SSIM实现 """ # 确保图像是单通道灰度图 if len(img1.shape) > 2: img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) if len(img2.shape) > 2: img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) # 转换图像为浮点类型 img1 = img1.astype(np.float64) img2 = img2.astype(np.float64) # SSIM常量 C1 = (0.01 * data_range) ** 2 C2 = (0.03 * data_range) ** 2 # 计算均值 kernel = cv2.getGaussianKernel(win_size, 1.5) kernel = np.outer(kernel, kernel.transpose()) mu1 = cv2.filter2D(img1, -1, kernel) mu2 = cv2.filter2D(img2, -1, kernel) # 计算方差和协方差 mu1_sq = mu1 ** 2 mu2_sq = mu2 ** 2 mu1_mu2 = mu1 * mu2 sigma1_sq = cv2.filter2D(img1 ** 2, -1, kernel) - mu1_sq sigma2_sq = cv2.filter2D(img2 ** 2, -1, kernel) - mu2_sq sigma12 = cv2.filter2D(img1 * img2, -1, kernel) - mu1_mu2 # 计算SSIM ssim_map = ((2 * mu1_mu2 + C1) * (2 * sigma12 + C2)) / ((mu1_sq + mu2_sq + C1) * (sigma1_sq + sigma2_sq + C2)) return np.mean(ssim_map) # 查找基准图像 def find_base_image(): # 支持的图像格式 image_extensions = ['png', 'jpg', 'jpeg', 'bmp', 'tiff', 'webp'] base_images = [] # 查找所有可能的图像文件 for ext in image_extensions: base_images.extend(glob.glob(f'*.{ext}')) if not base_images: raise FileNotFoundError("❌ 未找到基准图像!请确保当前目录下有图像文件") # 优先选择名称包含"base"或"reference"的图像 for img in base_images: if 'base' in img.lower() or 'reference' in img.lower(): return img # 如果没有明确标记的基准图,使用第一个找到的图像 return base_images[0] # 获取基准图像路径 base_image_path = find_base_image() print(f"✅ 找到基准图像: {base_image_path}") # 读取基准图像 base_image = cv2.imread(base_image_path) if base_image is None: raise ValueError(f"❌ 无法读取图像: {base_image_path}") # 转换为灰度图用于SSIM计算 base_image_gray = cv2.cvtColor(base_image, cv2.COLOR_BGR2GRAY) IMG_HEIGHT, IMG_WIDTH = base_image_gray.shape PIXEL_MAX = 255 # 8位图像像素最大值 SSIM_WINDOW_SIZE = 11 # SSIM默认滑动窗口大小 # 失真场景参数(根据图像尺寸动态调整) ROTATE_ANGLE = 3 # 几何倾斜角度(°) SALT_PEPPER_DENSITY = 0.1 # 椒盐噪声密度(10%) SCRATCH_SIZE = (max(10, IMG_WIDTH//100), max(10, IMG_HEIGHT//100)) # 局部划痕尺寸 SATURATION_AREAS = [ # 高反光饱和区域(左上角坐标+宽高) (IMG_WIDTH//4, IMG_HEIGHT//4, IMG_WIDTH//10, IMG_HEIGHT//10), (IMG_WIDTH//2, IMG_HEIGHT//3, IMG_WIDTH//10, IMG_HEIGHT//10), (IMG_WIDTH*3//4, IMG_HEIGHT*2//3, IMG_WIDTH//10, IMG_HEIGHT//10) ] # -------------------------- 2. 6类失真图生成函数(新增枕型变形)-------------------------- def generate_distorted_images(base_img, base_gray): distorted_imgs = {} # 存储失真图:key=失真类型,value=(图像, 保存路径) # (1)几何倾斜(3°旋转) h, w = base_gray.shape M_rotate = cv2.getRotationMatrix2D((w//2, h//2), ROTATE_ANGLE, 1) img_rotate = cv2.warpAffine(base_img, M_rotate, (w, h), borderValue=(255, 255, 255)) path_rotate = os.path.join(output_dir, "y1_geometric_rotation.jpg") cv2.imwrite(path_rotate, img_rotate) distorted_imgs["几何倾斜"] = (img_rotate, path_rotate) # (2)高反光像素饱和(指定区域像素=255) img_saturation = base_img.copy() for (x, y, w_sea, h_sea) in SATURATION_AREAS: # 创建白色矩形区域 cv2.rectangle(img_saturation, (x, y), (x+w_sea, y+h_sea), (255, 255, 255), -1) path_saturation = os.path.join(output_dir, "y2_high_reflection_saturation.jpg") cv2.imwrite(path_saturation, img_saturation) distorted_imgs["高反光饱和"] = (img_saturation, path_saturation) # (3)椒盐噪声(10%密度) img_salt_pepper = base_img.copy() noise_mask = np.random.choice([0, 1, 2], size=base_gray.shape[:2], p=[SALT_PEPPER_DENSITY/2, SALT_PEPPER_DENSITY/2, 1-SALT_PEPPER_DENSITY]) # 椒噪声(暗斑) img_salt_pepper[noise_mask == 0] = (0, 0, 0) # 盐噪声(亮斑) img_salt_pepper[noise_mask == 1] = (255, 255, 255) path_salt_pepper = os.path.join(output_dir, "y3_salt_pepper_noise.jpg") cv2.imwrite(path_salt_pepper, img_salt_pepper) distorted_imgs["椒盐噪声"] = (img_salt_pepper, path_salt_pepper) # (4)局部小划痕(黑色划痕) img_scratch = base_img.copy() scratch_x = IMG_WIDTH // 2 # 划痕中心x坐标 scratch_y = IMG_HEIGHT // 2 # 划痕中心y坐标 x1 = max(0, scratch_x - SCRATCH_SIZE[0] // 2) y1 = max(0, scratch_y - SCRATCH_SIZE[1] // 2) x2 = min(IMG_WIDTH, scratch_x + SCRATCH_SIZE[0] // 2) y2 = min(IMG_HEIGHT, scratch_y + SCRATCH_SIZE[1] // 2) cv2.rectangle(img_scratch, (x1, y1), (x2, y2), (0, 0, 0), -1) # 黑色划痕 path_scratch = os.path.join(output_dir, "y4_local_scratch.jpg") cv2.imwrite(path_scratch, img_scratch) distorted_imgs["局部小划痕"] = (img_scratch, path_scratch) # (5)透视变形(底部宽、顶部窄) # 原始四边形顶点(左上、右上、右下、左下) src_pts = np.float32([ [100, 100], [w-100, 100], [w-100, h-100], [100, h-100] ]) # 透视变换后顶点(顶部收缩,底部不变) dst_pts = np.float32([ [w//2 - w//8, 100], [w//2 + w//8, 100], [w-100, h-100], [100, h-100] ]) M_persp = cv2.getPerspectiveTransform(src_pts, dst_pts) img_perspective = cv2.warpPerspective(base_img, M_persp, (w, h), borderValue=(255, 255, 255)) path_perspective = os.path.join(output_dir, "y5_perspective_distortion.jpg") cv2.imwrite(path_perspective, img_perspective) distorted_imgs["透视变形"] = (img_perspective, path_perspective) # (6)新增:枕型变形(Pincushion Distortion) img_pincushion = base_img.copy() # 创建枕型变形映射 map_x = np.zeros((h, w), dtype=np.float32) map_y = np.zeros((h, w), dtype=np.float32) # 中心点坐标 cx, cy = w//2, h//2 # 枕型变形参数(可调整) strength = 0.0005 # 变形强度 for y in range(h): for x in range(w): # 计算相对中心点的距离(归一化到[-1,1]) dx = (x - cx) / cx dy = (y - cy) / cy # 计算径向距离 r = math.sqrt(dx*dx + dy*dy) # 枕型变形公式:向中心凹陷 factor = 1 + strength * r**4 # 应用变换 map_x[y, x] = cx + factor * (x - cx) map_y[y, x] = cy + factor * (y - cy) # 应用重映射 img_pincushion = cv2.remap(base_img, map_x, map_y, cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=(255, 255, 255)) path_pincushion = os.path.join(output_dir, "y6_pincushion_distortion.jpg") cv2.imwrite(path_pincushion, img_pincushion) distorted_imgs["枕型变形"] = (img_pincushion, path_pincushion) print(f"✅ 6类失真图已全部保存至:{output_dir}") return distorted_imgs # -------------------------- 3. 评分计算(SSIM+模拟主观评分) -------------------------- def calculate_scores(base_gray, distorted_imgs): scores = [] # 模拟主观评分(基于工程实际感知) subjective_scores = { "几何倾斜": 3.0, "高反光饱和": 2.8, "椒盐噪声": 2.5, "局部小划痕": 4.2, "透视变形": 2.9, "枕型变形": 3.2 # 新增枕型变形的主观评分 } for dist_type, (dist_img, dist_path) in distorted_imgs.items(): # 转换为灰度图用于SSIM计算 dist_gray = cv2.cvtColor(dist_img, cv2.COLOR_BGR2GRAY) # 计算SSIM评分(使用自定义的OpenCV实现) ssim_score = compute_ssim(base_gray, dist_gray, win_size=SSIM_WINDOW_SIZE, data_range=PIXEL_MAX) ssim_score = round(ssim_score, 2) # 保留2位小数 subj_score = subjective_scores.get(dist_type, 3.0) # 模拟主观评分 scores.append({ "失真类型": dist_type, "SSIM评分": ssim_score, "模拟主观评分(5分制)": subj_score, "失真图路径": dist_path }) # 添加基准图评分 base_ssim = compute_ssim(base_gray, base_gray, data_range=PIXEL_MAX) scores.insert(0, { "失真类型": "基准图像", "SSIM评分": round(base_ssim, 2), "模拟主观评分(5分制)": 5.0, "失真图路径": base_image_path }) return scores # -------------------------- 4. 结果可视化输出 -------------------------- def print_score_table(scores): print("\n" + "="*100) print("📊 SSIM评分与主观评分对比表") print("="*100) # 格式化输出表头 print(f"{'失真类型':<10} {'SSIM评分':<10} {'模拟主观评分(5分制)':<20} {'图像路径':<60}") print("-"*100) for item in scores: print( f"{item['失真类型']:<10} {item['SSIM评分']:<10} {item['模拟主观评分(5分制)']:<20} {item['失真图路径']:<60}" ) print("="*100 + "\n") # -------------------------- 5. 主流程执行 -------------------------- if __name__ == "__main__": try: # 步骤1:生成6类失真图 distorted_images = generate_distorted_images(base_image, base_image_gray) # 步骤2:计算评分 score_table = calculate_scores(base_image_gray, distorted_images) # 步骤3:输出结果 print_score_table(score_table) # 步骤4:打印SSIM局限性总结 print("⚠️ 基于评分结果的SSIM局限性总结:") print("1. 几何倾斜/透视变形:SSIM评分通常较高(≥0.9),但主观评分较低(~3.0)→ 无法有效识别几何失真;") print("2. 高反光饱和:SSIM评分较高(~0.88),但主观评分较低(~2.8)→ 像素饱和导致结构相似性被高估;") print("3. 椒盐噪声:SSIM评分中等(~0.75),主观评分较低(~2.5)→ 对稀疏噪声敏感性不足;") print("4. 局部小划痕:SSIM评分很高(~0.98),主观评分较高(~4.2)→ 局部小缺陷被全局平均掩盖;") print("5. 枕型变形:SSIM评分中等(~0.85),主观评分中等(~3.2)→ 对几何形变敏感度不足。") except Exception as e: print(f"❌ 程序执行出错: {str(e)}") print("请确保:") print("1. 当前目录存在图像文件(支持格式:png, jpg, jpeg, bmp, tiff, webp)") print("2. OpenCV库已正确安装") print("3. 有足够的磁盘空间保存生成的失真图像") 现有的代码运行,第六类失真图像基本没变化,请调整相关参数,使得枕型变形明显,给出完整代码,并说明对哪些参数进行了调整。
最新发布
12-19
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值