如何高效地在高空照片中找到地平线?

3
我正试图在高空拍摄的图像中检测地平线,以确定相机的方向。同时我想使其运行速度更快 - 理想情况下,我希望能够在树莓派上实时处理帧数(即每秒几帧)。到目前为止,我采用的方法是基于高空中天空非常暗淡这一事实,就像这样:Earth from space 我尝试从图像的各个部分进行采样,并将其分成光和暗的样本,并在它们之间画一条线。但由于大气层模糊的原因,这会导致地平线位于其实际位置之上:
以下是我的代码(为了方便网络演示使用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()

下图为结果(绿线是检测到的地平线,红色和蓝色是估计的越界):

enter image description here

怎样可以改进这个算法?是否有更高效的方式实现?最终程序可能会用Python或者C语言编写。


地平线不一定是漆黑的。它可能是深蓝色的;而我仍在努力寻找的是地面的边缘,而不是空气的边缘。 - Skyler
你不知道这张照片拍摄的高度吗? - Nuclearman
地球和大气层弯曲的角度是相同的,也许你可以利用这一点来抵消地平线。 - Nuclearman
我不知道,不确定如果太阳进入图像中该怎么办,就像这样:http://gaynorsnewsblog.files.wordpress.com/2010/03/icarus-3-1.jpg - Skyler
顺便提一下,在高海拔地区,大气非常稀薄,因此扩散效应非常不同。如果你将阳光纳入视野,那么最可能会应用一些相机滤镜和截止器来防止传感器受损(在稀薄的大气中直射阳光对像素有致命影响),所以图像可能会变形,即使进行了过滤,你也可能需要采用不同的方法来进行水平检测。 - Spektre
显示剩余2条评论
2个回答

3
考虑一些基本的通道混合和阈值处理,然后像@Spektre建议的那样进行垂直采样。以下是一些通道混合的选项:
1. 原始 2. 平坦的单色混合R+G+B 3. 红色通道 4. 2*R-B 5. R+G-B
看起来#4是最清晰的地平线(感谢@Spektre让我更仔细地检查了这一点),按比例混合颜色[红色2:绿色0:蓝色-1],你会得到这个单色图像。将蓝色设置为负数意味着利用天空中的蓝雾来消除模糊效果。
然后我们可以进一步澄清,如果您愿意,可以进行阈值处理(虽然您可以在采样后进行此操作),例如在25%灰度下。使用Spektre的方法对图像进行垂直采样,只需向下扫描,直到值超过25%为止。通过3条线,您应该会得到3个x,y对,从而知道它是一个抛物线。
为了增强鲁棒性,取多于3个样本并且舍弃离群值。

+1 不错...在识别之前进行适当的过滤总是比之后更好。当然,在不同的环境下,它可能会失去其属性,但对于地球来说,这是绝佳的... - Spektre
@Spektre,在您的评论后我重新制作了不同混合选项的比较图,并发现2*R-B(#4)比我最初选择的R+G-B(#5)更清晰。因此,我已经针对此进行了其他图像的重新制作。感谢您的提示,希望这样可以使关于雾霾的选择更加明确。 - Phil H
确实非常好的过滤器输出,如果我更经常知道我的问题所需的正确物理背景,那将为我节省很多时间... - Spektre
要学习物理,就要探索你所见到的世界,一直探索下去,直到你发现了乌龟或者对此失去兴趣为止。在一粒沙子中看到一个世界,在一朵野花中看到一个天堂,在手心中握住无限,感受一小时的永恒。(布莱克) - Phil H
谢谢!现在我只需要弄清楚如何使用OpenCV来实现这个(如果我理解正确的话,这将在树莓派上给我最佳性能)。 - Skyler
显示剩余3条评论

2
我会这样做:
  1. 转换为黑白图像

  2. 从每个侧面扫描水平和垂直线,如下所示:

    从顶部扫描垂直线

    Vertical line scan

    黑线显示线的位置。对于所选的那一行,绿色箭头表示扫描方向(向下)和颜色强度可视化方向(向右)。白色曲线是颜色强度图表(因此您实际上可以看到发生了什么)

    1. 选择一些网格步骤,即每个线之间的64点
    2. 创建临时数组int p[];来存储线
    3. 预处理每条线

      • p [0]是该行第一个像素的强度
      • p [1,...] H x 导数和 V y 导数(只需减去相邻的线像素)

      p [1,...]模糊几次以避免噪声问题(从两侧模糊以避免位置变化)。

    4. 扫描+积分后退

      积分就是求和c(i)= p [0] + p [1] + ... + p [i]。如果c低于阈值,则您在大气层外,因此开始扫描,如果从该区域的行的开始,您正在从右侧进行扫描。记住您到达阈值A点的位置,然后继续扫描直到达到峰值C点(第一个负导数值或实际峰值...局部最大值)。

    5. 计算B点

      为了简化起见,您可以执行B=0.5*(A+C),但如果您想要精确,则大气强度呈指数增长,因此从AC扫描派生函数并从中确定指数函数。如果派生函数的起点与它不同,则已到达B点,因此记住所有B点(对于每个线)。

  3. 现在您有一组B点

    因此删除所有无效的B点(每行应该有2个...从起点和终点),因此大气层较大的区域通常是正确的,除非视野中有一些接近的黑暗无缝物体。

  4. 通过剩余的B点近似某些曲线

[注]

您不能基于海拔高度来移动 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 是32位整数类型的(x,y)位置处的像素
  • bmpGDI位图
  • rgb2i() 将所有RGB像素转换为强度整数值<0-765> (r+g+b)

如您所见,所有水平点都在pnt[pnts]数组中,其中:

  • x,y 是地平线点的位置
  • l 是大气厚度(指数部分)
  • id{ 0,1,2,3 },用于标识扫描方向

这是输出图像(即使对于旋转的图像也能正常工作)

out normal

这对于太阳光晕图像不起作用,除非您添加一些重型过滤器。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接