加入手心手背识别

This commit is contained in:
jjLv 2025-12-04 18:03:25 +08:00
parent 4d2f0aa40b
commit 6eef3a1fb4
6 changed files with 106 additions and 26 deletions

View File

@ -311,4 +311,11 @@
# 保留无参构造方法
-keepclassmembers class * {
public <init>();
}
}
-keep class ai.onnxruntime.** { *; }
-keep interface ai.onnxruntime.** { *; }
-keep class com.google.gson.** { *; }
-keep class com.alibaba.fastjson2.** { *; }

View File

@ -21,6 +21,8 @@ import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.RectF;
@ -63,6 +65,7 @@ import com.bonus.canteen.entity.PhotoResponse;
import com.bonus.canteen.utils.OkHttpService;
import com.bonus.canteen.utils.ThreadPoolManager;
import com.bonus.canteen.utils.WorkConfig;
import com.bonus.canteen.utils.onnx.HandClassifier;
import com.bonus.canteen.utils.sound.Sound;
import com.bonus.canteen.utils.sound.SoundManager;
import com.google.gson.Gson;
@ -89,7 +92,7 @@ import cn.hutool.core.util.ObjectUtil;
public class BackHandFragment extends BaseFragment<ActivityBackHandBinding> {
private final Handler mHandler = new Handler(Looper.getMainLooper());
private OkHttpService service = new OkHttpService();
private int countdownSeconds = 4;
private int countdownSeconds = 3;
private Runnable countdownRunnable;
private SurfaceView mSurfaceView;
@ -107,7 +110,8 @@ public class BackHandFragment extends BaseFragment<ActivityBackHandBinding> {
private Size previewSize;
private String cameraId;
private static final String TAG = "PalmFragment";
// 1. 定义变量
private HandClassifier classifier;
@NonNull
@Override
protected ActivityBackHandBinding viewBindingInflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, boolean attachToRoot) {
@ -116,6 +120,8 @@ public class BackHandFragment extends BaseFragment<ActivityBackHandBinding> {
@Override
protected void initViews() {
// 2. 初始化 ( onCreate 中执行一次)
classifier = new HandClassifier(requireContext());
SoundManager.getInstance().play(Sound.createSimpleSound(R.raw.start_back_end));
Log.e("PalmFragment", "initViews: 手背界面初始化");
// 初始化摄像头预览
@ -126,7 +132,7 @@ public class BackHandFragment extends BaseFragment<ActivityBackHandBinding> {
}
private void startUpdateTime() {
countdownSeconds = 4;
countdownSeconds = 3;
countdownRunnable = new Runnable() {
@SuppressLint("SetTextI18n")
@Override
@ -373,8 +379,23 @@ public class BackHandFragment extends BaseFragment<ActivityBackHandBinding> {
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
// 保存图片
saveImage(bytes);
try {
ThreadPoolManager.getExecutor().execute(()->{
// 保存图片
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
String result = classifier.predict(bitmap);
if (result != null && result.equals("back_of_hand")) {
Log.e(TAG, "识别通过: " + result);
getActivity().runOnUiThread(() -> saveImage(bytes));
} else {
Log.e(TAG, "识别失败: " + result);
// 原拍照方法包含CameraCaptureSession.capture调用
getActivity().runOnUiThread(this::takePicture);
}
});
}catch (Exception e) {
Log.e(TAG, "处理图像线程错误: " + e.getMessage());
}
}
} catch (Exception e) {
Log.e(TAG, "处理图像错误: " + e.getMessage());

View File

@ -121,7 +121,7 @@ public class FaceFragment extends BaseFragment<ActivityFaceBinding> {
/**
* 活体检测的开关
*/
private boolean livenessDetect = true;
private boolean livenessDetect = false;
/**
* 用于记录人脸识别相关状态
*/

View File

@ -21,6 +21,8 @@ import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.RectF;
@ -63,6 +65,7 @@ import com.bonus.canteen.entity.PhotoResponse;
import com.bonus.canteen.utils.OkHttpService;
import com.bonus.canteen.utils.ThreadPoolManager;
import com.bonus.canteen.utils.WorkConfig;
import com.bonus.canteen.utils.onnx.HandClassifier;
import com.bonus.canteen.utils.sound.Sound;
import com.bonus.canteen.utils.sound.SoundManager;
import com.google.gson.Gson;
@ -89,7 +92,7 @@ import cn.hutool.core.util.ObjectUtil;
public class PalmFragment extends BaseFragment<ActivityPlamBinding> {
private final Handler mHandler = new Handler(Looper.getMainLooper());
private OkHttpService service = new OkHttpService();
private int countdownSeconds = 4;
private int countdownSeconds = 3;
private Runnable countdownRunnable;
private SurfaceView mSurfaceView;
@ -107,7 +110,8 @@ public class PalmFragment extends BaseFragment<ActivityPlamBinding> {
private Size previewSize;
private String cameraId;
private static final String TAG = "PalmFragment";
// 1. 定义变量
private HandClassifier classifier;
@NonNull
@Override
protected ActivityPlamBinding viewBindingInflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, boolean attachToRoot) {
@ -117,6 +121,8 @@ public class PalmFragment extends BaseFragment<ActivityPlamBinding> {
@Override
protected void initViews() {
Log.e("PalmFragment", "initViews: 手心界面初始化");
// 2. 初始化 ( onCreate 中执行一次)
classifier = new HandClassifier(requireContext());
SoundManager.getInstance().play(Sound.createSimpleSound(R.raw.start_plam));
// 初始化摄像头预览
mSurfaceView = findViewById(R.id.take_photo_surface_view);
@ -126,7 +132,7 @@ public class PalmFragment extends BaseFragment<ActivityPlamBinding> {
}
private void startUpdateTime() {
countdownSeconds = 4;
countdownSeconds = 3;
countdownRunnable = new Runnable() {
@SuppressLint("SetTextI18n")
@Override
@ -373,8 +379,24 @@ public class PalmFragment extends BaseFragment<ActivityPlamBinding> {
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
// 保存图片
saveImage(bytes);
try {
ThreadPoolManager.getExecutor().execute(()->{
// 保存图片
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
String result = classifier.predict(bitmap);
if (result != null && result.equals("plam")) {
Log.e(TAG, "识别通过: " + result);
getActivity().runOnUiThread(() -> saveImage(bytes));
} else {
Log.e(TAG, "识别失败: " + result);
// 原拍照方法包含CameraCaptureSession.capture调用
getActivity().runOnUiThread(this::takePicture);
}
});
}catch (Exception e) {
Log.e(TAG, "处理图像错误: " + e.getMessage());
}
}
} catch (Exception e) {
XToastUtils.error("处理图像时出错 ");

View File

@ -62,30 +62,38 @@ public class HandClassifier {
public String predict(Bitmap image) {
if (session == null) return "Error";
try {
// 1. 预处理缩放图片到 160x160 (必须与训练一致)
// 1. 预处理 (保持不变)
Bitmap resized = Bitmap.createScaledBitmap(image, 160, 160, true);
// 2. 转换数据Bitmap -> FloatBuffer (HWC CHW归一化)
FloatBuffer inputBuffer = preprocess(resized);
// 3. 创建 Tensor [1, 3, 160, 160]
// 2. 推理 (保持不变)
long[] shape = new long[]{1, 3, 160, 160};
OnnxTensor tensor = OnnxTensor.createTensor(env, inputBuffer, shape);
// 4. 运行推理
String inputName = session.getInputNames().iterator().next();
OrtSession.Result result = session.run(Collections.singletonMap(inputName, tensor));
// 5. 解析结果
// 3. 解析结果
float[][] output = (float[][]) result.get(0).getValue();
float[] scores = output[0]; // [score_back, score_plam]
float[] rawScores = output[0]; // 原始分数 (Logits)
// 6. 简单的比较大小 (Argmax)
// 如果 scores[1] > scores[0]说明是手心
if (scores[1] > scores[0]) {
return "plam"; // 手心
// 新增步骤计算概率 (Softmax)
float[] probs = softmax(rawScores);
float probBack = probs[0]; // 手背概率
float probPalm = probs[1]; // 手心概率
// 4. 阈值判断 (关键步骤)
// 设定一个严格的阈值比如 0.9 (90%)
// 只有当某一个类别的概率非常高时才认为是有效识别
float THRESHOLD = 0.65f;
if (probPalm > THRESHOLD) {
return "plam"; // 确信是手心
} else if (probBack > THRESHOLD) {
return "back_of_hand"; // 确信是手背
} else {
return "back_of_hand"; // 手背
// 两个概率都不高或者都很接近 (比如 0.6 vs 0.4)说明画面不清晰或没有手
return "none";
}
} catch (Exception e) {
@ -94,6 +102,26 @@ public class HandClassifier {
}
}
// 新增辅助方法Softmax 函数
// 将模型输出的原始数值转换为 0.0 ~ 1.0 的概率值
private float[] softmax(float[] input) {
float[] output = new float[input.length];
float sum = 0.0f;
// 为了数值稳定性先减去最大值
float max = Float.NEGATIVE_INFINITY;
for (float v : input) max = Math.max(max, v);
for (int i = 0; i < input.length; i++) {
output[i] = (float) Math.exp(input[i] - max);
sum += output[i];
}
for (int i = 0; i < input.length; i++) {
output[i] /= sum;
}
return output;
}
// 图像数据预处理将像素点转为 YOLO 需要的格式
private FloatBuffer preprocess(Bitmap bitmap) {
int width = 160;

View File

@ -36,11 +36,13 @@
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/take_photo_surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"/>
android:layout_gravity="center" />
<LinearLayout
android:id="@+id/left"
android:layout_width="465dp"