diff --git a/app/src/main/java/com/bonus/canteen/activity/SplashActivity.java b/app/src/main/java/com/bonus/canteen/activity/SplashActivity.java index eda4f79..a929dbc 100644 --- a/app/src/main/java/com/bonus/canteen/activity/SplashActivity.java +++ b/app/src/main/java/com/bonus/canteen/activity/SplashActivity.java @@ -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); + } + } /** * 菜单、返回键响应 */ diff --git a/app/src/main/java/com/bonus/canteen/service/data/UpdateBasicData.java b/app/src/main/java/com/bonus/canteen/service/data/UpdateBasicData.java index 5eec0ea..80c7928 100644 --- a/app/src/main/java/com/bonus/canteen/service/data/UpdateBasicData.java +++ b/app/src/main/java/com/bonus/canteen/service/data/UpdateBasicData.java @@ -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 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 list = new Gson().fromJson(jsonObject.getString("data"), new TypeToken>() { }.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 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 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 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() { diff --git a/app/src/main/java/com/bonus/canteen/utils/StringHelper.java b/app/src/main/java/com/bonus/canteen/utils/StringHelper.java index 9a7255d..b55f62a 100644 --- a/app/src/main/java/com/bonus/canteen/utils/StringHelper.java +++ b/app/src/main/java/com/bonus/canteen/utils/StringHelper.java @@ -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; + } } diff --git a/app/src/main/java/com/bonus/canteen/utils/UrlConfig.java b/app/src/main/java/com/bonus/canteen/utils/UrlConfig.java index f2ec485..be34649 100644 --- a/app/src/main/java/com/bonus/canteen/utils/UrlConfig.java +++ b/app/src/main/java/com/bonus/canteen/utils/UrlConfig.java @@ -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"); } diff --git a/app/src/main/java/com/bonus/canteen/utils/WorkConfig.java b/app/src/main/java/com/bonus/canteen/utils/WorkConfig.java index 94bb67f..1524d8a 100644 --- a/app/src/main/java/com/bonus/canteen/utils/WorkConfig.java +++ b/app/src/main/java/com/bonus/canteen/utils/WorkConfig.java @@ -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"; diff --git a/app/src/main/java/com/bonus/canteen/utils/rabbitmq/RabbitMqMqttHelper.java b/app/src/main/java/com/bonus/canteen/utils/rabbitmq/RabbitMqMqttHelper.java index 644ac00..1daba6d 100644 --- a/app/src/main/java/com/bonus/canteen/utils/rabbitmq/RabbitMqMqttHelper.java +++ b/app/src/main/java/com/bonus/canteen/utils/rabbitmq/RabbitMqMqttHelper.java @@ -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) { diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 7a1df84..5ebbb49 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -55,7 +55,7 @@ android:paddingStart="0dp" android:hint="请输入账号" android:maxLength="11" - android:text="tqjsss" + android:text="admin" tools:ignore="RtlSymmetry" />