Найдите линии и раскрасьте их одним щелчком мыши
@ALL Это редактирование исходного вопроса, чтобы пролить немного света на эту тему.
Постановка задачи
- Предположим, что есть промышленный участок P&ID.
- Стремление раскрасить только некоторые линии, важные для процесса.
- Пользователь должен только щелкнуть (щелчок левой кнопкой мыши) на отрезке линии, чтобы получить его цветной.
Проблемный подход
Я новичок в программировании -> с помощью Python (3.5), чтобы попробовать это. Как я понимаю, алгоритм выглядит так:
- Сюжет будет в формате.pdf. Поэтому я мог бы использовать PIL ImageGrab или конвертировать.pdf в.png, как показано в этом примере.
- Алгоритм будет искать пиксели вокруг щелчка мыши, затем сравнивать их с другой частью идентичного размера (скажем, полоса размером 6x3 пикселя), но на один шаг влево / вправо (будь то 1-5 пикселей)
- Проверка среднего значения их различий покажет нам, идентичны ли эти две полосы
- Таким образом, алгоритм должен найти оба конца строки, стрелки, углы или другие элементы
- Как только это будет найдено, записаны позиции и нарисована линия разметки, пользователь должен выбрать другую линию
Подвела
- Нажмите на нужную строку
- Возьмите небольшую часть изображения вокруг щелчка мышью
- Проверьте, является ли линия горизонтальной или вертикальной
- Обрезать горизонтальный / вертикальный срез заданного размера
- Найдите окончания строки и запишите позиции окончания
- Между двумя найденными позициями нарисуйте линию определенного цвета (скажем, зеленого цвета).
- Дождитесь выбора следующей строки и повторите
Другие мысли
- В приложении вы можете найти две фотографии образца изображения и то, что я пытаюсь достичь.
- Пытался найти "дыры" в срезах, используя подход, найденный здесь: OpenCV, чтобы найти окончания строк
- Не существует строгого правила придерживаться рутины ImageGrab или чего-либо подобного
- Если вы знаете другую тактику, которую я мог бы использовать, пожалуйста, не стесняйтесь комментировать
- Любой совет приветствуется и искренне ценится
Образец изображения:
Желаемый результат (изменено в Paint):
Добавление обновления к посту с работой, которую я опробовал до сих пор
Я сделал некоторые изменения в исходном коде, поэтому я опубликую его ниже. Все в комментариях либо для отладки, либо для объяснения. Ваша помощь высоко ценится! Не бойся вмешиваться.
import win32gui as w
from PIL import ImageStat, ImageChops, Image, ImageDraw
import win32api as wa
img=Image.open("Trials.jpg")
img_width=img.size[0]
img_height=img.size[1]
#Using 1920 x 1080 resolution
#Hide the taskbar to center the Photo Viewer
#Defining a way to make sure the mouse click is inside the image
#Substract the width from total and divide by 2 to get base point of the crop
width_lim = (1920 - img_width)/2
height_lim = (1080 - img_height)/2-7
#After several tests, the math in calculating the height is off by 7 pixels, hence the correction
#Use these values when doing the crop
#Check if left mouse button was pressed and record its position
left_p = wa.GetKeyState(0x01)
#print(left_p)
while True :
a=wa.GetKeyState(0x01)
if a != left_p:
left_p = a
if a<0 :
pos = w.GetCursorPos()
pos_x=pos[0]-width_lim
pos_y=pos[1]-height_lim
# print(pos_x,pos_y)
else:
break
#img.show()
#print(img.size)
#Define the crop height; size is doubled
height_size = 10
#Define max length limit
#Getting a horizontal strip
im_hor = img.crop(box=[0, pos_y-height_size, img_width, pos_y+height_size])
#im_hor.show()
#failed in trying crop a small square of 3x3 size using the pos_x
#sq_size = 3
#st_sq = im_hor.crop(box=[pos_x,0,pos_x+sq_size,height_size*2])
#st_sq.show()
#going back to the code it works
#crop a standard strip and compare with a new test one
#if the mean of difference is zero, the strips are identical
#still looking for a way to find the position of the central pixel (that would be the one with maximum value - black)
strip_len = 3
step = 3
i = pos_x
st_sq = im_hor.crop(box=[i,0,i+strip_len,height_size*2])
test_sq = im_hor.crop(box=[i+step,0,i+strip_len+step,height_size*2])
diff = ImageChops.difference(st_sq,test_sq)
stat=ImageStat.Stat(diff)
mean = stat.mean
mean1 = stat.mean
#print(mean)
#iterate to the right until finding a different strip, record position
while mean==[0,0,0]:
i = i+1
st_sq = im_hor.crop(box=[i,0,i+strip_len,height_size*2])
#st_sq.show()
test_sq = im_hor.crop(box=[i+step,0,i+strip_len+step,height_size*2])
#test_sq.show()
diff = ImageChops.difference(st_sq,test_sq)
#diff.show()
stat=ImageStat.Stat(diff)
mean = stat.mean
# print(mean)
print(i-1)
r = i-1
#print("STOP")
#print(r)
#record the right end as r = i-1
#iterate to the left until finding a different strip. record the position
while mean1==[0,0,0]:
i = i-1
st_sq = im_hor.crop(box=[i,0,i+strip_len,height_size*2])
#st_sq.show()
test_sq = im_hor.crop(box=[i+step,0,i+strip_len+step,height_size*2])
#test_sq.show()
diff = ImageChops.difference(st_sq,test_sq)
#diff.show()
stat=ImageStat.Stat(diff)
mean1 = stat.mean
# print(mean)
#print("STOP")
print(i+1)
l = i+1
#record the left end as l=i+1
test_draw = ImageDraw.Draw(img)
test_draw.line([l,pos_y,r,pos_y], fill=128)
img.show()
#find another approach or die trying!!!
Ниже приведен результат, который я получил. Это не то, на что я надеялся, но я чувствую себя на правильном пути. Я мог бы действительно использовать некоторую помощь в поиске положения пикселя в полосе и сделать его относительно положения пикселя большой картинки.
Еще одно подобное изображение, лучшего качества, но все же приносящее больше проблем.
1 ответ
Таким образом, это решение не является полным решением вашей конкретной проблемы, но я думаю, что это может быть хорошим подходом, который поможет вам хотя бы частично пройти путь. Проблемы, с которыми я сталкиваюсь в целом при подходах к обнаружению линий, заключается в том, что они обычно сильно зависят от нескольких гиперпараметров. Более досадно, что они медленные, так как они ищут широкий спектр углов; Ваши линии строго горизонтальны или вертикальны. Поэтому я бы рекомендовал использовать морфологию. Вы можете найти общий обзор морфологии на сайте OpenCV и увидеть, как он применяется для удаления музыкальных полос в партитуре в этом руководстве на сайте OpenCV.
Основная идея, которую я думал, была:
- Обнаружение горизонтальных и вертикальных линий
- Бежать
connectedComponents()
по обнаруженным линиям идентифицировать каждую строку отдельно - Получить пользовательскую позицию мыши и определить окно вокруг нее
- Если метка из подключенных компонентов находится в этом окне, то захватите этот компонент
- Нарисуйте этот компонент на изображении
Это очень базовая идея, которая игнорирует некоторые связанные с этим проблемы. Тем не менее, то, что это будет делать наверняка, так это то, что если вы щелкнете где-нибудь, и в окне этого клика есть линия на вашем изображении, вы получите его. Здесь нет пропущенных строк. Еще одна хорошая новость: он не игнорирует более толстые границы и такие на изображении, где вы, естественно, хотели бы, чтобы это прекратилось (обратите внимание, что эта проблема существует для схем обнаружения линий). Это будет обнаруживать только линии, которые имеют определенную ширину, и если линия становится толще (превращается в стрелку или попадает на линию, идущую в другом направлении), она обрезает ее. Плохая новость заключается в том, что для ваших линий используется заранее заданная ширина. Вы можете немного обойти это, используя преобразование типа "попал или не попал", но учтите, что в настоящее время реализация не работает для версий OpenCV старше 3.3-rc; смотрите здесь больше (вы можете легко обойти сломанную реализацию). В любом случае, преобразование типа "попал или не попал" позволяет вам сказать: "Я хочу горизонтальную линию, но она может быть шириной в несколько пикселей или шириной всего в один пиксель". Конечно, чем шире вы делаете это, тем больше вещей, которые не являются линиями, могут превратиться в одну. Вы можете отфильтровать их позже, хотя на основе размера (бросить все линии, которые меньше, чем некоторый размер с эрозией или расширением).
Теперь, как это выглядит в коде? Я решил сделать простой пример и применить его, но учтите, что код составлен так, что здесь нет реальных ошибок, и вы захотите написать это намного лучше. В любом случае это просто быстрый взлом, чтобы привести пример вышеописанного метода.
Сначала мы создадим изображение и нарисуем несколько линий:
import cv2
import numpy as np
img = 255*np.ones((500, 500), dtype=np.uint8)
cv2.line(img, (10, 350), (200, 350), color=0, thickness=1)
cv2.line(img, (100, 150), (400, 150), color=0, thickness=1)
cv2.line(img, (300, 250), (300, 500), color=0, thickness=1)
cv2.line(img, (100, 50), (100, 350), color=0, thickness=1)
bin_img = cv2.bitwise_not(img)
И обратите внимание, я также создал противоположное изображение, потому что я предпочитаю оставить то, что я пытаюсь обнаружить, белым, а черный - фоном.
Теперь мы возьмем эти горизонтальные и вертикальные линии с морфологией (в данном случае эрозия):
h_kernel = np.array([[0, 0, 0],
[1, 1, 1],
[0, 0, 0]], dtype=np.uint8)
v_kernel = np.array([[0, 1, 0],
[0, 1, 0],
[0, 1, 0]], dtype=np.uint8)
h_lines = cv2.morphologyEx(bin_img, cv2.MORPH_ERODE, h_kernel)
v_lines = cv2.morphologyEx(bin_img, cv2.MORPH_ERODE, v_kernel)
А теперь мы будем помечать каждую строку:
h_n, h_labels = cv2.connectedComponents(h_lines)
v_n, v_labels = cv2.connectedComponents(v_lines)
Эти изображения h_labels
а также v_labels
будет идентичным h_lines
а также v_lines
но вместо того, чтобы цвет / значение было белым в каждом пикселе, значение вместо этого является целым числом для каждого отдельного компонента в изображении. Таким образом, фоновые пиксели будут иметь значение 0, одна строка будет помечена 1 с, а другая - 2 с. И так далее для изображений с большим количеством строк.
Теперь мы определим окно вокруг щелчка мышью пользователя. Вместо того, чтобы реализовать этот конвейер здесь, я просто собираюсь жестко кодировать позицию щелчка мышью:
mouse_click = [101, 148] # x, y
click_radius = 3 # pixel width around mouse click
window = [[mouse_click[0] - i, mouse_click[1] - j]
for i in range(-click_radius, click_radius+1)
for j in range(-click_radius, click_radius+1)]
Последнее, что нужно сделать, это перебрать все места внутри window
и проверьте, является ли ярлык положительным (т.е. это не фон). Если это так, то мы попали в линию. Итак, теперь мы можем просто посмотреть на все пиксели, которые имеют эту метку, и это будет полная строка. Тогда мы можем использовать любое количество методов, чтобы нарисовать линию на оригинале img
,
label = 0
for pixel in window:
if h_labels[pixel[1], pixel[0]] > 0:
label = h_labels[pixel[1], pixel[0]]
bin_labeled = 255*(h_labels == label).astype(np.uint8)
elif v_labels[pixel[1], pixel[0]] > 0:
label = v_labels[pixel[1], pixel[0]]
bin_labeled = 255*(v_labels == label).astype(np.uint8)
if label > 0:
rgb_labeled = cv2.merge([img, img+bin_labeled, img])
break
IMO, этот код, приведенный выше, действительно неаккуратный, есть лучшие способы нарисовать это, но я не хотел тратить время на что-то не очень важное для вопроса.
Одним из простых способов улучшить это было бы соединение вблизи линий - вы могли бы сделать это еще с морфологией, прежде чем найдете компоненты. Возможно, лучшим способом рисования было бы просто найти минимальное / максимальное положения этой метки внутри изображения и использовать их в качестве координат конечной точки для OpenCV. line()
функция рисования, которая позволит вам легко выбирать цвета и толщину линий. Единственное, что я бы порекомендовал сделать, если это возможно, это показать эти линии при наведении мыши до того, как пользователь щелкнет (чтобы они знали, что щелкают в правой области). Таким образом, если пользователь находится рядом с двумя строками, он знает, какую из них он выбирает.