一、综述
如何采集图片?在windows环境下,我们可以使用dshow,在linux下,也有ffmpeg等基础类库,再不济,opencv自带的videocapture也是提供了基础的支撑。那么在andoird下,使用的肯定是Android自带的相关函数了。由于Android是基于java语言的,如果我们想要调用Android 的相关函数,那么必须通过JNI的方法。
这里有可以分为两种,一种是直接在java中实现比较完整的函数,在qt中,只需要调用这个函数就可以;另一种就是使用qt自带的jni机制,比如下面这样,打开摄像头,并且采集图片。我们首先介绍第二种方法,让大家最快进入情况。
二、通过JNI打开摄像头
a、填加头文件和命名空间,定义公共变量和宏:
#include <QtAndroid>
#include <QDebug>
#include <QAndroidJniEnvironment>
#include <QAndroidActivityResultReceiver>
#include <QDateTime>
#include <QFile>
using namespace cv;
using namespace QtAndroid ;
QString strFetchImage = "" ;
QString selectedFileName = "" ;
#define CHECK_EXCEPTION () \
if (env->ExceptionCheck())\
{\
qDebug () << "exception occured" ;\
env->ExceptionClear();\
}
其中需要注意的是, CHECK_EXCEPTION 是用来检查Android系统是否有异常的。这一点在使用JNI的时候非常重要和必要。
b、填加回调类,主要就是在一系列异常判断后,获得imagepath。该类集成自 ResultReceiver :
class ResultReceiver : public QAndroidActivityResultReceiver
{
public : ResultReceiver ( QString imagePath , QLabel * view ) : m_imagePath ( imagePath ), m_imageView ( view ) { }
void handleActivityResult ( int receiverRequestCode , int resultCode , const QAndroidJniObject & data ) {
qDebug () << "handleActivityResult, requestCode - " << receiverRequestCode << " resultCode - " << resultCode << " data - " << data . toString ();
if ( resultCode == - 1 && receiverRequestCode == 1 ) {
qDebug () << "captured image to - " << m_imagePath ;
qDebug () << "captured image exist - " << QFile :: exists ( m_imagePath );
m_imageView -> setPixmap ( QPixmap ( m_imagePath )); }
}
QString m_imagePath ;
QLabel * m_imageView ;
};
C、填加控件触发事件。一般来说我们选择pressed事件
d、编写拍照代码
//打开摄像头,采集图片
void MainWindow :: on_btn_capture_pressed ()
{
ui -> lbMain -> setScaledContents ( true ); //显示的图像自动缩放
b_canSave = false ; //图片没有采集完成,目前不可以保存
//引用JNI
QAndroidJniEnvironment env ;
//创建用于打开摄像头的content
QAndroidJniObject action = QAndroidJniObject :: fromString ( "android.media.action.IMAGE_CAPTURE" ); QAndroidJniObject ( intent ( "android/content/Intent" , "(Ljava/lang/String;)V" , action . object < jstring >());
//设定img路径
QString date = QDateTime :: currentDateTime (). toString ( "yyyyMMdd_hhmmss" );
QAndroidJniObject fileName = QAndroidJniObject :: fromString ( date + ".jpg" );
QAndroidJniObject savedDir = QAndroidJniObject :: callStaticObjectMethod ( "android/os/Environment" , "getExternalStorageDirectory" , "()Ljava/io/File;" );
//使用CHECK_EXCEPTION处理异常
CHECK_EXCEPTION ()
qDebug () << "savedDir - " << savedDir . toString ();
QAndroidJniObject savedImageFile ( "java/io/File" , "(Ljava/io/File;Ljava/lang/String;)V" , savedDir . object < jobject >(), fileName . object < jstring >());
CHECK_EXCEPTION ()
qDebug () << "savedImageFile - " << savedImageFile . toString ();
QAndroidJniObject savedImageUri = QAndroidJniObject :: callStaticObjectMethod ( "android/net/Uri" , "fromFile" , "(Ljava/io/File;)Landroid/net/Uri;" ,
savedImageFile . object < jobject >());
CHECK_EXCEPTION ()
//将输出路径传递过来
QAndroidJniObject mediaStoreExtraOutput = QAndroidJniObject :: getStaticObjectField ( "android/provider/MediaStore" , "EXTRA_OUTPUT" , "Ljava/lang/String;" );
CHECK_EXCEPTION ()
qDebug () << "MediaStore.EXTRA_OUTPUT - " << mediaStoreExtraOutput . toString ();
intent . callObjectMethod (
"putExtra" , "(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;" , mediaStoreExtraOutput . object < jstring >(),
savedImageUri . object < jobject >());
//获得采集图片的绝对路径,并且显示出来
ResultReceiver * resultReceiver = new ResultReceiver ( savedImageFile . toString (), ui -> lbMain );
startActivity ( intent , 1 , resultReceiver );
//获得返回的绝对地址(注意这句话一定要写在 CHECK_EXCEPTION 中)
strFetchImage = savedImageFile . toString ();
}
最终采集到的图片地址保存在 strFetchImage 中
e、编写处理代码。由于我这里主要进行的是图像处理操作,所以必须结合OpenCV相关函数进行
//图像处理操作
void MainWindow :: on_btn_process_pressed ()
{
b_canSave = false ;
if (strFetchImage != "" )
{
ui -> lbMain -> setScaledContents ( false );
Mat src = imread (strFetchImage. toStdString ());
Mat src2 ;
Mat rotated ;
主要算法/
cv :: resize ( src , src2 , cv :: Size ( 720 , 1000 )); //标准大小
Mat src_gray ;
Mat src_all = src2 . clone ();
Mat threshold_output ;
vector<vector< Point > > contours , contours2 ;
vector< Vec4i > hierarchy ;
//预处理
cvtColor ( src2 , src_gray , CV_BGR2GRAY );
blur ( src_gray , src_gray , Size ( 3 , 3 ) ); //模糊,去除毛刺
threshold ( src_gray , threshold_output , 100 , 255 , THRESH_OTSU );
//添加提示
ui -> lb_info -> setText ( "开始寻找轮廓!" );
//寻找轮廓
//第一个参数是输入图像 2值化的
//第二个参数是内存存储器,FindContours找到的轮廓放到内存里面。
//第三个参数是层级,**[Next, Previous, First_Child, Parent]** 的vector
//第四个参数是类型,采用树结构
//第五个参数是节点拟合模式,这里是全部寻找
findContours ( threshold_output , contours , hierarchy , CV_RETR_TREE , CHAIN_APPROX_NONE , Point ( 0 , 0 ) );
//添加提示
if ( contours .size()<= 10 )
{
ui -> lb_info -> setText ( "轮廓筛选错误,循环退出!请重新采集数据。" );
return ;
}
else
{
ui -> lb_info -> setText ( "开始寻找轮廓! 开始筛选轮廓!" );
}
//轮廓筛选
int c = 0 , ic = 0 , area = 0 ;
int parentIdx =- 1 ;
for ( int i = 0 ; i < contours .size(); i ++ )
{
//hierarchy[i][2] != -1 表示不是最外面的轮廓
if ( hierarchy [ i ][ 2 ] != - 1 && ic == 0 )
{
parentIdx = i ;
ic ++;
}
else if ( hierarchy [ i ][ 2 ] != - 1 )
{
ic ++;
}
//最外面的清0
else if ( hierarchy [ i ][ 2 ] == - 1 )
{
ic = 0 ;
parentIdx = - 1 ;
}
//找到定位点信息
if ( ic >= 2 )
{
contours2 .push_back( contours [ parentIdx ]);
ic = 0 ;
parentIdx = - 1 ;
}
}
//添加提示
if ( contours2 .size()< 3 )
{
ui -> lb_info -> setText ( "定位点选择错误,循环退出!请重新采集数据。" );
return ;
}
else
{
ui -> lb_info -> setText ( "开始寻找轮廓! 开始筛选轮廓!定位点选择正确!" );
}
//填充定位点,我们约定,必须要能够同时识别出4个点来
for ( int i = 0 ; i < contours2 .size(); i ++)
drawContours ( src_all , contours2 , i , CV_RGB ( 0 , 255 , 0 ) , - 1 );
//识别出来了关键区域,但是数量不对,显示当前识别结果,退出循环
if ( contours2 .size() != 4 )
{
QPixmap qpixmap = Mat2QImage ( src_all );
ui -> lbMain -> setPixmap ( qpixmap );
ui -> lb_info -> setText ( "定位点数量不为4!请重新采集数据。" );
return ;
}
else
{
//否则,进一步分割
Point point [ 4 ];
for ( int i = 0 ; i < contours2 .size(); i ++)
{
//筛选轮廓,
double d = contourArea ( contours2 [ i ]);
if ( d > 720 * 1000 / 4 )
{
ui -> lb_info -> setText ( "采集中有错误轮廓,请重新采集数据" );
QPixmap qpixmap = Mat2QImage ( src_all );
ui -> lbMain -> setPixmap ( qpixmap );
return ;
}
//定位重点,并重新排序
Point ptmp = Center_cal ( contours2 , i );
if ( ptmp . x < 720 / 4 && ptmp . y < 1000 / 4 )
{
point [ 0 ] = ptmp ;
}
else if ( ptmp . x < 720 / 4 && ptmp . y > 1000 / 4 )
{
point [ 2 ] = ptmp ;
}
else if ( ptmp . x > 720 / 4 && ptmp . y < 1000 / 4 )
{
point [ 1 ] = ptmp ;
}
else
{
point [ 3 ] = ptmp ;
}
}
//打印出来
for ( int i = 0 ; i < 3 ; i ++)
{
char cbuf [ 100 ];
sprintf ( cbuf , "%d" , i + 1 );
putText ( src_all , cbuf , point [ i ], FONT_HERSHEY_PLAIN , 5 , Scalar ( 0 , 0 , 0 ), 5 );
ui -> lb_info -> setText ( "结果识别正确,可以保存" );
}
//透视变换
cv :: Point2f src_vertices [ 4 ];
src_vertices [ 0 ] = point [ 0 ];
src_vertices [ 1 ] = point [ 1 ];
src_vertices [ 2 ] = point [ 2 ];
src_vertices [ 3 ] = point [ 3 ];
Point2f dst_vertices [ 4 ];
dst_vertices [ 0 ] = Point ( 0 , 0 );
dst_vertices [ 1 ] = Point ( 720 , 0 );
dst_vertices [ 2 ] = Point ( 0 , 1000 );
dst_vertices [ 3 ] = Point ( 720 , 1000 );
Mat warpMatrix = getPerspectiveTransform ( src_vertices , dst_vertices );
//执行透视变化
warpPerspective ( src2 , rotated , warpMatrix , rotated . size (), INTER_LINEAR , BORDER_CONSTANT );
}
//END 主要算法 END ///
// 将图片显示到label上
QPixmap qpixmap = Mat2QImage ( rotated );
ui -> lbMain -> setPixmap ( qpixmap );
matResult = rotated . clone ();
b_canSave = true ;
}
}
三、初步结果和继续研究需要解决的问题
按照设计,目前得到这样的结果
下一步注重解决以下问题
1、提高程序稳定性;
2、提高界面流程性和运行速度;
3、重构代码,进一步进行封装;
4、添加数据保存的相关功能。
感谢阅读至此,希望有所帮助!
一、综述
如何采集图片?在windows环境下,我们可以使用dshow,在linux下,也有ffmpeg等基础类库,再不济,opencv自带的videocapture也是提供了基础的支撑。那么在andoird下,使用的肯定是Android自带的相关函数了。由于Android是基于java语言的,如果我们想要调用Android 的相关函数,那么必须通过JNI的方法。
这里有可以分为两种,一种是直接在java中实现比较完整的函数,在qt中,只需要调用这个函数就可以;另一种就是使用qt自带的jni机制,比如下面这样,打开摄像头,并且采集图片。我们首先介绍第二种方法,让大家最快进入情况。
二、通过JNI打开摄像头
a、填加头文件和命名空间,定义公共变量和宏:
#include <QtAndroid>
#include <QDebug>
#include <QAndroidJniEnvironment>
#include <QAndroidActivityResultReceiver>
#include <QDateTime>
#include <QFile>
using namespace cv;
using namespace QtAndroid ;
QString strFetchImage = "" ;
QString selectedFileName = "" ;
#define CHECK_EXCEPTION () \
if (env->ExceptionCheck())\
{\
qDebug () << "exception occured" ;\
env->ExceptionClear();\
}
其中需要注意的是, CHECK_EXCEPTION 是用来检查Android系统是否有异常的。这一点在使用JNI的时候非常重要和必要。
b、填加回调类,主要就是在一系列异常判断后,获得imagepath。该类集成自 ResultReceiver :
class ResultReceiver : public QAndroidActivityResultReceiver
{
public : ResultReceiver ( QString imagePath , QLabel * view ) : m_imagePath ( imagePath ), m_imageView ( view ) { }
void handleActivityResult ( int receiverRequestCode , int resultCode , const QAndroidJniObject & data ) {
qDebug () << "handleActivityResult, requestCode - " << receiverRequestCode << " resultCode - " << resultCode << " data - " << data . toString ();
if ( resultCode == - 1 && receiverRequestCode == 1 ) {
qDebug () << "captured image to - " << m_imagePath ;
qDebug () << "captured image exist - " << QFile :: exists ( m_imagePath );
m_imageView -> setPixmap ( QPixmap ( m_imagePath )); }
}
QString m_imagePath ;
QLabel * m_imageView ;
};
C、填加控件触发事件。一般来说我们选择pressed事件
d、编写拍照代码
//打开摄像头,采集图片
void MainWindow :: on_btn_capture_pressed ()
{
ui -> lbMain -> setScaledContents ( true ); //显示的图像自动缩放
b_canSave = false ; //图片没有采集完成,目前不可以保存
//引用JNI
QAndroidJniEnvironment env ;
//创建用于打开摄像头的content
QAndroidJniObject action = QAndroidJniObject :: fromString ( "android.media.action.IMAGE_CAPTURE" ); QAndroidJniObject ( intent ( "android/content/Intent" , "(Ljava/lang/String;)V" , action . object < jstring >());
//设定img路径
QString date = QDateTime :: currentDateTime (). toString ( "yyyyMMdd_hhmmss" );
QAndroidJniObject fileName = QAndroidJniObject :: fromString ( date + ".jpg" );
QAndroidJniObject savedDir = QAndroidJniObject :: callStaticObjectMethod ( "android/os/Environment" , "getExternalStorageDirectory" , "()Ljava/io/File;" );
//使用CHECK_EXCEPTION处理异常
CHECK_EXCEPTION ()
qDebug () << "savedDir - " << savedDir . toString ();
QAndroidJniObject savedImageFile ( "java/io/File" , "(Ljava/io/File;Ljava/lang/String;)V" , savedDir . object < jobject >(), fileName . object < jstring >());
CHECK_EXCEPTION ()
qDebug () << "savedImageFile - " << savedImageFile . toString ();
QAndroidJniObject savedImageUri = QAndroidJniObject :: callStaticObjectMethod ( "android/net/Uri" , "fromFile" , "(Ljava/io/File;)Landroid/net/Uri;" ,
savedImageFile . object < jobject >());
CHECK_EXCEPTION ()
//将输出路径传递过来
QAndroidJniObject mediaStoreExtraOutput = QAndroidJniObject :: getStaticObjectField ( "android/provider/MediaStore" , "EXTRA_OUTPUT" , "Ljava/lang/String;" );
CHECK_EXCEPTION ()
qDebug () << "MediaStore.EXTRA_OUTPUT - " << mediaStoreExtraOutput . toString ();
intent . callObjectMethod (
"putExtra" , "(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;" , mediaStoreExtraOutput . object < jstring >(),
savedImageUri . object < jobject >());
//获得采集图片的绝对路径,并且显示出来
ResultReceiver * resultReceiver = new ResultReceiver ( savedImageFile . toString (), ui -> lbMain );
startActivity ( intent , 1 , resultReceiver );
//获得返回的绝对地址(注意这句话一定要写在 CHECK_EXCEPTION 中)
strFetchImage = savedImageFile . toString ();
}
最终采集到的图片地址保存在 strFetchImage 中
e、编写处理代码。由于我这里主要进行的是图像处理操作,所以必须结合OpenCV相关函数进行
//图像处理操作
void MainWindow :: on_btn_process_pressed ()
{
b_canSave = false ;
if (strFetchImage != "" )
{
ui -> lbMain -> setScaledContents ( false );
Mat src = imread (strFetchImage. toStdString ());
Mat src2 ;
Mat rotated ;
主要算法/
cv :: resize ( src , src2 , cv :: Size ( 720 , 1000 )); //标准大小
Mat src_gray ;
Mat src_all = src2 . clone ();
Mat threshold_output ;
vector<vector< Point > > contours , contours2 ;
vector< Vec4i > hierarchy ;
//预处理
cvtColor ( src2 , src_gray , CV_BGR2GRAY );
blur ( src_gray , src_gray , Size ( 3 , 3 ) ); //模糊,去除毛刺
threshold ( src_gray , threshold_output , 100 , 255 , THRESH_OTSU );
//添加提示
ui -> lb_info -> setText ( "开始寻找轮廓!" );
//寻找轮廓
//第一个参数是输入图像 2值化的
//第二个参数是内存存储器,FindContours找到的轮廓放到内存里面。
//第三个参数是层级,**[Next, Previous, First_Child, Parent]** 的vector
//第四个参数是类型,采用树结构
//第五个参数是节点拟合模式,这里是全部寻找
findContours ( threshold_output , contours , hierarchy , CV_RETR_TREE , CHAIN_APPROX_NONE , Point ( 0 , 0 ) );
//添加提示
if ( contours .size()<= 10 )
{
ui -> lb_info -> setText ( "轮廓筛选错误,循环退出!请重新采集数据。" );
return ;
}
else
{
ui -> lb_info -> setText ( "开始寻找轮廓! 开始筛选轮廓!" );
}
//轮廓筛选
int c = 0 , ic = 0 , area = 0 ;
int parentIdx =- 1 ;
for ( int i = 0 ; i < contours .size(); i ++ )
{
//hierarchy[i][2] != -1 表示不是最外面的轮廓
if ( hierarchy [ i ][ 2 ] != - 1 && ic == 0 )
{
parentIdx = i ;
ic ++;
}
else if ( hierarchy [ i ][ 2 ] != - 1 )
{
ic ++;
}
//最外面的清0
else if ( hierarchy [ i ][ 2 ] == - 1 )
{
ic = 0 ;
parentIdx = - 1 ;
}
//找到定位点信息
if ( ic >= 2 )
{
contours2 .push_back( contours [ parentIdx ]);
ic = 0 ;
parentIdx = - 1 ;
}
}
//添加提示
if ( contours2 .size()< 3 )
{
ui -> lb_info -> setText ( "定位点选择错误,循环退出!请重新采集数据。" );
return ;
}
else
{
ui -> lb_info -> setText ( "开始寻找轮廓! 开始筛选轮廓!定位点选择正确!" );
}
//填充定位点,我们约定,必须要能够同时识别出4个点来
for ( int i = 0 ; i < contours2 .size(); i ++)
drawContours ( src_all , contours2 , i , CV_RGB ( 0 , 255 , 0 ) , - 1 );
//识别出来了关键区域,但是数量不对,显示当前识别结果,退出循环
if ( contours2 .size() != 4 )
{
QPixmap qpixmap = Mat2QImage ( src_all );
ui -> lbMain -> setPixmap ( qpixmap );
ui -> lb_info -> setText ( "定位点数量不为4!请重新采集数据。" );
return ;
}
else
{
//否则,进一步分割
Point point [ 4 ];
for ( int i = 0 ; i < contours2 .size(); i ++)
{
//筛选轮廓,
double d = contourArea ( contours2 [ i ]);
if ( d > 720 * 1000 / 4 )
{
ui -> lb_info -> setText ( "采集中有错误轮廓,请重新采集数据" );
QPixmap qpixmap = Mat2QImage ( src_all );
ui -> lbMain -> setPixmap ( qpixmap );
return ;
}
//定位重点,并重新排序
Point ptmp = Center_cal ( contours2 , i );
if ( ptmp . x < 720 / 4 && ptmp . y < 1000 / 4 )
{
point [ 0 ] = ptmp ;
}
else if ( ptmp . x < 720 / 4 && ptmp . y > 1000 / 4 )
{
point [ 2 ] = ptmp ;
}
else if ( ptmp . x > 720 / 4 && ptmp . y < 1000 / 4 )
{
point [ 1 ] = ptmp ;
}
else
{
point [ 3 ] = ptmp ;
}
}
//打印出来
for ( int i = 0 ; i < 3 ; i ++)
{
char cbuf [ 100 ];
sprintf ( cbuf , "%d" , i + 1 );
putText ( src_all , cbuf , point [ i ], FONT_HERSHEY_PLAIN , 5 , Scalar ( 0 , 0 , 0 ), 5 );
ui -> lb_info -> setText ( "结果识别正确,可以保存" );
}
//透视变换
cv :: Point2f src_vertices [ 4 ];
src_vertices [ 0 ] = point [ 0 ];
src_vertices [ 1 ] = point [ 1 ];
src_vertices [ 2 ] = point [ 2 ];
src_vertices [ 3 ] = point [ 3 ];
Point2f dst_vertices [ 4 ];
dst_vertices [ 0 ] = Point ( 0 , 0 );
dst_vertices [ 1 ] = Point ( 720 , 0 );
dst_vertices [ 2 ] = Point ( 0 , 1000 );
dst_vertices [ 3 ] = Point ( 720 , 1000 );
Mat warpMatrix = getPerspectiveTransform ( src_vertices , dst_vertices );
//执行透视变化
warpPerspective ( src2 , rotated , warpMatrix , rotated . size (), INTER_LINEAR , BORDER_CONSTANT );
}
//END 主要算法 END ///
// 将图片显示到label上
QPixmap qpixmap = Mat2QImage ( rotated );
ui -> lbMain -> setPixmap ( qpixmap );
matResult = rotated . clone ();
b_canSave = true ;
}
}
三、初步结果和继续研究需要解决的问题
按照设计,目前得到这样的结果
下一步注重解决以下问题
1、提高程序稳定性;
2、提高界面流程性和运行速度;
3、重构代码,进一步进行封装;
4、添加数据保存的相关功能。
感谢阅读至此,希望有所帮助!