opencv 卡尺法 测量边缘距离
参考来源 :https://github.com/crackwitz/metrology-demo
前言
一、测量方法
二、测量步骤
1.获取直线的像素
2.高斯滤波平滑曲线
3.计算跳变幅度值
4.计算距离值
5.显示和保存图片
总结
前言
halcon中有按照直线找边缘测量距离的工具,但是opencv中没有类似的工具可以直接实现该测量方式,参考网上的实现方式,可以实现。
测量的效果贴图
一、测量方法
根据测量线的两个端点,获取直线的像素,然后进行滤波过滤噪点,计算跳变幅度,获取最大最小值的位置,计算距离
二、测量步骤
1.获取直线的像素
将直线的通过仿射变换转成1D的图像,可以采用亚像素的算法,先获取仿射变换的矩阵
代码如下(示例):
def build_transform(p0, p1, stride=None, nsamples=None):
"builds an affine transform with x+ along defined line"
# use one of stride (in pixels) or nsamples (absolute value)
(x0, y0) = p0
(x1, y1) = p1
dx = x1 - x0
dy = y1 - y0
length = np.hypot(dx, dy)
if nsamples is not None:
#stride = length / nsamples
factor = 1 / nsamples
else:
if stride is None:
stride = 1.0
factor = stride / length
nsamples = int(round(length / stride))
# map: src <- dst (use WARP_INVERSE_MAP flag for warpAffine)
H = np.eye(3, dtype=np.float64) # homography
H[0:2, 0] = (dx, dy) # x unit vector
H[0:2, 1] = (-dy, dx) # y unit vector is x rotated by 90 degrees
x=H[0:2, 0:2]
H[0:2, 0:2] *= factor
H[0:2, 2] = (x0, y0) # translate onto starting point
# take affine part of homography
assert np.isclose(a=H[2], b=(0,0,1)).all() # we didn't touch those but let's better check
A = H[0:2, :]
return (nsamples, A)
然后再采用变换的方法获取图像的像素值
def sample_opencv(im, M, nsamples):
# use transform to get samples
# available: INTER_{NEAREST,LINEAR,AREA,CUBIC,LANCOS4)
samples = cv.warpAffine(im, M=M, dsize=(nsamples, 1), flags=cv.WARP_INVERSE_MAP | cv.INTER_CUBIC )
# flatten row vector
samples.shape = (-1,)
# INTER_CUBIC seems to break down beyond 1/32 sampling (discretizes).
# there might be fixed point algorithms at work
return samples
2.高斯滤波平滑曲线
samples = scipy.ndimage.gaussian_filter1d(samples, sigma=args.sigma / args.stride)
1
3.计算跳变幅度值
gradient = np.diff(samples) / args.stride
4.计算距离值
i_falling = np.argmin(gradient) # in samples
i_rising = np.argmax(gradient) # in samples
distance = np.abs(i_rising - i_falling) * args.stride # in pixels
完整代码:
#!/usr/bin/env python3
import sys
import argparse
import numpy as np
import cv2
import scipy.ndimage
### "business logic" ###################################################
def build_transform(p0, p1, stride=None, nsamples=None):
"builds an affine transform with x+ along defined line"
# use one of stride (in pixels) or nsamples (absolute value)
(x0, y0) = p0
(x1, y1) = p1
dx = x1 - x0
dy = y1 - y0
length = np.hypot(dx, dy)
if nsamples is not None:
# stride = length / nsamples
factor = 1 / nsamples
else:
if stride is None:
stride = 1.0
factor = stride / length
nsamples = int(round(length / stride))
# map: src <- dst (use WARP_INVERSE_MAP flag for warpAffine)
H = np.eye(3, dtype=np.float64) # homography
H[0:2, 0] = (dx, dy) # x unit vector
H[0:2, 1] = (-dy, dx) # y unit vector is x rotated by 90 degrees
H[0:2, 0:2] *= factor
H[0:2, 2] = (x0, y0) # translate onto starting point
# take affine part of homography
assert np.isclose(a=H[2], b=(0, 0, 1)).all() # we didn't touch those but let's better check
A = H[0:2, :]
return (nsamples, A)
def sample_opencv(im, M, nsamples):
# use transform to get samples
# available: INTER_{NEAREST,LINEAR,AREA,CUBIC,LANCOS4)
samples = cv2.warpAffine(im, M=M, dsize=(nsamples, 1), flags=cv2.WARP_INVERSE_MAP | cv2.INTER_CUBIC)
# flatten row vector
samples.shape = (-1,)
# INTER_CUBIC seems to break down beyond 1/32 sampling (discretizes).
# there might be fixed point algorithms at work
return samples
def sample_scipy(im, M, nsamples):
# coordinates to this function are (i,j) = (y,x)
# I could permute first and second rows+columns of M, or transpose input+output
Mp = M.copy()
Mp[(0, 1), :] = Mp[(1, 0), :] # permute rows
Mp[:, (0, 1)] = Mp[:, (1, 0)] # permute columns
samples = scipy.ndimage.interpolation.affine_transform(input=im, matrix=Mp, output_shape=(1, nsamples), order=2,
# 1: linear (C0, f' is piecewise constant), 2: C1 (f' is piecewise linear), 3: C2... https://en.wikipedia.org/wiki/Smoothness
mode='nearest' # border handling
)
# flatten row vector
samples.shape = (-1,)
return samples
### command line parsing utility functions #############################
def parse_linestr(arg):
pieces = arg.split(",")
pieces = [float(el) for el in pieces]
x0, y0, x1, y1 = pieces
return ((x0, y0), (x1, y1))
def parse_bool(arg):
if isinstance(arg, bool):
return arg
if arg.lower() in ('yes', 'true', 't', 'y', '1'):
return True
elif arg.lower() in ('no', 'false', 'f', 'n', '0'):
return False
else:
raise argparse.ArgumentTypeError(f'Boolean value expected, got {arg!r} instead')
def parse_float(arg):
import ast
if '/' in arg:
num, denom = arg.split('/', 1)
num = ast.literal_eval(num)
denom = ast.literal_eval(denom)
result = num / denom
else:
result = ast.literal_eval(arg)
return result
### main... ############################################################
if __name__ == '__main__':
# command line argument parsing
# change defaults here
parser = argparse.ArgumentParser()
parser.add_argument("--picture", dest="fname", metavar="PATH", type=str, default="dish-1.jpg", help="path to picture file")
parser.add_argument("--invert", type=parse_bool, default=True, metavar="BOOL", help="invert picture (cosmetic; distance between gradient extrema is absolute)")
parser.add_argument("--line", type=parse_linestr, default=((1320, 2500), (1320, 2100)), metavar="X0,Y0,X1,Y1", help="line to sample on")
parser.add_argument("--stride", type=parse_float, default=1 / 4, metavar="PX", help="stride in pixels to sample along line, fractions supported")
parser.add_argument("--method", type=lambda s: s.lower(), default="opencv", help="sampling methods: SciPy (slower, smoother, default), OpenCV (faster, less smooth)")
parser.add_argument("--sigma", type=float, default=2.0, metavar="PX", help="sigma for gaussian lowpass on sampled signal, before gradient is calculated")
parser.add_argument("--verbose", type=parse_bool, default=True, metavar="BOOL", help="chatty or not")
parser.add_argument("--display", type=parse_bool, default=True, metavar="BOOL", help="draw some plots")
parser.add_argument("--saveplot", type=str, default="plot.png", metavar="PATH", help="save a picture (use '--saveplot=' to disable)")
args = parser.parse_args()
########## here be dragons ##########
if args.stride > 1:
print(f"WARNING: stride should be <= 1, is {args.stride}")
stride_decimals = max(0, int(np.ceil(-np.log10(args.stride))))
if args.verbose: print("loading picture...", end=" ", flush=True)
im = cv2.imread(args.fname, cv2.IMREAD_GRAYSCALE)
imh, imw = im.shape[:2]
if args.invert:
im = 255 - im # invert
im = im.astype(np.float32) # * np.float32(1/255)
if args.verbose: print("done")
# build transform
p0, p1 = args.line
nsamples, M = build_transform(p0, p1, stride=args.stride)
if args.verbose: print(f"taking {nsamples} samples along line {p0} -> {p1}...", end=" ", flush=True)
# pick one
if args.method == 'opencv':
samples = sample_opencv(im, M, nsamples) # does "normal" cubic (4x4 support points, continuous first derivative)
elif args.method == 'scipy':
samples = sample_scipy(im, M, nsamples) # does some fancy "cubic" with continuous higher derivatives
else:
assert False, "method needs to be opencv or scipy"
if args.verbose: print("sampling done")
# smoothing to remove noise
if args.sigma > 0:
if args.verbose: print(f"lowpass filtering with sigma = {args.sigma} px...", end=" ", flush=True)
samples = scipy.ndimage.gaussian_filter1d(samples, sigma=args.sigma / args.stride)
if args.verbose: print("done")
# off-by-half in position because for values [0,1,1,0] this returns [+1,0,-1]
gradient = np.diff(samples) / args.stride
i_falling = np.argmin(gradient) # in samples
i_rising = np.argmax(gradient) # in samples
distance = np.abs(i_rising - i_falling) * args.stride # in pixels
if args.verbose:
print(f"distance: {distance:.{stride_decimals}f} pixels")
else:
print(distance)
# this was the result. algorithm is done.
# now follows displaying code
if args.display:
gradient *= 255 / np.abs(gradient).max()
# plot signal
plot = cv2.plot.Plot2d_create(np.arange(nsamples, dtype=np.float64), samples.astype(np.float64))
plot.setMinY(256 + 32)
plot.setMaxY(-32)
plot.setMinX(0)
plot.setMaxX(nsamples)
plot.setGridLinesNumber(5)
plot.setShowText(False) # callout for specific point, setPointIdxToPrint(index)
plot.setPlotGridColor((64,) * 3)
canvas1 = plot.render()
# plot gradient
plot = cv2.plot.Plot2d_create(np.arange(nsamples - 1) + 0.5, gradient.astype(np.float64))
plot.setMinY(256 + 64)
plot.setMaxY(-256 - 64)
plot.setMinX(0)
plot.setMaxX(nsamples)
plot.setGridLinesNumber(5)
plot.setShowText(False) # callout for specific point, setPointIdxToPrint(index)
plot.setPlotGridColor((64,) * 3)
canvas2 = plot.render()
# arrange vertically
canvas = np.vstack([canvas1, canvas2]) # 600 wide, 800 tall
# draw lines at edges (largest gradients)
# plots are 600x400 pixels... and there's no way to plot multiple or plot lines in "plot space"
px_falling = int(600 * (i_falling + 0.5) / nsamples)
px_rising = int(600 * (i_rising + 0.5) / nsamples)
cv2.line(canvas, (px_falling, 0), (px_falling, 400 * 2), color=(255, 0, 0))
cv2.line(canvas, (px_rising, 0), (px_rising, 400 * 2), color=(255, 0, 0))
# some text to describe the picture
cv2.putText(canvas, f"{nsamples * args.stride:.0f} px, {p0} -> {p1}", (10, 350), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (255, 255, 255), thickness=1, lineType=cv2.LINE_AA)
cv2.putText(canvas, f"stride {args.stride} px, {nsamples} samples, sigma {args.sigma}", (10, 350 + 35), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (255, 255, 255), thickness=1, lineType=cv2.LINE_AA)
cv2.putText(canvas, f"distance: {distance:.{stride_decimals}f} px", (10, 350 + 70), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (255, 255, 255), thickness=1, lineType=cv2.LINE_AA)
# save for posterity
if args.saveplot:
cv2.imwrite(args.saveplot, canvas)
if args.display:
cv2.imshow("plot", canvas)
if args.verbose:
print("press Ctrl+C in the terminal, or press any key while the imshow() window is focused")
while True:
keycode = cv2.waitKey(100)
if keycode == -1:
continue
# some key...
if args.verbose:
print(f"keycode: {keycode}")
cv2.destroyAllWindows()
break
总结
提示:显示的程序包含了opencv pilo,这个需要引入opencv-contrib-python模块:
原文链接:https://blog.csdn.net/hong3731/article/details/119649418
使用过程报错:
module 'cv2' has no attribute 'plot'
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/jacke121/article/details/122118816
内容来源于网络,如有侵权,请联系作者删除!