Vista OpenCV Birdseye sin pérdida de datos

Estoy usando OpenCV para obtener una vista de ojo de pájaro de los cuadros capturados. Esto se hace proporcionando un patrón de tablero de ajedrez en el plano que formará la vista de ojo de pájaro.

enter image description here

Aunque parece que la cámara ya está bastante arriba de esta llanura, necesito que sea perfecta para determinar la relación entre píxeles y centímetros.

En la siguiente fase, los fotogtwigs de captura se están deformando. Da el resultado esperado:

enter image description here

Sin embargo, al realizar esta transformación, se pierden datos fuera del patrón de tablero de ajedrez. Lo que necesito es rotar la imagen en lugar de deformar un cuadrilátero conocido.

Pregunta: ¿Cómo rotar una imagen por un ángulo de cámara para que sea de arriba hacia abajo?


Algunos códigos para ilustrar lo que estoy haciendo actualmente:

Size chessboardSize = new Size(12, 8); // Size of the chessboard Size captureSize = new Size(1920, 1080); // Size of the captured frames Size viewSize = new Size((chessboardSize.width / chessboardSize.height) * captureSize.height, captureSize.height); // Size of the view MatOfPoint2f imageCorners; // Contains the imageCorners obtained in a earlier stage Mat H; // Homography 

El código que encuentra las esquinas:

 Mat grayImage = new Mat(); //Imgproc.resize(source, temp, new Size(source.width(), source.height())); Imgproc.cvtColor(source, grayImage, Imgproc.COLOR_BGR2GRAY); Imgproc.threshold(grayImage, grayImage, 0.0, 255.0, Imgproc.THRESH_OTSU); imageCorners = new MatOfPoint2f(); Imgproc.GaussianBlur(grayImage, grayImage, new Size(5, 5), 5); boolean found = Calib3d.findChessboardCorners(grayImage, chessboardSize, imageCorners, Calib3d.CALIB_CB_NORMALIZE_IMAGE + Calib3d.CALIB_CB_ADAPTIVE_THRESH + Calib3d.CALIB_CB_FILTER_QUADS); if (found) { determineHomography(); } 

El código que determina la homografía:

 Point[] data = imageCorners.toArray(); if (data.length < chessboardSize.area()) { return; } Point[] roi = new Point[] { data[0 * (int)chessboardSize.width - 0], // Top left data[1 * (int)chessboardSize.width - 1], // Top right data[((int)chessboardSize.height - 1) * (int)chessboardSize.width - 0], // Bottom left data[((int)chessboardSize.height - 0) * (int)chessboardSize.width - 1], // Bottom right }; Point[] roo = new Point[] { new Point(0, 0), new Point(viewSize.width, 0), new Point(0, viewSize.height), new Point(viewSize.width, viewSize.height) }; MatOfPoint2f objectPoints = new MatOfPoint2f(), imagePoints = new MatOfPoint2f(); objectPoints.fromArray(roo); imagePoints.fromArray(roi); Mat H = Imgproc.getPerspectiveTransform(imagePoints, objectPoints); 

Finalmente, los cuadros capturados están siendo deformados:

 Imgproc.warpPerspective(capture, view, H, viewSize); 

[Edit2] progreso actualizado

Puede haber más de solo rotación presente, así que probaría esto en su lugar:

  1. imagen preprocesada

    Puede aplicar muchos filtros para eliminar el ruido de la imagen y / o normalizar las condiciones de iluminación (parece que su imagen publicada no la necesita). Luego simplemente binarice la imagen para simplificar los pasos. ver relacionado

    • OpenCV para OCR: Cómo calcular los niveles de umbral para OCR de imagen gris
  2. detectar puntos de esquina cuadrados

    y almacenar sus coordenadas en alguna matriz con su topología

     double pnt[col][row][2]; 

    donde (col,row) es el índice del tablero de ajedrez y [2] almacena (x, y). Puede usar int pero double/float evitará conversiones y redondeos innecesarios durante la instalación …

    Las esquinas se pueden detectar (a menos que la inclinación / rotación esté cerca de 45 grados) al escanear los píxeles vecinales diagonales de esta manera:

    detectar cruces

    Una diagonal debe estar en un color y la otra en diferente. Este patrón detectará un grupo de puntos alrededor del cruce, así que encuentre los puntos cercanos y calcule su promedio.

    Si escanea toda la imagen, la parte superior for eje del ciclo también ordenará la lista de puntos, por lo que no es necesario clasificarla más. Después de promediar ordenar / ordenar los puntos a la topología de la cuadrícula (por ejemplo, por dirección entre los 2 puntos más cercanos)

  3. Topología

    Para que sea robusto, utilizo una imagen rotada y torcida para que la detección de topología sea un poco complicada. Después de un tiempo de elaboración llego a esto:

    1. encontrar el punto p0 cerca del medio de la imagen

      Eso debería asegurar que haya vecinos para ese punto.

    2. encontrar el punto más cercano p

      Pero ignore los puntos diagonales ( |x/y| -> 1 +/- escala de cuadrados). A partir de este punto calcula el primer vector de base, déjalo llamar por ahora.

    3. encontrar el punto más cercano p

      De la misma manera que en el n . ° 2, pero esta vez también ignore los puntos en la dirección +/- u ( |(uv)|/(|u|.|v|) -> 1 +/- sesgo / rotaciones). A partir de este punto, calcule el segundo vector de base, deje que lo llame v por ahora.

    4. normalizar u, v

      Elegí u puntos vectoriales a las direcciones +x y v a +y . Así que el vector de base con más grande |x| el valor debe ser u y con mayor |y| debería ser v . Así que prueba y intercambia si es necesario. Entonces solo niegue si el signo equivocado. Ahora tenemos vectores de base para la mitad de la pantalla (más lejos podrían cambiar).

    5. topología de cómputo

      Establezca el punto p0 como (u=0,v=0) como punto de inicio. Ahora recorra todos los puntos sin igual p . Para cada posición predicha de cálculo de los vecinos agregando / restando vectores de base desde su posición. Luego encuentre el punto más cercano a esta ubicación y si lo encuentra debería ser vecino, entonces ajuste su coordenada (u,v) a +/-1 del punto original p . Ahora actualice los vectores de base para estos puntos y buclee todo hasta que no se encuentre ninguna coincidencia nueva. El resultado debería ser que la mayoría de los puntos deberían haber calculado sus coordenadas (u,v) que es lo que necesitamos.

    Después de esto, puede encontrar el min(u),min(v) y cambiarlo a (0,0) para que los índices no sean negativos si es necesario.

  4. adaptarse a un polinomio para los puntos de esquina

    por ejemplo algo como:

     pnt[i][j][0]=fx(i,j) pnt[i][j][1]=fy(i,j) 

    donde fx,fy son funciones polinomiales. Puedes probar cualquier proceso de adaptación. Intenté ajustar el polinomio cúbico con el uso de la búsqueda de aproximación, pero el resultado no fue tan bueno como la interpolación bicúbica nativa (posiblemente debido a la distorsión no uniforme de la imagen de prueba), así que cambié a interpolación bicúbica en lugar de ajustar. Eso es más simple, pero hace que la informática inversa sea muy difícil, pero puede evitarse a costa de la velocidad. Si necesita calcular el inverso de todos modos ver

    • Tabla de búsqueda 2D compleja inversa

    Estoy usando una interpolación simple cúbica como esta:

     d1=0.5*(pp[2]-pp[0]); d2=0.5*(pp[3]-pp[1]); a0=pp[1]; a1=d1; a2=(3.0*(pp[2]-pp[1]))-(2.0*d1)-d2; a3=d1+d2+(2.0*(-pp[2]+pp[1])); } coordinate = a0+(a1*t)+(a2*t*t)+(a3*t*t*t); 

    donde pp[0..3] son 4 puntos de control conocidos consecuentes (nuestros cruces de cuadrícula), a0..a3 son coeficientes polinómicos calculados y la coordinate es punto sobre curva con el parámetro t . Esto se puede ampliar a cualquier cantidad de dimensiones.

    Las propiedades de esta curva son simples, es continua, comienza en pp[1] y termina en pp[2] mientras que t=<0.0,1.0> . La continuidad con segmentos vecinos está asegurada con una secuencia común a todas las curvas cúbicas.

  5. reasignar píxeles

    simplemente use i,j como valores flotantes con un paso alrededor del 75% del tamaño del píxel para evitar espacios vacíos. A continuación, simplemente recorra todas las posiciones (i,j) calcule (x,y) y copie el píxel de la imagen de origen en (x,y) a (i*sz,j*sz)+/-offset donde se quiere sz cuadrícula tamaño en píxeles.

Aquí el C ++ :

 //--------------------------------------------------------------------------- picture pic0,pic1; // pic0 - original input image,pic1 output //--------------------------------------------------------------------------- struct _pnt { int x,y,n; int ux,uy,vx,vy; _pnt(){}; _pnt(_pnt& a){ *this=a; }; ~_pnt(){}; _pnt* operator = (const _pnt *a) { x=a->x; y=a->y; return this; }; //_pnt* operator = (const _pnt &a) { ...copy... return this; }; }; //--------------------------------------------------------------------------- void vision() { pic1=pic0; // copy input image pic0 to pic1 pic1.enhance_range(); // maximize dynamic range of all channels pic1.treshold_AND(0,127,255,0); // binarize (remove gray shades) pic1&=0x00FFFFFF; // clear alpha channel for exact color matching pic1.save("out_binarised.png"); int i0,i,j,k,l,x,y,u,v,ux,uy,ul,vx,vy,vl; int qi[4],ql[4],e,us,vs,**uv; _pnt *p,*q,p0; List<_pnt> pnt; // detect square crossings point clouds into pnt[] pnt.allocate(512); pnt.num=0; p0.ux=0; p0.uy=0; p0.vx=0; p0.vy=0; for (p0.n=1,p0.y=2;p0.yn) // skip deleted points for (p0=*p,j=i+1,q=p+1;jn) // skip deleted points { if (q->y>p0.y+4) continue; // scan only up do y distance <=4 (clods are not bigger then that) x=p0.xq->x; x*=x; // compute distance^2 y=p0.yq->y; y*=y; x+=y; if (x>25) continue; // skip too distant points p->x+=q->x; // add coordinates (average) p->y+=q->y; p->n++; // increase ussage q->n=0; // mark current point as deleted } // divide the average coordinates and delete marked points for (p=pnt.dat,i=0,j=0;in) // skip deleted points { p->x/=p->n; p->y/=p->n; p->n=1; pnt.dat[j]=*p; j++; } pnt.num=j; // n is now encoded (u,v) so set it as unmatched (u,v) first #define uv2n(u,v) ((((v+32768)&65535)<<16)|((u+32768)&65535)) #define n2uv(n) { u=n&65535; u-=32768; v=(n>>16)&65535; v-=32768; } for (p=pnt.dat,i=0;in=0; // p0,i0 find point near middle of image x=pic1.xs>>2; y=pic1.ys>>2; for (p=pnt.dat,i=0;ix>=x)&&(p->x<=x+x+x) &&(p->y>=y)&&(p->y<=y+y+y)) break; p0=*p; i0=i; // q,j find closest point to p0 vl=pic1.xs+pic1.ys; k=0; for (p=pnt.dat,i=0;ix-p0.x; y=p->y-p0.y; l=sqrt((x*x)+(y*y)); if (abs(abs(x)-abs(y))*5x-p0.x; uy=q->y-p0.y; ul=sqrt((ux*ux)+(uy*uy)); // q,k find closest point to p0 not in u direction vl=pic1.xs+pic1.ys; k=0; for (p=pnt.dat,i=0;ix-p0.x; y=p->y-p0.y; l=sqrt((x*x)+(y*y)); if (abs(abs(x)-abs(y))*575) continue;// ignore paralel to u directions if (l<=vl) { k=i; vl=l; } // remember smallest distance } q=pnt.dat+k; vx=q->x-p0.x; vy=q->y-p0.y; vl=sqrt((vx*vx)+(vy*vy)); // normalize directions u -> +x, v -> +y if (abs(ux)n=uv2n(x,0); p->ux=ux; p->uy=uy; p->vx=vx; p->vy=vy; p=pnt.dat+k; // p0 +/- v basis vector p->n=uv2n(0,y); p->ux=ux; p->uy=uy; p->vx=vx; p->vy=vy; // qi[k],ql[k] find closest point to p0 #define find_neighbor \ for (ql[k]=0x7FFFFFFF,qi[k]=-1,q=pnt.dat,j=0;jx-p0.x; \ y=q->y-p0.y; \ l=(x*x)+(y*y); \ if (ql[k]>=l) { ql[k]=l; qi[k]=j; } \ } // process all matched points for (e=1;e;) for (e=0,p=pnt.dat,i=0;in) { // prepare variables ul=(p->ux*p->ux)+(p->uy*p->uy); vl=(p->vx*p->vx)+(p->vy*p->vy); // find neighbors near predicted position p0 k=0; p0.x=p->xp->ux; p0.y=p->yp->uy; find_neighbor; if (ql[k]<<1>ul) qi[k]=-1; // u-1,v k++; p0.x=p->x+p->ux; p0.y=p->y+p->uy; find_neighbor; if (ql[k]<<1>ul) qi[k]=-1; // u+1,v k++; p0.x=p->xp->vx; p0.y=p->yp->vy; find_neighbor; if (ql[k]<<1>vl) qi[k]=-1; // u,v-1 k++; p0.x=p->x+p->vx; p0.y=p->y+p->vy; find_neighbor; if (ql[k]<<1>vl) qi[k]=-1; // u,v+1 // update local u,v basis vectors for found points (and remember them) n2uv(p->n); ux=p->ux; uy=p->uy; vx=p->vx; vy=p->vy; k=0; if (qi[k]>=0) { q=pnt.dat+qi[k]; if (!q->n) { e=1; q->n=uv2n(u-1,v); q->ux=-(q->xp->x); q->uy=-(q->yp->y); } ux=q->ux; uy=q->uy; } k++; if (qi[k]>=0) { q=pnt.dat+qi[k]; if (!q->n) { e=1; q->n=uv2n(u+1,v); q->ux=+(q->xp->x); q->uy=+(q->yp->y); } ux=q->ux; uy=q->uy; } k++; if (qi[k]>=0) { q=pnt.dat+qi[k]; if (!q->n) { e=1; q->n=uv2n(u,v-1); q->vx=-(q->xp->x); q->vy=-(q->yp->y); } vx=q->vx; vy=q->vy; } k++; if (qi[k]>=0) { q=pnt.dat+qi[k]; if (!q->n) { e=1; q->n=uv2n(u,v+1); q->vx=+(q->xp->x); q->vy=+(q->yp->y); } vx=q->vx; vy=q->vy; } // copy remembered local u,v basis vectors to points where are those missing k=0; if (qi[k]>=0) { q=pnt.dat+qi[k]; if (!q->vy) { q->vx=vx; q->vy=vy; }} k++; if (qi[k]>=0) { q=pnt.dat+qi[k]; if (!q->vy) { q->vx=vx; q->vy=vy; }} k++; if (qi[k]>=0) { q=pnt.dat+qi[k]; if (!q->ux) { q->ux=ux; q->uy=uy; }} k++; if (qi[k]>=0) { q=pnt.dat+qi[k]; if (!q->ux) { q->ux=ux; q->uy=uy; }} } // find min,max (u,v) ux=0; uy=0; vx=0; vy=0; for (p=pnt.dat,i=0;in) { n2uv(p->n); if (ux>u) ux=u; if (vx>v) vx=v; if (uyn) { n2uv(p->n); u-=ux; v-=vx; p->n=uv2n(u,v); uv[u][v]=i; } // bi-cubic interpolation double a0,a1,a2,a3,d1,d2,pp[4],qx[4],qy[4],t,fu,fv,fx,fy; // compute cubic curve coefficients a0..a3 from 1D points pp[0..3] #define cubic_init { d1=0.5*(pp[2]-pp[0]); d2=0.5*(pp[3]-pp[1]); a0=pp[1]; a1=d1; a2=(3.0*(pp[2]-pp[1]))-(2.0*d1)-d2; a3=d1+d2+(2.0*(-pp[2]+pp[1])); } // compute cubic curve cordinates =f(t) #define cubic_xy (a0+(a1*t)+(a2*t*t)+(a3*t*t*t)); // safe access to grid (u,v) point copies it to p0 // points utside grid are computed by mirroring #define point_uv(u,v) \ { \ if ((u>=0)&&(u=0)&&(v=us) uu=us-1; \ if (vv<0) vv=0; \ if (vv>=vs) vv=vs-1; \ p0=pnt.dat[uv[uu][vv]]; \ uu=u-uu; vv=v-vv; \ p0.x+=(uu*p0.ux)+(vv*p0.vx); \ p0.y+=(uu*p0.uy)+(vv*p0.vy); \ } \ } //---------------------------------------- //--- Debug draws: ----------------------- //---------------------------------------- // debug recolor white to gray to emphasize debug render pic1.recolor(0x00FFFFFF,0x00404040); // debug draw basis vectors for (p=pnt.dat,i=0;iCanvas->Pen->Color=clRed; pic1.bmp->Canvas->Pen->Width=1; pic1.bmp->Canvas->MoveTo(p->x,p->y); pic1.bmp->Canvas->LineTo(p->x+p->ux,p->y+p->uy); pic1.bmp->Canvas->Pen->Color=clBlue; pic1.bmp->Canvas->MoveTo(p->x,p->y); pic1.bmp->Canvas->LineTo(p->x+p->vx,p->y+p->vy); pic1.bmp->Canvas->Pen->Width=1; } // debug draw crossings AnsiString s; pic1.bmp->Canvas->Font->Height=12; pic1.bmp->Canvas->Brush->Style=bsClear; for (p=pnt.dat,i=0;in); if (p->n) { pic1.bmp->Canvas->Font->Color=clWhite; s=AnsiString().sprintf("%i,%i",u,v); } else{ pic1.bmp->Canvas->Font->Color=clGray; s=i; } x=p->x-(pic1.bmp->Canvas->TextWidth(s)>>1); y=p->y-(pic1.bmp->Canvas->TextHeight(s)>>1); pic1.bmp->Canvas->TextOutA(x,y,s); } pic1.bmp->Canvas->Brush->Style=bsSolid; pic1.save("out_topology.png"); // debug draw of bi-cubic interpolation fit/coveradge with half square step pic1=pic0; pic1.treshold_AND(0,200,0x40,0); // binarize (remove gray shades) pic1.bmp->Canvas->Pen->Color=clAqua; pic1.bmp->Canvas->Brush->Color=clBlue; for (fu=-1;fuCanvas->Ellipse(fx-t,fy-t,fx+t,fy+t); } pic1.save("out_fit.png"); // linearizing of original image DWORD col; double grid_size=32.0; // linear grid square size in pixels double grid_step=0.01; // u,v step <= 1 pixel pic1.resize((us+1)*grid_size,(vs+1)*grid_size); // resize target image pic1.clear(0); // clear target image for (fu=-1;fu=0)&&(x=0)&&(y=0)&&(x=0)&&(y 

Lo siento, sé que es un montón de código, pero al menos lo comenté tanto como pude. El código no está optimizado en aras de la simplicidad y la capacidad de comprensión, la linealización de la imagen final se puede escribir mucho más rápido. También elegí grid_size y grid_step en esa parte del código de forma manual. Se debe calcular a partir de la imagen y las propiedades físicas conocidas en su lugar.

Utilizo mi propia clase de imágenes para las imágenes, por lo que algunos miembros son:

  • xs,ys tamaño de la imagen en píxeles
  • p[y][x].dd es píxel en la posición (x,y) como tipo entero de 32 bits
  • clear(color) : borra toda la imagen
  • resize(xs,ys) - cambia el tamaño de la imagen a una nueva resolución
  • bmp - Mapa de bits GDI encapsulado en VCL con acceso de canvas

También uso mi plantilla de lista dinámica así que:

  • List xxx; es lo mismo que double xxx[];
  • xxx.add(5); agrega 5 al final de la lista
  • xxx[7] elemento de matriz de acceso (seguro)
  • xxx.dat[7] access array element (inseguro pero acceso directo rápido)
  • xxx.num es el tamaño real utilizado de la matriz
  • xxx.reset() borra la matriz y establece xxx.num = 0
  • xxx.allocate(100) preasignar espacio para 100 elementos

Aquí están las imágenes de salida del resultado secundario. Para hacer las cosas más robustas, cambié la imagen de entrada a una más distorsionada:

ejemplo

Para hacerlo visualmente más agradable recoloque el blanco al gris. Las líneas rojas son locales y los vectores básicos son azules . Los números blancos de vectores 2D son coordenadas de topología (u,v) y los números escalares grises son índices de cruces en pnt[] para topología pero puntos sin igual.

[Notas]

Este enfoque no funcionará para rotaciones de cerca de 45 grados. En tales casos, debe cambiar la detección de cruces del patrón cruzado al más y también las condiciones de topología y las ecuaciones cambian un poco. Sin mencionar u, v dirección de selección.