У меня есть роботизированное устройство, которое сгибает проволоку, и я пытаюсь определить, удалось ли сгибание, просматривая желаемые и истинные измерения угла.
Я новичок в обработке изображений, поэтому извините за грубую терминологию.
Я имею в виду обнаружить провод и нарисовать линию для каждого сегмента провода, после чего это становится всего лишь небольшой тригонометрической задачей. Но я не могу найти способ нарисовать и получить координаты этих линий.
Спасибо за ответ, только что обновил образец фото, провод черный, а фон белый.
необходим подход «машинного зрения». разверните несколько линий отбора проб, чтобы найти участки интересующей вас детали.
Я опубликую ответ сегодня вечером... Я буду рад еще одной или двум фотографиям, показывающим ожидаемое изменение измеряемого объекта (перемещение, вращение, угол изгиба и т. д.).
Я бы попробовал следующие шаги:
Обновлено:
import cv2
import numpy as np
from skimage import morphology, graph
from skan import Skeleton
def angle(v1, v2):
rad = np.arctan2(v2[0], v2[1]) - np.arctan2(v1[0], v1[1])
return np.abs((np.rad2deg(rad)))
# read an threshold image
img = cv2.imread('img.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
inverted = 255 - gray
ret, thresh = cv2.threshold(inverted, 50, 255, cv2.THRESH_BINARY)
# skeletonize image and split skeleton to paths, for now assume there is only one
skeleton = morphology.skeletonize(thresh, method='lee')
g = Skeleton(skeleton)
path = np.array(g.path_coordinates(0).astype(int))
# draw path for demonstration purpose
for p in path:
img[p[0], p[1]] = [0, 0, 255]
# get reference points by percental position on path
l = len(path)
a1 = path[int(0.02 * l)]
a2 = path[int(0.10 * l)]
b1 = path[int(0.98 * l)]
b2 = path[int(0.90 * l)]
# results
print(f"Degrees: {angle(a1 - a2, b1 - b2):0.2f}")
cv2.line(img, (a1[1], a1[0]), (a2[1], a2[0]), [0, 255, 0], 2)
cv2.line(img, (b1[1], b1[0]), (b2[1], b2[0]), [0, 255, 0], 2)
cv2.imwrite("out.png", img)
Выход:
Degrees: 32.32
Было бы лучше ответить, если бы вы показали код и вывод вашего кода.
Большое спасибо за Вашу помощь. Я попробую решение, которое вы сказали.
интересный подход!
Я предполагаю, что заготовка всегда находится примерно в одном и том же положении, с некоторым пространством для маневра.
Этот подход склоняется к «машинному зрению», то есть типам операций, которые вы получаете от коммерческих программ MV, предназначенных для автоматического оптического контроля. Эти программы позволяют «проводить» измерения визуально и в интерактивном режиме. Это избавляет от множества проб и ошибок и кодирования.
Я вручную выбрал области, в которых будет измеряться изогнутая проволока. Для каждой области я определил две конечные точки, а затем объявил пять пересекающихся линий по длине, по которым я буду сканировать изображение.
Эти линии не обязательно должны быть прямыми. Имея больше вуду, вы можете легко создать для этого дуги, скажем, можно ли более или менее согнуть конец. cv.polarToCart
может в этом помочь.
Это образцы высотой 5 пикселей и шириной 101 пиксель, увеличенные, первая левая строка:
(А это из концентрических дуг, наизнанку, а лево есть право... эх, просто вопрос знаков)
Для каждой из выбранных линий я вычисляю градиент и нахожу там минимум и максимум. Это должны быть края заготовки по каждой линии.
Вот график одного из них с градиентом:
А вот точки на картинке, соответствующие этим экстремумам:
Затем я беру середину каждого из них и прокладываю линию.
Тогда угол между обеими линиями (их векторами) составляет около ~30 градусов.
Здесь есть некоторые крайние случаи, например, что происходит, если линия отбора проб не пересекает заготовку, как я спровоцировал на кончике «рабочего конца». Вы можете увидеть, насколько это незначительно, на выборках размером 101 x 5 пикселей.
Такие случаи можно решить, оценив величину экстремумов и отбросив линию, если она слишком мала.
Код скоро появится...
import numpy as np
from numpy.linalg import norm, inv
import cv2 as cv
# utility functions
def vec(*a):
if len(a) == 1:
(a,) = a
return np.asarray(a)
def normalize(v):
return np.asarray(v) / norm(v)
def translate2(dx=0, dy=0):
T = np.eye(3)
T[0:2, 2] = [dx, dy]
return T
def scale2(s=1, sx=1, sy=1):
return np.diag([s*sx, s*sy, 1])
def normal2(v):
x,y = v
return np.asarray([-y, +x])
def to_basis(origin, ax, ay):
T = np.eye(3)
T[0:2, 0] = ax
T[0:2, 1] = ay
T[0:2, 2] = origin
return T
def index_grid(nx, ny=1):
return np.mgrid[0:ny, 0:nx].transpose(1, 2, 0)[:,:,::-1]
def transform_grid(H, grid):
(h, w, inner_dim) = grid.shape
assert inner_dim == 2
output_points = cv.transform(
src=grid.reshape( (-1, 1, inner_dim) ).astype(np.float32),
m=H)
# hoping [..., 2] is all ones
output_points = output_points[..., :-1]
output_points.shape = (h, w, inner_dim)
return output_points
def grid_along_line(p0, p1, width, nx, ny):
v0 = p1 - p0
length = norm(v0)
v0 = v0 / length
dx = width / (nx-1)
dy = length / (ny-1)
grid = index_grid(nx, ny)
T = scale2(sx=dx, sy=dy) @ translate2(-(nx-1)/2, 0)
T = to_basis(p0, -normal2(v0), v0) @ T
grid = transform_grid(T, grid)
return T, grid
def get_midpoints(sampled, T):
midpoints = []
for i, row in enumerate(sampled):
grad = np.gradient(row)
jrising = np.argmax(grad)
jfalling = np.argmin(grad)
# vrising = grad[jrising]
# vfalling = grad[jfalling]
# print(vrising - vfalling)
p_rising = T @ vec(jrising, i, 1)
p_falling = T @ vec(jfalling, i, 1)
p_mid = (p_rising + p_falling) / 2
midpoints.append(p_mid[:2])
return np.asarray(midpoints)
im = 255 - cv.imread("JqI70V2C.png", cv.IMREAD_GRAYSCALE)
(height, width) = im.shape[:2]
# manually defined bands across which to take sample lines
working_end_p0 = vec(376, 315)
working_end_p1 = vec(585, 181)
T_working_end, working_end_grid = grid_along_line(working_end_p0, working_end_p1, 200.0, 101, 5)
standing_end_p0 = vec(4, 505)
standing_end_p1 = vec(678, 487)
T_standing_end, standing_end_grid = grid_along_line(standing_end_p0, standing_end_p1, 200.0, 101, 5)
working_end_sampled = cv.remap(im, map1=working_end_grid, map2=None, interpolation=cv.INTER_LANCZOS4)
standing_end_sampled = cv.remap(im, map1=standing_end_grid, map2=None, interpolation=cv.INTER_LANCZOS4)
vecs = []
for (sampled, T) in [
(working_end_sampled, T_working_end),
(standing_end_sampled, T_standing_end)
]:
points = get_midpoints(sampled, T)
cov = np.cov(points.T)
(eigenvalues, eigenvectors) = np.linalg.eig(cov) # column vectors
major_axis = eigenvectors[:, np.argmax(eigenvalues)]
x_ = points[:,0].mean()
y_ = points[:,1].mean()
p0 = vec(x_, y_)
v = normalize(vec(major_axis))
vecs.append(v)
[v1, v2] = vecs
angle = np.arccos(v1 @ v2)
print(f"{np.degrees(angle):.1f} deg")
Я пропустил визуализации, потому что они довольно тривиальны (вызов circle()
для каждой точки). В последнем цикле вы можете рисовать линии следующим образом:
canvas = cv.cvtColor(im, cv.COLOR_GRAY2BGR)
# draw major axis
cv.line(
img=canvas,
pt1=(p0 - 1000*v).round().astype(int),
pt2=(p0 + 1000*v).round().astype(int),
color=(0, 255, 0),
thickness=2)
если у вас есть дополнительные вопросы, пожалуйста, дайте мне знать. Если этот ответ решит вашу проблему, вы можете сообщить об этом людям, отметив его галочкой как «принятый». цель этого представлена в туре по сайту.
Точки «work_end_p0», «work_end_p1», «standing_end_p0», «standing_end_p1» берутся вручную. Есть ли способ автоматически определить эти точки?
это параметры процесса. их точное положение не имеет значения. им нужно только захватить область, в которой предположительно будут находиться сегменты. -- Мне пришлось обобщить на одном примере. Я сделал все возможное, чтобы предвидеть вариации. Если у вас есть изображение, на которое мой ответ не подходит, покажите мне его.
Спасибо @Christoph Rackwitz. Я буду обновлять новые фотографии.
Вам следует подумать (и представить): «Насколько вы можете упростить проблему?». Если вы можете установить некоторые предварительные условия относительно входного изображения (например, условия о шуме, фоне, проводе {цвет, толщина, расположение, форма, существование} и т. д.), программа может зависеть от них. Без каких-либо предварительных условий это означает «Любое входное изображение», и задача будет очень и очень сложной.