Realización de búsqueda de sellos en OpenCV sin neuronas, registro y SMS


No hace mucho tiempo, nos enfrentamos con la tarea de encontrar y extraer sellos de los documentos. ¿Para qué? Por ejemplo, para verificar la presencia de sellos en los contratos de dos partes (partes del contrato). En los contenedores ya teníamos un prototipo para encontrarlos, escrito en OpenCV, pero estaba húmedo. Decidimos desenterrar esta reliquia, sacudir el polvo y hacer una solución funcional sobre la base.


La mayoría de las técnicas descritas aquí se pueden aplicar fuera de la búsqueda de sellos. Por ejemplo:


  • segmentación de color;
  • buscar objetos redondos / círculos;
  • conversión de imagen a sistema de coordenadas polares;
  • intersección de objetos, Intersección sobre Unión (IoU, coeficiente Jacquard).

Como resultado, teníamos dos opciones: resolver usando redes neuronales o resucitar un prototipo en OpenCV. ¿Por qué decidimos tomar OpenCV? La respuesta está al final del artículo.


Los ejemplos de código se presentarán en Python y C #. Para Python, necesita los paquetes opencv-python y numpy , para C # necesita OpenCvSharp y opencv .


Espero que estés familiarizado con los algoritmos básicos de OpenCV. No profundizaré en ellos, ya que la mayoría de ellos merecen un artículo separado y detallado, pero dejaré enlaces con el tapete. parte de todo sediento.


, ( HLS-), . - , , .


. – .


, ? .



. / , , -, , .


, , python . ( , , ). ( Intersection over Union), .


:



:



, , , , - . , , , :



( 0.6) , F1- ( — ).


IoU
import sys
from collections import namedtuple

Metrics = namedtuple("metrics", ["tp", "fp", "fn"])

def bb_intersection_over_union(a, b):
   #           .
   # 0 -  X    ,
   # 1 -  Y    ,
   # 2 -  X    ,
   # 3 -  Y    .
   x_a = max(a[0], b[0])
   y_a = max(a[1], b[1])
   x_b = min(a[2], b[2])
   y_b = min(a[3], b[3])

   #  .
   inter_area = max(0, x_b - x_a + 1) * max(0, y_b - y_a + 1)

   #  .
   a_area = (a[2] - a[0] + 1) * (a[3] - a[1] + 1)
   b_area = (b[2] - b[0] + 1) * (b[3] - b[1] + 1)

   # Intersection over Union.
   iou = inter_area / float(a_area + b_area - inter_area)

   return iou

def same_stamp(a, b):
   """,         """
   iou = bb_intersection_over_union(a, b)
   print(f'iou: {iou}')
   return iou > 0.6

def compare_stamps(extracted, mapped):
   """
    
   :param extracted: ,  .
   :param mapped:  () .
   """
   tp = []
   fp = []
   fn = list(mapped)
   for stamp in extracted:
       for check in fn:
           if same_stamp(stamp, check):
               tp.append(check)
               fn.remove(check)
               break
       else:
           fp.append(stamp)
   return Metrics(len(tp), len(fp), len(fn))

def compare(file, sectors):
   """
       .
   :param file:   .
   :param sectors:  .
   """
   print(f'file: {file}')
   try:
       stamps = extract_stamps(file) #  ,     .
       metrics = compare_stamps(stamps, [ss for ss in sectors if 'stamp' in ss['tags']])
       return file, metrics
   except:
       print(sys.exc_info())
       return file, Metrics(0, 0, 0)

if __name__ == '__main__':
   file_metrics = {}

   # dataset - ,            () .
   for file, sectors in dataset.items():
       file_metrics[file] = compare(file, sectors)

   #  ,   F1-.
   total_metrics = Metrics(*(sum(x) for x in zip(*file_metrics.values())))
   precision = total_metrics.tp / (total_metrics.tp + total_metrics.fp) if total_metrics.tp > 0 else 0
   recall = total_metrics.tp / (total_metrics.tp + total_metrics.fn) if total_metrics.tp > 0 else 0
   f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0

   print('precision\trecall\tf1')
   print('{:.4f}\t{:.4f}\t{:.4f}'.format(precision * 100, recall * 100, f1 * 100).replace('.', ','))
   print('tp\tfp\tfn')
   print('{}\t{}\t{}'.format(total_metrics.tp, total_metrics.fp, total_metrics.fn))

   print('tp\tfp\tfn')
   for file in dataset.keys():
       metric = file_metrics.get(file)
       print(f'{metric.tp}\t{metric.fp}\t{metric.fn}')

   print(f'precision: {precision}, recall: {recall}, f1: {f1}, {total_metrics}')

HLS-


() - . , . ( ):



.


HLS . Hue, Lightness, Saturation, , , . :



? - . , ~180 ~280.



, - , , .


(Python)
def colored_mask(img, threshold = -1):
   #     .
   denoised = cv2.medianBlur(img, 3)
   cv2.imwrite('denoised.bmp', denoised)

   #      .
   gray = cv2.cvtColor(denoised, cv2.COLOR_BGR2GRAY)
   cv2.imwrite('gray.bmp', gray)

   #    .
   adaptiveThreshold = threshold if threshold >= 0 else cv2.mean(img)[0]
   color = cv2.cvtColor(denoised, cv2.COLOR_BGR2HLS)
   mask = cv2.inRange(color, (0, int(adaptiveThreshold / 6), 60), (180, adaptiveThreshold, 255))

   #     .
   dst = cv2.bitwise_and(gray, gray, mask=mask)
   cv2.imwrite('colors_mask.bmp', dst)
   return dst

(C#)
private void ColoredMask(Mat src, Mat dst, double threshold = -1)
{
 using (var gray = new Mat())
 using (var color = new Mat())
 using (var mask = new Mat())
 using (var denoised = new Mat())
 {
   //     .
   Cv2.MedianBlur(src, denoised, 3);
  denoised.Save("colors_denoised.bmp");

   //      .
   Cv2.CvtColor(denoised, gray, ColorConversionCodes.BGR2GRAY);
   gray.Save("colors_gray.bmp");

   //    .
   var adaptiveThreshold = threshold < 0 ? src.Mean()[0] : threshold;
   Cv2.CvtColor(denoised, color, ColorConversionCodes.BGR2HLS);
   Cv2.InRange(color, new Scalar(0, adaptiveThreshold / 6, 60), new Scalar(180, adaptiveThreshold, 255), mask);

   //     .
   Cv2.BitwiseAnd(gray, gray, dst, mask);
   dst.Save("colors_mask.bmp");
 }
}

:



, HLS OpenCV 0 360, 0 180. — uint8/uchar/byte, 0 255, 360 2.



, , ? , , “ ”.


, . , ( ) , , — . , .


, , . OpenCV , , . , , .


(Python)
# mask - ,   ;
# method:    ( OpenCV   Gradient);
# dp: ,      (1 —    , 2 —  /  ..     (..  ),   “”   - ,     .   ,   “”    );
# minDist:     (  %   );
# param1:   ,     (     2  );
# param2:     ;
# minRadius:   ;
# maxRadius:   ;
circles = cv2.HoughCircles(mask, cv2.HOUGH_GRADIENT, 1, 20, param1, param2, minRadius, maxRadius)

#   .
circles = np.uint16(np.around(circles))
cimg = cv2.cvtColor(img,cv2.COLOR_GRAY2BGR)
for i in circles[0,:]:
    cv2.circle(cimg,(i[0],i[1]),i[2],(165,25,165),2)

(C#)
// mask - ,   ;
// method:    ( OpenCV   Gradient);
// dp: ,      (1 —    , 2 —  /  ..     (..  ),   “”   - ,     .   ,   “”    );
// minDist:     (  %   );
// param1:   ,     (     2  );
// param2:     ;
// minRadius:   ;
// maxRadius:   ;
var houghCircles = Cv2.HoughCircles(morphed, HoughMethods.Gradient, 1, max, cannyEdgeThreshold, houghThreshold, min, max);

//   .
foreach (var circle in houghCircles)
 Cv2.Circle(morphed, circle.Center, (int)circle.Radius, new Scalar(165, 25, 165), 2);

:



, , . , , . , , , . (- “” - , - ).



- . .


, , . “” , , . — , .



, , , . . , . “” .


(Python)
def equals(first, second, epsilon):
   diff = cv2.subtract(first, second)
   nonZero = cv2.countNonZero(diff)
   area = first.size * epsilon
   return nonZero <= area

for i in circles[0, :]:
   empty = np.zeros((256, 256, 1), dtype="uint8")
   cv2.circle(empty, (i[0], i[1]), i[2], (255, 255, 255), -1)
   crop = img * (empty.astype(img.dtype))

   cv2.imwrite('crop.bmp', crop)

   if not equals(crop, empty, threshold):
       result.append(i)

(C#)
foreach (var circle in circles)
{
  var x = (int) circle.Center.X;
  var y = (int) circle.Center.Y;
  var radius = (int) Math.Floor(circle.Radius);

  using (var empty = Mat.Zeros(src.Rows, src.Cols, MatType.CV_8UC1))
  using (var mask = empty.ToMat())
  using (var crop = new Mat())
  {
    //   ()   .
    Cv2.Circle(mask, x, y, radius, Scalar.White, -1);
    src.CopyTo(crop, mask);

    crop.Save("crop.bmp");

    //    .
    if (!MatEquals(crop, empty, threshold))
    result.Add(circle);
  }
}

private bool MatEquals(Mat a, Mat b, double epsilon = 0.0)
{
  var notEquals = a.NotEquals(b);
  var nonZero = Cv2.CountNonZero(notEquals);
  var area = a.Rows * epsilon * a.Cols;
  return nonZero <= area;
}

. , , , - ?


, – , – (minor) (major) ( , , ).



: , , , .


() , , voila – .



– , – , – , – , .


(Python)
segments = []
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for contour in contours:
   if contour.shape[0] < 5:
       continue

   ellipse = cv2.fitEllipse(contour)
   width = ellipse[1][0]
   height = ellipse[1][1]
   minor = min(width, height)
   major = max(width, height)

   if minor / 2 > minorMin and major / 2 < majorMax:
       r1 = math.fabs(1 - math.fabs(major - minor) / max(minor, major))
       cv2.ellipse(src, ellipse, (255, 0, 0), 3)

       if r1 > roundness:
           segments.append((ellipse[0], major / 2))
           cv2.ellipse(src, ellipse, (0, 255, 0), 3)
   else: cv2.ellipse(src, ellipse, (0, 0, 255), 1)

cv2.imwrite('test_res.bmp', src)

(Python)
def distance(p1, p2):
   return math.sqrt(math.pow(p2[0] - p1[0], 2) + math.pow(p2[1] - p1[1], 2))

def isNested(inner, outer, epsilon):
   distance = distance(inner[0], outer[0])
   radius = outer[1] * epsilon
   return distance < radius and inner[1] < radius - distance

nested = []
for i in inner:
   for o in outer:
       if (isNested(i, o, 1.3)):
           if (distance(i[0], i[1]) < 30 and i[1] / o[1] > 0.75):
               nested.append(i)
           else: nested.append(o)

(C#)
Cv2.FindContours(src, out var contours, hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple);
  foreach (var contour in contours)
  {
    //      5 .
    if (contour.Height < 5)
    continue;

    var ellipse = Cv2.FitEllipse(contour);
    var minor = Math.Min(ellipse.Size.Width, ellipse.Size.Height);
    var major = Math.Max(ellipse.Size.Width, ellipse.Size.Height);

    if (minor / 2 > minorSize && major / 2 < majorSize)
    {
    //     .   ,      .
    var r1 = Math.Abs(1 - Math.Abs(major - minor) / Math.Max(minor, major));
    if (r1 > roundness)
    {
        var circle = new CircleSegment(ellipse.Center, major / 2);
        segments.Add(circle);
    }
    }
    contour.Dispose();
  }
}

(C#)
private bool IsCirclesNested(CircleSegment inner, CircleSegment outer, double epsilon)
{
  var distance = inner.Center.DistanceTo(outer.Center);
  var secondRadius = outer.Radius * epsilon;
  return distance < secondRadius && inner.Radius < secondRadius - distance;
}

var nested = new List<CircleSegment>();
foreach (var i in inner)
foreach (var o in outer)
{
  if (IsCirclesNested(i, o, 1.3))
  {
    if (i.Center.DistanceTo(o.Center) < 30 &&
        i.Radius / o.Radius > 0.75)
    nested.Add(i);
    else nested.Add(o);
  }
}

return outer.Union(inner).Except(nested).ToList();

, , , . . — , .


“ ”, , - . “ ”? “” . ? , - . :



, , : , , . :



— X Y , X, . , , , .



— , , ( ). , , ( , , , ).


, , . ( ) 90 .


(Python)
maxRadius = math.sqrt(math.pow(src.shape[0], 2) + math.pow(src.shape[1], 2)) / 2
magnitude = src.shape[0] / math.log(maxRadius)
center = (src.shape[0] / 2, src.shape[1] / 2)
polar = cv2.logPolar(src, center, magnitude, cv2.INTER_AREA)

(C#)
var maxRadius = Math.Sqrt(Math.Pow(stampImage.Width, 2) + Math.Pow(stampImage.Height, 2)) / 2;
var magnitude = stampImage.Width / Math.Log(maxRadius);
var center = new Point2f(stampImage.Width / 2, stampImage.Height / 2);
Cv2.LogPolar(stampImage, cartesianImage, center, magnitude, InterpolationFlags.Area);

, , OpenCV’ . , , , , OpenCV. , .


:



, , 90 , .


(Python)
#  .
sobel = cv2.Sobel(polar, cv2.CV_16S, 1, 0)
kernel = cv2.getStructuringElement(shape=cv2.MORPH_RECT, ksize=(1, 5))
img = cv2.convertScaleAbs(sobel)
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
img = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)

#   .
def is_vertical(img_src, line):
   tolerance = 10
   coords = line[0]
   angle = math.atan2(coords[3] - coords[1], coords[2] - coords[0]) * 180.0 / math.pi
   edge = img_src.shape[0] * 0.66
   out_of_bounds = coords[0] < edge and coords[2] < edge
   return math.fabs(90 - math.fabs(angle)) <= tolerance and not out_of_bounds

lines = cv2.HoughLinesP(img, 1, math.pi / 180, 15, img.shape[0] / 5, 10)
vertical = [line for line in lines if is_vertical(img, line)]
correct_lines = len(vertical)

(C#)
//  .
using (var sobel = new Mat())
using (var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(1, 5)))
{
  Cv2.Sobel(img, sobel, MatType.CV_16S, 1, 0);
  Cv2.ConvertScaleAbs(sobel, img);
  Cv2.Threshold(img, img, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu);
  Cv2.MorphologyEx(img, img, MorphTypes.Open, kernel);
  Cv2.MorphologyEx(img, img, MorphTypes.Close, kernel);
}

//   .
bool AlmostVerticalLine(LineSegmentPoint line)
{
  const int tolerance = 10;
  var angle = Math.Atan2(line.P2.Y - line.P1.Y, line.P2.X - line.P1.X) * 180.0 / Math.PI;
  var edge = edges.Width * 0.66;
  var outOfBounds = line.P1.X < edge && line.P2.X < edge;
  return Math.Abs(90 - Math.Abs(angle)) <= tolerance && !outOfBounds;
}
var lines = Cv2.HoughLinesP(img, 1, Math.PI / 180, 15, img.Width / 5, 10);
var correctLinesCount = lines.Count(AlmostVerticalLine);

, 85%. :


  • ( , );
  • ( — );
  • , , ;
  • , .

, — “ ?”. :


  • ;
  • ( );
  • ;
  • .

, , , . OpenCV , . U-Net, OpenCV , .

Source: https://habr.com/ru/post/undefined/


All Articles