前言
在计算机视觉日常开发中,我们总会遇到两类高频痛点:一是倾斜拍摄的票据、文档无法直接用于 OCR 识别,二是过暗、过曝、对比度不足的图像丢失关键细节。本文基于 OpenCV,拆解两大核心解决方案 ------四点透视变换 与直方图增强技术,提供可直接运行的工业级代码,从核心原理到落地实战一步到位。
=====》 
一、透视变换:倾斜票据一键转正,实现文档 "扫描级" 矫正
1.1 核心原理
透视变换的本质,是通过 3×3 的变换矩阵,将图像中任意四边形的目标区域,线性映射为标准矩形区域,彻底解决拍摄视角带来的畸变问题。OpenCV 中核心依赖两个 API:
cv2.getPerspectiveTransform(src, dst):输入原始四边形顶点与目标矩形顶点,计算透视变换矩阵 Mcv2.warpPerspective(src, M, dsize):输入原图与变换矩阵,输出矫正后的完整图像
1.2 核心代码拆解
我们的实现分为坐标排序、透视变换、轮廓检测、主流程执行四个核心模块,解决了顶点无序、坐标精度丢失、轮廓筛选等工业场景的核心问题。
核心工具函数
python
import cv2
import numpy as np
def cv_show(img):
"""图像展示封装"""
cv2.imshow("img",img)
cv2.waitKey(0)
def order_points(pts):
"""
四边形顶点排序:固定输出 左上、右上、右下、左下
解决顶点无序导致的透视变换扭曲问题
"""
rect = np.zeros((4, 2), dtype="float32")
# 左上:x+y和最小,右下:x+y和最大
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
# 右上:y-x差最小,左下:y-x差最大
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
return rect
def four_point_transform(img, pts):
"""四点透视变换核心实现"""
# 排序顶点坐标
rect = order_points(pts)
(tl, tr, br, bl) = rect
# 计算矫正后图像的最大宽高,保证内容无丢失
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
maxWidth = max(int(widthA), int(widthB))
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
maxHeight = max(int(heightA), int(heightB))
# 目标矩形坐标
dst = np.array([
[0, 0],
[maxWidth - 1, 0],
[maxWidth - 1, maxHeight - 1],
[0, maxHeight - 1]], dtype="float32")
# 计算变换矩阵并执行透视变换
M = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(img, M, (maxWidth, maxHeight))
return warped
def resize(image, width=None, height=None, inter=cv2.INTER_AREA):
"""图像等比例缩放,保证轮廓检测稳定性"""
dim = None
(h, w) = image.shape[:2]
if width is None and height is None:
return image
if width is None:
r = height / float(h)
dim = (int(w * r), height)
else:
r = width / float(w)
dim = (width, int(h * r))
resized = cv2.resize(image, dim, interpolation=inter)
return resized
主流程:票据矫正全链路
python
#读取输入
image = cv2.imread("fapiao.jpg")
cv2.imshow("image",image)
#图片过大,进行缩小处理
ratio = image.shape[0] / 500.0 #计算缩小比率
orig = image.copy()
image = resize(orig,height=500)
cv2.imshow("1",image)
#轮廓检测
print("step1:轮廓检测")
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY) #读取灰度图
# 大津法自动阈值二值化,适配不同光照场景
edged = cv2.threshold(gray,0,255,cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] # 自动寻找值值化
# 提取所有轮廓
cnts = cv2.findContours(edged.copy(),cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)[-2]
image_contours = cv2.drawContours(image.copy(),cnts,-1,(0,0,255),1)
cv2.imshow("image_contours",image_contours)
print("step 2:获取最大轮廓")
screenCnt = sorted(cnts,key=cv2.contourArea,reverse=True)[0] #获取面积最大的轮廓
print(screenCnt.shape)
peri = cv2.arcLength(screenCnt,True) #计算轮廓周长
screenCnt = cv2.approxPolyDP(screenCnt,0.05*peri,True) #轮廓近似,# 多边形近似,得到四边形顶点
print(screenCnt.shape)
image_contours = cv2.drawContours(image.copy(),[screenCnt],-1,(0,0,255),1)
cv2.imshow("image_contours",image_contours)
cv2.waitKey(0)
#透视变换
warped = four_point_transform(orig,screenCnt.reshape(4,2)*ratio)
cv2.imshow("invoice_new.jpg",warped)
cv2.namedWindow("xx",cv2.WINDOW_NORMAL)
cv2.imshow("xx",warped)
cv2.waitKey(0)
cv2.destroyAllWindows()
1.3 关键踩坑指南
- 顶点顺序必须严格匹配 :
src和dst的顶点顺序必须一一对应,否则会出现图像翻转、扭曲,order_points函数是解决该问题的核心 - 坐标精度还原 :预处理时对图像做了缩放,最终透视变换必须乘以缩放比例
ratio,还原到原图尺寸,避免分辨率损失 - 轮廓异常校验:生产环境需增加判断,确保多边形近似后得到 4 个顶点,否则会触发程序报错
1.4效果图展示

二、直方图处理:从像素分布到画质增强,拯救低质图像
2.1 直方图核心概念
图像直方图是像素灰度级的分布统计,直观反映了图像的亮度、对比度特征。cv2.calcHist是 OpenCV 中直方图计算的核心 API,参数精准拆解如下:
| 参数 | 核心说明 |
|---|---|
| images | 输入图像,需用中括号包裹,如[img],支持 uint8/float32 格式 |
| channels | 统计的通道,灰度图为[0],彩色 BGR 图对应[0][1][2] |
| mask | 掩膜,统计整图设为 None,统计局部区域需传入二值掩膜 |
| histSize | BINS 数量,即灰度区间的划分个数,灰度图常用 256 |
| ranges | 像素值范围,灰度图固定为[0,256] |
2.2 核心代码实现
我们覆盖了直方图绘制、掩膜局部统计、全局均衡化、自适应均衡化四大核心能力,解决不同场景的画质问题。
2.2.1直方图绘制
代码
python
#cv2.calcHist(images,channels,mask,histSize,ranges) 计算图像的直方图,用于表示图像中像素灰度级别的分布情况.
#images:原图像图像格式为uint8 或f0at32。当传入函数时应用中括号[]括来例如[img]
#channels:表示传入的图像通道数。如果输入图像是灰度图它的值就是[0]。
# 如果是彩色图像 的传入的参数可以是[0][1][2]它们分别对应着 BGR。
# mask:掩模图像。统计整幅图像的直方图就把它为None。但是如果你想统计图像某一部分的直方图,你就制作一个掩模图像并使用它。
# histSize:BINS的数目。也需用中括号括来 (分成多少个区间)
# BINS:上面的直方图显示了每个像素值的像素数,即从0到255。即您需要256个值才能显示上述直方图。
# 但是请考虑一下,如果您不需要单独查找所有像素值的像素数,而是在像素值间隔内查找像素数,
#该怎么办?例如,您需要找到介于0到15之间的像素数,然后是 16到 31、32到47...、240到 255。
#您只需要16 个值来表示直方图。
#因此,只需将整个直方图拆分为16个子部分,每个子部分的值就是其中所有像素计数的总和。
#这每个子部分部称为"BIN"。在第一种情况下,条柱数为256(每个像素一个),而在第二种情况下,它只有16。
# BINS在QpenCV文档中由术语histSiz表示。#ranges:像素值范围常为[0256]
import matplotlib.pyplot as plt
import cv2
import numpy as np
'''---------------------- 1. 直方图绘制 ----------------------'''
heizi = cv2.imread('shua.png',cv2.IMREAD_GRAYSCALE)
#将图像转换为一维数组
a = heizi.ravel()
#这里使用了numpy的ravel函数,将多维转化为一维
#绘制直方图
plt.hist(a, bins=256) # 使用matplotlib的hist函数绘制直方图
#参数解释:
#- a:一维数组,即图像的像素值组成的数组。
#-bins=256:指定直方图的条数,即灰度级的数量。
heizi_hist = cv2.calcHist([heizi],[0],None,[16],[0,256])
plt.plot(heizi_hist)#使用calcHist的值绘制曲线图
plt.show()
# 彩色图分通道直方图
img = cv2.imread('shua.png')
color = ('b','g','r')
for i,col in enumerate(color):
hei = cv2.calcHist([img],[i],None,[256],[0,256])
plt.plot(hei,color=col)
plt.show()
效果展示



2.2.2掩膜局部统计
代码
python
'''---------------------- 2. 掩膜局部直方图统计 ----------------------'''
####什么是mask?掩膜,ps pr
###mask参数如何使用? mask为掩模图像,先来看一下mask效果
mask = np.zeros(heizi.shape[:2],np.uint8) #创建黑白图像,用于制作mask
mask[10:200,200:300] = 255
cv2. imshow('mask',mask)
cv2.waitKey(0)
##cv2.bitwise-and():对图像(灰度图像或彩色图像均可)每个像素值进行二进制"与"操作,1&1=1,1&0=0,0&1=0,0&0=0# # bitwise_and(src1,src2,dst=None,mask=None)参数:
##src1、src2:为输入图像或标量,标src1和src2相与。
## dst:可选输出变量,如果需要使用非None则要先定义,且其大小与输入变量相同
##mask:图像掩膜,可选参数,用于指定要更改的输出图像数组的元素,mask为0的值,srC1和Src2相与的值都为0.
##非O的值,为src1和src2相与的值。
heizi_mask = cv2.bitwise_and(heizi,heizi,mask=mask)
cv2.imshow('heizi_mask',heizi_mask)
cv2.waitKey(0)
heizi_hist_mask = cv2.calcHist([heizi],[0],mask,[256],[0,256])
plt.plot(heizi_hist_mask) #使用calcHist的值绘制曲线图
plt.show()
效果展示

2.2.3全局均衡化
代码
python
#直方图均衡化:直方图均衡化是一种图像增强技术,它可以通过增加图像的对比度和亮度来改善图像的质量。
# 直方图均衡化通过将图像的像素值分布均匀化来实现这一目标。
# 在Python OpenCV中,可以使用cv2.equalizeHist()函数来实现直方图均衡化。
### 该函数将输入图像转换为灰度图像,并将其像素值分布均匀化,从而增强图像的对比度和亮度。
''' ---------------------- 3. 全局直方图均衡化 ----------------------'''
black = cv2.imread('black.jpg',cv2.IMREAD_GRAYSCALE)
# 原图直方图
plt.hist(black.ravel(),bins=256)#numpy中的ravel将数组多维度拉成一维数组
plt.show()
#全局均衡化
black_equalize = cv2.equalizeHist(black)
plt.hist(black_equalize.ravel(),bins=256) #numpy中的ravel将数组多维度拉成一维数组
plt.show()
#原图与结果恒星拼接展示
res = np.hstack((black,black_equalize)) #hstack:横向拼接,将多个数组按水平方向(列顺序)堆叠成一个新的数组。 Vstack:纵向
cv2.imshow('Global Equalize',res)
cv2.waitKey(0)
效果展示



2.2.4自适应均衡化
代码
python
###自适应直方图均衡化局部直方图处理),通过局部调整图像的直方图分布来提升图像的对比度和细节表现力,当需要保存细节特征,需要做局部处理# #
# cv2.createCLAHE([, clipLimit[, tileGridSize]]) > retval
#参数说明:
# clipLimit:颜色对比度的阈值,可选项,默认值8##
# titleGridSize:局部直方图均衡化的模板(邻域)大小,可选项,默认值(8,8)
'''---------------------- 4. 自适应直方图均衡化CLAHE(工业首选) ----------------------'''
# 创建CLAHE对象,clipLimit限制对比度放大,tileGridSize设置分块大小
clahe = cv2.createCLAHE(clipLimit=10,tileGridSize=(8,8)) #通过类创建了一个局部均衡化对象
black_clahe = clahe.apply(black)
# 三者效果对比
res = np.hstack((black,black_equalize,black_clahe))
cv2.imshow('CLAHE vs Global',res)
cv2.waitKey(0)
cv2.destroyAllWindows()
效果展示

2.3 核心能力与使用技巧
- 全局均衡化:实现简单,适合整体亮度均匀的图像,但易放大噪声、丢失局部细节,不适合复杂场景
- CLAHE 自适应均衡化 :工业界首选方案,将图像划分为多个子块分别做均衡化,通过
clipLimit限制对比度放大上限,既提升了对比度,又保留了局部细节,完美解决全局均衡化的缺陷 - 彩色图像处理避坑:不要直接对 BGR 三通道分别均衡化,会导致色彩失真。正确做法是转换到 HSV 空间,仅对 V(亮度)通道做均衡化,再转回 BGR
- 参数调优 :
clipLimit默认值 8,图像噪声多可降低至 2-5,需要强对比度可提升至 10-15;高分辨率图像可适当增大tileGridSize,提升处理效率
三、工业级实战组合拳:矫正 + 增强全流程
在票据识别、文档扫描等真实场景中,两大技术的组合使用是标准预处理流程:
- 图像读取与等比例缩放预处理
- 二值化与轮廓检测,提取目标区域的四个顶点
- 四点透视变换,得到正视角的矫正图像
- 对矫正后的图像做 CLAHE 自适应直方图均衡化,提升画质与 OCR 识别率
- 输出最终图像,用于后续识别或存档
本文提供的所有代码均可直接运行,仅需替换图片路径即可实现效果。透视变换解决了视角畸变问题,直方图增强解决了画质缺陷问题,二者结合是计算机视觉预处理环节的黄金组合,广泛应用于票据识别、文档扫描、工业质检等多个场景。