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

IT技术 javascript python c algorithm image-processing
2021-03-04 14:32:28

我试图检测从高空拍摄的图像中的地平线,以确定相机的方向。我也试图让它运行得更快 - 理想情况下,我希望能够在 Raspberry Pi 上实时处理帧(即每秒几帧)。到目前为止,我一直采用的方法是基于这样一个事实,即在高海拔地区天空非常黑暗,如下所示:

从太空看地球

我所尝试的是从整个图像中取样并将它们分成明暗样本,并在它们之间画一条线。然而,由于大气的模糊性,这会将地平线置于其实际位置之上:

这是我的代码(使用 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个回答

考虑一些基本的通道混合和阈值处理,然后是@Spektre 建议的垂直样本。[根据@Spektre 的评论编辑为 2*RB 而不是 R+GB]

以下是通道混合的一些选项:

多项选择

  1. 原来的
  2. 单声道混音 R+G+B
  3. 红色通道
  4. 2*R-B
  5. R + G - B

看起来#4 是最清晰的地平线(感谢@Spektre 让我更仔细地检查),按比例混合颜色 [Red 2: Green 0: Blue -1],你会得到这张单色图像:

混合通道单声道

设置蓝色负值意味着地平线上的蓝色薄雾被用来消除那里的模糊。事实证明,这比仅使用红色和/或绿色(使用 GIMP 中的通道混合器尝试)更有效。

然后我们可以进一步澄清,如果你愿意,通过阈值(虽然你可以在采样后这样做),这里是 25% 灰色:

25pc 阈值

使用 Spektre 对图像进行垂直采样的方法,只需向下扫描,直到您看到值超过 25%。使用 3 条线,您应该获得 3 个 x,y 对,从而重建知道它是抛物线的曲线。

为了获得更高的稳健性,请取 3 个以上的样本并丢弃异常值。

@Spektre,在您发表评论后,我回去制作不同混合选项的比较图像,发现 2*RB(#4)比我最初选择的 R+GB(#5)更清晰。因此,我根据这一点重做了其他图像。感谢您的提示,希望这能让关于雾霾的选择更加清晰。
2021-04-20 14:32:28
顺便说一句,您是否仅尝试过红色频道?红色穿过我们的大气层,由于散射造成的问题最少(这就是卫星主要使用红外摄像机的原因)
2021-04-29 14:32:28
+1 不错......识别前的适当过滤总是比识别后更好。当然,在不同的大气中它可能会失去它的特性,但对于地球来说,这真是太棒了......
2021-04-30 14:32:28
@Spektre,是的,我在 GIMP 中使用了通道混音器。如果你看雾霾,它开始是蓝色的,但最终更接近白色。看红色通道,那里有很轻的薄雾;红色的散射不是零,只是比蓝色小得多。通过减去蓝色,红色通道的雾度被去除,只留下非蓝色的颜色。
2021-05-02 14:32:28
为此目的确实非常好的过滤器输出:) 如果我只更频繁地了解我的问题的正确物理背景......会为我节省很多时间......
2021-05-17 14:32:28

我会这样做:

  1. 转换为 BW

  2. 像这样从每一侧扫描水平线和垂直线

    垂直线从顶部扫描

    垂直线扫描

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

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

      • p[0] 是线的第一个像素的强度
      • p[1,...]是由xforH和 by yforV线推导的(只需减去相邻的线像素)

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

    4. 扫描 + 集成回来

      整合只是总结c(i)=p[ 0 ] + p[ 1 ] + ... + p[ i ]如果c低于阈值,则您在大气层之外,因此开始扫描,如果从行首开始,您正在从右侧扫描该区域。记住到达阈值的位置A-point并继续扫描,直到达到峰值C-point(第一个负推导值或实际峰值...局部最大值)。

    5. 计算 B-point

      为方便起见,您可以这样做,B = 0.5*(A+C)但如果您想要精确,那么大气强度会呈指数增长,因此扫描来自Ato 的导数并从中C确定指数函数。如果派生开始与您到达的不同,请B-point记住所有内容B-points(对于每一行)。

  3. 现在你有一套 B-points

    所以删除所有无效的B-points(你应该每行有 2 个......从开始和结束)所以具有更大气氛的区域通常是正确的,除非你有一些黑暗的无缝近距离物体。

  4. 通过剩余的近似一些曲线 B-points

[笔记]

您不能B-point根据高度改变位置,因为大气的视觉厚度还取决于观察者和光源(太阳)的位置。此外,您应该过滤剩余的B-points部分,因为查看中的某些星星可能会造成混乱。但我认为曲线近似应该足够了。

[Edit1] 做了一些有趣的事情

所以我在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 位整数类型
  • bmpGDI位图
  • rgb2i()将所有RGB像素转换为强度整数值<0-765> (r+g+b)

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

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

这是输出图像(即使对于旋转图像也能很好地工作)

正常

除非您添加一些重型过滤,否则这不适用于日光图像