将Android OpenCV相机预览旋转为竖屏

43

我正在尝试使用OpenCV 2.4.3.2创建一个相机应用程序并进行一些opencv处理。我希望它能够具有多个UI方向,而不仅仅是横屏。

问题在于,当我将方向更改为纵向时,图像会侧着出现。

我知道 我可以在处理图像之前旋转输入图像(因此仅保留横向方向),这很好并且有效,但并不能解决我的UI其余部分将处于错误方向的问题。

我还尝试使用 这个代码 将相机旋转90度,但似乎并没有起作用。

mCamera.setDisplayOrientation(90);

它要么没有效果,要么只是导致预览被黑掉。

有人用OpenCV成功地做到了这一点吗?我的类继承自JavaCameraView。 portrait image with sideways preview

编辑

我已经做出了改进,即在显示在CameraBridgeViewBase.java类中的图像内旋转了它。

在deliver和draw frame方法中:

if (canvas != null) {
            canvas.drawColor(0, android.graphics.PorterDuff.Mode.CLEAR);
            //canvas.drawBitmap(mCacheBitmap, (canvas.getWidth() - mCacheBitmap.getWidth()) / 2, (canvas.getHeight() - mCacheBitmap.getHeight()) / 2, null);
            //Change to support portrait view
            Matrix matrix = new Matrix();
            matrix.preTranslate((canvas.getWidth() - mCacheBitmap.getWidth()) / 2,(canvas.getHeight() - mCacheBitmap.getHeight()) / 2);

            if(getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT)
                matrix.postRotate(90f,(canvas.getWidth()) / 2,(canvas.getHeight()) / 2);
            canvas.drawBitmap(mCacheBitmap, matrix, new Paint());
基本上,这只是将输入图像旋转如下

输入图像旋转90度

这已经更好了,但我显然希望它能够全屏显示。


我刚刚添加了一张图片来解释这个行为,之前我没有解释清楚。实际上,我正在将方向设置为纵向。 - Jameo
哪个样本?我想我已经尝试了所有的样本,它们似乎都是相同的(而且错误的)。 - Jameo
@Jameo 我目前遇到了同样的困难。我也是openCv的新手。我有一个想法(我不知道它是否有意义),但您觉得如果我们使用Android Camera API,例如surface view等,能否将摄像头整洁地显示出来(正确方向、全屏或按需求)并作为后端相机逻辑使用openCv处理数据。唯一的障碍可能是可能需要打开两个摄像头。我对这个想法还不太确定。欢迎评论。 - test
我也考虑过这个问题。问题是,我花了几天时间查看源代码,试图弄清楚CameraBridgeViewBase.java实际上是做什么的,但我仍然不太理解帧是如何转换的。我仍然没有一个好的解决方案,我的主要关注点是为什么setOrienation不起作用? - Jameo
不,这个问题仍然困扰着我。我最终只是将应用锁定为横向模式,并手动旋转任何用户界面元素。非常麻烦。但当时,来自openCV的人们正在谈论解决它,所以如果现在还没有解决,我会感到惊讶。 - Jameo
显示剩余10条评论
20个回答

13
我在尝试实现OpenCV时遇到了同样的问题。通过对deliverAndDrawFrame方法进行以下更改,我成功解决了该问题。
  1. 旋转画布对象

  2. Canvas canvas = getHolder().lockCanvas();
    // Rotate canvas to 90 degrees
    canvas.rotate(90f, canvas.getWidth()/2, canvas.getHeight()/2);
    
  3. 绘制之前将位图调整大小以适应画布的整个尺寸

  4. // Resize
    Bitmap bitmap = Bitmap.createScaledBitmap(mCacheBitmap, canvas.getHeight(), canvas.getWidth(), true);
    // Use bitmap instead of mCacheBitmap
    canvas.drawBitmap(bitmap, new Rect(0,0,bitmap.getWidth(), bitmap.getHeight()), new Rect(
        (int)((canvas.getWidth() - mScale*bitmap.getWidth()) / 2),
        (int)((canvas.getHeight() - mScale*bitmap.getHeight()) / 2),
        (int)((canvas.getWidth() - mScale*bitmap.getWidth()) / 2 + mScale*bitmap.getWidth()),
        (int)((canvas.getHeight() - mScale*bitmap.getHeight()) / 2 + mScale*bitmap.getHeight()
      )), null);
    
    // Unlock canvas
    getHolder().unlockCanvasAndPost(canvas);
    

Wrboleski,问题在于当您使用FeatureDetector时,它不起作用。 特征检测器现在侧视图图像。 - Jenia Ivanov
当我运行它时,这会导致我的相机崩溃。 - mheavers
为了适应屏幕 canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()), new Rect(-(canvas.getHeight() - canvas.getWidth()) / 2, (canvas.getHeight() - canvas.getWidth()) / 2, (canvas.getHeight() - canvas.getWidth()) / 2 + canvas.getWidth(), canvas.getHeight() - (canvas.getHeight() - canvas.getWidth()) / 2), null); - Kelvin Ng
@KelvinNg 这个可以用,但是照片分辨率比较低,怎么才能改善呢? - Raj Kumar

10

我将CameraBridgeViewBase.java进行了如下修改:

protected Size calculateCameraFrameSize(List<?> supportedSizes, ListItemAccessor accessor, int surfaceWidth, int surfaceHeight) {
    int calcWidth = 0;
    int calcHeight = 0;

    if(surfaceHeight > surfaceWidth){
        int temp = surfaceHeight;
        surfaceHeight = surfaceWidth;
        surfaceWidth = temp;
    }

在函数"deliverAndDrawFrame"中:

            if (mScale != 0) {
                if(canvas.getWidth() > canvas.getHeight()) {
                canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
                     new Rect((int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2),
                     (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2),
                     (int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2 + mScale*mCacheBitmap.getWidth()),
                     (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2 + mScale*mCacheBitmap.getHeight())), null);
                } else {
                    canvas.drawBitmap(mCacheBitmap, rotateMe(canvas, mCacheBitmap), null);
                }

其中rotateMe的定义如下:

private Matrix rotateMe(Canvas canvas, Bitmap bm) {
    // TODO Auto-generated method stub
    Matrix mtx=new Matrix();
    float scale = (float) canvas.getWidth() / (float) bm.getHeight();
    mtx.preTranslate((canvas.getWidth() - bm.getWidth())/2, (canvas.getHeight() - bm.getHeight())/2);
    mtx.postRotate(90,canvas.getWidth()/2, canvas.getHeight()/2);
    mtx.postScale(scale, scale, canvas.getWidth()/2 , canvas.getHeight()/2 );
    return mtx;
}

预览帧率较慢是因为与横向模式相比存在计算开销。


3
这个很好用。但是我正在使用的人脸识别示例不再识别任何人脸了,它默认是横屏模式。对此有什么想法吗?谢谢。 - dasAnderl ausMinga
相机在竖屏模式下会变慢。而且横屏模式对双方都不起作用。 - Samvel Kartashyan
这个方法可以工作,但正如你所说的那样,帧率会急剧下降,这有点违背了初衷。有没有一种更有效的方法来实现类似的结果? - M. Azyoksul

10

实际上,您只需使宽度或高度与父元素匹配(全屏)即可。

if (canvas != null) {
        Bitmap bitmap = Bitmap.createScaledBitmap(mCacheBitmap, canvas.getHeight(), canvas.getWidth(), true);
        canvas.rotate(90,0,0);
        float scale = canvas.getWidth() / (float)bitmap.getHeight();
        float scale2 = canvas.getHeight() / (float)bitmap.getWidth();
        if(scale2 > scale){
            scale = scale2;
        }
        if (scale != 0) {
            canvas.scale(scale, scale,0,0);
        }
        canvas.drawBitmap(bitmap, 0, -bitmap.getHeight(), null);

此外,您可以将预览大小调整为大于屏幕。只需修改比例即可。


8

就像其他回答一样,我编写了个人版本的deliverAndDrawFrame(我也通过注释通知了我的代码起始点和结束点):

protected void deliverAndDrawFrame(CvCameraViewFrame frame) {
    Mat modified;

    if (mListener != null) {
        modified = mListener.onCameraFrame(frame);
    } else {
        modified = frame.rgba();
    }

    boolean bmpValid = true;
    if (modified != null) {
        try {
            Utils.matToBitmap(modified, mCacheBitmap);
        } catch(Exception e) {
            Log.e(TAG, "Mat type: " + modified);
            Log.e(TAG, "Bitmap type: " + mCacheBitmap.getWidth() + "*" + mCacheBitmap.getHeight());
            Log.e(TAG, "Utils.matToBitmap() throws an exception: " + e.getMessage());
            bmpValid = false;
        }
    }

    if (bmpValid && mCacheBitmap != null) {
        Canvas canvas = getHolder().lockCanvas();
        if (canvas != null) {
            canvas.drawColor(0, android.graphics.PorterDuff.Mode.CLEAR);
            if (BuildConfig.DEBUG) {
                Log.d(TAG, "mStretch value: " + mScale);
            }

            // Start of the fix
            Matrix matrix = new Matrix();
            matrix.preTranslate( ( canvas.getWidth() - mCacheBitmap.getWidth() ) / 2f, ( canvas.getHeight() - mCacheBitmap.getHeight() ) / 2f );
            matrix.postRotate( 90f, ( canvas.getWidth()) / 2f, canvas.getHeight() / 2f );
            float scale = (float) canvas.getWidth() / (float) mCacheBitmap.getHeight();
            matrix.postScale(scale, scale, canvas.getWidth() / 2f , canvas.getHeight() / 2f );
            canvas.drawBitmap( mCacheBitmap, matrix, null );

            // Back to original OpenCV code
            if (mFpsMeter != null) {
                mFpsMeter.measure();
                mFpsMeter.draw(canvas, 20, 30);
            }

            getHolder().unlockCanvasAndPost(canvas);
        }
    }

}

预览现在是纵向模式,如下所示:

Portrait preview


嗨@Roses,你有没有处理过屏幕分辨率的问题? 你的代码运行良好,但图片分辨率被固定在一个常数分辨率上,在我的设备上分辨率是480*864,但我的设备支持更高分辨率,该如何解决这个问题? - Raj Kumar

6

很遗憾,Opencv4Android不支持竖屏相机。但是有一种方法可以克服它。 1)编写自定义相机并将其方向设置为竖屏。 2)注册其预览回调。 3)在onPreviewFrame(byte[]data, Camera camera)中创建预览字节的Mat

Mat mat = new Mat(previewSize.height, previewSize.width, CvType.CV_8UC1);
mat.put(0, 0, data);

Core.transpose(mat, mat);
Core.flip(mat, mat, -1); // rotates Mat to portrait
CvType 依赖于相机使用的预览格式。
PS. 完成操作后,不要忘记释放所有已创建的 Mat 实例。
PPS. 为了不过载 UI 线程进行检测,最好在单独的线程中管理您的相机。

3

我也有同样的问题,我已经解决了!这是我的解决方案:

首先,在CameraBridgeViewBase.Java文件中的两个构造函数中添加WindowManager的初始化:

public CameraBridgeViewBase(Context context, int cameraId) {  
   super(context);  
   mCameraIndex = cameraId;  
   getHolder().addCallback(this);  
   mMaxWidth = MAX_UNSPECIFIED;  
   mMaxHeight = MAX_UNSPECIFIED;  
   windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);  
}  


public CameraBridgeViewBase(Context context, AttributeSet attrs) {  
   super(context, attrs);  
   int count = attrs.getAttributeCount();  
   Log.d(TAG, "Attr count: " + Integer.valueOf(count));  

   TypedArray styledAttrs = getContext().obtainStyledAttributes(attrs, R.styleable.CameraBridgeViewBase);  
   if (styledAttrs.getBoolean(R.styleable.CameraBridgeViewBase_show_fps, false))  
       enableFpsMeter();  

   mCameraIndex = styledAttrs.getInt(R.styleable.CameraBridgeViewBase_camera_id, -1);  

   getHolder().addCallback(this);  
   mMaxWidth = MAX_UNSPECIFIED;  
   mMaxHeight = MAX_UNSPECIFIED;  
   windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);  
   styledAttrs.recycle();  
}  

那么,您需要将函数deliverAndDrawFrame(CvCameraViewFrame frame)替换为以下内容:

protected void deliverAndDrawFrame(CvCameraViewFrame frame) {  
  Mat modified;  

  if (mListener != null) {  
      modified = mListener.onCameraFrame(frame);  
  } else {  
      modified = frame.rgba();  
  }  

  boolean bmpValid = true;  
  if (modified != null) {  
      try {  
          Utils.matToBitmap(modified, mCacheBitmap);  
      } catch (Exception e) {  
          Log.e(TAG, "Mat type: " + modified);  
          Log.e(TAG, "Bitmap type: " + mCacheBitmap.getWidth() + "*" + mCacheBitmap.getHeight());  
          Log.e(TAG, "Utils.matToBitmap() throws an exception: " + e.getMessage());  
          bmpValid = false;  
      }  
  }  

  if (bmpValid && mCacheBitmap != null) {  
      Canvas canvas = getHolder().lockCanvas();  
      if (canvas != null) {  
          canvas.drawColor(0, android.graphics.PorterDuff.Mode.CLEAR);  
          int rotation = windowManager.getDefaultDisplay().getRotation();  
          int degrees = 0;  
          // config degrees as you need  
          switch (rotation) {  
              case Surface.ROTATION_0:  
                  degrees = 90;  
                  break;  
              case Surface.ROTATION_90:  
                  degrees = 0;  
                  break;  
              case Surface.ROTATION_180:  
                  degrees = 270;  
                  break;  
              case Surface.ROTATION_270:  
                  degrees = 180;  
                  break;  
          }  

          Matrix matrix = new Matrix();  
          matrix.postRotate(degrees);  
          Bitmap outputBitmap = Bitmap.createBitmap(mCacheBitmap, 0, 0, mCacheBitmap.getWidth(), mCacheBitmap.getHeight(), matrix, true);  

          if (outputBitmap.getWidth() <= canvas.getWidth()) {  
              mScale = getRatio(outputBitmap.getWidth(), outputBitmap.getHeight(), canvas.getWidth(), canvas.getHeight());  
          } else {  
              mScale = getRatio(canvas.getWidth(), canvas.getHeight(), outputBitmap.getWidth(), outputBitmap.getHeight());  
          }  

          if (mScale != 0) {  
              canvas.scale(mScale, mScale, 0, 0);  
          }  
          Log.d(TAG, "mStretch value: " + mScale);  

          canvas.drawBitmap(outputBitmap, 0, 0, null);  

          if (mFpsMeter != null) {  
              mFpsMeter.measure();  
              mFpsMeter.draw(canvas, 20, 30);  
          }  
          getHolder().unlockCanvasAndPost(canvas);
      }  
   }  
}  

并且添加这个额外的功能,
private float getRatio(int widthSource, int heightSource, int widthTarget, int heightTarget) {  
   if (widthTarget <= heightTarget) {  
       return (float) heightTarget / (float) heightSource;  
   } else {  
       return (float) widthTarget / (float) widthSource;  
   }  
}  

没问题,如果这个答案对您有用,请标记为“接受”的帮助声誉。


即使在构造函数中,我在deliverAndDrawFrame访问windowManager时仍然会出现错误。除非我这样说WindowManager windowManager = ...,否则我无法声明windowManager而不出错。 - mheavers

2
所有的答案都是一些技巧。我更喜欢这个解决方案:
在JavaCameraView代码中进行更改:
mBuffer = new byte[size];
mCamera.setDisplayOrientation(90); //add this
mCamera.addCallbackBuffer(mBuffer);

第二个变化:

//                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
//                        mSurfaceTexture = new SurfaceTexture(MAGIC_TEXTURE_ID);
//                        mCamera.setPreviewTexture(mSurfaceTexture);
//                    } else
//                       mCamera.setPreviewDisplay(null);
                    mCamera.setPreviewDisplay(getHolder());

1
这将移除绿色人脸检测框。 - Samad
这个解决方案仅适用于预览图像,但是任何在其上绘制的内容或任何类型的对象检测都无法正常工作。 - Brugui
这也会破坏显示任何类型的处理结果,例如自适应滤波等。 - mcy

1
您需要考虑以下几点:
  • onPreviewFrame()始终以其组合旋转方式提供原始相机数据
  • getSupportedPreviewSizes()提供相应的宽高比
  • 算法需要在纵向分析帧以正确检测对象。
  • 创建的位图(Java端)用于存储结果帧也需要正确的宽高比
因此,为了快速和高分辨率的解决方案,我更改了JavaCameraView.java和我的JNI部分。 在JavaCameraView.java中:
...

      if (sizes != null) {
         /* Select the size that fits surface considering maximum size allowed */
         Size frameSize;
         if(width > height)
         {
            frameSize = calculateCameraFrameSize(sizes, new JavaCameraSizeAccessor(), width, height);
         }else{
            frameSize = calculateCameraFrameSize(sizes, new JavaCameraSizeAccessor(), height, width);
         }
...

         mCamera.setParameters(params);
         params = mCamera.getParameters();

         int bufFrameWidth, bufFrameHeight;
         bufFrameWidth = params.getPreviewSize().width;
         bufFrameHeight = params.getPreviewSize().height;

         if(width > height) {
             mFrameWidth = params.getPreviewSize().width;
             mFrameHeight = params.getPreviewSize().height;
         }else{
             mFrameWidth = params.getPreviewSize().height;
             mFrameHeight = params.getPreviewSize().width;
         }
...

         mFrameChain = new Mat[2];
         mFrameChain[0] = new Mat(bufFrameHeight + (bufFrameHeight/2), bufFrameWidth, CvType.CV_8UC1);
         mFrameChain[1] = new Mat(bufFrameHeight + (bufFrameHeight/2), bufFrameWidth, CvType.CV_8UC1);

         AllocateCache();

         mCameraFrame = new JavaCameraFrame[2];
         mCameraFrame[0] = new JavaCameraFrame(mFrameChain[0], bufFrameWidth, bufFrameHeight);
         mCameraFrame[1] = new JavaCameraFrame(mFrameChain[1], bufFrameWidth, bufFrameHeight);

通过这些更改,我们确保在纵向模式下使用了可用的最高分辨率(在calculateCameraFrameSize中交换高度/宽度)。我们仍然从onPreviewFrame()处理横向输入,但创建了一个位图以在纵向模式下绘制(AllocateCache)。
最后,我们需要将纵向帧提供给算法,以便让它检测“站立”对象并返回保存和渲染位图所需的结果。因此,对您的Activity进行以下修改:
public Mat rot90(Mat matImage, int rotflag){
    //1=CW, 2=CCW, 3=180
    Mat rotated = new Mat();
    if (rotflag == 1){
        rotated = matImage.t();
        flip(rotated, rotated, 1); //transpose+flip(1)=CW
    } else if (rotflag == 2) {
        rotated = matImage.t();
        flip(rotated, rotated,0); //transpose+flip(0)=CCW
    } else if (rotflag ==3){
        flip(matImage, rotated,-1);    //flip(-1)=180
    } else if (rotflag != 0){ //if not 0,1,2,3:
       Log.e(TAG, "Unknown rotation flag("+rotflag+")");
    }
    return rotated;
}

public Mat onCameraFrame(CvCameraViewFrame inputFrame) {

    mRgba = rot90(inputFrame.rgba(), 1);
    mGray = rot90(inputFrame.gray(), 1);
...

1

1
似乎新的OpenCV CameraBridgeViewBase.java类太高级了,无法对相机预览布局进行足够的控制。看一下我的示例代码,它基于一些较旧的OpenCV示例并使用纯Android代码。为了使用在onPreviewFrame中传递的字节数组,请将其放入Mat中并从YUV转换为RGB:
mYuv = new Mat(previewHeight + previewHeight/2, previewWidth, CvType.CV_8UC1);
mYuv.put(0, 0, mBuffer);
Imgproc.cvtColor(mYuv, mRgba, Imgproc.COLOR_YUV420sp2RGBA, 4);

你可能能够在互联网上找到旧的OpenCV4Android样例,尽管它们在几个版本前被删除了。然而,上面链接的示例代码和片段应该足以让你入门。

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