如何在高空照片中有效找到地平线?
我正在尝试在高空拍摄的图像中检测地平线,以确定相机的方向。同时,我希望这个过程能快速运行——理想情况下,我希望能在树莓派上实时处理图像(也就是每秒处理几帧)。我目前采用的方法是基于这样一个事实:在高空,天空看起来非常黑暗,如下图所示:
我尝试从整个图像中采样,将样本分为明亮和黑暗的部分,然后在它们之间画一条线。然而,由于大气的模糊性,这条线把地平线画得比实际位置要高:
这是我的代码(为了方便网络演示,我用的是Javascript):
function mag(arr) {
return Math.sqrt(arr[0]*arr[0]+arr[1]*arr[1]+arr[2]*arr[2])
}
// return (a, b) that minimize
// sum_i r_i * (a*x_i+b - y_i)^2
function linear_regression( xy ) {
var i,
x, y,
sumx=0, sumy=0, sumx2=0, sumy2=0, sumxy=0, sumr=0,
a, b;
for(i=0;i<xy.length;i++) {
x = xy[i][0]; y = xy[i][2];
r = 1
sumr += r;
sumx += r*x;
sumx2 += r*(x*x);
sumy += r*y;
sumy2 += r*(y*y);
sumxy += r*(x*y);
}
b = (sumy*sumx2 - sumx*sumxy)/(sumr*sumx2-sumx*sumx);
a = (sumr*sumxy - sumx*sumy)/(sumr*sumx2-sumx*sumx);
return [a, b];
}
var vals = []
for (var i=0; i<resolution; i++) {
vals.push([])
for (var j=0; j<resolution; j++) {
x = (canvas.width/(resolution+1))*(i+0.5)
y = (canvas.height/(resolution+1))*(j+0.5)
var pixelData = cr.getImageData(x, y, 1, 1).data;
vals[vals.length-1].push([x,y,pixelData])
cr.fillStyle="rgb("+pixelData[0]+","+pixelData[1]+","+pixelData[2]+")"
cr.strokeStyle="rgb(255,255,255)"
cr.beginPath()
cr.arc(x,y,10,0,2*Math.PI)
cr.fill()
cr.stroke()
}
}
var line1 = []
var line2 = []
for (var i in vals) {
i = parseInt(i)
for (var j in vals[i]) {
j = parseInt(j)
if (mag(vals[i][j][3])<minmag) {
if ((i<(vals.length-2) ? mag(vals[i+1][j][4])>minmag : false)
|| (i>0 ? mag(vals[i-1][j][5])>minmag : false)
|| (j<(vals[i].length-2) ? mag(vals[i][j+1][6])>minmag : false)
|| (j>0 ? mag(vals[i][j-1][7])>minmag : false)) {
cr.strokeStyle="rgb(255,0,0)"
cr.beginPath()
cr.arc(vals[i][j][0],vals[i][j][8],10,0,2*Math.PI)
cr.stroke()
line1.push(vals[i][j])
}
}
else if (mag(vals[i][j][9])>minmag) {
if ((i<(vals.length-2) ? mag(vals[i+1][j][10])<minmag : false)
|| (i>0 ? mag(vals[i-1][j][11])<minmag : false)
|| (j<(vals[i].length-2) ? mag(vals[i][j+1][12])<minmag : false)
|| (j>0 ? mag(vals[i][j-1][13])<minmag : false)) {
cr.strokeStyle="rgb(0,0,255)"
cr.beginPath()
cr.arc(vals[i][j][0],vals[i][j][14],10,0,2*Math.PI)
cr.stroke()
line2.push(vals[i][j])
}
}
}
}
eq1 = linear_regression(line1)
cr.strokeStyle = "rgb(255,0,0)"
cr.beginPath()
cr.moveTo(0,eq1[1])
cr.lineTo(canvas.width,canvas.width*eq1[0]+eq1[1])
cr.stroke()
eq2 = linear_regression(line2)
cr.strokeStyle = "rgb(0,0,255)"
cr.beginPath()
cr.moveTo(0,eq2[1])
cr.lineTo(canvas.width,canvas.width*eq2[0]+eq2[1])
cr.stroke()
eq3 = [(eq1[0]+eq2[0])/2,(eq1[1]+eq2[1])/2]
cr.strokeStyle = "rgb(0,255,0)"
cr.beginPath()
cr.moveTo(0,eq3[1])
cr.lineTo(canvas.width,canvas.width*eq3[0]+eq3[1])
cr.stroke()
这是结果(绿色线是检测到的地平线,红色和蓝色是估计的边界外部分):
我该如何改进这个方法?有没有更有效的方式来实现?最终的程序可能会用Python编写,或者如果速度太慢的话,也可能用C语言。
2 个回答
我会这样做:
先把图像转换为黑白
从每一侧扫描水平和垂直线,像这样
垂直线从上方开始扫描
图中的黑线表示线的位置。选中的线旁边的绿色箭头表示扫描方向(向下)和颜色强度可视化的方向(向右)。白色曲线是颜色强度图(这样你可以看到发生了什么)
- 选择一些网格步长,这里是线之间的64个点
- 创建一个临时数组
int p[];
来存储线的数据 对每一条线进行预处理
p[0]
是线的第一个像素的强度p[1,...]
是通过x
对H
线和通过y
对V
线的导数(就是减去相邻线的像素值)
对
p[1,...]
进行几次模糊处理,以避免噪声问题(从两侧模糊以避免位置偏移)。扫描并将其整合回去
整合就是把
c(i)=p[ 0 ] + p[ 1 ] + ... + p[ i ]
相加。如果c
低于阈值,说明你在大气层外,所以开始扫描。如果从线的起点开始扫描的区域在右侧,记住你到达阈值的地方A-point
,然后继续扫描直到达到峰值C-point
(第一个负导数值或真正的峰值...局部最大值)。计算
B-point
为了简单起见,你可以用
B = 0.5*(A+C)
,但如果想要更精确,由于大气强度是指数增长的,所以从A
扫描到C
并确定指数函数。如果导数的起始值与之不同,你就达到了B-point
,所以记住所有的B-points
(每条线都有)。
现在你有了一组
B-points
所以删除所有无效的
B-points
(每条线应该有两个...一个从开始,一个从结束),因此大气层较厚的区域通常是正确的,除非你视野中有一些黑色无缝的近距离物体。通过剩余的
B-points
近似一些曲线
[备注]
你不能根据高度来移动 B-point
的位置,因为大气的视觉厚度还取决于观察者和光源(太阳)的位置。此外,你应该过滤剩余的 B-points
,因为视野中的一些星星可能会造成干扰。但我认为曲线近似应该足够了。
[编辑1] 为了好玩做了一些东西
所以我在 BDS2006 C++ VCL 中做了这个...所以你需要根据你的环境更改图像访问方式
void find_horizont()
{
int i,j,x,y,da,c0,c1,tr0,tr1;
pic1=pic0; // copy input image pic0 to pic1
pic1.rgb2i(); // RGB -> BW
struct _atm
{
int x,y; // position of horizont point
int l; // found atmosphere thickness
int id; // 0,1 - V line; 2,3 - H line;
};
_atm p,pnt[256];// horizont points
int pnts=0; // num of horizont points
int n[4]={0,0,0,0}; // count of id-type points for the best option selection
da=32; // grid step [pixels]
tr0=4; // max difference of intensity inside vakuum homogenous area <0,767>
tr1=10; // min atmosphere thickness [pixels]
// process V-lines
for (x=da>>1;x<pic1.xs;x+=da)
{
// blur it y little (left p[0] alone)
for (i=0;i<5;i++)
{
for (y= 0;y<pic1.ys-1;y++) pic1.p[y][x].dd=(pic1.p[y][x].dd+pic1.p[y+1][x].dd)>>1; // this shift left
for (y=pic1.ys-1;y> 0;y--) pic1.p[y][x].dd=(pic1.p[y][x].dd+pic1.p[y-1][x].dd)>>1; // this shift right
}
// scann from top to bottom
// - for end of homogenous area
for (c0=pic1.p[0][x].dd,y=0;y<pic1.ys;y++)
{
c1=pic1.p[y][x].dd;
i=c1-c0; if (i<0) i=-i;
if (i>=tr0) break; // non homogenous bump
}
p.l=y;
// - for end of exponential increasing intensity part
for (i=c1-c0,y++;y<pic1.ys;y++)
{
c0=c1; c1=pic1.p[y][x].dd;
j = i; i =c1-c0;
if (i*j<=0) break; // peak
if (i+tr0<j) break; // non exponential ... increase is slowing down
}
// add horizont point if thick enough atmosphere found
p.id=0; p.x=x; p.y=y; p.l-=y; if (p.l<0) p.l=-p.l; if (p.l>tr1) { pnt[pnts]=p; pnts++; n[p.id]++; }
// scann from bottom to top
// - for end of homogenous area
for (c0=pic1.p[pic1.ys-1][x].dd,y=pic1.ys-1;y>=0;y--)
{
c1=pic1.p[y][x].dd;
i=c1-c0; if (i<0) i=-i;
if (i>=tr0) break; // non homogenous bump
}
p.l=y;
// - for end of exponential increasing intensity part
for (i=c1-c0,y--;y>=0;y--)
{
c0=c1; c1=pic1.p[y][x].dd;
j = i; i =c1-c0;
if (i*j<=0) break; // peak
if (i+tr0<j) break; // non exponential ... increase is slowing down
}
// add horizont point
// add horizont point if thick enough atmosphere found
p.id=1; p.x=x; p.y=y; p.l-=y; if (p.l<0) p.l=-p.l; if (p.l>tr1) { pnt[pnts]=p; pnts++; n[p.id]++; }
}
// process H-lines
for (y=da>>1;y<pic1.ys;y+=da)
{
// blur it x little (left p[0] alone)
for (i=0;i<5;i++)
{
for (x= 0;x<pic1.xs-1;x++) pic1.p[y][x].dd=(pic1.p[y][x].dd+pic1.p[y][x+1].dd)>>1; // this shift left
for (x=pic1.xs-1;x> 0;x--) pic1.p[y][x].dd=(pic1.p[y][x].dd+pic1.p[y][x-1].dd)>>1; // this shift right
}
// scann from top to bottom
// - for end of homogenous area
for (c0=pic1.p[y][0].dd,x=0;x<pic1.xs;x++)
{
c1=pic1.p[y][x].dd;
i=c1-c0; if (i<0) i=-i;
if (i>=tr0) break; // non homogenous bump
}
p.l=x;
// - for end of eyponential increasing intensitx part
for (i=c1-c0,x++;x<pic1.xs;x++)
{
c0=c1; c1=pic1.p[y][x].dd;
j = i; i =c1-c0;
if (i*j<=0) break; // peak
if (i+tr0<j) break; // non eyponential ... increase is slowing down
}
// add horizont point if thick enough atmosphere found
p.id=2; p.y=y; p.x=x; p.l-=x; if (p.l<0) p.l=-p.l; if (p.l>tr1) { pnt[pnts]=p; pnts++; n[p.id]++; }
// scann from bottom to top
// - for end of homogenous area
for (c0=pic1.p[y][pic1.xs-1].dd,x=pic1.xs-1;x>=0;x--)
{
c1=pic1.p[y][x].dd;
i=c1-c0; if (i<0) i=-i;
if (i>=tr0) break; // non homogenous bump
}
p.l=x;
// - for end of eyponential increasing intensitx part
for (i=c1-c0,x--;x>=0;x--)
{
c0=c1; c1=pic1.p[y][x].dd;
j = i; i =c1-c0;
if (i*j<=0) break; // peak
if (i+tr0<j) break; // non eyponential ... increase is slowing down
}
// add horizont point if thick enough atmosphere found
p.id=3; p.y=y; p.x=x; p.l-=x; if (p.l<0) p.l=-p.l; if (p.l>tr1) { pnt[pnts]=p; pnts++; n[p.id]++; }
}
pic1=pic0; // get the original image
// chose id with max horizont points
j=0;
if (n[j]<n[1]) j=1;
if (n[j]<n[2]) j=2;
if (n[j]<n[3]) j=3;
// draw horizont line from pnt.id==j points only
pic1.bmp->Canvas->Pen->Color=0x000000FF; // Red
for (i=0;i<pnts;i++) if (pnt[i].id==j) { pic1.bmp->Canvas->MoveTo(pnt[i].x,pnt[i].y); break; }
for ( ;i<pnts;i++) if (pnt[i].id==j) pic1.bmp->Canvas->LineTo(pnt[i].x,pnt[i].y);
}
输入图像是 pic0
,输出图像是 pic1
,它们是我的类,所以一些成员是:
xs,ys
是图像的像素大小p[y][x].dd
是(x,y)
位置的像素,类型为32位整数bmp
是GDI 位图rgb2i()
将所有RGB 像素转换为强度整数值<0-765>
(r+g+b)
如你所见,所有的地平线点都在 pnt[pnts]
数组中,其中:
x,y
是地平线点的位置l
是大气的厚度(指数部分)id
是{ 0,1,2,3 }
,用于标识扫描方向
这是输出图像(即使对于旋转的图像也能很好地工作)
这对于太阳光辉的图像是行不通的,除非你添加一些强力过滤。
考虑一些基本的通道混合和阈值处理,然后按照@Spektre的建议进行垂直采样。[根据@Spektre的评论,编辑为2*R-B,而不是R+G-B]
这里有一些通道混合的选项:
- 原始图像
- 平坦的单色混合 R+G+B
- 红色通道
- 2*R - B
- R + G - B
看起来选项#4是最清晰的效果(感谢@Spektre让我更仔细地检查这个),在颜色混合的比例为[红色 2: 绿色 0: 蓝色 -1]时,你会得到这个单色图像:
设置蓝色为负值意味着蓝色的雾气会用来消除地平线上的模糊感。结果发现,这比单纯使用红色和/或绿色要有效得多(可以在GIMP的通道混合器中试试)。
然后,如果你愿意,我们可以进一步清晰化,通过阈值处理(虽然你也可以在采样后进行),这里设置为25%的灰色:
使用Spektre的方法进行垂直采样,只需向下扫描,直到看到值超过25%。通过3条线,你应该能获得3个x,y坐标对,从而重建出这个曲线,知道它是一个抛物线。
为了更稳健,可以多采样几次,并丢弃异常值。