近景摄影测量中,像点坐标量测是获取观测值的过程,是大部分任务中必要的数据基础,像点获取之后可以完成基于共线条件方程的误差方程解算、直接线性变换等解析计算。
下面编写一个程序用来量测拍摄得到的相片上的控制点、量测点点位的像点坐标,要求能够打开影像,在影像上框选点位,实现计算机自动找到标志点的中心像点坐标,并且保证一定的精确度,要求达到亚像素级提取,同时将标志点中心像点坐标输出。
主要提取对象是以下两种标志点:
一、影像获取
获取两张影像构成立体像对,要求相片中的控制点分布均匀、量测容易、尽量减少遮挡、物方坐标可得。要求定焦拍摄(MF模式,光圈优先模式),影像清晰、明亮。
我使用的相机:富士-XE4定焦镜头
拍摄地点:武汉大学遥感信息工程学院室内控制场
拍摄过程中需要保证相机不调焦,并且能够拍摄到大范围的清晰影像,而物距又无法调整的很大,所以只能通过增大光圈号数(缩小孔径大小)来增大景深(清晰范围)。而如此一来进光量将减少,想要保证影像的亮度,就要提高ISO值或者延长快门速度,提高ISO会降低影像质量,增加噪点;延长快门速度防抖能力将会降低;最终我使用大光圈号数(F11左右)、适当的ISO值(1000左右)、较长的快门速度(1/25左右)借助三脚架稳定相机进行拍摄。
二、像点坐标量测程序
使用的方法是圆拟合找出最小包含标志点的圆形,然后使用灰度重心化的方法找出圆心位置,达到亚像素级提取。
该步骤存在用户与程序的交互操作,故可以考虑使用MFC应用程序或python等。
我使用的语言与环境:python+opencv
2.1打开图片与范围框选
1)使用opencv打开图像并可视化(由于图片过大,所以使用namewindow与、cv2.WINDOW_NORMAL来将图像完整显示到窗口大小),然后进行范围框选,由于需要多次框选,所以编写了循环读取打开图像的程序。
2)标志点范围的框选使用鼠标事件实现。
select_box是一个回调函数,它会在鼠标事件(如左键按下、移动、释放)时被触发。这个函数用于在图像上绘制一个矩形框。
- 当鼠标左键按下时,它记录起始点坐标(ix, iy)。
- 当鼠标移动时,如果正在绘制(drawing为True),则在图像副本上绘制一个矩形框,并实时更新显示。
- 当鼠标左键释放时,它计算矩形的坐标,并在原图上绘制一个最终的矩形框。同时,它裁剪出这个矩形区域内的图像,并存储在crop_img变量中。然后,它设置done为True,表示框选已完成。
import cv2
import numpy as np
import math
#(0,0)-------------x-> /pixel
#|
#|
#|
#y
ctr_point={}#控制点使用字典保存 点号:(x,y)
id_point = 0#点号
for i in range(100):
# 读取图像
image = cv2.imread('2.jpg')
# 创建一个窗口显示图像
cv2.namedWindow('Image',cv2.WINDOW_NORMAL)
cv2.imshow('Image', image)
# 标志变量,表示是否完成框选
done = False
if id_point=='0':
break
def select_box(event, x, y, flags, param):
global ix, iy, drawing, image_copy,crop_img,x1,y1
# 当按下鼠标左键时
if event == cv2.EVENT_LBUTTONDOWN:
drawing = True
ix, iy = x, y
# 当鼠标移动时
elif event == cv2.EVENT_MOUSEMOVE:
if drawing:
image_copy = image.copy()
cv2.rectangle(image_copy, (ix, iy), (x, y), (0, 255, 0), 4)
cv2.imshow("Image", image_copy)
# 当释放鼠标左键时
elif event == cv2.EVENT_LBUTTONUP:
drawing = False
if ix != -1 and iy != -1:
# 计算矩形的坐标
x1 = min(x, ix)
y1 = min(y, iy)
rect = (x1, y1, x - x1, y - y1)
cv2.rectangle(image, (x1, y1), (x, y), (0, 255, 0), 2)
cv2.imshow('Image', image)
# 裁剪图像
crop_img = image[y1:y1 + rect[3], x1:x1 + rect[2]]
#cv2.imshow('Cropped Image', crop_img)
# 设置标志变量,表示已完成框选
global done
done = True
# 初始化变量
global ix, iy, drawing, image_copy,crop_img,x1,y1
drawing = False # 标志变量,表示是否正在绘制矩形框
ix, iy = -1, -1 # 矩形框起始点的坐标
image_copy = None # 图像的副本,用于绘制矩形框
# 设置鼠标回调函数
cv2.setMouseCallback("Image", select_box)
# 等待按键,直到完成框选
while not done:
cv2.waitKey(0)
2.2标志点提取与中心坐标计算
最小外接圆&灰度重心化方法
无论是圆形标志点,还是十字形标志点,似乎都可以使用最小外接圆来找到包括住它们的最小的圆形,于是我准备尝试使用最小外接圆的方法来完成这一任务。
- 二值化,将图像变为黑白分明的图像
- 在图像中查找轮廓,使用cv2.findContours函数,可能会找出很多轮廓,所以框选尽可能只选中圆形部分图像,减少干扰。
- 使用cv2.minEnclosingCircle()函数查找轮廓contours中所有轮廓的最小外接圆。
- 上一步会获取很多圆,半径大小不一,通过尝试,找到最合适的阈值来筛选正确的最小外接圆。
- 为了提高精度,不直接使用该最小外接圆的圆心作为结果,而是继续利用该圆将框住的范围保留,圆形外的部分全部留白,去除目标圆形以外的干扰。
- 将二值化图像黑白取反,将像素坐标进行重心化,即为最终结果,该结果可以达到亚像素级别。
过程中的分步处理结果图:
![]() | ![]() | ![]() |
二值化 | 最小外接圆外取白 | 黑白取反 |
图.最小外接圆处理过程截图
经过多次检测实验,该方法效果非常好,无论是圆形还是十字形标志点都能够正确的检测,且代码简洁运行速度快。
将计算得到的圆心点绘制到图像上再次展示,可以观测到圆心点提取是否精确,有无偏离。
注意,计算得到的坐标是框选范围内的像素坐标,还需要加上框选范围左上角在整个原图像中的坐标,乘以像素大小才可以得到标志点中心在原图像中的像平面坐标。
#完成框选,开始进行标志点检测 crop_img是框选得到的图像
img = crop_img # 0或者cv2.IMREAD_GRAYSCALE 读取为灰度图像
#要确保传入的图片类型正确,彩色or灰度,单通or多通
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)#灰度化
height, width = img_gray.shape
ret , binary = cv2.threshold(img_gray , 100 , 255 ,cv2.THRESH_BINARY |cv2.THRESH_OTSU )#灰度二值化
cv2.waitKey(0)
contours , hirrarchy = cv2.findContours( binary , cv2.RETR_TREE ,
cv2.CHAIN_APPROX_NONE)#寻找轮廓
#cv2.imshow('二值化',binary)
for contour in contours:
(x,y) , radius = cv2.minEnclosingCircle(contour)#对每个轮廓找最小包含圆
if radius>height/3 and radius < height/2:#自定义阈值
center = (int(x) , int(y))#获取中心坐标
radius = int(radius)#获取半径
cv2.circle(img_gray , center , 2 , (0,0,255) , 2)#绘制提取到的圆形范围
#下面的二层循环是为了将圆形范围外的像素变为白色,消除灰度重心化时的外界误差
for x0 in range(width):
for y0 in range(height):
dis = math.sqrt((x0-x)*(x0-x)+(y0-y)*(y0-y))
if dis < radius and binary[y0,x0] == 0:
continue
else:
binary[y0,x0] = 255
#cv2.imshow('框选',binary)
#灰度重心化计算亚像素级圆心坐标
cv2.bitwise_not(binary,binary)
#cv2.imshow('取反',binary)
cv2.waitKey(0)
M=cv2.moments(binary)
cx=(M["m10"]/M["m00"])
cy=(M["m01"]/M["m00"])
center2 = (int(cx) , int(cy))
cv2.circle(img_gray , center2 , 2 , (255,255,255) , 2)
cv2.namedWindow('img',cv2.WINDOW_NORMAL)
cv2.imshow("img",img_gray)
cv2.waitKey(0)
#输入点号,保存为字典
id_point=input("请输入点号:")
print("圆心坐标为:",cx+x1,cy+y1)
#如果输入的点号不是0或1则保存
if id_point != '0' and id_point != '1':#if 0
ctr_point[id_point]=[cx+x1,cy+y1]
#如果是0则结束程序,写入文件
if id_point=='0':
print(ctr_point)
with open('pointallRR.txt', 'w') as file:
# 遍历字典的项
for key, value in ctr_point.items():
file.write(f"{key} {value[0]} {value[1]}\n" )
2.3像点坐标保存与输出
完成了以上像点坐标提取后,只需要最后一步,将自动提取到的像点坐标标记上序号保存输出即可。
- 输入标志点点号:
图.键入序号程序界面
2.将标志点中心坐标和序号组成键值对,保存在字典中(这里的像点坐标是上一步手动框选图像中的像素坐标,加上手动框选的图像在原图像中的坐标后计算得到的坐标,是像点在原始图像中的坐标):
3.保存为文件,以空格间隔:
表: 像点量测结果文件(单位/mm)
像点号 | 量测x坐标 | 量测y坐标 |
340 | 3488.7729852440407 | 3463.123344684071 |
341 | 3480.232709519935 | 2915.161106590724 |
344 | 3445.9122557726464 | 1202.829840142096 |
345 | 3434.537372147915 | 737.9976396538159 |
346 | 3424.1253462603877 | 259.07790858725764 |
141 | 3313.3726260641783 | 4034.6950447500544 |
143 | 3283.066207184628 | 2554.620509607352 |
144 | 3261.5910143584993 | 1745.5479388605836 |
145 | 3250.8248536695182 | 1119.5056280954525 |
146 | 3234.8441532258066 | 470.79637096774195 |
223 | 2873.4306748466256 | 1599.1601226993864 |
三、使用方法:
框选待提取标志点范围,计算中心坐标,可视化提取点位
- 若目测点位精确,则键入点号保存该点
若提取错误,想重新框选,则键入1,跳过该次提取
若结束提取,再任意框选一个标志点,键入0,会自动保存为文件
四、总结
该代码程序可以实现像点坐标的手动框选与提取中心点位,且具有较高的精度,并且可以输出可用的像点坐标文件。可以完成后续后方交会和直接线性变换解析。
不足:
在原理与数据精度方面没有太大问题,可以优化的是程序交互操作。
循环框选的程序可以更加便捷便于操作,添加交互程序窗口与界面等,程序结束与错误点跳过也可以设计更加人性化的操作方法加以优化,可以加入鼠标滚动功能,放大缩小图像便于框选操作等。