2017-8-13
前言
實習了一個月,搞了一個月的人臉識別,終于研究出結果,就和大家分享一下,雖然感覺不是真正意義上的人臉識別,但還是有很高識別度的,代碼我就只貼出了比較重要的代碼和邏輯,源碼已經在Github上了。完整的項目分為客戶端和服務器端,圖片的對比和存儲以及一些注冊信息就存在服務器端,不讓客戶端處理,但是客戶端還是存在人臉對比的代碼的(Compare類)。比較基礎一點的搭建opencv for android 和 opencv for java 的環境就不說了,說一點干貨???(黑人問號.jpg) 講一講遇到的問題,分析一下流程和原理。
閑話不多說,我們開始吧。
https://github.com/Hyyzt/FaceRecognition
客戶端
1.邏輯
ControlActivity:
用來控制整個程序的流程,進行注冊和登錄
FaceLoginActivity:
進行人臉注冊,根據服務器返回的數據判斷是否可以進形注冊
InfoActivity:
根據服務器返回的數據進行注冊信息的數據,并將數據提交給服務器保存
FaceReconginzedAcitvity:
進行人臉識別,上傳數據至服務器,返回人臉識別是否成功的信息
SuccessActivity:
人臉識別成功后從服務器返回注冊時的信息并展示
2.重要代碼
- 初始化opencv類庫
若我們需要使用opencv類庫,則必須進行初始化,盡量在Application中的oncreate()中初始化,每次啟動的時候只加載一次類庫。
System.loadLibrary("opencv_java");
- 剔除opencv manager的關聯
使用人臉識別類庫的時候,官方規定你必須安裝opencv manager才可以使用這些類庫,但我們可以通過一些操作來剔除依賴。
首先將opencv目錄下的sdk/native/libs下的文件全部拷貝出來,并在自己的程序目錄下建立一個與main同級并命名為jniLibs的文件夾,將之前的拷貝文件全被拷貝進去,并在onCreate中加入以下代碼
System.loadLibrary("detection_based_tracker");
try {
InputStream is = getResources().openRawResource(R.raw.lbpcascade_frontalface);
File cascadeDir = getDir("cascade", Context.MODE_PRIVATE);
mCascadeFile = new File(cascadeDir, "lbpcascade_frontalface.xml");
FileOutputStream os = new FileOutputStream(mCascadeFile);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
is.close();
os.close();
mJavaDetector = new CascadeClassifier(mCascadeFile.getAbsolutePath());
if (mJavaDetector.empty()) {
Log.e(TAG, "Failed to ControlActivity cascade classifier");
mJavaDetector = null;
} else
Log.i(TAG, "Loaded cascade classifier from " + mCascadeFile.getAbsolutePath());
mNativeDetector = new DetectionBasedTracker(mCascadeFile.getAbsolutePath(), 0);
cascadeDir.delete();
} catch (IOException e) {
e.printStackTrace();
}
mOpenCvCameraView.enableView();
完成之后你就可以在沒有opencv manager的情況下進行使用了
- 開始人臉識別
首先,你需要在布局中加入一個opencv自己定義的控件,這個控件就是我們進行人臉檢測和識別的控件,這個控件是一個視頻流控件,它初始化是后置攝像頭,你需要將它前置,但是前置過后每一幀會出現鏡像的結果,我們在回調中處理這個問題。
你需要在activity中引入一個接口CvCameraViewListener2,并實現它的方法
<org.opencv.android.JavaCameraView
android:id="@+id/fd_activity_surface_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
//前置攝像頭
mOpenCvCameraView.setCameraIndex(CameraBridgeViewBase.CAMERA_ID_FRONT);
實現接口
//視頻流開始
//mGray和mRgba分別是每一幀圖像的灰度化圖像和彩色圖像
public void onCameraViewStarted(int width, int height) {
Log.e("TAG", "onCameraViewStarted");
mGray = new Mat();
mRgba = new Mat();
}
//視頻流結束
public void onCameraViewStopped() {
Log.e("TAG", "onCameraViewStopped");
mGray.release();
mRgba.release();
}
//使用視頻流時每一幀的回調
public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
mRgba = inputFrame.rgba();
mGray = inputFrame.gray();
//處理前置后鏡像的攝像頭
//倒轉鏡像的攝像頭
Core.flip(mRgba, mRgba, 1);
Core.flip(mGray, mGray, 1);
//將視頻流控制住,只在一定區域內可以檢測人臉
Point point = new Point(mGray.width() / 2 - 375, mGray.height() / 2 - 375);
Rect rect = new Rect(point, new Size(750, 750));
mGray = new Mat(mGray, rect);
if (mAbsoluteFaceSize == 0) {
int height = mGray.rows();
if (Math.round(height * mRelativeFaceSize) > 0) {
mAbsoluteFaceSize = Math.round(height * mRelativeFaceSize);
}
mNativeDetector.setMinFaceSize(mAbsoluteFaceSize);
}
MatOfRect faces = new MatOfRect();
if (mDetectorType == JAVA_DETECTOR) {
if (mJavaDetector != null)
mJavaDetector.detectMultiScale(mGray, faces, 1.1, 2, 2, // TODO: objdetect.CV_HAAR_SCALE_IMAGE
new Size(mAbsoluteFaceSize, mAbsoluteFaceSize), new Size());
} else if (mDetectorType == NATIVE_DETECTOR) {
if (mNativeDetector != null)
mNativeDetector.detect(mGray, faces);
} else {
Log.e(TAG, "Detection method is not selected!");
}
//這個facesArray數組是每一幀我們提取到的人臉個數,我們需要將它篩選,剔除掉錯誤的識別情況
Rect[] facesArray = faces.toArray();
if (facesArray.length > 0) {
for (int i = 0; i < facesArray.length; i++){
Point point1 = new Point(facesArray[i].x + point.x, facesArray[i].y + point.y);
facesArray[i] = new Rect(point1, facesArray[i].size());
//遍歷數組時,此處剔除,并將正確的臉部在屏幕上顯示出來,且返回這個臉部頭像bitmap
if(facesArray[i].width > 350) {
Core.rectangle(mRgba, facesArray[i].tl(), facesArray[i].br(), FACE_RECT_COLOR, 3);
//根據矩陣和臉部大小裁剪成圖片
bitmap = FaceUtils.cutDownFaceROI(mRgba, facesArray[i]);
}
}
}
return mRgba;
}
在請求服務器時,我們盡量不要在onCameraFrame中請求,在開始識別后,延時消息發送bitmap至服務器,也不要在onCameraFrame中進行復雜的邏輯判斷,否則會出現視頻流卡死的情況。
在銷毀和恢復activity時,要對JavaCameraView進行銷毀和恢復
服務器端
1.開發環境
編譯工具:Eclipse
服務器:Tomcat
數據庫:MySQL
2.相關類庫
OpenCV相關Jar包:opencv-2411.jar, opencv-windows-x86_64.jar等
JavaCV相關Jar包:javacv.jar, javacpp.jar等
其他Jar包:gson-2.3.1.jar
3.實現邏輯
客戶端請求時,根據不同請求執行不同邏輯并返回結果:
1.客戶端注冊時對發送的人臉數據進行臨時存儲,并對比數據庫匹配,若匹配結果達到設定閾值(75%),則返回已注冊過;反之,則返回未注冊過。
2.客戶端成功注冊后對其發送的用戶基本信息和人臉數據進行處理和入庫存儲,其中人臉數據以圖片格式(.png)存儲本地。
3.客戶端人臉驗證登陸時發送的人臉數據進行臨時存儲,并對比數據庫匹配,若匹配結果達到設定閾值(80%)則取出庫中對應的用戶數據和人臉圖片url返回;反之,則返回登陸失敗。
4.數據庫設計
數據庫face-detect-database,表user-info,
主要字段id: int類型, 主鍵;
name: text類型, 用戶名;
age: int類型, 年齡,
sex: int類型, 性別,
birthday: text類型, 出生日期;
face_path: text類型, 人臉圖片本地存儲路徑。
5.接口設計
1.GetData: 注冊時判斷用戶是否注冊過
public class GetData extends HttpServlet{
...
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 存儲臨時圖片
ServletInputStream is = request.getInputStream();
String tempPath = ImageUtils.saveImageToLocal(is, "temp");
// 遍歷數據庫,比較相似度
MySQLDatabaseHelper helper = new MySQLDatabaseHelper();
List<User> userList = helper.query();
helper.close();
PrintWriter writer = response.getWriter();
// FaceRecognizer匹配
User user = MyFaceRecognizer.matchByTrainAndPredict(userList, tempPath);
if (user != null) {
writer.write("Login");
} else {
writer.write("NoLogin");
}
// 灰度匹配
// double similarity = 0;
// for (int i = 0; i < userList.size(); i++) {
// similarity = FaceMatchUtils.
// histogramMatch(userList.get(i).getFace_pic(), tempPath);
// if (similarity > 0.75) {
// writer.write("Login");
// return;
// }
// }
// writer.write("NoLogin");
}
...
}
2.GetJson: 處理和存儲用戶基本信息和人臉圖片
public class GetJson extends HttpServlet {
...
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 獲取圖片流和Json
ServletInputStream is = request.getInputStream(); // 圖片流
// String jsonData = request.getParameter("Info");
// 轉碼
String str = request.getParameter("Info");
String jsonData = new String(str.getBytes("ISO-8859-1"), "utf-8");
System.out.println(jsonData);
Gson gson = new Gson();
UserInfo userInfo = gson.fromJson(jsonData, UserInfo.class);
// 獲取數據后返回Success
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.write("Success");
// 存儲圖片和更新數據庫
MySQLDatabaseHelper helper = new MySQLDatabaseHelper();
int userNum = helper.query().size();
String facePath = ImageUtils.saveImageToLocal(is, "face" + (userNum));
User user = new User();
user.setName(userInfo.name);
user.setAge(Integer.parseInt(userInfo.age));
user.setSex(userInfo.sex);
user.setBirthday(userInfo.birthday);
user.setFace_pic(facePath);
System.out.println(user.toString());
helper.insert(user);
helper.close();
}
...
}
3.VerifyLogin: 人臉驗證登陸
public class VerifyLogin extends HttpServlet {
...
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 接收圖片并存儲
ServletInputStream is = request.getInputStream(); // 圖片流
String tempPath = ImageUtils.saveImageToLocal(is, "verify");
// 遍歷數據庫,驗證登陸
MySQLDatabaseHelper helper = new MySQLDatabaseHelper();
List<User> userList = helper.query();
helper.close();
PrintWriter writer = response.getWriter();
// FaceRecognizer匹配
User user = MyFaceRecognizer.matchByTrainAndPredict(userList, tempPath);
if(user != null) {
Gson gson = new Gson();
String json = gson.toJson(new UserInfo(user.getName(),
user.getSex(), user.getAge(), user.getBirthday(),
url + user.getFace_pic().
substring(user.getFace_pic().lastIndexOf("/") + 1)));
System.out.println(json);
response.setContentType("text/html");
writer.write(json);
} else {
writer.write("Fail");
}
}
...
}
6.關鍵代碼
1.基于圖像灰度直方圖比較的人臉匹配算法
public class FaceMatchUtils {
// 利用灰度直方圖計算圖像相似度,輸要求入人臉圖像的均為正方形
public static double histogramMatch(String face, String testFace) {
Mat faceMat = Highgui.imread(face);
Mat testFaceMat = Highgui.imread(testFace);
// 圖像灰度化
System.out.println("histogramMatch: 圖像灰度化");
Imgproc.cvtColor(faceMat, faceMat, Imgproc.COLOR_RGB2GRAY);
Imgproc.cvtColor(testFaceMat, testFaceMat, Imgproc.COLOR_RGB2GRAY);
// 直方圖均衡化,暫時注釋
// System.out.println("histogramMatch: 直方圖均衡化");
// Imgproc.equalizeHist(faceMat, faceMat);
// Imgproc.equalizeHist(testFaceMat, testFaceMat);
// 把Mat矩陣的type轉換為Cv_32F,因為在c++代碼中會判斷他的類型
faceMat.convertTo(faceMat, CvType.CV_32F);
testFaceMat.convertTo(testFaceMat, CvType.CV_32F);
// 直方圖匹配
System.out.println("histogramMatch: 直方圖匹配");
double similarity = Imgproc.compareHist(faceMat, testFaceMat, Imgproc.CV_COMP_CORREL);
System.out.println("灰度直方圖相似性結果: " + face + " : "+ similarity);
return similarity;
}
}
2.基于FaceRecognizer人臉訓練和預測的人臉匹配算法
public static User matchByTrainAndPredict(List<User> userList, String path) {
List<String> pathList = new ArrayList<String>();
for (User user : userList) {
pathList.add(user.getFace_pic());
}
MatVector images = new MatVector(pathList.size());
Mat labels = new Mat(pathList.size(), 1, CV_32SC1);
IntBuffer labelsBuf = labels.createBuffer();
for (int i = 0; i < pathList.size(); i++) {
String p = pathList.get(i);
Mat img = imread(p, CV_LOAD_IMAGE_GRAYSCALE);
images.put(i, img);
labelsBuf.put(i, i);
}
// FaceRecognizer faceRecognizer = createFisherFaceRecognizer();
// FaceRecognizer faceRecognizer = createEigenFaceRecognizer();
FaceRecognizer faceRecognizer = createLBPHFaceRecognizer();
faceRecognizer.train(images, labels);
Mat testImage = imread(path, CV_LOAD_IMAGE_GRAYSCALE);
IntPointer label = new IntPointer(1);
DoublePointer confidence = new DoublePointer(1);
faceRecognizer.predict(testImage, label, confidence);
int predictedLabel = label.get(0);
System.out.println("Predicted label: " + predictedLabel);
System.out.println("Confidence: " + confidence.get(0));
if (confidence.get(0) > 10000) {
System.out.println(userList.get(predictedLabel).toString());
return userList.get(predictedLabel);
} else {
System.out.println("沒有匹配");
return null;
}
}
問題
- 在進行圖像相似度對比時,要注意對比的圖像大小要一致,否則會出現傳入非法參數的異常
- 由于對比方法使用的是灰度直方圖在歸一化之后對比,使得圖像對環境的光照強烈十分的敏感,而且對于拍攝條件有一點的限制,注冊時的背景和識別時的背景不能相差過大,拍攝距離要控制好,不能太遠也不能太近,要保證拍攝的質量,而且拍攝后的照片在經過壓縮處理后,會損失一部分的精度,使相似度下降了10個百分點。在上述條件都確保的情況下,對比的結果還是非常精確的,高達80%左右
- 由于opencv類庫本身是一個圖像處理的庫,而不能提取人臉特征,只能通過對比識別人臉后的人臉圖像來查看差異,而灰度直方圖是在各種方法中最準確的方法
- Opencv高版本提供了一個FaceRecognizer類,對人臉進行特征對比和匹配,但是它沒有對應的JAVA API,而JavaCV對這個類進行了封裝,提供了對應接口,卻沒有給出人臉特征點數據,而是通過對比直接返回了匹配結果,且由于源碼沒開放,無法控制相似度閾值
結束語
下一階段開始研究Arcgis for android 的三維地圖,等研究出東西再和大家分享吧.
到這里就差不多結束了,希望能幫到你們,多多支持哦!!!