Как проверить, есть ли 5 ​​флажков или 6 в срезе изображения? - PullRequest
0 голосов
/ 17 сентября 2018

У меня есть два типа срезов, один имеет 6 флажков, а другой - 5.

  1. Slice with 6 checkboxes

  2. Slice with 5 checkboxes

Вы можете проверить полный набор данных здесь

Мой подход (плохо работает)

Я взял среднее значение изображений, используя np.mean(image), и выставил пороговое значение (140), чтобы, если значение было больше, чем это, тогда у изображения было шесть флажков, в противном случае у него было пять. Идея этого подхода заключается в том, что, на мой взгляд, срез с шестью флажками имеет больше черных пикселей, чем с пятью.

Вопрос

Итак, мой вопрос: что еще я могу сделать, чтобы получить точную классификацию? Я использую Python 3.6 и OpenCV, поэтому некоторые решения, использующие их, будут оценены.

По желанию, хотя у меня нет таких данных, чтобы проводить глубокий процесс обучения. Мне интересно знать, может ли здесь помочь и глубокое обучение.

Спасибо.

EDIT

Забыл упомянуть об этом, я также пытался найти контуры и формы (квадраты и прямоугольники), но они не согласованы из-за низкого разрешения и из-за того, что на прямоугольниках тоже могут быть отметки. Я получаю 2-3 ящика для обоих, но этого недостаточно, чтобы сказать мне разницу

Ответы [ 2 ]

0 голосов
/ 19 сентября 2018

После нескольких неудачных попыток следующий подход, похоже, дает удовлетворительные результаты для предоставленного входного набора данных.


Prelude

При первом осмотре я заметил, что все образцы изображений были одинаковой формы, поэтому я мог легко их сложить. Я начал с наблюдения изображения, содержащего все входные изображения, сложенные вертикально (используя numpy.vstack)

All input images vertically stacked

Я сделал следующие наблюдения:

  • Все изображения (и флажки) имеют одинаковый масштаб
  • Существует 2 вида макетов (5 блоков, 6 блоков), и расположение флажков редко пересекается (хороший критерий)
  • Для каждого типа макета флажки находятся примерно в одном и том же месте.
  • Вертикальные края каждого флажка кажутся наиболее заметными чертами

Играя с графическим редактором, я определил, что следующие маски являются хорошей оценкой расположения флажков:

  • 5 коробок: Mask for 5 boxes
  • 6 коробок: Mask for 6 boxes

или, в коде Python, с отображением пар первого / последнего столбца для каждого региона:

# Define the zones (x axis ranges) where checkboxes may occur
zones_a = [(50, 72), (144, 166), (243, 265), (328, 350), (436, 458)] # 5 box scenario
zones_b = [(42,  64), (122, 144), (207, 229), (276, 298), (369, 391), (496, 518)] # 6 box scanario

Имея это в виду, я пришел к следующему подходу:

  • Минимизируйте шум (некоторые, если входные изображения очень плохие)
  • Старайтесь подчеркивать вертикальные линии флажков, исключая как можно больше остальных
  • Найдите, где вертикальные линии сгруппированы
  • Найдите шаблон, которому они лучше соответствуют

Proprocessing

Для демонстрации я пойду с одним из противных:

Nastiest sample input

Во-первых, я прочитал его как изображение в оттенках серого

img = cv2.imread(filename, cv2.IMREAD_GRAYSCALE)

и его преобразование в двоичную форму - адаптивный порог с довольно большим размером блока, по-видимому, хорошо удаляет большую часть шума при сохранении соответствующих деталей (хотя в этом случае еще много нежелательного мусора осталось)

thresh = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, 2)

NB: Поскольку на данный момент мы работаем с черным текстом на белом, значения erode и dilate меняются местами - эрозия расширяет черные части, расширяет их уменьшает. (интуитивно понятно, как только вы ухватились за тему)

Далее я пытаюсь выделить вертикальные края с помощью морфологических операций

thresh = cv2.morphologyEx(thresh, cv2.MORPH_ERODE, np.ones((1,3),np.uint8))

, а затем снять выделение с горизонтальных краев (включая большую часть текста)

thresh = cv2.morphologyEx(thresh, cv2.MORPH_DILATE, np.ones((3,1),np.uint8))

В качестве следующего шага я использую Детектор краев Canny , чтобы найти все края

edges = cv2.Canny(thresh, 40, 120, apertureSize=5)

NB: Теперь края белые, а остальные черные, поэтому морфологические операции работают [наивно] ожидаемо. (Опять же, интуитивно понятно, как только вы ухватились за тему)

Теперь я делаю морфологическое открытие в порядке, чтобы устранить горизонтальные края (которые теперь обычно являются однопиксельными линиями), сохраняя при этом вертикальные края.

edges = cv2.morphologyEx(edges, cv2.MORPH_OPEN, np.ones((5,1),np.uint8))

И я следую этому, подчеркивая вертикальные края, используя расширение

edges = cv2.morphologyEx(edges, cv2.MORPH_DILATE, np.ones((1,3),np.uint8))

Intermediate images


Анализ

Изучив предварительно обработанное изображение, я заметил, что там, где присутствовали флажки, было много столбцов, содержащих большинство белых пикселей, тогда как в других местах это было не так.

Я использовал технику, называемую «вертикальная проекция», чтобы уменьшить двумерное изображение до 1 измерения, взяв среднюю интенсивность каждого столбца.

projection = np.mean(edges, 0).flatten()

сглаживает его, используя средний фильтр, примерно такой же ширины, как каждое потенциальное расположение флажка

projection = cv2.blur(projection, (1, 21)).flatten()

, а затем снова сглаживает его на половине пролета

projection = cv2.blur(projection, (1, 11)).flatten()

У последней кривой projection теперь были видные пики, где были расположены флажки.

На следующем графике показаны результаты этой обработки (желтый = оригинальный, красный = pass1, синий = pass2).

vertical projections

Следующим шагом было нахождение пиков на этой кривой - оказалось, что scipy.signal.find_peaks дает желаемые результаты.

peaks = find_peaks(projection)[0]

Поскольку в пределах области ящика потенциально может быть несколько пиков, я решил сохранить соответствующие значения для каждого пика (для последующей дискриминации)

peak_values = projection[peaks]

Теперь я могу генерироватьхороший график для визуализации вероятного расположения флажков, а также обнаруженных пиков, а также диапазонов, в которых флажки должны находиться в двух сценариях.

Smoothed projection, peaks and zones

На этом графике:

  • синяя кривая - сглаженная вертикальная проекция
  • вертикальные фиолетовые линии показывают обнаруженные пики
  • слабый желтый и красныйобласти показывают, где могут присутствовать флажки ** желтые, если присутствуют 5 присутствующих ** красные, если присутствуют 6 присутствующих

В этот момент я знал места, где флажки моглиbe (местоположение пика), вместе с индикатором того, насколько вероятно это (значение на пике).Этого было достаточно, чтобы решить, какой сценарий лучше всего подходит.

Первым шагом было «сгладить пики».Для каждого сценария был набор диапазонов, каждый из которых определял минимальную и максимальную координату X.Я использовал следующую функцию для сбора пиков для каждого потенциального местоположения флажка:

def bin_peaks(peaks, values, zones):
    bins = [[] for x in xrange(len(zones))]

    for peak, value in zip(peaks, values):
        for i, zone in enumerate(zones):
            if (peak >= zone[0]) and (peak <= zone[1]):
                bins[i].append((peak, value))

    return bins

В этот момент для каждого потенциального местоположения флажка у меня был список из 0 или более пиков, которые ему соответствуют.


Чтобы решить, какой из двух сценариев лучше подходит, мне нужно было свести все к одному значению с плавающей запятой, представляющему качество соответствия.Простые правила - выигрывает сценарий с более высоким показателем качества.

В качестве отправной точки я решил использовать сумму весов для каждой позиции, нормированную на количество позиций.

Для каждой позициибыло 3 варианта:

  • нет пиков в регионе - вес 0,0
  • один пик в регионе - вес = пиковое значение
  • более одного пикав регионе - вес = максимальное пиковое значение

В коде:

def analyze_bins(bins):
    total_weight = 0.0
    for i, bin in enumerate(bins):
        weight = 0.0
        if len(bin) > 0:
            best_bin = sorted(bin, key=lambda x: x[1], reverse=True)[0]
            weight = best_bin[1]

        total_weight += weight

    total_weight /= len(bins)
    return total_weight

Отладочный вывод этого алгоритма для каждого сценария:


На данный момент у меня была отдельная метрика для каждого сценария, и решение было простым - чем выше был победитель.

weight_a = analyze_bins(bins_a)
weight_b = analyze_bins(bins_b)

checkbox_count = 5 if (weight_a > weight_b) else 6

Result


Заключение

Альбом изображений отчета

И изображение с обобщением результатов для всех входных выборок:


Полный сценарий создания всех отчетов:

import cv2
import numpy as np
import glob
import math

import StringIO

from scipy.signal import find_peaks

# ============================================================================

# Define the zones (x axis ranges) where checkboxes may occur
zones_a = [(50, 72), (144, 166), (243, 265), (328, 350), (436, 458)] # 5 box scenario
zones_b = [(42,  64), (122, 144), (207, 229), (276, 298), (369, 391), (496, 518)] # 6 box scanario

# ============================================================================

# Bonus -- plot a detailed analysis report as a PNG image
def plot_report(filename, report):
    from matplotlib import pyplot as plt
    from matplotlib.gridspec import GridSpec

    IMAGE_KEYS = ['img', 'thresh', 'thresh_1', 'thresh_2', 'canny', 'canny_1', 'canny_2']
    PLOT_SPAN = 5
    TEXT_SPAN = 2
    ROW_COUNT = (len(IMAGE_KEYS) + 1) + 3 * (PLOT_SPAN + 1) + 2 * (TEXT_SPAN)

    fig = plt.figure()
    plt.suptitle(filename)
    gs = GridSpec(ROW_COUNT, 2)

    row = 0

    for key in IMAGE_KEYS:
        plt.subplot(gs[row,:])
        plt.gca().set_title(key)
        plt.imshow(report[key], cmap='gray', aspect='equal')
        plt.axis('off')
        row += 1

    proj_width = len(report['projection'])
    proj_x = np.arange(proj_width)

    plt.subplot(gs[row+1:row+1+PLOT_SPAN,:])
    plt.gca().set_title('Vertical Projections (Raw and Smoothed)')
    plt.plot(proj_x, report['projection'], 'y-')
    plt.plot(proj_x, report['projection_1'], 'r-')
    plt.plot(proj_x, report['projection_2'], 'b-')
    plt.xlim((0, proj_width - 1))
    plt.ylim((0, 255))

    row += PLOT_SPAN + 1

    plt.subplot(gs[row+1:row+1+PLOT_SPAN,:])
    plt.gca().set_title('Smoothed Projection with Peaks and Zones')
    plt.plot(proj_x, report['projection_2'])

    for zone in zones_a:
        plt.axvspan(zone[0], zone[1], facecolor='y', alpha=0.1)
    for zone in zones_b:
        plt.axvspan(zone[0], zone[1], facecolor='r', alpha=0.1)
    for x in report['peaks']:
        plt.axvline(x=x, color='m')

    plt.xlim((0, proj_width - 1))
    plt.ylim((0, report['projection_2'].max()))

    row += PLOT_SPAN + 1

    plt.subplot(gs[row+1:row+1+TEXT_SPAN,0], frameon=False)
    plt.gca().set_title('Details - 5 boxes')
    plt.axis([0, 1, 0, 1])
    plt.gca().axes.get_yaxis().set_visible(False)
    plt.gca().axes.get_xaxis().set_visible(False)
    plt.text(0, 1, report['details_a'], family='monospace', fontsize=8, ha='left', va='top')

    plt.subplot(gs[row+1:row+1+TEXT_SPAN,1], frameon=False)
    plt.gca().set_title('Details - 6 boxes')
    plt.axis([0, 1, 0, 1])
    plt.gca().axes.get_yaxis().set_visible(False)
    plt.gca().axes.get_xaxis().set_visible(False)
    plt.text(0, 1, report['details_b'], family='monospace', fontsize=8, ha='left', va='top')

    row += TEXT_SPAN

    plt.subplot(gs[row+1:row+1+PLOT_SPAN,:])
    plt.gca().set_title('Weights')
    plt.barh([2, 1]
        , [report['weight_a'], report['weight_b']]
        , align='center'
        , color=['y', 'r']
        , tick_label=['5 boxes', '6 boxes'])
    plt.ylim((0.5, 2.5))

    row += PLOT_SPAN + 1
    row += 1

    plt.subplot(gs[row,:])
    plt.gca().set_title('Input Image')
    plt.imshow(report['img'], cmap='gray', aspect='equal')
    plt.axis('off')

    row += 1

    plt.subplot(gs[row:row+TEXT_SPAN,:], frameon=False)
    plt.axis([0, 1, 0, 1])
    plt.gca().axes.get_yaxis().set_visible(False)
    plt.gca().axes.get_xaxis().set_visible(False)
    result_text = "The image contains %d boxes." % report['checkbox_count']
    plt.text(0.5, 1, result_text, family='monospace', weight='semibold', fontsize=24, ha='center', va='top')

    fig.set_size_inches(12, ROW_COUNT * 0.8)
    plt.savefig('plot_%s.png' % filename[:2], bbox_inches="tight")
    plt.close(fig)

# ----------------------------------------------------------------------------

# Bonus - create summary image showing inputs along with coloured result annotations.
def summary_report(result):
    ROW_HEIGHT = result[0][0].shape[0]
    images = [i[0] for i in result]
    stacked = np.vstack(images)
    extended = cv2.copyMakeBorder(stacked, 0, 0, 80, 0, cv2.BORDER_CONSTANT)
    result = cv2.cvtColor(extended, cv2.COLOR_GRAY2BGR)
    for i, entry in enumerate(result):
        cv2.putText(result, '%d boxes' % entry[0]
            , (4, ROW_HEIGHT * (i+1) - 4)
            , cv2.FONT_HERSHEY_SIMPLEX
            , 0.5
            , [(0, 255, 255), (0, 0, 255)][entry[0] - 5]
            , 1)
    return result

# ============================================================================

# Collect peaks that fall into each potential checkbox location
def bin_peaks(peaks, values, zones):
    bins = [[] for x in xrange(len(zones))]

    for peak, value in zip(peaks, values):
        for i, zone in enumerate(zones):
            if (peak >= zone[0]) and (peak <= zone[1]):
                bins[i].append((peak, value))

    return bins

# ----------------------------------------------------------------------------

# Select best peaks for each bin, weigh them and return total weight + details text
def analyze_bins(bins):
    buf = StringIO.StringIO()

    total_weight = 0.0
    for i, bin in enumerate(bins):
        buf.write("Position %d: " % i)
        weight = 0.0
        if len(bin) == 0:
            buf.write("no peaks")
        else:
            best_bin = sorted(bin, key=lambda x: x[1], reverse=True)[0]
            weight = best_bin[1]
            if len(bin) == 1:
                buf.write("single peak @ %d (value=%0.3f)" % best_bin)
            else:
                buf.write("%d peaks, best @ %d (value=%0.3f)" % (len(bin), best_bin[0], best_bin[1]))

        buf.write(" | weight=%0.3f\n" % weight)

        total_weight += weight

    total_weight /= len(bins)
    buf.write("Total weight = %0.3f" % total_weight)
    return total_weight, buf.getvalue()

# ----------------------------------------------------------------------------

# Process an input image, return checkbox count along with detailed debugging info in a dict
def process_image(filename):
    report = {}

    img = cv2.imread(filename, cv2.IMREAD_GRAYSCALE)
    report['img'] = img.copy()

    thresh = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, 2)
    report['thresh'] = thresh.copy()

    thresh = cv2.morphologyEx(thresh, cv2.MORPH_ERODE, np.ones((1,3),np.uint8))
    report['thresh_1'] = thresh.copy()

    thresh = cv2.morphologyEx(thresh, cv2.MORPH_DILATE, np.ones((3,1),np.uint8))
    report['thresh_2'] = thresh.copy()

    edges = cv2.Canny(thresh, 40, 120, apertureSize=5)
    report['canny'] = edges.copy()

    edges = cv2.morphologyEx(edges, cv2.MORPH_OPEN, np.ones((5,1),np.uint8))
    report['canny_1'] = edges.copy()

    edges = cv2.morphologyEx(edges, cv2.MORPH_DILATE, np.ones((1,3),np.uint8))
    report['canny_2'] = edges.copy()

    projection = np.mean(edges, 0).flatten()
    report['projection'] = projection.copy()

    projection = cv2.blur(projection, (1, 21)).flatten()
    report['projection_1'] = projection.copy()

    projection = cv2.blur(projection, (1, 11)).flatten()
    report['projection_2'] = projection.copy()

    peaks = find_peaks(projection)[0]
    report['peaks'] = peaks.copy()

    peak_values = projection[peaks]
    report['peak_values'] = peak_values.copy()

    bins_a = bin_peaks(peaks, peak_values, zones_a)
    report['bins_a'] = list(bins_a)

    bins_b = bin_peaks(peaks, peak_values, zones_b)
    report['bins_b'] = list(bins_b)

    weight_a, details_a = analyze_bins(bins_a)
    report['weight_a'] = weight_a
    report['details_a'] = details_a
    weight_b, details_b = analyze_bins(bins_b)
    report['weight_b'] = weight_b
    report['details_b'] = details_b

    checkbox_count = 5 if (weight_a > weight_b) else 6
    report['checkbox_count'] = checkbox_count

    return checkbox_count, report

# ============================================================================

result = []
for filename in glob.glob('*-*.png'):
    box_count, report = process_image(filename)
    plot_report(filename, report)
    result.append((report['img'], report['checkbox_count']))

cv2.imwrite('summary.png', summary_report(result))

Не стесняйтесь исправлять любые опечатки и дайте мне знать обо всем, что необходимо уточнить.

0 голосов
/ 17 сентября 2018

Это должно помочь вам https://www.pyimagesearch.com/2016/02/08/opencv-shape-detection/

По сути, все сводится к нахождению контуров на изображении, а затем с помощью cv2.approxPolyDP выясняется, сколько сторон фигуры.Затем просто проверьте, сколько у вас четырехсторонних фигур.

Возможно, вы получите один для внешнего ящика, но вы можете просто установить флажки без других ящиков внутри, чтобы изолировать свои флажки.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...