攻克验证码干扰线,意外收获 RGB 欧几里得新知识
作者:高玉涵
时间:2025.04.11 15:15
博客:blog.csdn.net/cg_i
环境:Windows10、Python 3.11.3、PIL、Tesseract-OCR。
“对呀对呀!茴字有四样写法,你知道么?”——孔乙己
一、缘起:验证码干扰线处理难题
先前写过一篇博文《Python 轻松去除验证码干扰点,让识别不再犯难》。相对于干扰点,干扰线形态各异,有的像错综复杂的蛛网,有的则是歪歪扭扭的曲线,它们与验证码字符相互交织,使得字符的识别变得异常困难(图1-1)。我首先尝试用以往文章提到的方法对图像进行处理,但效果不尽如人意,当存在干扰线穿越验证码时,清除干扰线会造成验证码字符出现断裂(图1-2),严重影响着验证码的识别准确率(图1-3)。
1.1 清除干扰线代码(详见先前博文):
def retain():
filename = "captchas/057609707563DBD01744265731000 (1).png"
image = Image.open(filename)
image = image.convert("RGB")
width, height = image.size
# 遍历图像的每个像素
for x in range(width):
for y in range(height):
r, g, b = image.getpixel((x, y))
# 检查当前像素的颜色是否不等于要保留的颜色
# 验证码的颜色可通过 Windows 画图工具获取
if (r, g, b) != (68,115,145):
# 如果不等于,则将该像素的颜色设置为替换颜色
image.putpixel((x, y), (255, 255, 255))
image.save("captchas/process1.png") # 保存处理后的图片
从人类视觉角度审视,经处理后的图片近乎臻美,清晰的视觉效果,字符完整且辨识度极高,无论是线条的流畅度还是颜色的一致性,都无可挑剔,完全符合人类对清晰验证码图像的直观认知,除了字符中偶尔出现的几处断裂。
然而,在计算机的数字世界里,情况却截然不同。计算机遵循着极为严苛、精确的逻辑规则运行,它没有人类视觉所具备的容错与模糊识别能力。哪怕只是一个像素点的数值,从 0 变为 1 这样极其微小的差异,在计算机严谨的运算体系中,都如同一场 “数字风暴”。这一细微变化,足以改变图像在计算机眼中的整体特征与模式,进而导致后续的识别、分析等一系列操作出现偏差,最终致使整个任务流程走向失败,无法达成预期目标 。
1.2 尝试使用Tesseract-OCR来识别图中的验证码。
tesseract process1.png output
Estimating resolution as 196
从上面提示中,OCR 识别似乎成功。进一步查看输出文件output.txt
,结果却令人大失所望。原本应准确无误呈现的验证码内容,实际却与预期大相径庭。仔细端详,发现验证码首位的0
字符竟不翼而飞,不仅如此,字符串中还莫名出现了换行符,这一状况直接导致整个字符串支离破碎,完全丧失了连续性。
深入探究其背后的原因,罪魁祸首乃是验证码字符在前期处理过程中出现了断裂情况。这些断裂之处,如同一个个 “陷阱”,严重干扰了 OCR 算法的正常运行。OCR 算法在面对这种不完整、断裂的字符时,难以精准捕捉字符的完整形态与特征,无法依据既定的模式匹配规则准确识别,进而使得识别率大打折扣,最终呈现出如今这般错误百出的输出结果 。
二、我是不姓孔的孔乙己:RGB 颜色空间与欧几里得距离
在不断探索解决办法的过程中,我偶然接触到了 RGB 颜色空间以及其中的欧几里得距离概念。RGB 颜色空间,作为一种广泛应用的颜色表示方式,通过红(Red)、绿(Green)、蓝(Blue)三种基本颜色的不同强度组合来呈现丰富多彩的世界。每一种颜色都可以用一个三维向量,即 (R, G, B) 来表示,其中 R、G、B 的取值范围通常在 0 到 255 之间。
而欧几里得距离,原本是数学中用于衡量空间中两点之间距离的概念,在 RGB 颜色空间里,它被用来衡量两个颜色之间的差异程度。对于两个 RGB 颜色 C1=(r1,g1,b1) 和 C2=(r2,g2,b2),它们之间的欧几里得距离 d 的计算公式为:
d
=
(
r
1
−
r
2
)
2
+
(
g
1
−
g
2
)
2
+
(
b
1
−
b
2
)
2
d = \sqrt{(r_1 - r_2)^2 + (g_1 - g_2)^2 + (b_1 - b_2)^2}
d=(r1−r2)2+(g1−g2)2+(b1−b2)2
例如,计算颜色((255, 0, 0))(红色)和((0, 255, 0))(绿色)之间的欧几里得距离:
d
=
(
255
−
0
)
2
+
(
0
−
255
)
2
+
(
0
−
0
)
2
=
25
5
2
+
25
5
2
=
2
×
25
5
2
≈
360.62
d = \sqrt{(255 - 0)^2 + (0 - 255)^2 + (0 - 0)^2} = \sqrt{255^2 + 255^2} = \sqrt{2\times255^2} \approx 360.62
d=(255−0)2+(0−255)2+(0−0)2=2552+2552=2×2552≈360.62
距离与颜色差异的关系
- 欧几里得距离越大,说明两个颜色在 RGB 颜色空间中相距越远,它们的视觉差异也就越大。
- 反之,距离越小,颜色越相似。通过设定一个距离阈值,可以判断一个颜色是否在另一个颜色的一定相似范围内。
RGB 颜色空间中的欧几里得距离为我们提供了一种量化颜色差异的方法,在图像处理、计算机视觉等领域有着广泛的应用,比如颜色识别、图像分割、图像过滤等任务中,都可以通过计算颜色的欧几里得距离来判断颜色之间的关系,进而实现相应的功能。
注:限于篇幅,关于欧几里得空间的概念,这里就此打住,有兴趣的同学,可自习搜索相关资料补充。
三、尝试:应用像素间距离修复验证码断裂
了解到 RGB 颜色空间中的欧几里得距离后,我仿佛看到了一丝曙光,觉得也许可以运用距离和特定的像素搜索策略,修复这些断裂的验证码字符。
3.1 修复验证码断裂的 fill_gap
函数
fill_gap
函数是整个代码的核心,它的使命是修复因去除干扰线而断裂的验证码字符。该函数接收四个参数:image
(一个 PIL 图像对象)、retain_color
(要保留的颜色,即验证码字符的颜色)、background_color
(默认的背景颜色,通常为白色 (255, 255, 255)
)以及 max_distance
(最大搜索距离,用于控制搜索范围)。
def fill_gap(image, retain_color, background_color=(255, 255, 255), max_distance=2):
"""
修复验证码中因去除干扰线造成的断裂
:param image: PIL 图像对象
:param retain_color: 要保留的颜色,格式为 (R, G, B)
:param background_color: 背景颜色,默认是白色 (255, 255, 255)
:param max_distance: 最大搜索距离
:return: 处理后的 PIL 图像对象
"""
width, height = image.size
for x in range(width):
for y in range(height):
current_color = image.getpixel((x, y))
if current_color == retain_color:
# 向右搜索
for dx in range(1, max_distance + 1):
if x + dx < width:
next_color = image.getpixel((x + dx, y))
if next_color == retain_color:
for i in range(1, dx):
mid_color = image.getpixel((x + i, y))
if mid_color == background_color:
image.putpixel((x + i, y), retain_color)
break
# 向下搜索
for dy in range(1, max_distance + 1):
if y + dy < height:
next_color = image.getpixel((x, y + dy))
if next_color == retain_color:
for i in range(1, dy):
mid_color = image.getpixel((x, y + i))
if mid_color == background_color:
image.putpixel((x, y + i), retain_color)
break
return image
遍历图像像素
函数首先获取输入图像的宽度 width
和高度 height
,随后通过嵌套的 for
循环遍历图像中的每一个像素点。对于每个像素,获取其颜色 current_color
。当发现某个像素颜色与 retain_color
(验证码字符颜色)相同时,便开启修复流程。
水平方向搜索与修复
当检测到验证码字符颜色的像素时,从该像素开始向右进行搜索。在 max_distance
范围内,检查每个相邻像素的颜色。若遇到另一个与 retain_color
相同的像素,说明这两个字符像素之间可能存在因干扰线去除而形成的断裂,中间可能夹杂着背景色像素。此时,再次遍历这两个字符像素之间的所有像素,如果发现某个像素颜色为 background_color
(背景色),则将其颜色替换为 retain_color
,从而实现水平方向上断裂字符的修复。一旦完成修复或搜索超出 max_distance
范围,便停止向右搜索。
垂直方向搜索与修复
类似地,在垂直方向上,从检测到的验证码字符颜色像素开始向下搜索。同样在 max_distance
范围内查找下一个 retain_color
像素,若找到,则检查它们之间的像素,将背景色像素替换为 retain_color
,完成垂直方向上的断裂修复。最后,函数返回经过修复的图像对象。
运行示例:
将图 1-2 去除带干扰线后的验证码图片process1.png
进行修复。
from PIL import Image
import math
def fill_gap(image, retain_color, background_color=(255, 255, 255), max_distance=2):
为结省篇幅,此处省略,可参考先前代码
# 使用示例
if __name__ == "__main__":
# 替换为你的图片路径
filename = "captchas/process1.png"
image = Image.open(filename)
image = image.convert("RGB")
retain_color = (68, 115, 145)
processed_image = fill_gap(image, retain_color)
output_filename = filename.rsplit('.', 1)[0] + '_repaired.png'
processed_image.save(output_filename)
print(f"处理后的图片已保存为 {output_filename}")
如上图 3-1 所示,我的代码能够在一定程度上修复因去除干扰线而断裂的验证码字符。这种方法为验证码识别前的预处理工作提供了一种有效的解决方案。然而,它也并非完美无缺。例如,max_distance
参数的设置需要根据不同验证码的特点进行调整,若设置不当,可能导致修复过度或修复不足(图 3-2 max_distance=5)。此外,该方法仅适用于水平和垂直方向上的断裂修复,对于倾斜或更复杂的断裂情况可能效果不佳。
3.2 再次尝试使用Tesseract-OCR来识别图中的验证码。
tesseract process1_repaired.png output
Estimating resolution as 197
从识别结果来看,我仅仅启用了 Tesseract - OCR 命令的默认功能,未对其进行任何复杂的参数调校,便出人意料地收获了精准无误的识别成果。在图像处理环节,我也只是运用了最为基础、简单的操作,没有涉及任何高深复杂的算法。这一轻松达成的成效,让我不禁感慨,为自己在这次实践中能有如此灵光乍现的时刻深感自豪。这让我愈发坚信想象力比知识重要这一观点。知识固然是宝贵的财富,它为我们的探索提供了坚实的基础与指引,但想象力却能赋予我们超越常规、开拓新领域的力量。在面对诸多未知与挑战时,丰富的想象力能够引领我们跳出既定的思维框架,去尝试那些看似不可能的路径,从而发现全新的可能性。
四、展望:继续前行的动力
总之,这次与 RGB 欧几里得距离的邂逅,虽然没有帮助我解决当下的实际问题,但却如同一盏明灯,照亮了我在图像处理领域继续探索的道路,带来了知识的增长和眼界的开拓,让我对未来的技术探索充满了期待。