字符分割是将文本图像中的字符进行分离的过程,其应用于字符识别、光学字符识别(OCR)、自然语言处理等领域。

其应用场景包括但不限于:

  1. 光学字符识别 (OCR): 在OCR任务中,字符分割是一个重要的预处理步骤,通过将文本图像中的字符分割开来,可以提高字符识别准确率。字符分割可以帮助识别单个字符,并将其提供给OCR引擎以进行识别。

  2. 自然语言处理 (NLP): 在自然语言处理任务中,字符分割常用于文本的分词。将连续的文本分割成单个字符或单词可以提供更好的特征表示,用于诸如情感分析、机器翻译、文本分类等任务。

  3. 手写文字识别: 在手写文字识别中,字符分割用于将连续的手写文本分隔成单个字符,以便进行字符级别的识别。

  4. 验证码识别: 字符分割常用于验证码识别中,通过将验证码中的字符分割开来,可以使识别过程更加准确和可靠。

  5. 场景文本识别: 在场景文本识别任务中,字符分割用于将场景图像中的文本区域分割成单个字符,以便进行字符级别的识别。

字符分割方法有很多,常用的包括基于图像处理的技术,例如阈值化、边缘检测、连通组件分析等,以及基于机器学习的技术,例如基于深度学习的方法。这些方法可以根据具体的应用场景和需求选择和调整。总而言之,字符分割在各种涉及文本识别、处理和分析的任务中都是至关重要的一步,能够提高准确性和可靠性。因此本文就介绍一些相关的字符分割方法和原理,以便用于其他任务。


字符分割有许多不同的方法,以下是一些常用的字符分割方法:

  1. 基于图像处理的方法:

    • 阈值化:使用阈值将文本图像二值化,并基于连通区域分析将字符分割出来。
    • 边缘检测:通过检测图像的边缘特征,如Canny边缘检测,从而找到字符的边界进行分割。
    • 间隙分析:通过分析字符之间的间隙来进行分割。该方法通常用于等宽字符的分割。
    • 投影法:通过在水平或竖直方向上对图像进行投影,根据字符之间的间隙进行分割。
    • 连通组件分析:基于连通组件的属性,如面积、宽高比等,将字符进行分割。
  2. 基于机器学习的方法:

    • 基于分类器的方法:使用分类器,如支持向量机(SVM)或随机森林,对字符和非字符进行分类,从而进行字符分割。
    • 基于聚类的方法:将字符图像进行特征提取,如垂直、水平投影特征等,然后使用聚类算法,如k-means算法,对字符进行分割。
    • 基于深度学习的方法:使用深度学习模型,如卷积神经网络(CNN)或循环神经网络(RNN),对字符进行分割。这些方法通常需要大量带有标注的样本进行训练。
      除了以上方法,还有一些其他的字符分割方法,如基于形态学处理的方法、基于图论的方法等,具体的方法选择取决于应用场景和需求的不同。

需要注意的是,字符分割是一个有挑战性的任务,因为字符的形状、大小、字体、间距等因素都可能会影响分割的准确性。因此,在实际应用中,可能需要结合多种方法或采用多级分割策略来提高分割的准确性和鲁棒性。

一、基本方法

1、直接分割

  • 以下面这张手写数字图片为例,如果要将其中的字符分割出来,很容易想到的一个简单方法就是“直接裁剪”,就像有一个剪刀对着两两字符的中间空白位置进行裁剪,即可顺利的分割出所有字符。

实现起来也非常容易处理:首先进行左右裁剪,确保每张图片中只含有一个字符,然后再进行上下裁剪,这样可以保证字符图片具有一个合适的宽高比。具体步骤如下:

  • 1.首先将图片的颜色反转,得到“黑底白字”的图片;(不做反转也可以,但是后面进行边界判定时需要修改一下。)
  • 2.切割时希望将每个字符分割均匀,避免出现图片中空白部分占比过大的情形。因此第一步可以先确定出每个字符左右边界所在的像素位置x_{1}、x_{2}…x_{n},n=字符个数×2,同时将图片的左右边界x_{0}、x_{n+1}也考虑在内,这样一来只需要求[x_{0},x_{1}]、[x_{2},x_{3}]、…[x_{n},x_{n+1}]二者的均值,即可得到所有字符的左右切割位置。由于字符所在位置为白色(像素大小为255),空字符处为黑色(像素大小为0),因此在字符边界处的左右两列必然是有一列像素和为0,一列不为0,利用该性质很容易确定所有字符的两个边界位置。
  • 3.然后对已经得到单个字符图片进行上下切割,主要目的也是将多余的空白部分切掉。具体实现时只要保证上下切割位置离剩上下边界距离10个像素即可,这样最终分割得到的图片最多允许有20个像素高度的空白部分。

用一张图表示上面的过程

具体代码如下:

from PIL import  Image,ImageOps
import matplotlib.pyplot as plt

path = r"XXX.jpg"
img = Image.open(path).convert('L')
# 对灰度图像进行二值化处理
img = img.point(lambda x: 0 if x < 150 else 255, '1')
# 将图片转换为黑底白图
img = ImageOps.invert(img)


# 获取图片宽度和高度
width, height = img.size

# 初始化裁剪的起始位置
start_x = 0
# 用于保存裁剪后的图片
cropped_images = []

# 得到所有字符左右边界的x轴坐标
char_x = []
# 得到切割位置
cut_list = []

# 从左到右进行遍历
for x in range(0, width-1):
    # 获取当前列的像素值
    column_pixels = [img.getpixel((x, y)) for y in range(height)]
    # 获取当前列的左右两列的像素值
    column_pixels_left = [img.getpixel((x-1, y)) for y in range(height)]
    column_pixels3_right = [img.getpixel((x+1, y)) for y in range(height)]
    if sum(column_pixels) != 0 and (int(bool(sum(column_pixels_left))) ^ int(bool(sum(column_pixels3_right)))) : # 只要当前列像素和不为0,并且左右有一列的值为0,一列值为1就认为是边界
        char_x.append(x)

# 将图片的始末位置加入到char_x中,方便处理
char_x.insert(0,0)
char_x.append(width)

# 两两求平均,得到切割位置cut_list
for  i in range(0,len(char_x),2):
    cut_list.append(int((char_x[i] + char_x[i+1]) / 2))

# 切割出单个字符
for j in range(len(cut_list) - 1):
    cut_image = img.crop((cut_list[j], 0, cut_list[j+1], height))
    cropped_images.append(cut_image)

# 继续裁剪,将上下多余的部分裁剪掉
for a in cropped_images:
    width, height = a.size
    if height >=30:
        # 遍历上半部分,找到第一个白色像素的位置
        top = 0
        for y in range(height):
            pixel = [a.getpixel((x, y)) for x in range(width)]
            if sum(pixel) >10:
                    top = max(0,y - 10)
                    break

        # 遍历下半部分,找到最后一个白色像素的位置
        bottom = 0
        for y in range(height-1, -1, -1):
            pixel = [a.getpixel((x, y)) for x in range(width)]
            if sum(pixel) > 10:
                    bottom = min(y + 10,height-1)
                    break
        # 裁剪图像,只保留中间白色数字的部分
        cropped_images[cropped_images.index(a)] = a.crop((0, top, width, bottom+1))
    else:
        cropped_images[cropped_images.index(a)] = a

# 展示分割结果
for x in cropped_images:
    x = ImageOps.invert(x)
    plt.imshow(x.convert('RGB'))
    plt.show()
分割效果

  • 该方法实现很简单,对清晰、规范的字符有较好的分割效果。但是缺陷也很明显,就是对粘连字符分割不准确,因为这里边界判定过于简单,容易将多个字符分割成一个字符,如下图就是未能成功分割的情形。

2、 投影法分割

  • 该方法原理与上面直接分割的思路基本一致,只是在边界的处理上采用投影+阈值的方式找出需要切割的位置。对于上面的图片,他的垂直投影和水平投影如下:

![](https://guyueju.oss-cn-beijing.aliyuncs.com/Uploads/Editor/202311/20231120_93569.png)

  • 处理过程与第一种方法大同小异,直接来看代码:
from PIL import  Image,ImageOps
import matplotlib.pyplot as plt

path = r"XXX.jpg"
img = Image.open(path).convert('L')
# 对灰度图像进行二值化处理
img = img.point(lambda x: 0 if x < 150 else 255, '1')
# 将图片转换为黑底白图
img = ImageOps.invert(img)


# 获取图片宽度和高度
width, height = img.size
# 用于保存裁剪后的图片
cropped_images = []
cropped_im = []



y_pixels = []
x_pixels = []

# 获取某一方向上的投影,即白色像素的个数
def get_pix(image,direct):
    pixels = []

    #direct=1表示垂直方向,=0表示水平方向
    if direct == 1:
        for x in range(0, image.size[0]):
            # 获取当前列的像素值
            column_pixels = [image.getpixel((x, y)) for y in range(image.size[1])]
            pixels.append(sum(column_pixels))

    if direct == 0:
        for y in range(0, image.size[1]):
            # 获取当前列的像素值
            row_pixels = [img.getpixel((x, y)) for x in range(image.size[0])]
            pixels.append(sum(row_pixels)) 
    return pixels

# 获取垂直方向和水平方向的投影
y_pixels = get_pix(img,1)
x_pixels = get_pix(img,0)

# # 可视化投影------------------
# plt.plot(list(range(width)), y_pixels, color='blue')
# plt.fill_between(list(range(width)), y_pixels, color='black') # 填充区域
# plt.title('vertical direction')
# plt.show()

# plt.plot(list(range(height)), x_pixels, color='red')
# plt.fill_between(list(range(height)), x_pixels, color='black') # 填充区域
# plt.title('horizontal direction')
# plt.show()
# #--------------------------


def get_cut_list(pixelss,T=200):
    cut_list = []       # 得到切割位置
    T = 200             # 设定阈值T,低于阈值以下的都视为空白部分
    pix_list = [1 if num > T else 0 for num in pixelss]
    pix_index = []   # -1代表字符所在像素,0代表空白部分
    for index , value in enumerate(pix_list):
        if value == 0 :
            pix_index.append(index)
        else :
            pix_index.append(-1)

    sliced_lists = []
    sub_list = []
    for num in pix_index:
        if num != -1:
            sub_list.append(num)
        else:
            if sub_list:
                sliced_lists.append(sub_list)
                sub_list = []
    if sub_list:
        sliced_lists.append(sub_list)

    for i in sliced_lists:
        cut_list.append(int(sum(i) / len(i)))
    return cut_list

cut_list = get_cut_list(y_pixels,T=200)  
# 列方向切割
for j in range(len(cut_list) - 1):
    cut_image = img.crop((cut_list[j], 0, cut_list[j+1], height))
    cropped_images.append(cut_image) 

# 对切割后的图片再按行方向切割
for im in cropped_images:
    x_pixels = get_pix(im,0)
    cut_list = get_cut_list(x_pixels,T=200)  

    for j in range(len(cut_list) - 1):
        cut_image = im.crop((0,cut_list[j],  im.size[0], cut_list[j+1]))
        cropped_im.append(cut_image) 

# 展示分割结果
for x in cropped_im:
    x = ImageOps.invert(x)
    plt.imshow(x.convert('RGB'))
    plt.show()
分割效果

  • 不过有相比于第一种方法也有升级的地方,那就是可以对多行文字进行裁剪。如下图效果:

3、基于边缘检测分割

基于边缘检测的字符分割是利用字符与背景之间的边缘信息进行分离。字符通常具有边缘明显、颜色或灰度差异明显的特点,通过检测边缘可以将字符与背景分割开来,该方法也是一种常用的字符分割技术。以下是基于边缘检测的图像字符分割的步骤:

  1. 图像预处理: 对图像进行预处理,包括灰度化、降噪等操作。灰度化将彩色图像转换为灰度图像,降噪可以使用各种滤波器进行平滑处理。

  2. 边缘检测: 使用边缘检测算法检测图像中的边缘。常用的边缘检测算法包括Sobel、Canny等。这些算法对图像中的边缘进行检测,并生成包含边缘信息的二值图像。

  3. 字符轮廓提取: 根据边缘图像,使用轮廓提取算法(如OpenCV的findContours()函数)提取字符的轮廓信息。这些轮廓是字符与背景之间的边界。

  4. 字符分割: 根据字符的轮廓,找到轮廓的最小外接矩形框,然后沿着矩形框进行分割操作。

  5. 后处理: 可选的步骤,根据需要进行字符区域的修正和优化。例如,可以应用形态学操作来填补字符中的空洞或去除噪声。

以上是基于边缘检测的图像字符分割的基本原理和步骤。具体实现的细节和方法可能因应用场景和具体需求而有所不同。过程也不难,直接看代码:

import cv2
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt

# 使用PIL库读取图像
pil_image = Image.open( r"XXX.jpg")
# 将图像转换为OpenCV格式
open_cv_image = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)

# 将图像转换为灰度图像
gray = cv2.cvtColor(open_cv_image, cv2.COLOR_BGR2GRAY)

# 进行二值化处理
_, binary = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY_INV)

# 检测轮廓
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# 得到切割位置
cut_list = []

# 用于保存裁剪后的图片
cropped_images = []

# 绘制字符边界框
for contour in contours:
    # 计算边界框
    x, y, w, h = cv2.boundingRect(contour)
    cut_list.append([x,y,w,h])
    # 在OpenCV图像上绘制边界框
    cv2.rectangle(open_cv_image, (x, y), (x + w, y + h), (0, 255, 0), 2)

# 按照从左到右进行排序
cut_list = sorted(cut_list, key=lambda x: x[0])
for i in cut_list:
    cut_image = pil_image.crop((i[0], i[1], i[0] + i[2], i[1] + i[3]))    
    cropped_images.append(cut_image)


# 将OpenCV图像转换为PIL格式
pil_image_result = Image.fromarray(cv2.cvtColor(open_cv_image, cv2.COLOR_BGR2RGB))
# 显示加上矩形框图像
pil_image_result.show()

# 展示分割结果
## 设置子图的行数和列数
rows = 1
cols = len(cropped_images)

# 在一排显示图片
fig, axes = plt.subplots(rows, cols, figsize=(15, 5))

# 在每个子图中显示图片
for i in range(cols):
    axes[i].imshow(cropped_images[i].convert('RGB'), cmap='gray')  # 在子图中显示灰度图像
    # axes[i].axis('off')  # 隐藏坐标轴

plt.show()
  • 对第一张图片分割效果:

  • 但是这里明显看出多分割了一个图像,“数字5”上面那一横也被分割了出来,这其实是我们不想要的。解决也很好解决,对图像做一个膨胀处理再进行分割就好了
  • 只需要将上面转换为灰度图像步骤之前的代码替换成下面代码即可。
import cv2
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
from PIL import ImageFilter

# 使用PIL库读取图像
pil_image = Image.open( r"XXX.jpg")
img = pil_image

# 膨胀处理
img = img.filter(ImageFilter.MinFilter(size = 13)) #注意:因为原图片是白底黑字,这里采用MinFilter;如果是黑底白字,则采用MaxFilter。

# 将图像转换为OpenCV格式
open_cv_image = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
最终分割效果

下面这种投影有重叠的也可以分割

该方法优点:对图片中字符所在区域、位置没什么要求,不像前面两种方法一旦不同字符所占区域的水平和垂直投影有重合时,就很难分割准确。
缺点:需要动态调整膨胀操作中s i z e sizesize大小,一般要根据不同的图片选取合适的s i z e sizesize大小才能做到准确分割,否则容易出现漏分割或者分割太细的问题。

4、 基于连通域分割

  • 基于连通域的字符分割的一般步骤:
  1. 二值化:将原始文本图像进行二值化处理,将字符部分设置为前景色(白色或1),背景部分设置为背景色(黑色或0)。
  2. 连通域标记:对二值图像进行连通域标记,将相邻的前景像素组成的区域标记为不同的连通域。这可以通过扫描图像的每个像素,并根据其前景像素的位置和连通关系进行标记。
  3. 连通域过滤:根据连通域的特征,如面积、宽高比等,过滤掉不符合要求的连通域。这样可以排除噪声和非字符的连通域,只保留可能是字符的连通域。
    4. 字符分割:根据剩余的连通域的位置信息,进行字符分割。可以根据连通域之间的间距、相对位置等特征来确定字符的边界。
import cv2
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
from PIL import ImageFilter

# 使用PIL库读取图像
pil_image = Image.open(r"XXX.JPG")
img = pil_image


# 将图像转换为OpenCV格式
open_cv_image = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)

# 将图像转换为灰度图像
gray = cv2.cvtColor(open_cv_image, cv2.COLOR_BGR2GRAY)

# 进行二值化处理
_, binary = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY_INV)

# 膨胀处理
kernel2 = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
binary = cv2.dilate(binary, kernel2, iterations=2)

# 用于保存裁剪后的图片
cropped_images = []


# 连通域分析
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(binary, connectivity=8)
print(num_labels)
'''
num_labels:连通域数量,注意这里空白区域也是连通域,因此分割无误的情况下,
                num_labels = 字符数量 + 1

stats:连通域的信息,是一个数组,一个5列的矩阵,每一行对应每个连通区域的外接矩形的x、y、width、height和面积

centroids:连通域的中心点.

labels:图像上每一像素的标记,用数字1、2、3…表示(不同的数字表示不同的连通域)
'''


# 显示图片中所有连通域
output = np.zeros((binary.shape[0], binary.shape[1], 3), np.uint8)
for i in range(1, num_labels):

    mask = labels == i
    output[:, :, 0][mask] = np.random.randint(0, 255)
    output[:, :, 1][mask] = np.random.randint(0, 255)
    output[:, :, 2][mask] = np.random.randint(0, 255)


# 分割图片
stats = (stats[stats[:, -1].argsort()])[:-1,:] # 避免分割整个区域

stats = stats[stats[:, 0].argsort()]  # 从左到右排序待分割字符


for i in stats: 
    cut_image = pil_image.crop((i[0], i[1], i[0] + i[2], i[1] + i[3]))    
    cropped_images.append(cut_image)


# 展示分割结果
## 设置子图的行数和列数
rows = 1
cols = len(cropped_images)

# 在一排显示图片
fig, axes = plt.subplots(rows, cols, figsize=(15, 5))

# 在每个子图中显示图片
for i in range(cols):
    axes[i].imshow(cropped_images[i].convert('RGB'), cmap='gray')  # 在子图中显示灰度图像
    # axes[i].axis('off')  # 隐藏坐标轴

plt.show()

# 展示图片中所有的连通域
cv2.imshow('oginal', output)
cv2.waitKey(0)
cv2.destroyAllWindows()
  • 前面图片成功分割出来了,,下面测试一下其他图片。
分割效果

检测到的连通域:

缺点:对于字符形状的变化和扭曲比较敏感。可以发现,对于左右结构和上下结构一些间隙较大的字体分割时出现了问题,将一个字符分割成了两个甚至多个。另外还有一点,在处理不同字体和字号的字符时可能存在一定的局限性。

5、基于滴水算法分割

  • 该算法在分割粘连字符上拥有较好的效果,原理是通过模拟水滴从高处向低处滴落的过程来对粘连字符进行切分。

  • 水滴从字符顶部在重力的作用下,沿着字符轮廓向下滴落或水平滚动,当水滴陷在轮廓的凹处时,将渗漏到字符笔划中,经穿透笔划后继续滴落,最终水滴所经过的轨迹就构成了字符的分割路径。

  • 搞懂三个关键因素

  1、起始滴落点
  2、水滴运动规则
  3、运动方向

假设n_{0}表示水滴当前的位置,水滴的下一步的滴落位置由五个像素点决定:它下方的三个像素点和它左右两个像素。给予编号如下:

算法根据水滴下落方向不同会有不同的分割路,以某种方法为例,假设:

w:代表白色像素 \\ b:代表黑色像素 \\ ∗:代表白色和黑色都有可能

水滴下一滴落位置的选择:

  • 假设( x , y )为当前位置,下一滴的落点位置的选取由下式决定:

  • W_i又由下式决定:

    • 根据实际图片的情形适当调整水滴的运动路径才能实现很好的分割效果。
    • 代码部分自己没有动手去实现,参考了这篇博客—— 滴水算法
    from itertools import groupby
    from PIL import Image
    
    def binarizing(img,threshold):
    
        """传入image对象进行灰度、二值处理"""
        img = img.convert("L") # 转灰度
        # 遍历所有像素,大于阈值的为黑色   
        img = img.point(lambda x: 0 if x < threshold else 255, '1')
        return img
    
    def vertical(img):
        """传入二值化后的图片进行垂直投影"""
        result = []
        for x in range(0, img.size[0]):
                # 获取当前列的像素值
                column_pixels = [img.getpixel((x, y)) for y in range(img.size[1])]
                result.append(sum(column_pixels))
        return result
    
    
    def get_start_x(hist_width):
        """根据图片垂直投影的结果来确定起点
        hist_width为中间值 前后取4个值 再这范围内取最小值
        """
        mid = len(hist_width) // 2 
        temp = hist_width[mid-4:mid+5]
        return mid - 4 + temp.index(min(temp))
    
    
    def get_nearby_pix_value(img_pix,x,y,j):
        """获取临近5个点像素数据,n1~n5"""
        if j == 1:
            return 0 if img_pix[x-1,y+1] == 0 else 1 # n1
        elif j ==2:
            return 0 if img_pix[x,y+1] == 0 else 1   # n2
        elif j ==3:
            return 0 if img_pix[x+1,y+1] == 0 else 1 # n3
        elif j ==4:
            return 0 if img_pix[x+1,y] == 0 else 1   # n4
        elif j ==5:
            return 0 if img_pix[x-1,y] == 0 else 1   # n5
        else:
            raise Exception("get_nearby_pix_value error")
    
    
    def get_end_route(img,start_x,height):
        """获取滴水路径"""
        left_limit = 0
        right_limit = img.size[0] - 1
        end_route = []
        cur_p = (start_x,0)
        last_p = cur_p
        end_route.append(cur_p)
        while cur_p[1] < (height-1):
            sum_n = 0
            max_w = 0
            next_x = cur_p[0]
            next_y = cur_p[1]
            pix_img = img.load()
            for i in range(1,6):
                cur_w = get_nearby_pix_value(pix_img,cur_p[0],cur_p[1],i) * (6-i)
                sum_n += cur_w
                if max_w < cur_w:
                    max_w = cur_w
            if sum_n == 0:
                # 如果全黑则看惯性
                max_w = 4
            if sum_n == 15:
                max_w = 6
            if max_w == 1:
                next_x = cur_p[0] - 1
                next_y = cur_p[1]
            elif max_w == 2:
                next_x = cur_p[0] + 1
                next_y = cur_p[1]
            elif max_w == 3:
                next_x = cur_p[0] + 1
                next_y = cur_p[1] + 1
            elif max_w == 5:
                next_x = cur_p[0] - 1
                next_y = cur_p[1] + 1
            elif max_w == 6:
                next_x = cur_p[0]
                next_y = cur_p[1] + 1
            elif max_w == 4:
                if next_x > cur_p[0]:
                    # 向右
                    next_x = cur_p[0] + 1
                    next_y = cur_p[1] + 1
                if next_x < cur_p[0]:
                    next_x = cur_p[0]
                    next_y = cur_p[1] + 1
                if sum_n == 0:
                    next_x = cur_p[0]
                    next_y = cur_p[1] + 1
            else:
                raise Exception("get end route error")
    
            if last_p[0] == next_x and last_p[1] == next_y:
                if next_x < cur_p[0]:
                    max_w = 5
                    next_x = cur_p[0] + 1
                    next_y = cur_p[1] + 1
                else:
                    max_w = 3
                    next_x = cur_p[0] - 1
                    next_y = cur_p[1] + 1
            last_p = cur_p
            if next_x > right_limit:
                next_x = right_limit
                next_y = cur_p[1] + 1
            if next_x < left_limit:
                next_x = left_limit
                next_y = cur_p[1] + 1
            cur_p = (next_x,next_y)
            end_route.append(cur_p)
        return end_route
    
    
    def do_split(source_image, starts, filter_ends):
        """
        切割图像
        source_image:要进行切割的图像。
        starts      :每一行的起始点,是一个包含元组的列表。每个元组代表一个起始点的坐标。
        filter_ends :每一行的终止点,与starts参数类似,代表每行的终止点坐标。
        """
    
        # 首先,初始化左边界left、上边界top、右边界right和下边界bottom,
        # 然后通过遍历starts和filter_ends计算出切割后图像的宽度和高度。
        left = starts[0][0]
        top = starts[0][1]
        right = filter_ends[0][0]
        bottom = filter_ends[0][1]
        pixdata = source_image.load()
    
        for i in range(len(starts)):
            left = min(starts[i][0], left)
            top = min(starts[i][1], top)
            right = max(filter_ends[i][0], right)
            bottom = max(filter_ends[i][1], bottom)
        width = right - left + 1
        height = bottom - top + 1
        image = Image.new('RGB', (width, height), (255,255,255))
        for i in range(height):
            start = starts[i]
            end = filter_ends[i]
            for x in range(start[0], end[0]+1):
                if pixdata[x,start[1]] == 0:
                    image.putpixel((x - left, start[1] - top), (0,0,0))
        return image
    
    
    def drop_fall(img):
        """滴水分割"""
        width,height = img.size
        # 1 二值化
        b_img = binarizing(img,200)
        # 2 垂直投影
        hist_width = vertical(b_img)
        # 3 获取起点
        start_x = get_start_x(hist_width)
    
        # 4 开始滴水算法
        start_route = []
        for y in range(height):
            start_route.append((0,y))
    
        end_route = get_end_route(img,start_x,height)
        filter_end_route = [max(list(k)) for _,k in groupby(end_route,lambda x:x[1])] # 注意这里groupby
        img1 = do_split(img,start_route,filter_end_route)
        img1.show('cut-1.png')
    
        start_route = list(map(lambda x : (x[0]+1,x[1]),filter_end_route)) # python3中map不返回list需要自己转换
        end_route = []
        for y in range(height):
            end_route.append((width-1,y))
        img2 = do_split(img,start_route,end_route)
        img2.show('cut-2.png')
    
    
    if __name__ == '__main__':
        p = Image.open(r"0001.JPG")
        drop_fall(p)
    
    分割效果

    • 优点:对粘连字符分割的很好
    • 缺点:原理、步骤实现起来稍显复杂

    总结

    • 在文本处理方面,字符分割可以帮助我们将一长串的文本拆分成有意义的词语或短语,从而方便进行文本分析、搜索和机器翻译等任务。在图像处理方面,字符分割是识别和提取图像中的文字信息的关键步骤。
      因此本文介绍了一些简单基础的分割算法,对于规范、空间位置均匀的一些字符,算法可以实现不错的分割效果。但是一旦出现粘连、字体间隙较大、结构差异较大等一些情形,还需要调整算法,或者结合多种算法才能实现一个较好的效果。
    • 觉得有帮助的话,给个赞吧