文本提取 - 逐行提取

11
我正在使用Google Vision API,主要是为了提取文本。它的效果不错,但对于需要API扫描整行并在移动到下一行之前输出文本的特定情况,它似乎使用了某种逻辑,使其从左侧自上而下地扫描并向右侧移动,然后再进行自上而下的扫描。我希望API能够从左到右阅读,然后向下移动等等。
例如,考虑以下图像:

enter image description here

API返回的文本如下:
“ Name DOB Gender: Lives In John Doe 01-Jan-1970 LA ”

然而,我本以为会有类似这样的东西:

 Name: John Doe DOB: 01-Jan-1970 Gender: M Lives In: LA 

我想应该有一种方法来定义块大小或边距设置,以逐行读取图像/扫描线?

感谢您的帮助。 Alex

6个回答

10
这可能是一个晚回答,但为了以后的参考,您可以在JSON请求中添加功能提示以获得所需的结果。
{
  "requests": [
    {
      "image": {
        "source": {
          "imageUri": "https://istack.dev59.com/TRTXo.webp"
        }
      },
      "features": [
        {
          "type": "DOCUMENT_TEXT_DETECTION"
        }
      ]
    }
  ]
}

对于相隔较远的文本,DOCUMENT_TEXT_DETECTION 也无法提供适当的行分割。下面的代码根据字符多边形坐标进行简单的行分割。

enter image description here

https://github.com/sshniro/line-segmentation-algorithm-to-gcp-vision


我看到了这段代码,它很短,但是我想在Java中使用它,如何转换? - nobjta_9x_tq
语法大致相同。该算法使用多边形计算库,因此在Java中查找点是否在多边形内应使用类似的库。 - Nirojan Selvanathan
1
谢谢,我用Java编写了这个程序,它可以计算两个矩形间的空间重叠。 - nobjta_9x_tq
1
这段 JavaScript 代码对我有效,但我能否得到相同的 Python 代码? - Deepak S. Gavkar
我已经复制了这个仓库的网页浏览器版本,在gitHub上这里 - vee

4

这里是一个简单的代码,逐行读取文本。y轴代表每一行,x轴代表每行中的每个单词。

items = []
lines = {}

for text in response.text_annotations[1:]:
    top_x_axis = text.bounding_poly.vertices[0].x
    top_y_axis = text.bounding_poly.vertices[0].y
    bottom_y_axis = text.bounding_poly.vertices[3].y

    if top_y_axis not in lines:
        lines[top_y_axis] = [(top_y_axis, bottom_y_axis), []]

    for s_top_y_axis, s_item in lines.items():
        if top_y_axis < s_item[0][1]:
            lines[s_top_y_axis][1].append((top_x_axis, text.description))
            break

for _, item in lines.items():
    if item[1]:
        words = sorted(item[1], key=lambda t: t[0])
        items.append((item[0], ' '.join([word for _, word in words]), words))

print(items)

由于某种原因,Google Vision 将总数分成了几个部分,例如:161.765,31。它将其拆分为五个单词 [161, ., 765, ,, 31]。我是否缺少配置? - LTroya
如果图像稍微旋转,这种方法就行不通了。 - Said Al Souti

1
我获取最大和最小的y值,并迭代y以获取所有可能的线条,下面是完整代码。
import io
import sys
from os import listdir

from google.cloud import vision


def read_image(image_file):
    client = vision.ImageAnnotatorClient()

    with io.open(image_file, "rb") as image_file:
        content = image_file.read()

    image = vision.Image(content=content)

    return client.document_text_detection(
        image=image,
        image_context={"language_hints": ["bg"]}
    )


def extract_paragraphs(image_file):
    response = read_image(image_file)

    min_y = sys.maxsize
    max_y = -1
    for t in response.text_annotations:
        poly_range = get_poly_y_range(t.bounding_poly)
        t_min = min(poly_range)
        t_max = max(poly_range)
        if t_min < min_y:
            min_y = t_min
        if t_max > max_y:
            max_y = t_max
    max_size = max_y - min_y

    text_boxes = []
    for t in response.text_annotations:
        poly_range = get_poly_y_range(t.bounding_poly)
        t_x = get_poly_x(t.bounding_poly)
        t_min = min(poly_range)
        t_max = max(poly_range)
        poly_size = t_max - t_min
        text_boxes.append({
            'min_y': t_min,
            'max_y': t_max,
            'x': t_x,
            'size': poly_size,
            'description': t.description
        })

    paragraphs = []
    for i in range(min_y, max_y):
        para_line = []
        for text_box in text_boxes:
            t_min = text_box['min_y']
            t_max = text_box['max_y']
            x = text_box['x']
            size = text_box['size']

            # size < max_size excludes the biggest rect
            if size < max_size * 0.9 and t_min <= i <= t_max:
                para_line.append(
                    {
                        'text': text_box['description'],
                        'x': x
                    }
                )
        # here I have to sort them by x so the don't get randomly shuffled
        para_line = sorted(para_line, key=lambda x: x['x'])
        line = " ".join(map(lambda x: x['text'], para_line))
        paragraphs.append(line)
        # if line not in paragraphs:
        #     paragraphs.append(line)

    return "\n".join(paragraphs)


def get_poly_y_range(poly):
    y_list = []
    for v in poly.vertices:
        if v.y not in y_list:
            y_list.append(v.y)
    return y_list


def get_poly_x(poly):
    return poly.vertices[0].x




def extract_paragraphs_from_image(picName):
    print(picName)
    pic_path = rootPics + "/" + picName

    text = extract_paragraphs(pic_path)

    text_path = outputRoot + "/" + picName + ".txt"
    write(text_path, text)


这段代码正在进行中。

最终,我会得到多次相同的行,并进行后处理以确定确切的值(paragraphs变量)。如果需要澄清任何内容,请告诉我。


嗨,@Borislav Stoilov!你还记得你给常量“PARAGRAPH_HEIGHT”赋了什么值吗?如果你能回答的话,那会对我非常有帮助。谢谢! - SimpForJS
1
@SimpForJS 不,我完全改变了我的方法。现在我获取所有矩形中的最大x和最大y。然后我从minY迭代到maxY,每个在当前y中的矩形都是潜在的线条。我将在答案中发布我的代码。 - Borislav Stoilov

1
您可以根据每行的边界提取文本,也可以使用boundyPoly将同一行中的文本连接起来。
"boundingPoly": {
        "vertices": [
          {
            "x": 87,
            "y": 148
          },
          {
            "x": 411,
            "y": 148
          },
          {
            "x": 411,
            "y": 206
          },
          {
            "x": 87,
            "y": 206
          }
        ]

例如,这两个单词在同一“行”中。
"description": "you",
      "boundingPoly": {
        "vertices": [
          {
            "x": 362,
            "y": 1406
          },
          {
            "x": 433,
            "y": 1406
          },
          {
            "x": 433,
            "y": 1448
          },
          {
            "x": 362,
            "y": 1448
          }
        ]
      }
    },
    {
      "description": "start",
      "boundingPoly": {
        "vertices": [
          {
            "x": 446,
            "y": 1406
          },
          {
            "x": 540,
            "y": 1406
          },
          {
            "x": 540,
            "y": 1448
          },
          {
            "x": 446,
            "y": 1448
          }
        ]
      }
    }

谢谢,那是一个可能性。 - Alagappan Narayanan

0
受Borislav的回答启发,我刚刚写了一些Python代码,也适用于手写文字识别。虽然有点混乱,但我认为你可以了解如何实现这个功能。
一个类来保存每个单词的一些扩展数据,例如单词的平均y位置,我用它来计算单词之间的差异:
import re
from operator import attrgetter

import numpy as np

class ExtendedAnnotation:
    def __init__(self, annotation):
        self.vertex = annotation.bounding_poly.vertices
        self.text = annotation.description
        self.avg_y = (self.vertex[0].y + self.vertex[1].y + self.vertex[2].y + self.vertex[3].y) / 4
        self.height = ((self.vertex[3].y - self.vertex[1].y) + (self.vertex[2].y - self.vertex[0].y)) / 2
        self.start_x = (self.vertex[0].x + self.vertex[3].x) / 2

    def __repr__(self):
        return '{' + self.text + ', ' + str(self.avg_y) + ', ' + str(self.height) + ', ' + str(self.start_x) + '}'

使用该数据创建对象:

def get_extended_annotations(response):
    extended_annotations = []
    for annotation in response.text_annotations:
        extended_annotations.append(ExtendedAnnotation(annotation))

    # delete last item, as it is the whole text I guess.
    del extended_annotations[0]
    return extended_annotations

计算阈值。
首先,将所有单词按其y位置排序,即定义为所有4个角的平均值。此时x位置不相关。 然后,计算每个单词和其后续单词之间的差异。对于一条完全直线的单词,您期望每两个单词之间的y位置差异为0。即使是手写体,也应该在1~10左右。
但是,每当有换行时,前一行的最后一个单词与新一行的第一个单词之间的差异要比那大得多,例如50或60。
因此,为了决定是否应在两个单词之间换行,使用差异的标准偏差。

def get_threshold_for_y_difference(annotations):
    annotations.sort(key=attrgetter('avg_y'))
    differences = []
    for i in range(0, len(annotations)):
        if i == 0:
            continue
        differences.append(abs(annotations[i].avg_y - annotations[i - 1].avg_y))
    return np.std(differences)

计算出阈值后,将所有单词的列表按照相应的行进行分组。

def group_annotations(annotations, threshold):
    annotations.sort(key=attrgetter('avg_y'))
    line_index = 0
    text = [[]]
    for i in range(0, len(annotations)):
        if i == 0:
            text[line_index].append(annotations[i])
            continue
        y_difference = abs(annotations[i].avg_y - annotations[i - 1].avg_y)
        if y_difference > threshold:
            line_index = line_index + 1
            text.append([])
        text[line_index].append(annotations[i])
    return text

最后,每一行都按照它们的x位置进行排序,以便从左到右将它们放入正确的顺序中。
然后使用一些正则表达式来删除标点符号前面的空格。
def sort_and_combine_grouped_annotations(annotation_lists):
    grouped_list = []
    for annotation_group in annotation_lists:
        annotation_group.sort(key=attrgetter('start_x'))
        texts = (o.text for o in annotation_group)
        texts = ' '.join(texts)
        texts = re.sub(r'\s([-;:?.!](?:\s|$))', r'\1', texts)
        grouped_list.append(texts)
    return grouped_list

0

根据Borislav Stoilov的最新回答,我为需要的任何人编写了C#代码。以下是代码:

public static List<TextParagraph> ExtractParagraphs(IReadOnlyList<EntityAnnotation> textAnnotations)
    {
        var min_y = int.MaxValue;
        var max_y = -1;
        foreach (var item in textAnnotations)
        {
            var poly_range = Get_poly_y_range(item.BoundingPoly);
            var t_min = poly_range.Min();
            var t_max = poly_range.Max();
            if (t_min < min_y) min_y = t_min;
            if (t_max > max_y) max_y = t_max;
        }
        var max_size = max_y - min_y;
        var text_boxes = new List<TextBox>();

        foreach (var item in textAnnotations)
        {
            var poly_range = Get_poly_y_range(item.BoundingPoly);
            var t_x = Get_poly_x(item.BoundingPoly);
            var t_min = poly_range.Min();
            var t_max = poly_range.Max();
            var poly_size = t_max - t_min;
            text_boxes.Add(new TextBox
            {
                Min_y = t_min,
                Max_y = t_max,
                X = t_x,
                Size = poly_size,
                Description = item.Description
            });
        }

        var paragraphs = new List<TextParagraph>();
        for (int i = min_y; i < max_y; i++)
        {
            var para_line = new List<TextLine>();
            foreach (var text_box in text_boxes)
            {
                int t_min = text_box.Min_y;
                int t_max = text_box.Max_y;
                int x = text_box.X;
                int size = text_box.Size;

                //# size < max_size excludes the biggest rect
                if (size < (max_size * 0.9) && t_min <= i && i <= t_max)
                    para_line.Add(
                        new TextLine
                        {
                            Text = text_box.Description,
                            X = x
                        }
                    );
            }

            // here I have to sort them by x so the don't get randomly enter code hereshuffled
            para_line = para_line.OrderBy(x => x.X).ToList();
            var line = string.Join(" ", para_line.Select(x => x.Text));
            var paragraph = new TextParagraph
            {
                Order = i,
                Text = line,
                WordCount = para_line.Count,
                TextBoxes = para_line
            };
            paragraphs.Add(paragraph);
        }
        return paragraphs;
        //return string.Join("\n", paragraphs);

    }

    private static List<int> Get_poly_y_range(BoundingPoly poly)
    {
        var y_list = new List<int>();
        foreach (var v in poly.Vertices)
        {
            if (!y_list.Contains(v.Y))
            {
                y_list.Add(v.Y);
            }
        }
        return y_list;
    }

    private static int Get_poly_x(BoundingPoly poly)
    {
        return poly.Vertices[0].X;
    }

调用ExtractParagraphs()方法将返回一个字符串列表,其中包含文件中的双精度数值。我还编写了一些自定义代码来解决这个问题。如果您需要任何帮助处理双精度数值,请告诉我,我可以提供其余的代码。
示例:
图片中的文本:"我想让这个东西24/7地工作!"
代码将返回:
"我"
"我想"
"我想要"
"我想要制作"
"我想要制作这个"
"我想要制作这个东西"
"我想要制作这个东西工作"
"我想要制作这个东西工作24/7!"
"要制作这个东西工作24/7!"
"制作这个东西工作24/7!"
"这个东西工作24/7!"
"东西工作24/7!"
"工作24/7!"

我还实现了将PDF解析为PNG的功能,因为Google Cloud Vision Api不接受未存储在云存储桶中的PDF文件。如果需要,我可以提供它。 祝编码愉快!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接