This commit is contained in:
jjLv 2025-09-30 14:56:27 +08:00
parent f4f13c2ef8
commit 6e091fdf78
7 changed files with 374 additions and 16 deletions

View File

@ -17,8 +17,11 @@
package com.bonus.canteen.activity;
import android.util.Log;
import android.view.KeyEvent;
import com.arcsoft.face.ErrorInfo;
import com.arcsoft.face.FaceEngine;
import com.bonus.canteen.db.AppDatabase;
import com.bonus.canteen.db.entity.base.ParamSettingInfo;
import com.bonus.canteen.upgrade.UpdateDown;
@ -83,9 +86,29 @@ public class SplashActivity extends BaseSplashActivity implements CancelAdapt {
}
}
private void navigateToNextPage() {
// activateFaceEngine();
ActivityUtils.startActivity(MainActivity.class);
finish();
}
private void activateFaceEngine() {
Log.e("TAG", "开始激活引擎");
int code = -1;
String APP_ID = WorkConfig.getAppId();
String SDK_KEY = WorkConfig.getAppKey();
try {
code = FaceEngine.activeOnline(this, APP_ID, SDK_KEY);
}catch(Exception e){
Log.e("TAG", "激活引擎异常: " + e.getMessage());
}
Log.i("TAG", "开始激活引擎--activeOnline: " + code);
if (code == ErrorInfo.MOK) {
Log.i("TAG", "FaceEngine activated successfully");
} else if (code == ErrorInfo.MERR_ASF_ALREADY_ACTIVATED) {
Log.i("TAG", "FaceEngine already activated");
} else {
Log.i("TAG", "FaceEngine activation failed, code: " + code);
}
}
/**
* 菜单返回键响应
*/

View File

@ -18,9 +18,17 @@
package com.bonus.canteen.service.data;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
import com.alibaba.fastjson2.JSONObject;
import com.arcsoft.face.ErrorInfo;
import com.arcsoft.face.FaceEngine;
import com.arcsoft.face.FaceFeature;
import com.arcsoft.face.FaceInfo;
import com.arcsoft.face.enums.DetectFaceOrientPriority;
import com.arcsoft.face.enums.DetectMode;
import com.bonus.canteen.db.AppDatabase;
import com.bonus.canteen.db.beans.base.CustPhotoFulInfo;
import com.bonus.canteen.db.dao.base.DeviceInfoDao;
@ -31,11 +39,16 @@ import com.bonus.canteen.service.data.entity.ResponseVo;
import com.bonus.canteen.utils.AppUtil;
import com.bonus.canteen.utils.OkHttpService;
import com.bonus.canteen.utils.SM4EncryptUtils;
import com.bonus.canteen.utils.StringHelper;
import com.bonus.canteen.utils.ThreadPoolManager;
import com.bonus.canteen.utils.UrlConfig;
import com.bonus.canteen.utils.WorkConfig;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.File;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import cn.hutool.core.util.ObjectUtil;
@ -60,7 +73,12 @@ public class UpdateBasicData {
private static final String DEVICE_ERROR = "获取设备信息失败!";
private static final String PARAM_SETTING_ERROR = "获取参数设置信息失败!";
private static final int PHONE_LENGTH = 11;
/**
* 用于特征提取的引擎
*/
private FaceEngine frEngine;
private int frInitCode = -1;
private static final int MAX_DETECT_NUM = 10;
public UpdateBasicData(Context context) {
this.context = context;
}
@ -122,9 +140,15 @@ public class UpdateBasicData {
*/
public ResponseVo getFacePhoto(String time, int userId) {
Log.i(TAG, "getFacePhoto: " + time);
List<CustPhotoFulInfo> uploadList = new ArrayList<>();
initEngine();
try{
Thread.sleep(1000);
}catch (Exception e){
return new ResponseVo(false, 1, "获取人脸特征值失败!");
}
personFaceNum = 0;
JSONObject json = new JSONObject();
// json.put(UPDATE_TIME, "");
json.put("userId", userId);
String jsonString = json.toString();
Log.i(TAG, "getFacePhoto jsonString: " + jsonString);
@ -141,12 +165,15 @@ public class UpdateBasicData {
if (!ObjectUtil.isEmpty(result)) {
JSONObject jsonObject = JSONObject.parseObject(result);
if (jsonObject.containsKey("data")) {
AppDatabase.getDatabase(context).custPhotoFulDao().deleteAll();
List<CustPhotoFulInfo> list = new Gson().fromJson(jsonObject.getString("data"), new TypeToken<List<CustPhotoFulInfo>>() {
}.getType());
for (CustPhotoFulInfo custPhotoFulInfo : list) {
AppDatabase.getDatabase(context).custPhotoFulDao().insert(custPhotoFulInfo);
faceRecognition(custPhotoFulInfo,uploadList);
}
if (!uploadList.isEmpty()){
uploadToServer(uploadList);
}
ThreadPoolManager.getExecutor().execute(this::unInitEngine);
personFaceNum = list.size();
Log.d(TAG, "人脸更新完成!更新" + personFaceNum + "条数据");
} else {
@ -165,6 +192,235 @@ public class UpdateBasicData {
}
return new ResponseVo(true, 0, "获取人脸信息成功!更新" + personFaceNum + "条数据");
}
private void uploadToServer(List<CustPhotoFulInfo> uploadList) {
JSONObject json = new JSONObject();
json.put("userFaceList", uploadList);
String jsonString = json.toString();
Log.i("getPersonMessage jsonString", jsonString);
// 定义 JSON MediaType
MediaType mediaType = MediaType.parse(MEDIA_TYPE);
// 创建 RequestBody
RequestBody body = RequestBody.create(mediaType, jsonString);
String result = service.httpPost(WorkConfig.getBaseUrl() + UrlConfig.SAVE_APP_FACE_EIGENVALUE, body, context);
if (!ObjectUtil.isEmpty(result)) {
JSONObject jsonObject = JSONObject.parseObject(result);
if (jsonObject.getString("msg").equals("操作成功") || jsonObject.getInteger("code") == 200) {
Log.d(TAG, "人脸特征值上传成功");
} else {
Log.d(TAG, "人脸特征值上传失败");
}
} else {
Log.d(TAG, "人脸特征值上传失败");
}
}
public void faceRecognition(CustPhotoFulInfo custPhotoFulInfo,List<CustPhotoFulInfo> uploadList) {
String photoUrl = custPhotoFulInfo.getPhotoUrl();
if (!photoUrl.contains("http")) {
photoUrl = WorkConfig.getFileUrl() + photoUrl;
}
Log.e(TAG, "faceRecognition - photoUrl: " + photoUrl);
String imagePath = StringHelper.downloadImage(photoUrl);
// 检查文件是否存在
File imageFile = new File(imagePath);
if (!imageFile.exists() || imageFile.length() == 0) {
Log.e(TAG, "下载的图片不存在或为空: " + imagePath);
return;
}
Log.e(TAG, "faceRecognition - imagePath: " + imagePath);
// 原始图像
// 解码图片
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
//imagePath 必须为本地图片路径
Bitmap originalBitmap = BitmapFactory.decodeFile(imagePath, options);
if (originalBitmap == null) {
Log.e(TAG, "无法解码图片文件");
return;
}
// 输出图片信息用于调试
Log.d(TAG, "图片尺寸: " + originalBitmap.getWidth() + "x" + originalBitmap.getHeight());
int targetWidth = 800;
int targetHeight = 1200;
Bitmap bitmap = Bitmap.createScaledBitmap(originalBitmap, targetWidth, targetHeight, true);
Log.d(TAG, "缩放后图片尺寸: " + bitmap.getWidth() + "x" + bitmap.getHeight());
// 为图像数据分配内存 - 使用NV21格式与后续提取特征时保持一致
byte[] nv21Data = bitmapToNv21(bitmap, bitmap.getWidth(), bitmap.getHeight());
Log.d(TAG, "bitmap转换为NV21数据长度: " + nv21Data.length);
if (nv21Data == null) {
Log.e(TAG, " bitmap转换为NV21失败");
}
//延迟100ms等待引擎初始化完成
List<FaceInfo> faceInfoList = new ArrayList<>();
int detectCode = frEngine.detectFaces(nv21Data, bitmap.getWidth(), bitmap.getHeight(),
FaceEngine.CP_PAF_NV21, faceInfoList);
Log.e(TAG, "人脸检测错误码: " + detectCode);
if (detectCode != ErrorInfo.MOK) {
Log.e(TAG, "人脸检测失败,错误码: " + detectCode + ", 错误信息: " + detectCode + " " + WorkConfig.getFileUrl() + custPhotoFulInfo.getPhotoUrl());
}else{
Log.d(TAG, "人脸检测成功,检测到 " + faceInfoList.size() + " 张人脸 " + custPhotoFulInfo.getUserId() + " " + WorkConfig.getFileUrl() + custPhotoFulInfo.getPhotoUrl());
}
if (faceInfoList.size() > 0) {
Log.i(TAG, "检测到人脸数量: " + faceInfoList.size());
FaceFeature faceFeature = new FaceFeature();
// 提取特征时使用相同的格式
int extractCode = frEngine.extractFaceFeature(nv21Data, bitmap.getWidth(), bitmap.getHeight(),
FaceEngine.CP_PAF_NV21, faceInfoList.get(0), faceFeature);
if (extractCode == ErrorInfo.MOK) {
Log.i(TAG, "人脸特征提取成功");
// 保存人脸特征值
Log.i(TAG, "人脸特征: " + Base64.getEncoder().encodeToString(faceFeature.getFeatureData()));
// TODO: 保存人脸特征值到数据库或文件中
custPhotoFulInfo.setFeatures(Base64.getEncoder().encodeToString(faceFeature.getFeatureData()));
AppDatabase.getDatabase(context).custPhotoFulDao().insert(custPhotoFulInfo);
//上传到后台
uploadList.add(custPhotoFulInfo);
} else {
Log.i(TAG, "人脸特征提取失败, 错误码: " + extractCode);
}
} else {
Log.i(TAG, "未检测到人脸, 错误码: " + detectCode);
// 根据错误码提供更具体的信息
switch(detectCode) {
case 5:
Log.i(TAG, "错误原因: 未检测到人脸");
break;
case -1:
Log.i(TAG, "错误原因: 引擎未初始化");
break;
// 可以添加更多错误码的解释
}
}
// 释放资源
if (originalBitmap != null && !originalBitmap.isRecycled()) {
originalBitmap.recycle();
}
if (bitmap != null && !bitmap.isRecycled() && bitmap != originalBitmap) {
bitmap.recycle();
}
}
public static byte[] bitmapToNv21(Bitmap bitmap, int width, int height) {
if (bitmap == null || width <= 0 || height <= 0) {
Log.e(TAG, "无效参数: bitmap=" + bitmap + ", width=" + width + ", height=" + height);
return null;
}
// 确保宽度和高度为偶数避免UV计算时的边界问题
width = width % 2 == 0 ? width : width - 1;
height = height % 2 == 0 ? height : height - 1;
if (width <= 0 || height <= 0) {
Log.e(TAG, "调整后尺寸无效: width=" + width + ", height=" + height);
return null;
}
// 确保Bitmap尺寸与指定尺寸一致
if (bitmap.getWidth() != width || bitmap.getHeight() != height) {
Log.w(TAG, "缩放Bitmap: " + bitmap.getWidth() + "x" + bitmap.getHeight() + " -> " + width + "x" + height);
bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
}
// 计算NV21数组所需的正确大小
int ySize = width * height;
int uvSize = (width * height) / 2;
int nv21Size = ySize + uvSize;
int[] argb = new int[width * height];
bitmap.getPixels(argb, 0, width, 0, 0, width, height);
byte[] nv21 = new byte[nv21Size];
boolean success = argbToNv21(argb, width, height, nv21);
return success ? nv21 : null;
}
/**
* 转换ARGB到NV21修复UV越界问题
*/
private static boolean argbToNv21(int[] argb, int width, int height, byte[] nv21) {
if (argb == null || nv21 == null || width <= 0 || height <= 0) {
Log.e(TAG, "无效的输入参数");
return false;
}
int frameSize = width * height;
// 检查nv21数组大小是否正确
if (nv21.length != frameSize * 3 / 2) {
Log.e(TAG, "NV21数组大小不正确: 预期=" + (frameSize * 3 / 2) + ", 实际=" + nv21.length);
return false;
}
int yIndex = 0;
int uvIndex = frameSize;
int maxUVIndex = nv21.length - 2; // 确保有足够空间存储V和U
for (int j = 0; j < height; j++) {
// 对于奇数行跳过UV计算
boolean isEvenRow = (j % 2 == 0);
for (int i = 0; i < width; i++) {
int index = j * width + i;
// 防止ARGB数组越界
if (index >= argb.length) {
Log.e(TAG, "ARGB数组越界: index=" + index + ", length=" + argb.length);
return false;
}
int r = (argb[index] >> 16) & 0xff;
int g = (argb[index] >> 8) & 0xff;
int b = argb[index] & 0xff;
// 计算Y分量
int y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
nv21[yIndex++] = (byte) Math.max(0, Math.min(255, y));
// 仅在偶数行和偶数列计算UV且确保不会越界
if (isEvenRow && i % 2 == 0 && uvIndex <= maxUVIndex) {
// 计算U和V分量
int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
nv21[uvIndex++] = (byte) Math.max(0, Math.min(255, v));
nv21[uvIndex++] = (byte) Math.max(0, Math.min(255, u));
}
}
}
// 验证UV索引是否在合理范围内
if (uvIndex > nv21.length) {
Log.e(TAG, "UV索引超出范围: uvIndex=" + uvIndex + ", length=" + nv21.length);
return false;
}
return true;
}
/**
* 初始化引擎
*/
private void initEngine() {
frEngine = new FaceEngine();
int initMask = FaceEngine.ASF_FACE_DETECT | FaceEngine.ASF_FACE_RECOGNITION;
frInitCode = frEngine.init(context.getApplicationContext(), DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_0_ONLY,
16, MAX_DETECT_NUM, initMask);
if (frInitCode != ErrorInfo.MOK) {
Log.i(TAG, "initEngine: 初始化引擎失败,错误码: " + frInitCode);
}else{
Log.i(TAG, "initEngine: 初始化引擎成功,错误码: " + frInitCode);
}
}
private void unInitEngine() {
if (frInitCode == ErrorInfo.MOK && frEngine != null) {
synchronized (frEngine) {
int frUnInitCode = frEngine.unInit();
Log.i(TAG, "unInitEngine: " + frUnInitCode);
}
}
}
public ResponseVo getDeviceBase() {

View File

@ -18,11 +18,19 @@
package com.bonus.canteen.utils;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Calendar;
import java.util.HashMap;
@ -237,4 +245,70 @@ public class StringHelper {
return new String(decodedBytes, Charset.forName(charsetName));
}
public static String downloadImage(String photoUrl) {
if (TextUtils.isEmpty(photoUrl)) {
Log.e("ImageDownload", "图片URL为空");
return null;
}
InputStream inputStream = null;
OutputStream outputStream = null;
try {
// 创建URL对象
URL url = new URL(photoUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 设置连接参数
connection.setConnectTimeout(5000);
connection.setReadTimeout(10000);
connection.setRequestMethod("GET");
connection.setDoInput(true);
connection.connect();
// 检查响应码
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
Log.e("ImageDownload", "下载失败,响应码: " + connection.getResponseCode());
return null;
}
// 创建保存目录
File storageDir = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), "FaceImages");
if (!storageDir.exists() && !storageDir.mkdirs()) {
Log.e("ImageDownload", "无法创建保存目录");
return null;
}
// 生成唯一文件名
String fileName = "face_" + System.currentTimeMillis() + ".jpg";
File imageFile = new File(storageDir, fileName);
// 读取输入流并写入文件
inputStream = connection.getInputStream();
outputStream = new FileOutputStream(imageFile);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
Log.d("ImageDownload", "图片下载成功: " + imageFile.getAbsolutePath());
return imageFile.getAbsolutePath();
} catch (MalformedURLException e) {
Log.e("ImageDownload", "无效的URL: " + photoUrl, e);
} catch (IOException e) {
Log.e("ImageDownload", "下载过程出错", e);
} finally {
// 关闭流
try {
if (inputStream != null) inputStream.close();
if (outputStream != null) outputStream.close();
} catch (IOException e) {
Log.e("ImageDownload", "关闭流失败", e);
}
}
return null;
}
}

View File

@ -18,6 +18,8 @@
package com.bonus.canteen.utils;
public final class UrlConfig {
public static final String SAVE_APP_FACE_EIGENVALUE = "/userFace/saveHealthAppFaceEigenvalue";
private UrlConfig() {
throw new UnsupportedOperationException("Utility class");
}

View File

@ -27,13 +27,15 @@ public class WorkConfig {
//本地
// protected static String baseUrl = "http://192.168.0.34:48380/smart-canteen";
// protected static String prefixesUrl = "http://192.168.0.34:48380";
protected static String baseUrl = "http://192.168.0.244:48380/smart-canteen";
protected static String prefixesUrl = "http://192.168.0.244:48380";
protected static String fileUrl = "http://192.168.0.14:9090/lnyst/";
protected static String baseUrl = "http://192.168.20.234:48380/smart-canteen";
protected static String prefixesUrl = "http://192.168.20.234:48380";
// protected static String baseUrl = "http://192.168.0.244:48380/smart-canteen";
// protected static String prefixesUrl = "http://192.168.0.244:48380";
protected static String fileUrl = "http://192.168.20.234:9090/lnyst/";
protected static String updateUrl = "https://www.baidu.com";
protected static String serverUri = "tcp://192.168.0.244:1883";
protected static String MqttUserName = "admin";
protected static String MqttPassWord = "Bonus@admin123!";
protected static String serverUri = "tcp://192.168.20.234:1883";
protected static String MqttUserName = "guest";
protected static String MqttPassWord = "guest";
protected static String APP_ID = "52XE2dQBtdmMsfDMvyKmPCCPyFsc4jvo8TKvAdaYfr28";
protected static String APP_KEY = "9YFPa6eiuNQAFnzJUadn4LaR8w1bcw3a5ZWYZB6FB57Y";
protected static String FACE_PASS_RATE = "0.8";

View File

@ -15,6 +15,7 @@ import com.bonus.canteen.db.entity.base.UserInfo;
import com.bonus.canteen.face.util.FaceServer;
import com.bonus.canteen.service.data.GetBasicDataService;
import com.bonus.canteen.service.data.impl.GetBasicDataServiceImp;
import com.bonus.canteen.utils.AppUtil;
import com.bonus.canteen.utils.ThreadPoolManager;
import com.bonus.canteen.utils.WorkConfig;
@ -52,7 +53,7 @@ public class RabbitMqMqttHelper {
String serverUri = WorkConfig.getMqttUrl();
this.username = WorkConfig.getMqttUserName();
this.password = WorkConfig.getMqttPassWord();
String clientId = "AndroidClient_MQTT_";
String clientId = "AndroidClient_MQTT_" + AppUtil.getSn(context);
mqttAndroidClient = new MqttAndroidClient(context, serverUri, clientId);
}
@ -185,9 +186,9 @@ public class RabbitMqMqttHelper {
return;
}
subscriptionTopics = new String[]{
"morning-inspection-device-update-person-config-v4." + tenantId,
"device-update-info-v4." + tenantId + "." + sn,
"time-calibration-v4." + tenantId + "." + sn };
"morning-inspection-device-update-person-config-v4." + 99999999,
"device-update-info-v4." + 99999999 + "." + sn,
"time-calibration-v4." + 99999999 + "." + sn };
mqttAndroidClient.subscribe(subscriptionTopics, new int[]{1, 1, 1}, null, new IMqttActionListener() {
@Override
public void onSuccess(IMqttToken asyncActionToken) {

View File

@ -55,7 +55,7 @@
android:paddingStart="0dp"
android:hint="请输入账号"
android:maxLength="11"
android:text="tqjsss"
android:text="admin"
tools:ignore="RtlSymmetry" />
</LinearLayout>
<LinearLayout
@ -73,7 +73,7 @@
android:drawableStart="@drawable/ic_password"
android:paddingStart="0dp"
android:hint="请输入密码"
android:text="Bonus$2027"
android:text="Bonus$2031"
android:inputType="textPassword"
tools:ignore="RtlSymmetry" />
</LinearLayout>