假设我有一组线段,如此图片中的红线(或绿线)
我想知道如何用最接近它们的一个线段替换它们。或者也许您可以建议搜索什么,因为这可能是统计数据中的常见问题。
问题背景:这实际上来自于使用OpenCV的概率Hough变换。我想检测一张纸的角落。当我将它应用于图像时,我会在边缘上得到一组我想要转换为单行连续段的线。
我想到的一种方法是从线上取几个点然后使用线性回归来得到线的方程。一旦我有了,我应该把它切成一段。
答案 0 :(得分:1)
这是一个潜在的解决方案:
获取二进制图像。加载图像,转换为灰度,然后输入Otsu的阈值
执行形态学操作。我们将其变形以将轮廓合并为单个轮廓
查找凸包。我们创建一个空白蒙版,然后在二进制图像上找到凸包。我们将线条绘制到蒙版上,使其变形变形,然后找到轮廓并填充轮廓以获得实心图像
执行线性回归。我们找到最适合二元图像的线,然后将此结果线绘制到新蒙版上
按位凸包和线条蒙版。我们按位和两个蒙版在一起,并将此结果轮廓绘制到原始图像上。
以下是每个步骤的可视化结果:
使用此屏幕截图输入图像
二进制图片import pygame
import time
import math
# --- constants --- (UPPER_CASE_NAMES)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
DISPLAY_WIDTH = 800
DISPLAY_HEIGHT = 800
# --- classes ---
class Dial:
def __init__(self, x, y, radius, inputs, angle_step=45):
self.rect = pygame.Rect(x-radius, y-radius, 2*radius, 2*radius)
self.radius = radius
self.radius2 = radius - 30 # position for digits
self.inputs = inputs
self.angle_step = angle_step
def draw(self): # no need to add prefix/postfix 'Dial' to name `draw`
#for i in range(0, self.columns):
# `Dial` means single object so it should draw only one circle
pygame.draw.circle(window, RED, self.rect.center, self.radius)
pygame.draw.circle(window, BLACK, self.rect.center, self.radius, 1)
angle = 0
for number in self.inputs:
text = font.render(str(number), 1, BLACK)
# rotate image
text = pygame.transform.rotate(text, -angle)
# center image on circle
text_rect = text.get_rect(center=self.rect.center)
# move to border using `angle` and `radius`
angle_rad = math.radians(180-angle)
text_rect.centerx += self.radius2 * math.sin(angle_rad)
text_rect.centery += self.radius2 * math.cos(angle_rad)
window.blit(text, text_rect)
angle += self.angle_step
# --- functions ---
def level_1():
window.fill(WHITE)
dial1.draw()
dial2.draw()
dial3.draw()
# --- main ---
pygame.init()
window = pygame.display.set_mode((DISPLAY_WIDTH, DISPLAY_HEIGHT))
window_rect = window.get_rect()
pygame.display.set_caption("Dials")
clock = pygame.time.Clock()
font = pygame.font.SysFont('Arial', 50, True)
#tutorial = [pygame.image.load('Dial_Images/tutorial_base.png')]
tutorial_inputs = [[1,4,3,4], [1,4,3,2], [1,4,3,2], [1,4,3,4]]
dial1 = Dial(window_rect.centerx, window_rect.centery, window_rect.centerx-250, tutorial_inputs[0], 45)
dial2 = Dial(window_rect.centerx-250, window_rect.centery-250, window_rect.centerx-250, tutorial_inputs[1], 90)
dial3 = Dial(window_rect.centerx+250, window_rect.centery-250, window_rect.centerx-250, tutorial_inputs[2], 30)
score = 0
run = True
level1 = True
while run:
#keys = pygame.key.get_pressed()
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
pygame.display.update()
clock.tick(100)
if level1:
level_1()
pygame.quit()
变近了
->
凸包轮廓# Load image, grayscale, Otsu's threshold
image = cv2.imread('1.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
# Morph close
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
close = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=3)
已填充凸包
->
线性回归
# Create line mask and convex hull mask
line_mask = np.zeros(image.shape, dtype=np.uint8)
convex_mask = np.zeros(image.shape, dtype=np.uint8)
# Find convex hull on the binary image
cnts = cv2.findContours(close, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
cnt = cnts[0]
hull = cv2.convexHull(cnt,returnPoints = False)
defects = cv2.convexityDefects(cnt,hull)
for i in range(defects.shape[0]):
s,e,f,d = defects[i,0]
start = tuple(cnt[s][0])
end = tuple(cnt[e][0])
far = tuple(cnt[f][0])
cv2.line(convex_mask,start,end,[255,255,255],5)
# Morph close the convex hull mask, find contours, and fill in the outline
convex_mask = cv2.cvtColor(convex_mask, cv2.COLOR_BGR2GRAY)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
convex_mask = cv2.morphologyEx(convex_mask, cv2.MORPH_CLOSE, kernel, iterations=3)
cnts = cv2.findContours(convex_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
cv2.fillPoly(convex_mask, cnts, (255,255,255))
按位填充的凸包和线性回归蒙版在一起
# Perform linear regression on the binary image
[vx,vy,x,y] = cv2.fitLine(cnt,cv2.DIST_L2,0,0.01,0.01)
lefty = int((-x*vy/vx) + y)
righty = int(((image.shape[1]-x)*vy/vx)+y)
cv2.line(line_mask,(image.shape[1]-1,righty),(0,lefty),[255,255,255],2)
结果
# Bitwise-and the line and convex hull masks together
result = cv2.bitwise_and(line_mask, line_mask, mask=convex_mask)
result = cv2.cvtColor(result, cv2.COLOR_BGR2GRAY)
这是其他输入图像的结果
完整代码
# Find resulting contour and draw onto original image
cnts = cv2.findContours(result, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
cv2.drawContours(image, cnts, -1, (200,100,100), 3)
答案 1 :(得分:0)
这是一个很好的问题。正确看待这种情况的方法是沿每个线段整合错误。而不是简单的术语(y[i] - y_hat)^2
(其中y_hat
是回归线的预测值),您应该integral((y[i](t) - y_hat)^2, t, 0, 1)
,其中y[i](t)
是线段的参数化,{{1 (表示y[i](t) = t * y[i, 1] + (1 - t)*y[i, 0]
- 段的端点为i
和y[i, 0]
)。我认为你会发现你可以精确地计算积分,因此你可以获得仅涉及端点的平方误差之和的项。我遗漏了一些细节,但我认为这足以让你解决剩下的问题了。编辑:错误条款应该是平方的;我已相应调整了公式。
2ND编辑:我制定了一些公式(使用Maxima)。对于由y[i, 1]
表示的回归线,我得到y = alpha*x + beta
和alpha
的最小二乘估计值:
beta
其中alpha = (4*('sum(ll[k],k,1,n))*'sum(xx[k][2]*yy[k][2]*ll[k],k,1,n)
+2*('sum(ll[k],k,1,n))*'sum(xx[k][1]*yy[k][2]*ll[k],k,1,n)
+('sum(xx[k][2]*ll[k],k,1,n))*(-3*'sum(yy[k][2]*ll[k],k,1,n)
-3*'sum(yy[k][1]*ll[k],k,1,n))
+('sum(xx[k][1]*ll[k],k,1,n))*(-3*'sum(yy[k][2]*ll[k],k,1,n)
-3*'sum(yy[k][1]*ll[k],k,1,n))
+2*('sum(ll[k],k,1,n))*'sum(yy[k][1]*xx[k][2]*ll[k],k,1,n)
+4*('sum(ll[k],k,1,n))*'sum(xx[k][1]*yy[k][1]*ll[k],k,1,n))
/(4*('sum(ll[k],k,1,n))*'sum(xx[k][2]^2*ll[k],k,1,n)
+4*('sum(ll[k],k,1,n))*'sum(xx[k][1]*xx[k][2]*ll[k],k,1,n)
-3*('sum(xx[k][2]*ll[k],k,1,n))^2
-6*('sum(xx[k][1]*ll[k],k,1,n))*'sum(xx[k][2]*ll[k],k,1,n)
+4*('sum(ll[k],k,1,n))*'sum(xx[k][1]^2*ll[k],k,1,n)
-3*('sum(xx[k][1]*ll[k],k,1,n))^2)
beta = -((2*'sum(xx[k][2]*ll[k],k,1,n)+2*'sum(xx[k][1]*ll[k],k,1,n))
*'sum(xx[k][2]*yy[k][2]*ll[k],k,1,n)
+('sum(xx[k][2]*ll[k],k,1,n)+'sum(xx[k][1]*ll[k],k,1,n))
*'sum(xx[k][1]*yy[k][2]*ll[k],k,1,n)
+('sum(xx[k][2]^2*ll[k],k,1,n))*(-2*'sum(yy[k][2]*ll[k],k,1,n)
-2*'sum(yy[k][1]*ll[k],k,1,n))
+('sum(xx[k][1]*xx[k][2]*ll[k],k,1,n))
*(-2*'sum(yy[k][2]*ll[k],k,1,n)-2*'sum(yy[k][1]*ll[k],k,1,n))
+('sum(xx[k][1]^2*ll[k],k,1,n))*(-2*'sum(yy[k][2]*ll[k],k,1,n)
-2*'sum(yy[k][1]*ll[k],k,1,n))
+('sum(xx[k][2]*ll[k],k,1,n)+'sum(xx[k][1]*ll[k],k,1,n))
*'sum(yy[k][1]*xx[k][2]*ll[k],k,1,n)
+2*('sum(xx[k][1]*yy[k][1]*ll[k],k,1,n))*'sum(xx[k][2]*ll[k],k,1,n)
+2*('sum(xx[k][1]*ll[k],k,1,n))*'sum(xx[k][1]*yy[k][1]*ll[k],k,1,n))
/(4*('sum(ll[k],k,1,n))*'sum(xx[k][2]^2*ll[k],k,1,n)
+4*('sum(ll[k],k,1,n))*'sum(xx[k][1]*xx[k][2]*ll[k],k,1,n)
-3*('sum(xx[k][2]*ll[k],k,1,n))^2
-6*('sum(xx[k][1]*ll[k],k,1,n))*'sum(xx[k][2]*ll[k],k,1,n)
+4*('sum(ll[k],k,1,n))*'sum(xx[k][1]^2*ll[k],k,1,n)
-3*('sum(xx[k][1]*ll[k],k,1,n))^2)
和xx
是值对的列表,每个线段都有一对。也就是说,yy
是xx[k]
- 段的端点的x坐标,k
是yy[k]
- 段的端点的y坐标,k
是ll[k]
段的长度sqrt((xx[k][2] - xx[k][1])^2 + (yy[k][2] - yy[k][1])^2)
。
这是我推导出这些公式的程序。可能还有其他合理的方法来设置这个问题,产生类似但不同的公式。
k
以下是一些示例数据和我得到的结果。我推迟定义y_hat[k](l) := alpha * x[k](l) + beta;
x[k](l) := (1 - l/ll[k]) * xx[k][1] + l/ll[k] * xx[k][2];
y[k](l) := (1 - l/ll[k]) * yy[k][1] + l/ll[k] * yy[k][2];
e[k]:= y[k](l) - y_hat[k](l);
foo : sum (integrate (e[k]^2, l, 0, ll[k]), k, 1, n);
declare (nounify (sum), linear);
[da, db] : [diff (foo, alpha), diff (foo, beta)];
map (expand, %);
bar : solve (%, [alpha, beta]);
,dx
和dy
,因为它们都是常数,我不希望它们在ll
的解决方案中得到扩展和alpha
。
beta