【问题标题】:Measure how Straight/Smooth the Text Borders are Rendered in an Image测量图像中文本边框的笔直/平滑程度
【发布时间】:2020-06-26 09:29:02
【问题描述】:

我有两张图片:

我想测量文本边框呈现的笔直/平滑程度。

第一张图片被渲染得非常笔直,因此值得进行质量测量1。另一方面,第二张图像渲染了很多变体曲线(在某种程度上很粗糙),这就是为什么它应该得到一个低于1 的质量度量。我将如何使用图像处理或任何 Python 函数或任何用其他语言编写的函数来测量它?

澄清:

有些字体样式最初是用笔划呈现的,但也有一些字体样式可以像草书字体样式一样平滑地呈现。我真正追求的是通过质量衡量来区分字符的文本边框表面粗糙度。

我想测量文本边框在图像中呈现的笔直/平滑程度。 反过来,也可以说是想衡量文字边框在图片中渲染的粗糙程度。

【问题讨论】:

  • 你说“smooth”,但我想你的意思是说“straight”?
  • @CrisLuengo -> 是的,我的意思是直截了当。对不起
  • 那么下面的答案就没用了吗?我建议您编辑您的问题,以明确您的目标!
  • @CrisLuengo -> 好的,我会澄清的。它可以是直的或光滑的。

标签: python image image-processing text signal-processing


【解决方案1】:

我不知道任何 python 函数,但我会:

1) 使用potrace 跟踪边缘并将其转换为贝塞尔曲线。这是一个可视化:

2) 然后让我们放大到P 的顶部,例如: 您绘制垂直于曲线的有限长度(比如说 100 像素)的线。您在该线上绘制颜色强度(您可以转换为 HSI 或 HSV 并使用其中一个通道,或者只是转换为灰度并直接获取像素值):

3)然后你计算导数的标准偏差。小标准差意味着锐利的边缘,大的标准差意味着模糊的边缘。对于完美边缘,标准偏差为零。

4) 对于每条边,如果你画了一条垂直线,你现在就有了一个“平滑度”值。然后,您可以按照您认为合适的方式平均每个边缘、每个字母、每个单词或每个图像的所有平滑度值。此外,绘制的垂直线越多,平滑度值越准确,但计算量越大。

【讨论】:

  • 网页内容无障碍指南建议在测量文本-背景对比度之前转换为线性比例(相对亮度)。
【解决方案2】:

我会尝试一些简单的方法,例如使用 opencv 库中的一些函数创建“粗糙度”度量,因为它很容易在 Python(和 C++ 以及其他包装器)中使用。

例如(没有实际来源,因为我在手机上打字):

  1. 预处理以创建二进制图像(许多标准方式)。
  2. 使用cv2.findContours 获取字母的轮廓。
  3. 在每个轮廓上使用cv2.arcLength 作为分母。
  4. 使用cv2.approxPolyDP 简化每个轮廓。
  5. 在每个简化轮廓上使用cv2.arcLength 作为分子。
  6. 计算简化与完整弧长的比率。

在第 5 步中,接近 1.0 的比率需要较少的简化,因此它们可能不那么粗糙。接近 0.0 的比率需要大量简化,因此可能非常粗糙。当然,您必须调整轮廓查找代码以获得合适的轮廓,并且您需要管理数值精度以保持数学计算有意义,但希望这个想法足够清晰。

OpenCV 还具有有用的函数cv2.convexHullcv2.convexityDefects,您可能会在相关工作中发现它们感兴趣。但是,它们似乎不适合这里的字母,因为像 M 这样的字母的内部特征更难处理。

说到粗糙的东西,我承认这个算法大纲非常粗糙!不过,我希望它能给您提供一个有用的想法,让您尝试一下似乎很简单,可以快速实施以开始获得定量反馈。

【讨论】:

  • 我在任何轮廓、顶点计数方法中遇到的问题是对分辨率和比例的敏感性。以两种不同的分辨率对相同非圆弧曲率的两种情况进行成像;尽管“形式”不变,但您可能会产生两个不同的第 6 步比率。因此,除非您可以确定字符具有等效的“DPI”和比例,否则很难将图像与图像进行比较。出于这个原因,我在玩弄标准化基于顶点的方法的方法。有什么想法吗?
  • 我同意!我打算建议这个想法作为基于进一步(缺失)上下文的粗糙度度量的起点,而不仅仅是原始比率。如果不做字母 ID,使用真实测试数据在 python 中进行调整似乎很好。顶点计数似乎对字体字母变化不太健壮。我想考虑 res/scale(例如乘数、单位长度曲率,..),但我想先了解问题标准:在进入理论上的兔子洞之前澄清平滑/粗糙度、可接受的误差等。我不完全了解 OP 实际用例,但我希望简化比较概念值得一提。
  • 我的想法完全一样。我认为问题中没有足够的细节来提供明确的“答案”,或者实际上是否从根本上计算可以应用于渲染/打印质量、字体、分辨率和比例的“平滑度指标”甚至是可行的。我爆发的另一条思路是计算连续轮廓段的点积上的“拐点”或 CW/CCW 反转的数量(如果这有意义?)也许你有一些想法或者可以探索简化轮廓的想法?
  • @GeorgeKerwood 我有一个类似的 CW/CCW 想法,但选择了我的方式来解决 OP 希望第一个样本达到质量度量 1 的愿望。纯拐点计数会惩罚非常平滑但改变拐点的字母当您遍历它们的周边时(例如 M 或实际上大多数字母)。这就是为什么我采用简化比率的想法来衡量质量。我尝试将每个实际字母与其“试图成为”的字母进行比较(由 approxPolyDP w/good epsilon 估计)。因此,弧长会随着任一方向的噪声拐点被平滑而缩小,但更大的比例形状仍然存在。
  • @GeorgeKerwood 我在最初对您的回复中提到的“每长度曲率”的想法是为了表明一种与弯曲方向无关的弯曲度测量(受拓扑结构的启发)以消除“快速”变化在任一方向弯曲,同时保留“较慢”的弯曲部分。这似乎特别重要,因为 OP 提到了处理弯曲字体。我在一个实际项目中使用了这种单位长度曲率测量(按分辨率缩放)来消除通过分水岭检测到的粗糙轮廓中的噪声,并且它适用于该应用程序(RT 4k@60Hz)。
【解决方案3】:

一个想法可能是简单地使用 cv2.CHAIN_APPROX_SIMPLE 获得 Python/OpenCV 中每个字符的顶点数的平均值。

由于您有相同的字符并且您想知道它们的直线度,因此 CHAIN_APPROX_SIMPLE 仅测量水平和垂直角顶点。对于您的第一张图片,顶点应该比您的第二张图片少得多。

CHAIN_APPROX_SIMPLE 压缩水平、垂直和对角线 分段并只留下它们的端点。例如,直立式 矩形轮廓用 4 个点编码。

import cv2
import numpy as np

# read image
img = cv2.imread('lemper1.png')
#img = cv2.imread('lemper2.png')

# convert to grayscale
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

# threshold
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)[1]

# invert
thresh = 255 - thresh

# get contours and compute average number of vertices per character (contour)
result = img.copy()
contours = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
num_contours = 0
sum = 0
for cntr in contours:
    cv2.drawContours(result, [cntr], 0, (0,0,255), 1)
    num_vertices = len(cntr)
    sum = sum + num_vertices
    num_contours = num_contours + 1

smoothness = (sum / num_contours)
print(smoothness)

# save resulting images
cv2.imwrite('lemper1_contours.png',result)
#cv2.imwrite('lemper2_contours.png',result)

# show thresh and result    
cv2.imshow("thresh", thresh)
cv2.imshow("contours", result)
cv2.waitKey(0)
cv2.destroyAllWindows()


First image average number of vertices: 49.666666666666664

Second image average number of vertices: 179.14285714285714


因此,较少的顶点数意味着更直的字符。

【讨论】:

  • 我也玩过这个想法,它的问题,至少在我的测试中,是对缩放和分辨率的敏感性。例如,在这种情况下想象一个完美的“平滑”圆或“O”。在高分辨率下,该曲率会导致许多顶点,尽管它是“平滑的”,并且有问题的是,可能不仅仅是“粗糙”的低分辨率“O”。我正在考虑通过聚合轮廓长度或其他度量以某种方式标准化顶点数的想法。你怎么看?
  • 我第一个实现然后丢弃的方法是将顶点数除以周长,以便将每单位长度的计数作为标准化比例的一种手段。这对你来说更有意义吗?周长由 cv2.arcLength() 测量。
  • 这正是我的第一次尝试,但不幸的是,它产生的结果未能通过测试用例。我“手动”对并列的轮廓段求和,而不是 .acrLength(),但结果是一样的。
【解决方案4】:

前言

这里基于作为折线的字符轮廓属性提出了一些不错的想法。虽然这种方法存在一些固有缺陷,因为它们是分辨率和比例的函数,但我想提供一个进一步的中断。 我的算法仍然容易受到影响,但它可能会提供不同的视角。

理论

我建议的方法是通过轮廓中的拐点数来比较常见字符。在这种情况下,我所说的拐点是指连续折线段作为向量的叉积之间的符号变化。例如;考虑一个圆的折线轮廓,从中间 y 坐标和 x+ 最坐标开始。如果我们要围绕周界跟踪折线轮廓 CW(顺时针),则每个线段将递增地是先验的 CW 变换。如果在任何时候一个线段“远离”或“向外”,这个变换将是 CCW(逆时针)并且叉积将反转。因此,“粗糙”的圆会有拐点,“完美”或“平滑”的圆没有拐点。

算法

该算法使用Emgu.CV 遵循以下步骤。 C# 下面的代码:

  1. 图像被加载并通过阈值转换为二进制
  2. 然后对二值图像进行轮廓检测,这些轮廓按其边界框从左到右排序,因此它们的索引与它们轮廓的字符的出现顺序相匹配。
  3. 然后将每个轮廓重新指向相同数量的片段,以便标准化图像/字符之间的比例和分辨率差异。
  4. 每个轮廓都被“遍历”并计算其拐点数。
// [Some basic extensions are omitted for clarity] 
// Load the images
Image<Rgb, byte> baseLineImage = new Image<Rgb, byte>("BaseLine.png");
Image<Rgb, byte> testCaseImage = new Image<Rgb, byte>("TestCase.png");
// Convert them to Gray Scale
Image<Gray, byte> baseLineGray = baseLineImage.Convert<Gray, byte>();
Image<Gray, byte> testCaseGray = testCaseImage.Convert<Gray, byte>();
// Threshold the images to binary
Image<Gray, byte> baseLineBinary = baseLineGray.ThresholdBinaryInv(new Gray(100), new Gray(255));
Image<Gray, byte> testCaseBinary = testCaseGray.ThresholdBinaryInv(new Gray(100), new Gray(255));
// Some dilation required on the test image so that the characters are continuous
testCaseBinary = testCaseBinary.Dilate(3);

// Extract the the contours from the images to isolate the character profiles
// and sort them left to right so as the indicies match the character order
VectorOfVectorOfPoint baseLineContours = new VectorOfVectorOfPoint();
Mat baseHierarchy = new Mat();
CvInvoke.FindContours(
    baseLineBinary,
    baseLineContours,
    baseHierarchy,
    RetrType.External,
    ChainApproxMethod.ChainApproxSimple);
var baseLineContoursList = baseLineContours.ToList();
baseLineContoursList.Sort(new ContourComparer());

VectorOfVectorOfPoint testCaseContours = new VectorOfVectorOfPoint();
Mat testHierarchy = new Mat();
CvInvoke.FindContours(
    testCaseBinary,
    testCaseContours,
    testHierarchy,
    RetrType.External,
    ChainApproxMethod.ChainApproxSimple);
var testCaseContoursList = testCaseContours.ToList();
testCaseContoursList.Sort(new ContourComparer());

var baseLineRepointedContours = RepointContours(baseLineContoursList, 50);
var testCaseRepointedContours = RepointContours(testCaseContoursList, 50);

var baseLineInflectionCounts = GetContourInflections(baseLineRepointedContours);
var testCaseInflectionCounts = GetContourInflections(testCaseRepointedContours);

拐点检测/计数

static List<List<Point>> GetContourInflections(List<VectorOfPoint> contours)
{
    // A resultant list to return the inflection points
    List<List<Point>> result = new List<List<Point>>();

    // Calculate the forward to reverse cross product at each vertex
    List<double> crossProducts;
    // Points used to store 2D Vectors as X,Y (I,J)
    Point priorVector, forwardVector;
    foreach (VectorOfPoint contour in contours)
    {
        crossProducts = new List<double>();
        for (int p = 0; p < contour.Size; p++)
        {
            // Determine the vector to the prior to this vertex
            priorVector = p == 0 ?
            priorVector = new Point()
            {
                X = contour[p].X - contour[contour.Size - 1].X,
                Y = contour[p].Y - contour[contour.Size - 1].Y
            } :
            priorVector = new Point()
            {
                X = contour[p].X - contour[p - 1].X,
                Y = contour[p].Y - contour[p - 1].Y
            };

            // Determine the vector to the next vector
            // If this is the lst vertex, loop back to vertex 0
            forwardVector = p == contour.Size - 1 ?
            new Point()
            {
                X = contour[0].X - contour[p].X,
                Y = contour[0].Y - contour[p].Y,
            } :
            new Point()
            {
                X = contour[p + 1].X - contour[p].X,
                Y = contour[p + 1].Y - contour[p].Y,
            };

            // Calculate the cross product of the prior and forward vectors
            crossProducts.Add(forwardVector.X * priorVector.Y - forwardVector.Y * priorVector.X);
        }

        // Given the calculated cross products, detect the inflection points
        List<Point> inflectionPoints = new List<Point>();
        for (int p = 1; p < contour.Size; p++)
        {                   
            // If there is a sign change between this and the prior cross product, an inflection,
            // or change from CW to CCW bearing increments has occurred. To and from zero products
            // are ignored
            if ((crossProducts[p] > 0 && crossProducts[p-1] < 0) ||
                (crossProducts[p] < 0 && crossProducts[p-1] > 0))
            {
                inflectionPoints.Add(contour[p]);
            }
        }
        result.Add(inflectionPoints);
    }
    return result;
}

输出

L: Baseline Inflections:0 Testcase Inflections:22
E: Baseline Inflections:1 Testcase Inflections:16
M: Baseline Inflections:4 Testcase Inflections:15
P: Baseline Inflections:11 Testcase Inflections:17
E: Baseline Inflections:1 Testcase Inflections:10
R: Baseline Inflections:9 Testcase Inflections:16

轮廓(蓝色)和变形(红色)

【讨论】:

  • 这是个好主意 (+1),但实施需要工作。第一个例子中的字母没有你检测到的那么多拐点,对角直线不是每个像素都有拐点,它有 0 个这样的点。如果顶点应该处理这一点,则进行一些平滑处理,或者明确寻找直线的阶梯效应并忽略这些。
  • @CrisLuengo 感谢您的反馈。您已经发现了折线轮廓的一些限制之一,因为最终不是“曲线”,而只是“楼梯”。我想看到的也许是轮廓顶点到一些贝塞尔样条集的回归,可以分析真实的数学拐点或替代曲率梳方法。我将进一步考虑一种算法解决方案,以根据楼梯情况检测交替叉积符号。
  • @CrisLuengo 我添加了一个简单的轮廓重新指向,以允许非正交向量并为比例/分辨率提供某种程度的标准化。我不包括与正交向量之间的转换为拐点,从而导致“完美” Es 的单点。它远非完美,但我觉得这是一种进步。
  • 我同意,这看起来不错!当然,找到合适的规模并非易事。不错!
  • 谢谢!事后看来,您的第一条评论现在似乎很明显,但是如果没有一定程度的减少和平滑,每个备用顶点都将是一个拐点。谢谢你驱使我进一步思考;真正建设性的批评。
猜你喜欢
  • 2020-08-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-06-24
  • 1970-01-01
  • 1970-01-01
  • 2016-01-20
相关资源
最近更新 更多