e : clients.entrySet()) {
+ ClientSession s = e.getValue();
+ if (now - s.lastSeen > HEARTBEAT_TIMEOUT_MS) {
+ closeSession(s);
+ }
+ }
+
+ for (ClientSession s : unAuthSessions.values()) {
+ if (now - s.lastSeen > HEARTBEAT_TIMEOUT_MS * 2) {
+ closeSession(s);
+ }
+ }
+ }
+
+ // ============ 发送 ============
+ public boolean sendToClient(String clientId, JSONObject json) {
+ ClientSession s = clients.get(clientId);
+ return s != null && sendToSession(s, json);
+ }
+
+ public void broadcast(JSONObject json) {
+ for (ClientSession s : clients.values()) {
+ sendToSession(s, json);
+ }
+ }
+
+ private boolean sendToSession(ClientSession s, JSONObject json) {
+ try {
+ DataOutputStream out = s.out;
+
+ synchronized (out) {
+ byte[] data = json.toString().getBytes();
+ out.writeInt(data.length);
+ out.write(data);
+ out.flush();
+ }
+ return true;
+
+ } catch (Exception e) {
+ closeSession(s);
+ return false;
+ }
+ }
+
+ // ============ ACK ============
+ private JSONObject ack(String type) {
+ JSONObject j = new JSONObject();
+ try {
+ j.put("type", "ack");
+ j.put("ack", type);
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+
+ return j;
+ }
+
+ // ============ 停止 ============
+ public void stop() {
+ running = false;
+
+ try {
+ serverSocket.close();
+ } catch (Exception ignored) {
+ }
+
+ for (ClientSession s : clients.values()) closeSession(s);
+ for (ClientSession s : unAuthSessions.values()) closeSession(s);
+
+ acceptExecutor.shutdownNow();
+ clientExecutor.shutdownNow();
+ scheduler.shutdownNow();
+
+ System.out.println("LanCommunicationManager stopped");
+ }
+
+ // ============ 会话类 ============
+ public static class ClientSession {
+ public final Socket socket;
+ public final DataOutputStream out;
+ public volatile long lastSeen = System.currentTimeMillis();
+ public volatile String clientId;
+
+ public ClientSession(Socket socket) throws IOException {
+ this.socket = socket;
+ this.out = new DataOutputStream(socket.getOutputStream());
+ }
+
+ public void updateLastSeen() {
+ lastSeen = System.currentTimeMillis();
+ }
+ }
+
+ public void closeSession(ClientSession session) {
+ if (session == null) return;
+
+ try {
+ Socket socket = session.socket;
+
+ // 1. 从已认证表移除
+ if (session.clientId != null) {
+ ClientSession removed = clients.remove(session.clientId);
+ if (removed != null && listener != null) {
+ listener.onClientDisconnected(session.clientId);
+ }
+ }
+
+ // 2. 从未认证表移除
+ unAuthSessions.remove(socket);
+
+ // 3. 关闭输出流
+ try {
+ session.out.close();
+ } catch (Exception ignored) {
+ }
+
+ // 4. 关闭 socket
+ try {
+ if (!socket.isClosed()) socket.close();
+ } catch (Exception ignored) {
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/sw/face/collect/socket/LanServer.java b/app/src/main/java/com/sw/face/collect/socket/LanServer.java
new file mode 100644
index 0000000..8d57e1e
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/socket/LanServer.java
@@ -0,0 +1,21 @@
+package com.sw.face.collect.socket;
+
+public class LanServer {
+
+ private static volatile LanCommunicationManager instance;
+
+ public static LanCommunicationManager getInstance() {
+ if (instance == null) {
+ synchronized (LanServer.class) {
+ if (instance == null) {
+ instance = new LanCommunicationManager(
+ 5000, // 监听端口
+ 20, // 最大客户端数
+ 10_000 // 心跳超时时间 10 秒
+ );
+ }
+ }
+ }
+ return instance;
+ }
+}
diff --git a/app/src/main/java/com/sw/face/collect/socket/TcpClient.java b/app/src/main/java/com/sw/face/collect/socket/TcpClient.java
new file mode 100644
index 0000000..347906d
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/socket/TcpClient.java
@@ -0,0 +1,330 @@
+package com.sw.face.collect.socket;
+
+import android.util.Log;
+
+import org.json.JSONObject;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * TcpClient - Android ready
+ *
+ * Features:
+ * - length-prefix protocol (int length + bytes)
+ * - separate read thread and write queue & writer thread
+ * - auto-reconnect with exponential backoff
+ * - heartbeat scheduler
+ * - send queue with optional callback for send result
+ * - auto send "auth" JSON after connection
+ */
+public class TcpClient {
+
+ private static final String TAG = "TcpClient";
+
+ // configuration
+ private final String serverIp;
+ private final int serverPort;
+ private final String clientId; // will be sent in auth message
+ private final int connectTimeoutMs;
+ private final long heartbeatIntervalMs;
+ private final long heartbeatTimeoutMs;
+
+ // socket + streams
+ private Socket socket;
+ private DataOutputStream out;
+ private DataInputStream in;
+
+ // threads & executors
+ private final ExecutorService writerExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Writer"));
+ private final ExecutorService readerExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Reader"));
+ private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "TcpClient-Scheduler"));
+ private final ExecutorService connectExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Connect"));
+
+ // send queue
+ private final BlockingQueue sendQueue = new LinkedBlockingQueue<>();
+
+ // state
+ private final AtomicBoolean running = new AtomicBoolean(false);
+ private final AtomicBoolean connected = new AtomicBoolean(false);
+ private final AtomicBoolean authSent = new AtomicBoolean(false);
+
+ // reconnection/backoff
+ private final long baseReconnectDelayMs = 1000; // 1s
+ private final long maxReconnectDelayMs = 30_000; // 30s
+ private final AtomicInteger reconnectAttempt = new AtomicInteger(0);
+
+ // heartbeat task future
+ private ScheduledFuture> heartbeatFuture;
+
+ // listener
+ public interface Listener {
+ void onConnected();
+
+ void onDisconnected(Exception e);
+
+ void onMessage(JSONObject json);
+
+ void onSendSuccess(JSONObject json);
+
+ void onSendFailed(JSONObject json, Exception e);
+ }
+
+ private Listener listener;
+
+ public void setListener(Listener l) {
+ this.listener = l;
+ }
+
+ // ctor
+ public TcpClient(String serverIp, int serverPort, String clientId,
+ int connectTimeoutMs, long heartbeatIntervalMs, long heartbeatTimeoutMs) {
+ this.serverIp = serverIp;
+ this.serverPort = serverPort;
+ this.clientId = clientId;
+ this.connectTimeoutMs = connectTimeoutMs;
+ this.heartbeatIntervalMs = heartbeatIntervalMs;
+ this.heartbeatTimeoutMs = heartbeatTimeoutMs;
+ }
+
+ // start client (will attempt connect)
+ public void start() {
+ if (running.getAndSet(true)) return;
+ scheduleConnect(0);
+ // writer thread drains sendQueue
+ writerExecutor.execute(this::writerLoop);
+ }
+
+ // stop client and cleanup
+ public void stop() {
+ running.set(false);
+ cancelHeartbeat();
+ closeSocketQuiet();
+ writerExecutor.shutdownNow();
+ readerExecutor.shutdownNow();
+ scheduler.shutdownNow();
+ connectExecutor.shutdownNow();
+ sendQueue.clear();
+ }
+
+ // send JSON (queued). Non-blocking.
+ public void send(JSONObject json) {
+ if (!running.get()) return;
+ sendQueue.offer(json);
+ }
+
+ // AUTH shortcut (immediately send auth JSON)
+ private void sendAuth() {
+ try {
+ JSONObject auth = new JSONObject();
+ auth.put("type", "auth");
+ auth.put("clientId", clientId);
+ sendQueue.offer(auth);
+ authSent.set(true);
+ } catch (Exception ignored) {
+ }
+ }
+
+ // writer thread loop (serializes sends)
+ private void writerLoop() {
+ while (running.get()) {
+ try {
+ JSONObject json = sendQueue.take(); // blocks
+ if (connected.get() && out != null) {
+ try {
+ byte[] data = json.toString().getBytes();
+ synchronized (out) {
+ out.writeInt(data.length);
+ out.write(data);
+ out.flush();
+ }
+ if (listener != null) listener.onSendSuccess(json);
+ } catch (Exception e) {
+ if (listener != null) listener.onSendFailed(json, e);
+ // on write failure, attempt reconnect
+ safeCloseAndScheduleReconnect(e);
+ }
+ } else {
+ // not connected: requeue it and wait for connection
+ sendQueue.offer(json);
+ Thread.sleep(500); // avoid busy loop
+ }
+ } catch (InterruptedException ignored) {
+ break;
+ }
+ }
+ }
+
+ // reader loop (runs in readerExecutor)
+ private void startReaderLoop() {
+ readerExecutor.execute(() -> {
+ try {
+ while (running.get() && connected.get() && in != null) {
+ int length;
+ try {
+ length = in.readInt(); // will throw SocketTimeoutException if set
+ } catch (SocketTimeoutException ste) {
+ // used to detect socket liveness; continue loop
+ continue;
+ }
+ if (length <= 0 || length > 10 * 1024 * 1024) {
+ // invalid length, break
+ throw new RuntimeException("Invalid message length: " + length);
+ }
+ byte[] buf = new byte[length];
+ in.readFully(buf);
+ String s = new String(buf);
+ try {
+ JSONObject json = new JSONObject(s);
+ // update last seen time via heartbeat ack if needed
+ if ("heartbeat".equals(json.optString("type"))) {
+ // optionally respond or update time
+ } else if ("auth_ok".equals(json.optString("type")) || "ack".equals(json.optString("type"))) {
+ // ignore or process ack
+ } else {
+ if (listener != null) listener.onMessage(json);
+ }
+ } catch (Exception je) {
+ Log.w(TAG, "Invalid JSON from server: " + s, je);
+ }
+ }
+ } catch (Exception e) {
+ if (running.get()) {
+ safeCloseAndScheduleReconnect(e);
+ }
+ }
+ });
+ }
+
+ // schedule connect attempt with delay (ms)
+ private void scheduleConnect(long delayMs) {
+ connectExecutor.execute(() -> {
+ try {
+ if (delayMs > 0) Thread.sleep(delayMs);
+ } catch (InterruptedException ignored) {
+ }
+ if (!running.get()) return;
+ tryConnect();
+ });
+ }
+
+ // connect logic
+ private void tryConnect() {
+ if (!running.get()) return;
+ closeSocketQuiet(); // ensure closed
+ try {
+ Socket s = new Socket();
+ s.connect(new InetSocketAddress(serverIp, serverPort), connectTimeoutMs);
+ s.setSoTimeout((int) Math.max(heartbeatTimeoutMs, 5_000));
+ socket = s;
+ out = new DataOutputStream(socket.getOutputStream());
+ in = new DataInputStream(socket.getInputStream());
+ connected.set(true);
+ reconnectAttempt.set(0);
+ authSent.set(false);
+ // start reader
+ startReaderLoop();
+ // send auth immediately
+ sendAuth();
+ // start heartbeat
+ startHeartbeat();
+ if (listener != null) listener.onConnected();
+ Log.i(TAG, "Connected to " + serverIp + ":" + serverPort);
+ } catch (Exception e) {
+ Log.w(TAG, "Connect failed: " + e.getMessage());
+ scheduleReconnectWithBackoff();
+ }
+ }
+
+ // start heartbeat scheduler
+ private void startHeartbeat() {
+ cancelHeartbeat();
+ heartbeatFuture = scheduler.scheduleAtFixedRate(() -> {
+ if (!running.get() || !connected.get()) return;
+ try {
+ JSONObject hb = new JSONObject();
+ hb.put("type", "heartbeat");
+ hb.put("time", System.currentTimeMillis());
+ sendQueue.offer(hb);
+ } catch (Exception ignored) {
+ }
+ }, 0, heartbeatIntervalMs, TimeUnit.MILLISECONDS);
+ }
+
+ private void cancelHeartbeat() {
+ if (heartbeatFuture != null && !heartbeatFuture.isCancelled()) {
+ heartbeatFuture.cancel(true);
+ heartbeatFuture = null;
+ }
+ }
+
+ // close socket quietly and notify listener
+ private void safeCloseAndScheduleReconnect(Exception cause) {
+ closeSocketQuiet();
+ if (listener != null) listener.onDisconnected(cause);
+ scheduleReconnectWithBackoff();
+ }
+
+ private void scheduleReconnectWithBackoff() {
+ int attempt = reconnectAttempt.incrementAndGet();
+ long delay = Math.min(maxReconnectDelayMsFromAttempt(attempt), maxReconnectDelayMs);
+ Log.i(TAG, "Scheduling reconnect attempt " + attempt + " after " + delay + "ms");
+ scheduleConnect(delay);
+ }
+
+ // compute exponential backoff
+ private long maxReconnectDelayMsFromAttempt(int attempt) {
+ long d = baseReconnectDelayMs * (1L << Math.min(attempt, 30));
+ if (d < 0) d = maxReconnectDelayMs;
+ return Math.min(d, maxReconnectDelayMs);
+ }
+
+ // close socket and streams
+ private void closeSocketQuiet() {
+ connected.set(false);
+ cancelHeartbeat();
+ try {
+ if (out != null) {
+ out.close();
+ }
+ } catch (Exception ignored) {
+ }
+ try {
+ if (in != null) {
+ in.close();
+ }
+ } catch (Exception ignored) {
+ }
+ try {
+ if (socket != null && !socket.isClosed()) {
+ socket.close();
+ }
+ } catch (Exception ignored) {
+ }
+ out = null;
+ in = null;
+ socket = null;
+ }
+
+ // helper: when manually call reconnect (immediately)
+ public void reconnectNow() {
+ scheduleConnect(0);
+ }
+
+ // helper to set immediate send of a JSON and wait (blocking) until it is queued (not until delivered)
+ public boolean sendBlocking(JSONObject json, long timeoutMs) throws InterruptedException {
+ return sendQueue.offer(json, timeoutMs, TimeUnit.MILLISECONDS);
+ }
+}
diff --git a/app/src/main/java/com/sw/face/collect/socket/UdpDiscoveryClient.java b/app/src/main/java/com/sw/face/collect/socket/UdpDiscoveryClient.java
new file mode 100644
index 0000000..982c1d3
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/socket/UdpDiscoveryClient.java
@@ -0,0 +1,47 @@
+package com.sw.face.collect.socket;
+
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+
+/**
+ * Simple UDP discovery client:
+ * Sends "DISCOVER_SERVER" broadcast and waits for first reply "SERVER_FOUND:serverName:ip"
+ */
+public class UdpDiscoveryClient {
+
+ public interface Listener {
+ void onFound(String ip, String serverName);
+
+ void onError(Exception e);
+ }
+
+ public void discover(int discoveryPort, int timeoutMs, Listener listener) {
+ new Thread(() -> {
+ try (DatagramSocket socket = new DatagramSocket()) {
+ socket.setBroadcast(true);
+ byte[] data = "DISCOVER_SERVER".getBytes();
+ DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("255.255.255.255"), discoveryPort);
+ socket.send(packet);
+
+ socket.setSoTimeout(timeoutMs);
+ byte[] buf = new byte[512];
+ DatagramPacket resp = new DatagramPacket(buf, buf.length);
+ socket.receive(resp);
+
+ String msg = new String(resp.getData(), 0, resp.getLength());
+ if (msg.startsWith("SERVER_FOUND")) {
+ // format: SERVER_FOUND:serverName:ip
+ String[] parts = msg.split(":", 3);
+ if (parts.length >= 3) {
+ listener.onFound(parts[2], parts[1]);
+ return;
+ }
+ }
+ listener.onError(new Exception("Invalid response"));
+ } catch (Exception e) {
+ listener.onError(e);
+ }
+ }).start();
+ }
+}
diff --git a/app/src/main/java/com/sw/face/collect/socket/test.java b/app/src/main/java/com/sw/face/collect/socket/test.java
new file mode 100644
index 0000000..be18d9b
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/socket/test.java
@@ -0,0 +1,113 @@
+package com.sw.face.collect.socket;
+
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+
+public class test {
+ LanCommunicationManager manager = new LanCommunicationManager(
+ 9999, // 端口
+ 20, // 最大客户端线程
+ 30_000 // 心跳超时 30 秒
+ );
+
+ public void test() {
+ manager.setListener(new LanCommunicationManager.Listener() {
+ @Override
+ public void onClientConnected(String clientId) {
+ System.out.println("新的客户端上线:" + clientId);
+ }
+
+ @Override
+ public void onClientDisconnected(String clientId) {
+ System.out.println("客户端离线:" + clientId);
+ }
+
+ @Override
+ public void onMessageReceived(String clientId, JSONObject msg) {
+ System.out.println("收到 " + clientId + " 的消息:" + msg);
+ }
+ });
+
+// 启动
+ try {
+ manager.start();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ // 给某个客户端发送
+ JSONObject j = new JSONObject();
+ try {
+ j.put("type", "cmd");
+ j.put("content", "hello");
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ manager.sendToClient("device123", j);
+
+// 广播
+ manager.broadcast(j);
+ }
+
+
+ public void clientTets() {
+ // 1) discover server (optional)
+ UdpDiscoveryClient disc = new UdpDiscoveryClient();
+ disc.discover(9876, 3000, new UdpDiscoveryClient.Listener() {
+ @Override
+ public void onFound(String ip, String serverName) {
+ startClient(ip);
+ }
+
+ @Override
+ public void onError(Exception e) { /* fallback to manual IP */ }
+ });
+
+
+ }
+
+ // 2) start client
+ private TcpClient client;
+
+ private void startClient(String serverIp) {
+ client = new TcpClient(
+ serverIp,
+ 9999,
+ "device123", // clientId
+ 5000, // connectTimeoutMs
+ 10_000, // heartbeatIntervalMs
+ 30_000 // heartbeatTimeoutMs
+ );
+
+ client.setListener(new TcpClient.Listener() {
+ @Override
+ public void onConnected() {
+ Log.i("APP", "connected");
+ }
+
+ @Override
+ public void onDisconnected(Exception e) {
+ Log.i("APP", "disconnected", e);
+ }
+
+ @Override
+ public void onMessage(JSONObject json) {
+ Log.i("APP", "msg:" + json);
+ }
+
+ @Override
+ public void onSendSuccess(JSONObject json) {
+ }
+
+ @Override
+ public void onSendFailed(JSONObject json, Exception e) {
+ }
+ });
+
+ client.start();
+ }
+}
diff --git a/app/src/main/java/com/sw/face/collect/utils/Base64.java b/app/src/main/java/com/sw/face/collect/utils/Base64.java
new file mode 100644
index 0000000..4509ef6
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/utils/Base64.java
@@ -0,0 +1,265 @@
+package com.sw.face.collect.utils;
+
+public final class Base64 {
+
+ private static final int BASELENGTH = 128;
+ private static final int LOOKUPLENGTH = 64;
+ private static final int TWENTYFOURBITGROUP = 24;
+ private static final int EIGHTBIT = 8;
+ private static final int SIXTEENBIT = 16;
+ private static final int FOURBYTE = 4;
+ private static final int SIGN = -128;
+ private static char PAD = '=';
+ private static byte[] base64Alphabet = new byte[BASELENGTH];
+ private static char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH];
+
+ static {
+ for (int i = 0; i < BASELENGTH; ++i) {
+ base64Alphabet[i] = -1;
+ }
+ for (int i = 'Z'; i >= 'A'; i--) {
+ base64Alphabet[i] = (byte) (i - 'A');
+ }
+ for (int i = 'z'; i >= 'a'; i--) {
+ base64Alphabet[i] = (byte) (i - 'a' + 26);
+ }
+
+ for (int i = '9'; i >= '0'; i--) {
+ base64Alphabet[i] = (byte) (i - '0' + 52);
+ }
+
+ base64Alphabet['+'] = 62;
+ base64Alphabet['/'] = 63;
+
+ for (int i = 0; i <= 25; i++) {
+ lookUpBase64Alphabet[i] = (char) ('A' + i);
+ }
+
+ for (int i = 26, j = 0; i <= 51; i++, j++) {
+ lookUpBase64Alphabet[i] = (char) ('a' + j);
+ }
+
+ for (int i = 52, j = 0; i <= 61; i++, j++) {
+ lookUpBase64Alphabet[i] = (char) ('0' + j);
+ }
+ lookUpBase64Alphabet[62] = (char) '+';
+ lookUpBase64Alphabet[63] = (char) '/';
+
+ }
+
+ private static boolean isWhiteSpace(char octect) {
+ return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9);
+ }
+
+ private static boolean isPad(char octect) {
+ return (octect == PAD);
+ }
+
+ private static boolean isData(char octect) {
+ return (octect < BASELENGTH && base64Alphabet[octect] != -1);
+ }
+
+ /**
+ * Encodes hex octects into Base64
+ *
+ * @param binaryData Array containing binaryData
+ * @return Encoded Base64 array
+ */
+ public static String encode(byte[] binaryData) {
+
+ if (binaryData == null) {
+ return null;
+ }
+
+ int lengthDataBits = binaryData.length * EIGHTBIT;
+ if (lengthDataBits == 0) {
+ return "";
+ }
+
+ int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
+ int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
+ int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1
+ : numberTriplets;
+ char encodedData[] = null;
+
+ encodedData = new char[numberQuartet * 4];
+
+ byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0;
+
+ int encodedIndex = 0;
+ int dataIndex = 0;
+
+ for (int i = 0; i < numberTriplets; i++) {
+ b1 = binaryData[dataIndex++];
+ b2 = binaryData[dataIndex++];
+ b3 = binaryData[dataIndex++];
+
+ l = (byte) (b2 & 0x0f);
+ k = (byte) (b1 & 0x03);
+
+ byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
+ : (byte) ((b1) >> 2 ^ 0xc0);
+ byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4)
+ : (byte) ((b2) >> 4 ^ 0xf0);
+ byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6)
+ : (byte) ((b3) >> 6 ^ 0xfc);
+
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f];
+ }
+
+ // form integral number of 6-bit groups
+ if (fewerThan24bits == EIGHTBIT) {
+ b1 = binaryData[dataIndex];
+ k = (byte) (b1 & 0x03);
+
+ byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
+ : (byte) ((b1) >> 2 ^ 0xc0);
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4];
+ encodedData[encodedIndex++] = PAD;
+ encodedData[encodedIndex++] = PAD;
+ } else if (fewerThan24bits == SIXTEENBIT) {
+ b1 = binaryData[dataIndex];
+ b2 = binaryData[dataIndex + 1];
+ l = (byte) (b2 & 0x0f);
+ k = (byte) (b1 & 0x03);
+
+ byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
+ : (byte) ((b1) >> 2 ^ 0xc0);
+ byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4)
+ : (byte) ((b2) >> 4 ^ 0xf0);
+
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2];
+ encodedData[encodedIndex++] = PAD;
+ }
+
+ return new String(encodedData);
+ }
+
+ /**
+ * Decodes Base64 data into octects
+ *
+ * @param encoded string containing Base64 data
+ * @return Array containind decoded data.
+ */
+ public static byte[] decode(String encoded) {
+
+ if (encoded == null) {
+ return null;
+ }
+
+ char[] base64Data = encoded.toCharArray();
+ // remove white spaces
+ int len = removeWhiteSpace(base64Data);
+
+ if (len % FOURBYTE != 0) {
+ return null;// should be divisible by four
+ }
+
+ int numberQuadruple = (len / FOURBYTE);
+
+ if (numberQuadruple == 0) {
+ return new byte[0];
+ }
+
+ byte decodedData[] = null;
+ byte b1 = 0, b2 = 0, b3 = 0, b4 = 0;
+ char d1 = 0, d2 = 0, d3 = 0, d4 = 0;
+
+ int i = 0;
+ int encodedIndex = 0;
+ int dataIndex = 0;
+ decodedData = new byte[(numberQuadruple) * 3];
+
+ for (; i < numberQuadruple - 1; i++) {
+
+ if (!isData((d1 = base64Data[dataIndex++]))
+ || !isData((d2 = base64Data[dataIndex++]))
+ || !isData((d3 = base64Data[dataIndex++]))
+ || !isData((d4 = base64Data[dataIndex++]))) {
+ return null;
+ }// if found "no data" just return null
+
+ b1 = base64Alphabet[d1];
+ b2 = base64Alphabet[d2];
+ b3 = base64Alphabet[d3];
+ b4 = base64Alphabet[d4];
+
+ decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+ decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+ decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
+ }
+
+ if (!isData((d1 = base64Data[dataIndex++]))
+ || !isData((d2 = base64Data[dataIndex++]))) {
+ return null;// if found "no data" just return null
+ }
+
+ b1 = base64Alphabet[d1];
+ b2 = base64Alphabet[d2];
+
+ d3 = base64Data[dataIndex++];
+ d4 = base64Data[dataIndex++];
+ if (!isData((d3)) || !isData((d4))) {// Check if they are PAD characters
+ if (isPad(d3) && isPad(d4)) {
+ if ((b2 & 0xf) != 0)// last 4 bits should be zero
+ {
+ return null;
+ }
+ byte[] tmp = new byte[i * 3 + 1];
+ System.arraycopy(decodedData, 0, tmp, 0, i * 3);
+ tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
+ return tmp;
+ } else if (!isPad(d3) && isPad(d4)) {
+ b3 = base64Alphabet[d3];
+ if ((b3 & 0x3) != 0)// last 2 bits should be zero
+ {
+ return null;
+ }
+ byte[] tmp = new byte[i * 3 + 2];
+ System.arraycopy(decodedData, 0, tmp, 0, i * 3);
+ tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+ tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+ return tmp;
+ } else {
+ return null;
+ }
+ } else { // No PAD e.g 3cQl
+ b3 = base64Alphabet[d3];
+ b4 = base64Alphabet[d4];
+ decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+ decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+ decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
+
+ }
+
+ return decodedData;
+ }
+
+ /**
+ * remove WhiteSpace from MIME containing encoded Base64 data.
+ *
+ * @param data the byte array of base64 data (with WS)
+ * @return the new length
+ */
+ private static int removeWhiteSpace(char[] data) {
+ if (data == null) {
+ return 0;
+ }
+
+ // count characters that's not whitespace
+ int newSize = 0;
+ int len = data.length;
+ for (int i = 0; i < len; i++) {
+ if (!isWhiteSpace(data[i])) {
+ data[newSize++] = data[i];
+ }
+ }
+ return newSize;
+ }
+}
diff --git a/app/src/main/java/com/sw/face/collect/utils/BitmapFlipUtil.java b/app/src/main/java/com/sw/face/collect/utils/BitmapFlipUtil.java
new file mode 100644
index 0000000..c51842f
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/utils/BitmapFlipUtil.java
@@ -0,0 +1,39 @@
+package com.sw.face.collect.utils;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+
+public class BitmapFlipUtil {
+ /**
+ * 水平翻转Bitmap
+ * @param src 原始Bitmap
+ * @return 水平翻转后的Bitmap
+ */
+ public static Bitmap flipHorizontally(Bitmap src) {
+ Matrix matrix = new Matrix();
+ matrix.preScale(-1, 1); // 水平翻转
+ return Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, false);
+ }
+
+ /**
+ * 垂直翻转Bitmap
+ * @param src 原始Bitmap
+ * @return 垂直翻转后的Bitmap
+ */
+ public static Bitmap flipVertically(Bitmap src) {
+ Matrix matrix = new Matrix();
+ matrix.preScale(1, -1); // 垂直翻转
+ return Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, false);
+ }
+
+ /**
+ * 水平和垂直同时翻转(180°旋转)
+ * @param src 原始Bitmap
+ * @return 180°旋转后的Bitmap
+ */
+ public static Bitmap flipBoth(Bitmap src) {
+ Matrix matrix = new Matrix();
+ matrix.preScale(-1, -1); // 同时水平和垂直翻转
+ return Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, false);
+ }
+}
diff --git a/app/src/main/java/com/sw/face/collect/utils/BitmapToByteArrayUtil.kt b/app/src/main/java/com/sw/face/collect/utils/BitmapToByteArrayUtil.kt
new file mode 100644
index 0000000..cd6135f
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/utils/BitmapToByteArrayUtil.kt
@@ -0,0 +1,38 @@
+package com.sw.face.collect.utils
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import java.io.ByteArrayOutputStream
+
+object BitmapToByteArrayUtil {
+ /**
+ * Bitmap转PNG格式byte[]
+ * @param bitmap 原始Bitmap
+ * @return PNG格式的byte[]
+ */
+ fun bitmapToPngByteArray(bitmap: Bitmap): ByteArray {
+ val stream = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
+ return stream.toByteArray()
+ }
+
+ /**
+ * Bitmap转JPEG格式byte[]
+ * @param bitmap 原始Bitmap
+ * @param quality 压缩质量(0-100)
+ * @return JPEG格式的byte[]
+ */
+ fun bitmapToJpegByteArray(bitmap: Bitmap, quality: Int): ByteArray {
+ val stream = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream)
+ return stream.toByteArray()
+ }
+
+ /**
+ * byte[]转Bitmap(通用格式)
+ * @param data byte数组
+ * @return 解析后的Bitmap
+ */
+ fun byteArrayToBitmap(data: ByteArray): Bitmap {
+ return BitmapFactory.decodeByteArray(data, 0, data.size)
+ }
+}
diff --git a/app/src/main/java/com/sw/face/collect/utils/BitmapUtils.java b/app/src/main/java/com/sw/face/collect/utils/BitmapUtils.java
new file mode 100644
index 0000000..a06207f
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/utils/BitmapUtils.java
@@ -0,0 +1,36 @@
+package com.sw.face.collect.utils;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+
+public class BitmapUtils {
+
+ public static boolean isBlackWhite(Bitmap bmp) {
+ Color c = null;
+ //遍历图片的像素点
+ for (int y = 0; y < 100; y++) {
+ for (int x = 0; x < 100; x++) {
+ c = bmp.getColor(x, y);
+
+// L.e(c.red() + "===" + c.green() + "===" + c.blue());
+ //判断像素点的色偏差值Diff
+ if (getRGBDiff(c.red(), c.green(), c.blue()) != 0) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private static float getRGBDiff(float r, float g, float b) {
+ float test1 = Math.abs(r - g);
+ float test2 = Math.abs(r - b);
+ float test3 = Math.abs(g - b);
+
+ float max = Math.max(test1, test2);
+ max = Math.max(max, test3);
+ return max;
+ }
+
+}
diff --git a/app/src/main/java/com/sw/face/collect/utils/CameraUtils.kt b/app/src/main/java/com/sw/face/collect/utils/CameraUtils.kt
new file mode 100644
index 0000000..8e6b0ce
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/utils/CameraUtils.kt
@@ -0,0 +1,86 @@
+package com.sw.face.collect.utils
+
+import android.net.Uri
+import androidx.activity.ComponentActivity
+import androidx.camera.core.CameraSelector
+import androidx.camera.view.CameraController
+import androidx.camera.view.LifecycleCameraController
+import androidx.camera.view.PreviewView
+
+class CameraUtils(private var activity: ComponentActivity) {
+
+ private var cameraController: LifecycleCameraController? = null
+ private var photoCaptureHelper: PhotoCaptureHelper? = null
+ private var failCallback: ((msg: String) -> Unit)? = null
+
+ // private var isCameraReady = false
+ fun takePhoto(succCallback: (Uri) -> Unit, failCallback: (msg: String) -> Unit = { }) {
+ this.failCallback = failCallback
+ cameraController?.let {
+ if (photoCaptureHelper == null) {
+ initCaptureHelper()
+ }
+ }
+ photoCaptureHelper?.let {
+ it.addSuccessCallback(succCallback)
+ it.bindCameraCallback {
+ bind()
+ }
+ it.takePhoto()
+ }
+ }
+
+ private fun initCaptureHelper() {
+ photoCaptureHelper = PhotoCaptureHelper(
+ context = activity,
+ cameraController = cameraController!!,
+ onSuccess = {},
+ onError = { msg ->
+ //toast(msg)
+ failCallback?.invoke(msg)
+ }
+ )
+ }
+
+ fun setPreviewController(previewView: PreviewView?) {
+ if (previewView?.controller == null) {
+ previewView?.controller = cameraController
+ }
+ }
+
+ fun initCamera() {
+ if (cameraController == null) {
+ cameraController = LifecycleCameraController(activity).apply {
+ // 必须设置有效的用例
+ setEnabledUseCases(
+ CameraController.IMAGE_CAPTURE
+ // or CameraController.VIDEO_CAPTURE
+ )
+ cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+// cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
+// setWhiteBalanceMode(WhiteBalanceMode.AUTO)
+ }
+ bind()
+ }
+
+// if (isCameraReady.not()) {
+// try {
+// cameraController!!.initializationFuture.addListener({
+// isCameraReady = true
+// Timber.d("Camera initialized successfully")
+// }, ContextCompat.getMainExecutor(this))
+// } catch (e: Exception) {
+// Timber.d("Camera initialized error = ${e.message}")
+// }
+// }
+ }
+
+ fun bind() {
+ cameraController?.bindToLifecycle(activity)
+ }
+
+ fun unbind() {
+ cameraController?.unbind()
+ }
+
+}
diff --git a/app/src/main/java/com/sw/face/collect/utils/Debouncer.kt b/app/src/main/java/com/sw/face/collect/utils/Debouncer.kt
new file mode 100644
index 0000000..89984ca
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/utils/Debouncer.kt
@@ -0,0 +1,28 @@
+package com.sw.face.collect.utils
+
+/**
+ * 防抖工具
+ */
+class Debouncer(private val delayMillis: Long = 2000) {
+ private var lastActionTime = 0L
+
+ /**
+ * 执行防抖操作
+ * @param action 要执行的操作
+ * @return Boolean 是否执行了操作 (true=已执行, false=被防抖)
+ */
+ fun debounce(action: () -> Unit): Boolean {
+ val currentTime = System.currentTimeMillis()
+ if (currentTime - lastActionTime >= delayMillis) {
+ lastActionTime = currentTime
+ action()
+ return true
+ }
+ return false
+ }
+
+ // 重置防抖计时
+ fun reset() {
+ lastActionTime = 0L
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/face/collect/utils/FaceEngineUtils.kt b/app/src/main/java/com/sw/face/collect/utils/FaceEngineUtils.kt
new file mode 100644
index 0000000..a16ba48
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/utils/FaceEngineUtils.kt
@@ -0,0 +1,47 @@
+package com.sw.face.collect.utils
+
+import android.util.Log
+import com.arcsoft.face.ErrorInfo
+import com.sw.face.collect.base.GlobalData
+import com.sw.plate.App
+import com.sw.plate.utils.ToastUtils
+import com.sw.plate.utils.arcface.FaceApi
+
+object FaceEngineUtils {
+
+ private val faceApi: FaceApi by lazy { FaceApi() }
+
+ /**
+ * 激活人脸识别引擎
+ */
+ fun activeEngine() {
+ faceApi.activeEngine(
+ App.getContext(),
+ GlobalData.appId,
+ GlobalData.sdkKey,
+ GlobalData.activeKey,
+ object : FaceApi.ActiveCallback {
+ override fun onSuccess(activeCode: Int) {
+ Log.d("FaceEngineUtils", "activeEngine activeCode = $activeCode")
+ when (activeCode) {
+ ErrorInfo.MOK -> {
+ ToastUtils.showToast("激活引擎成功")
+ }
+
+ ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
+// ToastUtils.showToast("引擎已激活,无需再次激活")
+ }
+
+ else -> {
+ ToastUtils.showToast("激活引擎失败($activeCode)")
+ }
+ }
+ }
+
+ override fun onFail(e: Exception?) {
+ ToastUtils.showToast("激活引擎异常,${e?.message}")
+ }
+ })
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/face/collect/utils/FlowCountDown.kt b/app/src/main/java/com/sw/face/collect/utils/FlowCountDown.kt
new file mode 100644
index 0000000..57f9d44
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/utils/FlowCountDown.kt
@@ -0,0 +1,33 @@
+package com.sw.face.collect.utils
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+
+// 倒计时Flow扩展函数
+fun countDownByFlow(
+ total: Int,
+ scope: CoroutineScope,
+ onTick: (Int) -> Unit,
+ onStart: (() -> Unit)? = null,
+ onFinish: (() -> Unit)? = null
+): Job {
+ return flow {
+ for (i in total downTo 0) {
+ emit(i)
+ if (i != 0) delay(1000)
+ }
+ }.flowOn(Dispatchers.Main)
+ .onStart { onStart?.invoke() }
+ .onCompletion { onFinish?.invoke() }
+ .onEach { onTick.invoke(it) }
+ .launchIn(scope)
+}
diff --git a/app/src/main/java/com/sw/face/collect/utils/IntervalExecutor.kt b/app/src/main/java/com/sw/face/collect/utils/IntervalExecutor.kt
new file mode 100644
index 0000000..44a78c8
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/utils/IntervalExecutor.kt
@@ -0,0 +1,66 @@
+package com.sw.face.collect.utils
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+
+class IntervalExecutor {
+
+ /**
+ * 启动定时任务
+ * @param delayMillis 延迟时间(毫秒)
+ * @param action 要执行的方法
+ * @return Job 可用于取消任务
+ */
+ fun startIntervalTask(delayMillis: Long, action: suspend () -> Unit): Job {
+ return CoroutineScope(Dispatchers.Default).launch {
+ while (isActive) {
+ action()
+ delay(delayMillis)
+ }
+ }
+ }
+
+ /**
+ * 启动定时任务(带初始延迟)
+ * @param initialDelay 初始延迟时间(毫秒)
+ * @param delayMillis 后续执行间隔(毫秒)
+ * @param action 要执行的方法
+ * @return Job 可用于取消任务
+ */
+ fun startIntervalTaskWithInitialDelay(
+ initialDelay: Long,
+ delayMillis: Long,
+ action: suspend () -> Unit
+ ): Job {
+ return CoroutineScope(Dispatchers.Default).launch {
+ delay(initialDelay)
+ while (isActive) {
+ action()
+ delay(delayMillis)
+ }
+ }
+ }
+}
+
+// 使用示例
+fun main() = runBlocking {
+ val executor = IntervalExecutor()
+
+ // 示例1:每隔10秒执行一次
+ val job1 = executor.startIntervalTask(10000) {
+ println("定时任务执行: ${System.currentTimeMillis()}")
+ // 这里可以执行你的业务逻辑
+ }
+
+ // 示例2:先延迟5秒,然后每隔3秒执行一次
+ val job2 = executor.startIntervalTaskWithInitialDelay(5000, 3000) {
+ println("带初始延迟的定时任务: ${System.currentTimeMillis()}")
+ }
+
+ // 运行30秒后取消任务
+ delay(30000)
+ job1.cancel()
+ job2.cancel()
+ println("所有定时任务已取消")
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/face/collect/utils/NetworkUtils.java b/app/src/main/java/com/sw/face/collect/utils/NetworkUtils.java
new file mode 100644
index 0000000..e03e0fb
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/utils/NetworkUtils.java
@@ -0,0 +1,50 @@
+package com.sw.face.collect.utils;
+
+import androidx.annotation.RequiresPermission;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Enumeration;
+import java.util.LinkedList;
+
+public class NetworkUtils {
+
+ @RequiresPermission("android.permission.INTERNET")
+ public static String getIPAddress(boolean useIPv4) {
+ try {
+ Enumeration nis = NetworkInterface.getNetworkInterfaces();
+ LinkedList adds = new LinkedList();
+
+ while(nis.hasMoreElements()) {
+ NetworkInterface ni = (NetworkInterface)nis.nextElement();
+ if (ni.isUp() && !ni.isLoopback()) {
+ Enumeration addresses = ni.getInetAddresses();
+
+ while(addresses.hasMoreElements()) {
+ adds.addFirst(addresses.nextElement());
+ }
+ }
+ }
+
+ for(InetAddress add : adds) {
+ if (!add.isLoopbackAddress()) {
+ String hostAddress = add.getHostAddress();
+ boolean isIPv4 = hostAddress.indexOf(58) < 0;
+ if (useIPv4) {
+ if (isIPv4) {
+ return hostAddress;
+ }
+ } else if (!isIPv4) {
+ int index = hostAddress.indexOf(37);
+ return index < 0 ? hostAddress.toUpperCase() : hostAddress.substring(0, index).toUpperCase();
+ }
+ }
+ }
+ } catch (SocketException e) {
+ e.printStackTrace();
+ }
+
+ return "";
+ }
+}
diff --git a/app/src/main/java/com/sw/face/collect/utils/PhotoCaptureHelper.kt b/app/src/main/java/com/sw/face/collect/utils/PhotoCaptureHelper.kt
new file mode 100644
index 0000000..51b63a4
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/utils/PhotoCaptureHelper.kt
@@ -0,0 +1,97 @@
+package com.sw.face.collect.utils
+
+import android.content.Context
+import android.net.Uri
+import android.util.Log
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCaptureException
+import androidx.camera.view.CameraController
+import androidx.core.content.ContextCompat
+import java.io.File
+
+/**
+ * 拍照工具类
+ * @param context Context 上下文
+ * @param cameraController CameraController 相机控制器
+ * @param onSuccess (Uri) -> Unit 拍照成功回调
+ * @param onError (String) -> Unit 拍照失败回调
+ */
+class PhotoCaptureHelper(
+ private val context: Context,
+ private val cameraController: CameraController,
+ private val onSuccess: (Uri) -> Unit = {},
+ private val onError: (String) -> Unit = {}
+) {
+
+ companion object {
+ const val TAG = "PhotoCaptureHelper"
+ }
+
+ private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf()
+ fun addSuccessCallback(callback: (Uri) -> Unit) {
+ if (callbackList.contains(callback).not()) {
+ callbackList.add(callback)
+ }
+ }
+
+ private var bindCamera: (() -> Unit)? = null
+ fun bindCameraCallback(callback: () -> Unit) {
+ this.bindCamera = callback
+ }
+
+ /**
+ * 拍照方法
+ * @param fileNamePrefix 文件名前缀,默认为"IMG_"
+ * @param fileExtension 文件扩展名,默认为".jpg"
+ */
+ fun takePhoto(
+ fileNamePrefix: String = "IMG_",
+ fileExtension: String = ".jpg"
+ ) {
+ Log.d(TAG, "开始拍照采集")
+
+ try {
+ val executor = ContextCompat.getMainExecutor(context)
+ val cacheDir = context.cacheDir
+ val photoFile = File.createTempFile(
+ "${fileNamePrefix}${System.currentTimeMillis()}",
+ fileExtension,
+ cacheDir
+ )
+
+ val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
+
+ cameraController.takePicture(
+ outputOptions,
+ executor,
+ object : ImageCapture.OnImageSavedCallback {
+ override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
+ val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
+ Log.d(TAG, "照片保存成功: $photoUri")
+ onSuccess(photoUri)
+ callbackList.forEach {
+ it(photoUri)
+ }
+ }
+
+ override fun onError(exception: ImageCaptureException) {
+ val errorMsg = "拍照失败: ${exception.message}"
+ if (errorMsg.contains("Not bound to a valid Camera")) {
+ if (bindCamera != null) {
+ bindCamera?.invoke()
+ //takePhoto()
+ }
+
+ }
+ Log.e(TAG, errorMsg)
+ onError(errorMsg)
+ }
+ }
+ )
+ } catch (e: Exception) {
+ val errorMsg = "创建临时文件失败: ${e.message}"
+ Log.e(TAG, errorMsg)
+ onError(errorMsg)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/face/collect/utils/ToastUtils.java b/app/src/main/java/com/sw/face/collect/utils/ToastUtils.java
new file mode 100644
index 0000000..160ee51
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/utils/ToastUtils.java
@@ -0,0 +1,117 @@
+package com.sw.face.collect.utils;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.sw.face.collect.MyApp;
+import com.sw.face.collect.R;
+
+
+/**
+ * The type Toast utils.
+ */
+public class ToastUtils {
+
+ private static Toast toast;
+ @SuppressLint("StaticFieldLeak")
+ private static TextView textCenterView;
+
+ /**
+ * Show center toast.
+ *
+ * @param text the text
+ */
+ public static void showToast(String text, int duration) {
+ if (TextUtils.isEmpty(text)) {
+ return;
+ }
+ runOnMainThread(()->{
+ Context context = MyApp.Companion.getInstance();
+ if (toast == null) {
+ View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
+ textCenterView = view.findViewById(R.id.toast_tv);
+ toast = new Toast(context);
+ toast.setGravity(Gravity.CENTER, 0, 20);
+ toast.setDuration(duration);
+ toast.setView(view);
+ }
+ textCenterView.setText(text);
+ toast.show();
+ });
+ }
+
+ public static void runOnMainThread(Runnable runnable) {
+ new Handler(Looper.getMainLooper()).post(runnable);
+ }
+
+ /**
+ * Show center toast.
+ *
+ * @param text the text
+ */
+ public static void showToast(String text) {
+ showToast(text, Toast.LENGTH_SHORT);
+
+// if (snackbar == null) {
+// View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
+// textCenterView = view.findViewById(R.id.toast_tv);
+// snackbar = Snackbar.make(view, "", Snackbar.LENGTH_SHORT);
+//
+// }
+// textCenterView.setText(text);
+// snackbar.setDuration(Snackbar.LENGTH_SHORT);
+// snackbar.show();
+ }
+
+// private static Snackbar snackbar = null;
+
+//
+// public static void showLongToast(String text) {
+// runOnMainThread(()-> {
+// Context context = App.Companion.getInstance();
+// if (toast == null) {
+// View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
+// textCenterView = view.findViewById(R.id.toast_tv);
+// toast = new Toast(context);
+// toast.setGravity(Gravity.TOP, 0, 20);
+// toast.setDuration(Toast.LENGTH_LONG);
+// toast.setView(view);
+// }
+// textCenterView.setText(text);
+// showMyToast(toast, 1000000 * 30);
+// });
+// }
+//
+//
+// //自定义Toast控件
+// private static void showMyToast(final Toast toast, final int cnt) {
+// final Timer timer = new Timer();
+// timer.schedule(new TimerTask() {
+// @Override
+// public void run() {
+// toast.show();
+// }
+// }, 0, Toast.LENGTH_LONG);
+// new Timer().schedule(new TimerTask() {
+// @Override
+// public void run() {
+// toast.cancel();
+// timer.cancel();
+// }
+// }, cnt);
+// }
+//
+// public static void showSystemToast(String text) {
+// Context context = App.Companion.getInstance();
+// Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
+// }
+
+}
diff --git a/app/src/main/java/com/sw/face/collect/view/CustomDialog.java b/app/src/main/java/com/sw/face/collect/view/CustomDialog.java
new file mode 100644
index 0000000..a79a7d8
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/view/CustomDialog.java
@@ -0,0 +1,101 @@
+package com.sw.face.collect.view;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.view.Gravity;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+import com.sw.face.collect.R;
+
+/**
+ * 加载进度条弹窗
+ */
+public class CustomDialog extends Dialog {
+
+ /**
+ * 宽高由布局文件中指定(但是最底层的宽度无效,可以多嵌套一层解决)
+ */
+ public CustomDialog(Context context, View layout) {
+ super(context, R.style.LoadingDialog);
+ setContentView(layout);
+ initConfig();
+ }
+
+ public CustomDialog(Context context, View layout, int style) {
+ super(context, style);
+ setContentView(layout);
+ initConfig();
+ }
+
+ private void initConfig() {
+ setCancelable(true);
+ setCanceledOnTouchOutside(true);
+ Window window = getWindow();
+ if (window != null) {
+ window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+ WindowManager.LayoutParams params = window.getAttributes();
+ params.gravity = Gravity.CENTER;
+ params.width = WindowManager.LayoutParams.WRAP_CONTENT;
+ params.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ window.setAttributes(params);
+ }
+ }
+//
+// /**
+// * 宽高由该方法的参数设置
+// */
+// public CustomDialog(Context context, int width, int height, View layout,
+// int style) {
+// super(context, style);
+// // 设置内容
+// setContentView(layout);
+// // 设置窗口属性
+// Window window = getWindow();
+// WindowManager.LayoutParams params = window.getAttributes();
+// // 设置宽度、高度、密度、对齐方式
+// float density = getDensity(context);
+// params.width = (int) (width * density);
+// params.height = (int) (height * density);
+// params.gravity = Gravity.CENTER;
+// window.setAttributes(params);
+//
+// }
+//
+// /**
+// * 获取显示密度
+// *
+// * @param context
+// * @return
+// */
+// public float getDensity(Context context) {
+// Resources res = context.getResources();
+// DisplayMetrics dm = res.getDisplayMetrics();
+// return dm.density;
+// }
+//
+// private void fullScreenImmersive(View view) {
+// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+// int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+// | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+// | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+// | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+// | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+// | View.SYSTEM_UI_FLAG_FULLSCREEN;
+// view.setSystemUiVisibility(uiOptions);
+// }
+//
+// }
+
+// @Override
+// public void show() {
+// this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+// super.show();
+//// fullScreenImmersive(getWindow().getDecorView());
+//// this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+// }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/face/collect/view/LanServerListenerImpl.kt b/app/src/main/java/com/sw/face/collect/view/LanServerListenerImpl.kt
new file mode 100644
index 0000000..b910c07
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/view/LanServerListenerImpl.kt
@@ -0,0 +1,22 @@
+package com.sw.face.collect.view
+
+import android.util.Log
+import com.sw.face.collect.socket.LanCommunicationManager
+import org.json.JSONObject
+
+open class LanServerListenerImpl: LanCommunicationManager.Listener {
+ companion object {
+ private const val TAG = "LanServerListenerImpl"
+ }
+ override fun onClientConnected(clientId: String?) {
+ Log.d(TAG, "addSocketListener,onClientConnected: clientId = $clientId")
+ }
+
+ override fun onClientDisconnected(clientId: String?) {
+ Log.d(TAG, "addSocketListener,onClientDisconnected: clientId = $clientId")
+ }
+
+ override fun onMessageReceived(clientId: String?, message: JSONObject?) {
+ Log.d(TAG, "addSocketListener,onMessageReceived: clientId = $clientId,message = $message")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/face/collect/view/TcpClientListenerImpl.kt b/app/src/main/java/com/sw/face/collect/view/TcpClientListenerImpl.kt
new file mode 100644
index 0000000..82b4e2a
--- /dev/null
+++ b/app/src/main/java/com/sw/face/collect/view/TcpClientListenerImpl.kt
@@ -0,0 +1,34 @@
+package com.sw.face.collect.view
+
+import android.util.Log
+import com.sw.face.collect.socket.TcpClient
+import org.json.JSONObject
+import java.lang.Exception
+import kotlin.math.log
+
+open class TcpClientListenerImpl : TcpClient.Listener {
+
+ companion object {
+ private const val TAG = "TcpClientListenerImpl"
+ }
+
+ override fun onConnected() {
+ Log.d(TAG, "addSocketListener,onConnected: ")
+ }
+
+ override fun onDisconnected(e: Exception?) {
+ Log.d(TAG, "addSocketListener,onDisconnected: ${e?.toString()}")
+ }
+
+ override fun onMessage(json: JSONObject?) {
+ Log.d(TAG, "addSocketListener,onMessage: $json")
+ }
+
+ override fun onSendSuccess(json: JSONObject?) {
+ Log.d(TAG, "addSocketListener,onSendSuccess: $json")
+ }
+
+ override fun onSendFailed(json: JSONObject?, e: Exception?) {
+ Log.d(TAG, "addSocketListener,onSendFailed: $json,e:${e?.toString()}")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-xxhdpi/bg_collect_btn.png b/app/src/main/res/drawable-xxhdpi/bg_collect_btn.png
new file mode 100644
index 0000000..e8621c0
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/bg_collect_btn.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/bg_collect_page.png b/app/src/main/res/drawable-xxhdpi/bg_collect_page.png
new file mode 100644
index 0000000..17c061c
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/bg_collect_page.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/bg_collect_page2.png b/app/src/main/res/drawable-xxhdpi/bg_collect_page2.png
new file mode 100644
index 0000000..f5fd8ea
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/bg_collect_page2.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/bg_face_wrap.png b/app/src/main/res/drawable-xxhdpi/bg_face_wrap.png
new file mode 100644
index 0000000..af847c5
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/bg_face_wrap.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/bg_face_wrap2.png b/app/src/main/res/drawable-xxhdpi/bg_face_wrap2.png
new file mode 100644
index 0000000..2fa2e00
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/bg_face_wrap2.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/face_mask.png b/app/src/main/res/drawable-xxhdpi/face_mask.png
new file mode 100644
index 0000000..6cb6f06
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/face_mask.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_camera256.png b/app/src/main/res/drawable-xxhdpi/ic_camera256.png
new file mode 100644
index 0000000..cc72a98
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_camera256.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_camera_rect.png b/app/src/main/res/drawable-xxhdpi/ic_camera_rect.png
new file mode 100644
index 0000000..0e5dcdb
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_camera_rect.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/img_init.png b/app/src/main/res/drawable-xxhdpi/img_init.png
new file mode 100644
index 0000000..e8d0844
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/img_init.png differ
diff --git a/app/src/main/res/drawable/bg_collect_shape.xml b/app/src/main/res/drawable/bg_collect_shape.xml
new file mode 100644
index 0000000..a12686e
--- /dev/null
+++ b/app/src/main/res/drawable/bg_collect_shape.xml
@@ -0,0 +1,27 @@
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_splash.xml b/app/src/main/res/drawable/bg_splash.xml
new file mode 100644
index 0000000..56c76f4
--- /dev/null
+++ b/app/src/main/res/drawable/bg_splash.xml
@@ -0,0 +1,17 @@
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_take_photo.xml b/app/src/main/res/drawable/bg_take_photo.xml
new file mode 100644
index 0000000..0a69861
--- /dev/null
+++ b/app/src/main/res/drawable/bg_take_photo.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_top_mask.xml b/app/src/main/res/drawable/bg_top_mask.xml
new file mode 100644
index 0000000..ab2fac0
--- /dev/null
+++ b/app/src/main/res/drawable/bg_top_mask.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/dialog_waiting.xml b/app/src/main/res/drawable/dialog_waiting.xml
new file mode 100644
index 0000000..e7baf2a
--- /dev/null
+++ b/app/src/main/res/drawable/dialog_waiting.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..66cf345
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main_bak.xml b/app/src/main/res/layout/activity_main_bak.xml
new file mode 100644
index 0000000..acc1186
--- /dev/null
+++ b/app/src/main/res/layout/activity_main_bak.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_waiting.xml b/app/src/main/res/layout/dialog_waiting.xml
new file mode 100644
index 0000000..304abc1
--- /dev/null
+++ b/app/src/main/res/layout/dialog_waiting.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_camera_preview.xml b/app/src/main/res/layout/layout_camera_preview.xml
new file mode 100644
index 0000000..50a2a22
--- /dev/null
+++ b/app/src/main/res/layout/layout_camera_preview.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_face_recognize.xml b/app/src/main/res/layout/layout_face_recognize.xml
new file mode 100644
index 0000000..89d0efe
--- /dev/null
+++ b/app/src/main/res/layout/layout_face_recognize.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/toast_bg.xml b/app/src/main/res/layout/toast_bg.xml
new file mode 100644
index 0000000..8c87595
--- /dev/null
+++ b/app/src/main/res/layout/toast_bg.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..94f36cb
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..35c4719
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #FF000000
+ #FFFFFFFF
+ #FF03DAC5
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..3256434
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+ 人脸信息采集
+ %s 初始化失败,错误码:%d\n错误码常量名:%s
+ 权限被拒绝!
+ \n可能的原因:该设备不支持同时打开两个摄像头
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..b0963b6
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..738a53a
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000..f443c3c
--- /dev/null
+++ b/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..922f551
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,5 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.android) apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..20e2a01
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/lib_face/.gitignore b/lib_face/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/lib_face/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/lib_face/build.gradle.kts b/lib_face/build.gradle.kts
new file mode 100644
index 0000000..449f75b
--- /dev/null
+++ b/lib_face/build.gradle.kts
@@ -0,0 +1,72 @@
+plugins {
+// alias(libs.plugins.android.library)
+ id("com.android.library")
+}
+
+android {
+ namespace = "com.sw.plate"
+ compileSdk = 35
+
+ defaultConfig {
+ minSdk = 24
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+
+ ndk {
+ abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ sourceSets {
+ named("main") {
+ jniLibs.srcDirs("libs")
+ }
+ }
+}
+
+dependencies {
+ api(
+ fileTree(
+ mapOf(
+ "dir" to "libs",
+ "include" to listOf("*.aar", "*.jar")
+ )
+ )
+ )
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+
+// implementation("com.licheedev:android-serialport:2.1.5")
+
+ val roomVersion = "2.2.5"
+ implementation("androidx.room:room-runtime:$roomVersion")
+ annotationProcessor("androidx.room:room-compiler:$roomVersion")
+
+ implementation("io.reactivex.rxjava2:rxandroid:2.0.1")
+ implementation("com.google.code.gson:gson:2.8.6")
+
+ val glideVersion = "4.12.0"
+ implementation("com.github.bumptech.glide:glide:$glideVersion")
+ annotationProcessor("com.github.bumptech.glide:compiler:$glideVersion")
+
+ val objectboxVersion = "5.0.1"
+ debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion")
+// releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion")
+}
\ No newline at end of file
diff --git a/lib_face/consumer-rules.pro b/lib_face/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/lib_face/libs/WbIntelligentHardwareSDK-debug.aar b/lib_face/libs/WbIntelligentHardwareSDK-debug.aar
new file mode 100644
index 0000000..c25490d
Binary files /dev/null and b/lib_face/libs/WbIntelligentHardwareSDK-debug.aar differ
diff --git a/lib_face/libs/arcsoft_face.jar b/lib_face/libs/arcsoft_face.jar
new file mode 100644
index 0000000..b2e827c
Binary files /dev/null and b/lib_face/libs/arcsoft_face.jar differ
diff --git a/lib_face/libs/arcsoft_image_util.jar b/lib_face/libs/arcsoft_image_util.jar
new file mode 100644
index 0000000..2260c13
Binary files /dev/null and b/lib_face/libs/arcsoft_image_util.jar differ
diff --git a/lib_face/libs/armeabi-v7a/libarcsoft_face.so b/lib_face/libs/armeabi-v7a/libarcsoft_face.so
new file mode 100644
index 0000000..e37f157
Binary files /dev/null and b/lib_face/libs/armeabi-v7a/libarcsoft_face.so differ
diff --git a/lib_face/libs/armeabi-v7a/libarcsoft_face_engine.so b/lib_face/libs/armeabi-v7a/libarcsoft_face_engine.so
new file mode 100644
index 0000000..9a6e74c
Binary files /dev/null and b/lib_face/libs/armeabi-v7a/libarcsoft_face_engine.so differ
diff --git a/lib_face/libs/armeabi-v7a/libarcsoft_image_util.so b/lib_face/libs/armeabi-v7a/libarcsoft_image_util.so
new file mode 100644
index 0000000..b0ac1dd
Binary files /dev/null and b/lib_face/libs/armeabi-v7a/libarcsoft_image_util.so differ
diff --git a/lib_face/proguard-rules.pro b/lib_face/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/lib_face/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/lib_face/src/androidTest/java/com/sw/plate/ExampleInstrumentedTest.java b/lib_face/src/androidTest/java/com/sw/plate/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..99a82b5
--- /dev/null
+++ b/lib_face/src/androidTest/java/com/sw/plate/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.sw.plate;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("com.sw.plate.test", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/lib_face/src/main/AndroidManifest.xml b/lib_face/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..44008a4
--- /dev/null
+++ b/lib_face/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/lib_face/src/main/java/com/sw/plate/App.java b/lib_face/src/main/java/com/sw/plate/App.java
new file mode 100644
index 0000000..77caee7
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/App.java
@@ -0,0 +1,19 @@
+package com.sw.plate;
+
+import android.app.Application;
+import android.content.Context;
+
+public class App extends Application {
+ private static Context mContext;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mContext = this;
+ }
+
+ public static Context getContext() {
+ return mContext;
+ }
+
+}
diff --git a/lib_face/src/main/java/com/sw/plate/AppConst.java b/lib_face/src/main/java/com/sw/plate/AppConst.java
new file mode 100644
index 0000000..0e7be5a
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/AppConst.java
@@ -0,0 +1,30 @@
+package com.sw.plate;
+
+
+import android.os.Environment;
+
+public class AppConst {
+
+ public static final String BASE_FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/sw";
+
+// public static final String ARCSOFT_APP_ID = "J6jt8Lgou3cTW9Y1k9T8Zx4nP51ZgcHRv668znCcUu5g";
+// public static final String ARCSOFT_SDK_KEY = "8necG4J6MQeTnz4gvcZuaRUywJynZindJCt2geuBnYv9";
+
+
+// 85Q1-11DY-B13F-83WC
+// APP_ID:H7kCBZ6zf8xMiqVXRmiXeaCaFhHGB5ubUiDkocQRydfQ
+// SDK_KEY:7sLu3pXYUiBurhTJjWB5yWac8qYxjDTeR8iSqAG7dAnM
+
+ public static final String ARCSOFT_APP_ID = "H7kCBZ6zf8xMiqVXRmiXeaCaFhHGB5ubUiDkocQRydfQ";
+ public static final String ARCSOFT_SDK_KEY = "7sLu3pXYUiBurhTJjWB5yWac8qYxjDTeR8iSqAG7dAnM";
+ public static final String ARCSOFT_ACTIVE_KEY = "85Q1-11DY-B13F-83WC";
+ /**
+ * 方式二: 在激活界面读取本地配置文件进行激活
+ *
+ * 配置文件名称,格式如下:
+ * APP_ID:XXXXXXXXXXXXX
+ * SDK_KEY:XXXXXXXXXXXXXXX
+ * ACTIVE_KEY:XXXX-XXXX-XXXX-XXXX
+ */
+ public static final String ACTIVE_CONFIG_FILE_NAME = "activeConfig.txt";
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/AppUtil.java b/lib_face/src/main/java/com/sw/plate/utils/AppUtil.java
new file mode 100644
index 0000000..911374a
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/AppUtil.java
@@ -0,0 +1,751 @@
+package com.sw.plate.utils;
+
+import static android.content.Context.TELEPHONY_SERVICE;
+
+import android.annotation.SuppressLint;
+import android.bluetooth.BluetoothAdapter;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.core.content.FileProvider;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.net.NetworkInterface;
+import java.text.DecimalFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+
+public class AppUtil {
+ public static String getAppPackageName(Context context) {
+ String packageName = "";
+ try {
+ PackageManager pm = context.getPackageManager();
+ PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
+ packageName = pi.packageName;
+ if (AppUtil.isEmpty(packageName)) {
+ return "";
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return packageName;
+ }
+
+ public static String getAppVersionName(Context context) {
+ String versionName = "";
+ // int versioncode=1;
+ try {
+ PackageManager pm = context.getPackageManager();
+ PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
+ versionName = pi.versionName;
+ // versioncode = pi.versionCode;表示更新了多少次
+ if (versionName == null || versionName.length() <= 0) {
+ return "";
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return versionName;
+ }
+
+
+ public static int getAppVersionCode(Context context) {
+ int versioncode = 1;
+ try {
+ PackageManager pm = context.getPackageManager();
+ PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
+ versioncode = pi.versionCode;
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return versioncode;
+ }
+
+ //判断微信是否安装
+ public static boolean isWeixinInstalled(Context context) {
+ final PackageManager packageManager = context.getPackageManager();// 获取packagemanager
+ List pinfo = packageManager.getInstalledPackages(0);// 获取所有已安装程序的包信息
+ if (pinfo != null) {
+ for (int i = 0; i < pinfo.size(); i++) {
+ String pn = pinfo.get(i).packageName;
+ if (pn.equals("com.tencent.mm")) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 打电话
+ *
+ * Intent.ACTION_DIAL Intent.ACTION_CALL
+ *
+ * @param context
+ * @param mobile
+ */
+ public static void callUp(Context context, String mobile) {
+ Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:"
+ + mobile));
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+
+ /**
+ * 获取设备ID
+ *
+ * @param context
+ * @return
+ */
+ public static String getDevId(Context context) {
+ TelephonyManager TelephonyMgr = (TelephonyManager) context
+ .getSystemService(Context.TELEPHONY_SERVICE);
+ return TelephonyMgr.getDeviceId();
+ }
+
+ /**
+ * 姓名脱敏
+ *
+ * @param fullName
+ * @return
+ */
+ public static String desensitizedName(String fullName) {
+ if (fullName == null || fullName.length() <= 1) {
+ return fullName;
+ }
+ char[] nameArr = fullName.toCharArray();
+ if (nameArr.length > 2) {
+ for (int i = 1; i < nameArr.length - 1; i++) {
+ nameArr[i] = '*';
+ }
+ } else {
+ nameArr[1] = '*';
+ }
+
+ return new String(nameArr);
+ }
+
+
+ public static String formatDateGetFull(String date) {
+ if (isEmpty(date)) {
+ return "";
+ }
+ Date d = new Date(Long.parseLong(date));
+ SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+ return dateFormat1.format(d);
+ }
+
+ public static String formatDateGetCurrentTime() {
+ Date d = new Date(System.currentTimeMillis());
+ SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//.SSS
+ return dateFormat1.format(d);
+ }
+
+
+ public static String formatDateGetFull(long date) {
+ if (date == 0) {
+ return "";
+ }
+ Date d = new Date(date);
+ SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+ return dateFormat1.format(d);
+ }
+
+ public static String formatDateGetDay(long date) {
+ if (date == 0) {
+ return "";
+ }
+ Date d = new Date(date);
+ SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd");
+ return dateFormat1.format(d);
+ }
+
+ public static boolean isEmpty(String s) {
+ if (TextUtils.isEmpty(s) || s.trim().equals("null")) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+
+ /**
+ * 格式化浮点型
+ *
+ * @param data
+ * @return
+ */
+ public static String formatDouble(double data) {
+ return new DecimalFormat("0.00").format(data);
+ }
+
+
+ /**
+ * 格式化分钟
+ *
+ * @param minutes
+ * @return
+ */
+ public static String formatMinutes(int minutes) {
+ int hour = minutes / 60;
+ int minute = minutes % 60;
+ if (hour > 0 && minute > 0) {
+ return hour + "小时" + minute + "分钟";
+ } else if (hour > 0) {
+ return hour + "小时";
+ } else {
+ return minute + "分钟";
+ }
+ }
+
+
+ //com.fawan.news
+ public static void goToMarket(Context context, String packageName) {
+ Uri uri = Uri.parse("market://details?id=" + packageName);
+ Intent goToMarket = new Intent(Intent.ACTION_VIEW, uri);
+ try {
+ context.startActivity(goToMarket);
+ } catch (ActivityNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * true为存在,false为不存在
+ *
+ * @param context
+ * @param packageName
+ * @return
+ */
+ public static boolean isInstallApp(Context context, String packageName) {
+ try {
+ context.getPackageManager().getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
+ return true;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ /**
+ * 格式化float 保留两位小数
+ *
+ * @param data
+ * @return
+ */
+ public static float formatFloat2(float data) {
+// DecimalFormat decimalFormat = new DecimalFormat("0.00");//构造方法的字符格式这里如果小数不足2位,会以0补足.
+// return decimalFormat.format(data);//返回字符串
+
+ int scale = 1;//设置位数
+ int roundingMode = 4;//表示四舍五入,可以选择其他舍值方式,例如去尾,等等.
+ BigDecimal bd = new BigDecimal((double) data);
+ bd = bd.setScale(scale, roundingMode);
+ data = bd.floatValue();
+ return data;
+ }
+
+ /**
+ * Android 6.0 之前(不包括6.0)获取mac地址
+ * 必须的权限
+ *
+ * @param context * @return
+ */
+ public static String getMacDefault(Context context) {
+ String mac = "";
+ if (context == null) {
+ return mac;
+ }
+ WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ WifiInfo info = null;
+ try {
+ info = wifi.getConnectionInfo();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ if (info == null) {
+ return null;
+ }
+ mac = info.getMacAddress();
+ if (!TextUtils.isEmpty(mac)) {
+ mac = mac.toUpperCase(Locale.ENGLISH);
+ }
+ return mac;
+ }
+
+ /**
+ * Android 6.0-Android 7.0 获取mac地址
+ */
+ public static String getMacAddress() {
+ String macSerial = null;
+ String str = "";
+
+ try {
+ Process pp = Runtime.getRuntime().exec("cat/sys/class/net/wlan0/address");
+ InputStreamReader ir = new InputStreamReader(pp.getInputStream());
+ LineNumberReader input = new LineNumberReader(ir);
+
+ while (null != str) {
+ str = input.readLine();
+ if (str != null) {
+ macSerial = str.trim();//去空格
+ break;
+ }
+ }
+ } catch (IOException ex) {
+ // 赋予默认值
+ ex.printStackTrace();
+ }
+
+ return macSerial;
+ }
+
+ /**
+ * Android 7.0之后获取Mac地址
+ * 遍历循环所有的网络接口,找到接口是 wlan0
+ * 必须的权限
+ *
+ * @return
+ */
+ public static String getMacFromHardware() {
+ try {
+ ArrayList all = Collections.list(NetworkInterface.getNetworkInterfaces());
+ for (NetworkInterface nif : all) {
+ if (!nif.getName().equals("wlan0"))
+ continue;
+ byte[] macBytes = nif.getHardwareAddress();
+ if (macBytes == null) return "";
+ StringBuilder res1 = new StringBuilder();
+ for (Byte b : macBytes) {
+ res1.append(String.format("%02X:", b));
+ }
+ if (!TextUtils.isEmpty(res1)) {
+ res1.deleteCharAt(res1.length() - 1);
+ }
+ return res1.toString();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ return "";
+ }
+
+ /**
+ * 获取mac地址(适配所有Android版本)
+ *
+ * @return
+ */
+ public static String getMac(Context context) {
+ String mac = "";
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ mac = getMacDefault(context);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ mac = getMacAddress();
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ mac = getMacFromHardware();
+ }
+ return mac;
+ }
+
+ //把String转化为float
+ public static double convertToFloat(String number, double defaultValue) {
+ if (TextUtils.isEmpty(number)) {
+ return defaultValue;
+ }
+ try {
+ return Double.parseDouble(number);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+
+ }
+
+ /**
+ * 获取AndroidId
+ *
+ * @param context
+ * @return
+ */
+ public static String getAndroidId(Context context) {
+ String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
+ return androidId;
+ }
+
+ /**
+ * 获取设备唯一 UDID
+ *
+ * @param context
+ * @return
+ */
+ @SuppressLint("MissingPermission")
+ public static String getUDID(Context context) {
+// String androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
+// L.e("androidID===" + androidID);
+// return androidID;
+ String androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
+ if (!androidID.equals("")) {
+ try {
+ if (!"9774d56d682e549c".equals(androidID)) {
+ androidID = UUID.nameUUIDFromBytes(androidID.getBytes("utf8")).toString();
+ } else {
+ @SuppressLint("MissingPermission") final String deviceId = ((TelephonyManager) context.getSystemService(TELEPHONY_SERVICE)).getDeviceId();
+ androidID = deviceId != null ? UUID.nameUUIDFromBytes(deviceId.getBytes("utf8")).toString() : UUID.randomUUID().toString();
+ }
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ return androidID;
+ }
+
+ //需要权限 android.permission.READ_PHONE_STATE
+ TelephonyManager TelephonyMgr = (TelephonyManager) context.getSystemService(TELEPHONY_SERVICE);
+ String szImei = TelephonyMgr.getDeviceId();
+ if (!szImei.equals("")) {
+ return szImei;
+ }
+
+ //需要权限 android.permission.ACCESS_WIFI_STATE
+ WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ String m_szWLANMAC = wm.getConnectionInfo().getMacAddress();
+ if (!m_szWLANMAC.equals("")) {
+ return m_szWLANMAC;
+ }
+
+ //需要权限 android.permission.BLUETOOTH
+ BluetoothAdapter m_BluetoothAdapter = null;
+ m_BluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ String m_szBTMAC = m_BluetoothAdapter.getAddress();
+ if (!m_szBTMAC.equals("")) {
+ return m_szBTMAC;
+ }
+ return getUniquePsuedoID();
+ }
+
+ //获得 Psuedo ID
+ public static String getUniquePsuedoID() {
+ String serial = null;
+ String m_szDevIDShort = "35" +
+ Build.BOARD.length() % 10 + Build.BRAND.length() % 10 +
+ Build.CPU_ABI.length() % 10 + Build.DEVICE.length() % 10 +
+ Build.DISPLAY.length() % 10 + Build.HOST.length() % 10 +
+ Build.ID.length() % 10 + Build.MANUFACTURER.length() % 10 +
+ Build.MODEL.length() % 10 + Build.PRODUCT.length() % 10 +
+ Build.TAGS.length() % 10 + Build.TYPE.length() % 10 +
+ Build.USER.length() % 10; //13 位
+ try {
+ serial = Build.class.getField("SERIAL").get(null).toString();
+ //API>=9 使用serial号
+ return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
+ } catch (Exception exception) {
+ //serial需要一个初始化,随意值
+ serial = "serial";
+ }
+
+ //使用硬件信息拼凑出来的15位号码
+ return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
+ }
+
+ public static String getCPUSerial() {
+ String line = "";
+ String TAG = "aaa";
+ Log.e(TAG, " get_quck_Sn() ");
+ Class> c = null;
+ try {
+ c = Class.forName("android.os.SystemProperties");
+
+ Method get = c.getMethod("get", String.class);
+ line = (String) get.invoke(c, "ro.serialno");
+ } catch (ClassNotFoundException | NoSuchMethodException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ Log.e(TAG, " get_quck_Sn() " + line);
+ System.out.println("设备串号" + line);
+ return line;
+ }
+
+ /**
+ * 判断网络连接状态
+ *
+ * @param context
+ * @return
+ */
+ public static boolean isNetworkConnected(Context context) {
+ if (context != null) {
+ ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
+ if (mNetworkInfo != null) {
+ return mNetworkInfo.isAvailable();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 判断WiFi连接状态
+ *
+ * @param context
+ * @return
+ */
+ public static boolean isWifiConnected(Context context) {
+ if (context != null) {
+ ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo mWiFiNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+ if (mWiFiNetworkInfo != null) {
+ return mWiFiNetworkInfo.isAvailable();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 判断移动网络状态
+ *
+ * @param context
+ * @return
+ */
+ public static boolean isMobileConnected(Context context) {
+ if (context != null) {
+ ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo mMobileNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
+ if (mMobileNetworkInfo != null) {
+ return mMobileNetworkInfo.isAvailable();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 获取网络连接类型
+ *
+ * @param context
+ * @return
+ */
+ public static int getConnectedType(Context context) {
+ if (context != null) {
+ ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
+ if (mNetworkInfo != null && mNetworkInfo.isAvailable()) {
+ return mNetworkInfo.getType();
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * 根据字符的起始和结束索引提取子串
+ *
+ * @param input 原始字符串
+ * @param startIndex 起始索引(包含,从0开始)
+ * @param endIndex 结束索引(不包含)
+ * @return 子串,若输入无效或索引越界则返回空字符串
+ */
+ public static String getSubstringByIndices(String input, int startIndex, int endIndex) {
+ if (input == null) {
+ return "";
+ }
+ // 处理索引越界问题
+ int safeStart = Math.max(startIndex, 0);
+ int safeEnd = Math.min(endIndex, input.length());
+ if (safeStart > safeEnd) {
+ return "";
+ }
+ return input.substring(safeStart, safeEnd);
+ }
+
+ public static String getSubstringByIndex(String input, int startIndex, int length) {
+ if (input == null) {
+ return "";
+ }
+ // 处理索引越界问题
+ int safeStart = Math.max(startIndex, 0);
+ int safeEnd = Math.min(startIndex + length, input.length());
+ if (safeStart > safeEnd) {
+ return "";
+ }
+ return input.substring(safeStart, safeEnd);
+ }
+
+ /**
+ * 十进制转十六进制
+ *
+ * @param decimal
+ * @return
+ */
+ public static String decimalToHexWithPadding(int decimal, int padding) {
+ // 将十进制转换为十六进制,并转换为字符串
+ String hex = Integer.toHexString(decimal);
+
+ // 确保字符串长度为至少4位,不足部分前面补0
+ while (hex.length() < padding) {
+ hex = "0" + hex;
+ }
+
+ return hex.toUpperCase(); // 返回大写形式的十六进制字符串
+ }
+
+ /**
+ * 十进制转二进制,且返回的二进制为至少7位数
+ *
+ * @param decimal
+ * @return
+ */
+ public static String decimalToBinary(int decimal) {
+ // 如果输入为0,直接返回"0"
+ if (decimal == 0) {
+ return "0";
+ }
+
+ StringBuilder binary = new StringBuilder();
+
+ // 除2取余法,将余数加入二进制字符串
+ while (decimal > 0) {
+ int remainder = decimal % 2;
+ binary.insert(0, remainder);
+ decimal = decimal / 2;
+ }
+ int length = binary.length();
+ if (length < 7) {
+ int padding = 7 - length;
+ for (int i = 0; i < padding; i++) {
+ binary.insert(0, '0');
+ }
+ }
+ return binary.toString();
+ }
+
+ /**
+ * 将二进制字符串转换为十六进制字符串,每8位转换为两位十六进制,不足两位前面补零
+ *
+ * @param binaryStr 输入的二进制字符串(仅包含0和1)
+ * @return 转换后的十六进制字符串
+ * @throws IllegalArgumentException 如果输入不是有效的二进制字符串
+ */
+ public static String binaryToHex(String binaryStr) {
+ // 校验输入合法性
+ if (binaryStr == null || !binaryStr.matches("[01]+")) {
+ throw new IllegalArgumentException("Invalid binary string");
+ }
+
+ // 补前导零使长度成为8的倍数
+ int length = binaryStr.length();
+ int padding = (8 - (length % 8)) % 8; // 计算需要补零的数量
+ StringBuilder paddedBinary = new StringBuilder();
+ for (int i = 0; i < padding; i++) {
+ paddedBinary.append('0');
+ }
+ paddedBinary.append(binaryStr);
+
+ // 每8位转换为两位十六进制
+ StringBuilder hexStr = new StringBuilder();
+ for (int i = 0; i < paddedBinary.length(); i += 8) {
+ String byteStr = paddedBinary.substring(i, i + 8);
+ int decimalValue = Integer.parseInt(byteStr, 2);
+ hexStr.append(String.format("%02X", decimalValue & 0xFF));
+ }
+
+ return hexStr.toString();
+ }
+
+ /**
+ * 数据校验 异或处理
+ */
+ public static String getXor(String content) {
+ int a = 0;
+ for (int i = 0; i < content.length() / 2; i++) {
+ a = a ^ Integer.parseInt(content.substring(i * 2, (i * 2) + 2), 16);
+ }
+ String result = Integer.toHexString(a).toUpperCase();
+ if (result.length() == 1) {
+ return "0" + result;
+ } else {
+ return result;
+ }
+ }
+
+ public static double formatPersonInfo(String input, int startIndex, int length) {
+ if (input == null) {
+ return 0;
+ }
+ // 处理索引越界问题
+ int safeStart = Math.max(startIndex, 0);
+ int safeEnd = Math.min(startIndex + length, input.length());
+ if (safeStart > safeEnd) {
+ return 0;
+ }
+ String result = input.substring(safeStart, safeEnd);
+ double num = Integer.parseInt(result, 16);
+ return num;
+ }
+
+ /**
+ * 安装apk
+ *
+ * @param activity
+ * @param apkFile
+ */
+ public static void installApk(Context activity, File apkFile) {
+ //文件有所有者概念,现在是属于当前进程的,需要把这个文件暴露给系统安装程序(其他进程)去安装
+ //因此,可能会存在权限问题,需要做下面的设置
+ //如果文件是sdcard上的,就不需要这个操作了
+ try {
+ apkFile.setExecutable(true, false);
+ apkFile.setReadable(true, false);
+ apkFile.setWritable(true, false);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ Intent intent = new Intent();
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setAction(Intent.ACTION_VIEW);
+ Uri uri;
+
+ //TODO N FileProvider
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ uri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileProvider", apkFile);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+// intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ } else {
+ uri = Uri.fromFile(apkFile);
+ }
+
+ intent.setDataAndType(uri, "application/vnd.android.package-archive");
+ activity.startActivity(intent);
+
+ //TODO 0 INSTALL PERMISSION
+ //在AndroidManifest中加入权限即可
+ }
+
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/Base64.java b/lib_face/src/main/java/com/sw/plate/utils/Base64.java
new file mode 100644
index 0000000..eaf28da
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/Base64.java
@@ -0,0 +1,265 @@
+package com.sw.plate.utils;
+
+public final class Base64 {
+
+ private static final int BASELENGTH = 128;
+ private static final int LOOKUPLENGTH = 64;
+ private static final int TWENTYFOURBITGROUP = 24;
+ private static final int EIGHTBIT = 8;
+ private static final int SIXTEENBIT = 16;
+ private static final int FOURBYTE = 4;
+ private static final int SIGN = -128;
+ private static char PAD = '=';
+ private static byte[] base64Alphabet = new byte[BASELENGTH];
+ private static char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH];
+
+ static {
+ for (int i = 0; i < BASELENGTH; ++i) {
+ base64Alphabet[i] = -1;
+ }
+ for (int i = 'Z'; i >= 'A'; i--) {
+ base64Alphabet[i] = (byte) (i - 'A');
+ }
+ for (int i = 'z'; i >= 'a'; i--) {
+ base64Alphabet[i] = (byte) (i - 'a' + 26);
+ }
+
+ for (int i = '9'; i >= '0'; i--) {
+ base64Alphabet[i] = (byte) (i - '0' + 52);
+ }
+
+ base64Alphabet['+'] = 62;
+ base64Alphabet['/'] = 63;
+
+ for (int i = 0; i <= 25; i++) {
+ lookUpBase64Alphabet[i] = (char) ('A' + i);
+ }
+
+ for (int i = 26, j = 0; i <= 51; i++, j++) {
+ lookUpBase64Alphabet[i] = (char) ('a' + j);
+ }
+
+ for (int i = 52, j = 0; i <= 61; i++, j++) {
+ lookUpBase64Alphabet[i] = (char) ('0' + j);
+ }
+ lookUpBase64Alphabet[62] = (char) '+';
+ lookUpBase64Alphabet[63] = (char) '/';
+
+ }
+
+ private static boolean isWhiteSpace(char octect) {
+ return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9);
+ }
+
+ private static boolean isPad(char octect) {
+ return (octect == PAD);
+ }
+
+ private static boolean isData(char octect) {
+ return (octect < BASELENGTH && base64Alphabet[octect] != -1);
+ }
+
+ /**
+ * Encodes hex octects into Base64
+ *
+ * @param binaryData Array containing binaryData
+ * @return Encoded Base64 array
+ */
+ public static String encode(byte[] binaryData) {
+
+ if (binaryData == null) {
+ return null;
+ }
+
+ int lengthDataBits = binaryData.length * EIGHTBIT;
+ if (lengthDataBits == 0) {
+ return "";
+ }
+
+ int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
+ int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
+ int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1
+ : numberTriplets;
+ char encodedData[] = null;
+
+ encodedData = new char[numberQuartet * 4];
+
+ byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0;
+
+ int encodedIndex = 0;
+ int dataIndex = 0;
+
+ for (int i = 0; i < numberTriplets; i++) {
+ b1 = binaryData[dataIndex++];
+ b2 = binaryData[dataIndex++];
+ b3 = binaryData[dataIndex++];
+
+ l = (byte) (b2 & 0x0f);
+ k = (byte) (b1 & 0x03);
+
+ byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
+ : (byte) ((b1) >> 2 ^ 0xc0);
+ byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4)
+ : (byte) ((b2) >> 4 ^ 0xf0);
+ byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6)
+ : (byte) ((b3) >> 6 ^ 0xfc);
+
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f];
+ }
+
+ // form integral number of 6-bit groups
+ if (fewerThan24bits == EIGHTBIT) {
+ b1 = binaryData[dataIndex];
+ k = (byte) (b1 & 0x03);
+
+ byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
+ : (byte) ((b1) >> 2 ^ 0xc0);
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4];
+ encodedData[encodedIndex++] = PAD;
+ encodedData[encodedIndex++] = PAD;
+ } else if (fewerThan24bits == SIXTEENBIT) {
+ b1 = binaryData[dataIndex];
+ b2 = binaryData[dataIndex + 1];
+ l = (byte) (b2 & 0x0f);
+ k = (byte) (b1 & 0x03);
+
+ byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
+ : (byte) ((b1) >> 2 ^ 0xc0);
+ byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4)
+ : (byte) ((b2) >> 4 ^ 0xf0);
+
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2];
+ encodedData[encodedIndex++] = PAD;
+ }
+
+ return new String(encodedData);
+ }
+
+ /**
+ * Decodes Base64 data into octects
+ *
+ * @param encoded string containing Base64 data
+ * @return Array containind decoded data.
+ */
+ public static byte[] decode(String encoded) {
+
+ if (encoded == null) {
+ return null;
+ }
+
+ char[] base64Data = encoded.toCharArray();
+ // remove white spaces
+ int len = removeWhiteSpace(base64Data);
+
+ if (len % FOURBYTE != 0) {
+ return null;// should be divisible by four
+ }
+
+ int numberQuadruple = (len / FOURBYTE);
+
+ if (numberQuadruple == 0) {
+ return new byte[0];
+ }
+
+ byte decodedData[] = null;
+ byte b1 = 0, b2 = 0, b3 = 0, b4 = 0;
+ char d1 = 0, d2 = 0, d3 = 0, d4 = 0;
+
+ int i = 0;
+ int encodedIndex = 0;
+ int dataIndex = 0;
+ decodedData = new byte[(numberQuadruple) * 3];
+
+ for (; i < numberQuadruple - 1; i++) {
+
+ if (!isData((d1 = base64Data[dataIndex++]))
+ || !isData((d2 = base64Data[dataIndex++]))
+ || !isData((d3 = base64Data[dataIndex++]))
+ || !isData((d4 = base64Data[dataIndex++]))) {
+ return null;
+ }// if found "no data" just return null
+
+ b1 = base64Alphabet[d1];
+ b2 = base64Alphabet[d2];
+ b3 = base64Alphabet[d3];
+ b4 = base64Alphabet[d4];
+
+ decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+ decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+ decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
+ }
+
+ if (!isData((d1 = base64Data[dataIndex++]))
+ || !isData((d2 = base64Data[dataIndex++]))) {
+ return null;// if found "no data" just return null
+ }
+
+ b1 = base64Alphabet[d1];
+ b2 = base64Alphabet[d2];
+
+ d3 = base64Data[dataIndex++];
+ d4 = base64Data[dataIndex++];
+ if (!isData((d3)) || !isData((d4))) {// Check if they are PAD characters
+ if (isPad(d3) && isPad(d4)) {
+ if ((b2 & 0xf) != 0)// last 4 bits should be zero
+ {
+ return null;
+ }
+ byte[] tmp = new byte[i * 3 + 1];
+ System.arraycopy(decodedData, 0, tmp, 0, i * 3);
+ tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
+ return tmp;
+ } else if (!isPad(d3) && isPad(d4)) {
+ b3 = base64Alphabet[d3];
+ if ((b3 & 0x3) != 0)// last 2 bits should be zero
+ {
+ return null;
+ }
+ byte[] tmp = new byte[i * 3 + 2];
+ System.arraycopy(decodedData, 0, tmp, 0, i * 3);
+ tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+ tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+ return tmp;
+ } else {
+ return null;
+ }
+ } else { // No PAD e.g 3cQl
+ b3 = base64Alphabet[d3];
+ b4 = base64Alphabet[d4];
+ decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+ decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+ decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
+
+ }
+
+ return decodedData;
+ }
+
+ /**
+ * remove WhiteSpace from MIME containing encoded Base64 data.
+ *
+ * @param data the byte array of base64 data (with WS)
+ * @return the new length
+ */
+ private static int removeWhiteSpace(char[] data) {
+ if (data == null) {
+ return 0;
+ }
+
+ // count characters that's not whitespace
+ int newSize = 0;
+ int len = data.length;
+ for (int i = 0; i < len; i++) {
+ if (!isWhiteSpace(data[i])) {
+ data[newSize++] = data[i];
+ }
+ }
+ return newSize;
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/ByteUtil.java b/lib_face/src/main/java/com/sw/plate/utils/ByteUtil.java
new file mode 100644
index 0000000..d28529c
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/ByteUtil.java
@@ -0,0 +1,179 @@
+package com.sw.plate.utils;
+
+public class ByteUtil {
+ /**
+ * 字节数组转换成对应的16进制表示的字符串
+ *
+ * @param src
+ * @return
+ */
+ public static String bytes2HexStr(byte[] src) {
+ StringBuilder builder = new StringBuilder();
+ if (src == null || src.length <= 0) {
+ return "";
+ }
+ char[] buffer = new char[2];
+ for (int i = 0; i < src.length; i++) {
+ buffer[0] = Character.forDigit((src[i] >>> 4) & 0x0F, 16);
+ buffer[1] = Character.forDigit(src[i] & 0x0F, 16);
+ builder.append(buffer);
+ }
+ return builder.toString().toUpperCase();
+ }
+
+ /**
+ * 十六进制字节数组转字符串
+ *
+ * @param src 目标数组
+ * @param dec 起始位置
+ * @param length 长度
+ * @return
+ */
+ public static String bytes2HexStr(byte[] src, int dec, int length) {
+ byte[] temp = new byte[length];
+ System.arraycopy(src, dec, temp, 0, length);
+ return bytes2HexStr(temp);
+ }
+
+ /**
+ * 16进制字符串转10进制数字
+ *
+ * @param hex
+ * @return
+ */
+ public static long hexStr2decimal(String hex) {
+ return Long.parseLong(hex, 16);
+ }
+
+ /**
+ * 把十进制数字转换成足位的十六进制字符串,并补全空位
+ *
+ * @param num
+ * @return
+ */
+ public static String decimal2fitHex(long num) {
+ String hex = Long.toHexString(num).toUpperCase();
+ if (hex.length() % 2 != 0) {
+ return "0" + hex;
+ }
+ return hex.toUpperCase();
+ }
+
+ /**
+ * 把十进制数字转换成足位的十六进制字符串,并补全空位
+ *
+ * @param num
+ * @param strLength 字符串的长度
+ * @return
+ */
+ public static String decimal2fitHex(long num, int strLength) {
+ String hexStr = decimal2fitHex(num);
+ StringBuilder stringBuilder = new StringBuilder(hexStr);
+ while (stringBuilder.length() < strLength) {
+ stringBuilder.insert(0, '0');
+ }
+ return stringBuilder.toString();
+ }
+
+ public static String fitDecimalStr(int dicimal, int strLength) {
+ StringBuilder builder = new StringBuilder(String.valueOf(dicimal));
+ while (builder.length() < strLength) {
+ builder.insert(0, "0");
+ }
+ return builder.toString();
+ }
+
+ /**
+ * 字符串转十六进制字符串
+ *
+ * @param str
+ * @return
+ */
+ public static String str2HexString(String str) {
+ char[] chars = "0123456789ABCDEF".toCharArray();
+ StringBuilder sb = new StringBuilder();
+ byte[] bs = null;
+ try {
+
+ bs = str.getBytes("utf8");
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ int bit;
+ for (int i = 0; i < bs.length; i++) {
+ bit = (bs[i] & 0x0f0) >> 4;
+ sb.append(chars[bit]);
+ bit = bs[i] & 0x0f;
+ sb.append(chars[bit]);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 把十六进制表示的字节数组字符串,转换成十六进制字节数组
+ *
+ * @param
+ * @return byte[]
+ */
+ public static byte[] hexStr2bytes(String hex) {
+ int len = (hex.length() / 2);
+ byte[] result = new byte[len];
+ char[] achar = hex.toUpperCase().toCharArray();
+ for (int i = 0; i < len; i++) {
+ int pos = i * 2;
+ result[i] = (byte) (hexChar2byte(achar[pos]) << 4 | hexChar2byte(achar[pos + 1]));
+ }
+ return result;
+ }
+
+ /**
+ * 把16进制字符[0123456789abcde](含大小写)转成字节
+ *
+ * @param c
+ * @return
+ */
+ private static int hexChar2byte(char c) {
+ switch (c) {
+ case '0':
+ return 0;
+ case '1':
+ return 1;
+ case '2':
+ return 2;
+ case '3':
+ return 3;
+ case '4':
+ return 4;
+ case '5':
+ return 5;
+ case '6':
+ return 6;
+ case '7':
+ return 7;
+ case '8':
+ return 8;
+ case '9':
+ return 9;
+ case 'a':
+ case 'A':
+ return 10;
+ case 'b':
+ case 'B':
+ return 11;
+ case 'c':
+ case 'C':
+ return 12;
+ case 'd':
+ case 'D':
+ return 13;
+ case 'e':
+ case 'E':
+ return 14;
+ case 'f':
+ case 'F':
+ return 15;
+ default:
+ return -1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib_face/src/main/java/com/sw/plate/utils/CabinetLockCommand.java b/lib_face/src/main/java/com/sw/plate/utils/CabinetLockCommand.java
new file mode 100644
index 0000000..a7401fd
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/CabinetLockCommand.java
@@ -0,0 +1,230 @@
+package com.sw.plate.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class CabinetLockCommand {
+ /**
+ * 生成开柜指令(含校验位)
+ *
+ * @param boxNumber 柜门号(1-65535)
+ * @return 十六进制格式指令字符串,如 "5A2100017A"
+ */
+ public static String generateOpenCommand(int boxNumber) {
+ if (boxNumber < 1 || boxNumber > 0xFFFF) {
+ throw new IllegalArgumentException("柜门号范围应为1-65535");
+ }
+
+ // 固定头+功能码
+ byte head = 0x5A;
+ byte functionCode = 0x21;
+
+ // 大端序箱门号(2字节)
+ byte[] boxCh = {
+ (byte) ((boxNumber >> 8) & 0xFF),
+ (byte) (boxNumber & 0xFF)
+ };
+
+ // 计算异或校验(head + functionCode + boxCh)
+ byte xorCheck = head;
+ xorCheck ^= functionCode;
+ xorCheck ^= boxCh[0];
+ xorCheck ^= boxCh[1];
+
+ // 拼接完整指令
+ return String.format("%02X%02X%02X%02X%02X",
+ head, functionCode, boxCh[0], boxCh[1], xorCheck);
+ }
+
+
+ /**
+ * 生成查询开关门指令(含校验位)
+ *
+ * @param boxNumber 柜门号(1-65535)
+ * @return 十六进制格式指令字符串,如 "5A2100017A"
+ */
+ public static String generateBoxStatusCommand(int boxNumber) {
+ if (boxNumber < 1 || boxNumber > 0xFFFF) {
+ throw new IllegalArgumentException("柜门号范围应为1-65535");
+ }
+
+ // 固定头+功能码
+ byte head = 0x5A;
+ byte functionCode = 0x22;
+
+ // 大端序箱门号(2字节)
+ byte[] boxCh = {
+ (byte) ((boxNumber >> 8) & 0xFF),
+ (byte) (boxNumber & 0xFF)
+ };
+
+ // 计算异或校验(head + functionCode + boxCh)
+ byte xorCheck = head;
+ xorCheck ^= functionCode;
+ xorCheck ^= boxCh[0];
+ xorCheck ^= boxCh[1];
+
+ // 拼接完整指令
+ return String.format("%02X%02X%02X%02X%02X",
+ head, functionCode, boxCh[0], boxCh[1], xorCheck);
+ }
+
+ /**
+ * 生成查询是否存放指令(含校验位)
+ *
+ * @param boxNumber 柜门号(1-65535)
+ * @return 十六进制格式指令字符串,如 "5A2100017A"
+ */
+ public static String generateBoxHasCommand(int boxNumber) {
+ if (boxNumber < 1 || boxNumber > 0xFFFF) {
+ throw new IllegalArgumentException("柜门号范围应为1-65535");
+ }
+
+ // 固定头+功能码
+ byte head = 0x5A;
+ byte functionCode = 0x25;
+
+ // 大端序箱门号(2字节)
+ byte[] boxCh = {
+ (byte) ((boxNumber >> 8) & 0xFF),
+ (byte) (boxNumber & 0xFF)
+ };
+
+ // 计算异或校验(head + functionCode + boxCh)
+ byte xorCheck = head;
+ xorCheck ^= functionCode;
+ xorCheck ^= boxCh[0];
+ xorCheck ^= boxCh[1];
+
+ // 拼接完整指令
+ return String.format("%02X%02X%02X%02X%02X",
+ head, functionCode, boxCh[0], boxCh[1], xorCheck);
+ }
+
+ private static final byte TURN_ON = (byte) 0xB1;
+ private static final byte TURN_OFF = (byte) 0xB2;
+ private static final byte TURN_UVC_ON = (byte) 0xB3;
+ private static final byte TURN_UVC_OFF = (byte) 0xB4;
+
+ /**
+ * 生成灯光控制指令
+ *
+ * @param deviceNumber 设备号(1-255)
+ * @param isTurnOn true=开灯, false=关灯
+ * @return 十六进制指令字符串
+ */
+ public static String generateLightCommand(int deviceNumber, boolean isTurnOn) {
+ if (deviceNumber < 1 || deviceNumber > 255) {
+ throw new IllegalArgumentException("设备号范围应为1-255");
+ }
+
+ byte[] command = new byte[5];
+ command[0] = 0x55;
+ command[1] = (byte) deviceNumber;
+ command[2] = isTurnOn ? TURN_ON : TURN_OFF;
+ command[3] = 0x5F;
+ command[4] = 0x00;
+
+ // 计算校验位
+ byte checksum = command[0];
+ for (int i = 1; i < command.length - 1; i++) {
+ checksum ^= command[i];
+ }
+ command[command.length - 1] = checksum;
+
+ // 转换为十六进制字符串
+ StringBuilder sb = new StringBuilder();
+ for (byte b : command) {
+ sb.append(String.format("%02X", b));
+ }
+ return sb.toString().trim();
+ }
+
+ /**
+ * 生成紫外线灯光控制指令
+ *
+ * @param deviceNumber 设备号(1-255)
+ * @param isTurnOn true=开灯, false=关灯
+ * @return 十六进制指令字符串
+ */
+ public static String generateUVCLightCommand(int deviceNumber, boolean isTurnOn) {
+ if (deviceNumber < 1 || deviceNumber > 255) {
+ throw new IllegalArgumentException("设备号范围应为1-255");
+ }
+
+ byte[] command = new byte[5];
+ command[0] = 0x55;
+ command[1] = (byte) deviceNumber;
+ command[2] = isTurnOn ? TURN_UVC_ON : TURN_UVC_OFF;
+ command[3] = 0x5F;
+ command[4] = 0x00;
+
+ // 计算校验位
+ byte checksum = command[0];
+ for (int i = 1; i < command.length - 1; i++) {
+ checksum ^= command[i];
+ }
+ command[command.length - 1] = checksum;
+
+ // 转换为十六进制字符串
+ StringBuilder sb = new StringBuilder();
+ for (byte b : command) {
+ sb.append(String.format("%02X", b));
+ }
+ return sb.toString().trim();
+ }
+
+ /**
+ * 解析箱门状态数据
+ *
+ * @param data 原始数据字符串,如"5AA2000100161008E017"
+ * @return 包含所有箱门状态的Map,key为箱门号,value为开关状态(true=开)
+ */
+ public static Map parseBoxStatus(String data) {
+ Map statusMap = new HashMap<>();
+
+ // 验证数据长度至少要有10个字符(5字节)
+ if (data == null || data.length() < 10) {
+ return statusMap;
+ }
+
+ try {
+ // 解析起始箱号和结束箱号
+ int startBox = Integer.parseInt(data.substring(4, 8), 16);
+ int endBox = Integer.parseInt(data.substring(8, 12), 16);
+
+ // 计算箱门总数和需要的字节数
+ int boxCount = endBox - startBox + 1;
+ int byteCount = (boxCount + 7) / 8;
+
+ // 验证数据长度是否足够
+ if (data.length() < 12 + byteCount * 2) {
+ return statusMap;
+ }
+
+ // 解析状态字节
+ String stateStr = data.substring(12, 12 + byteCount * 2);
+
+ // 处理每个字节
+ for (int i = 0; i < byteCount; i++) {
+ // 获取当前字节(低字节在前)
+ String byteStr = stateStr.substring(i * 2, i * 2 + 2);
+ int byteValue = Integer.parseInt(byteStr, 16);
+
+ // 处理字节中的每一位
+ for (int bit = 0; bit < 8; bit++) {
+ int boxNum = startBox + i * 8 + bit;
+ if (boxNum > endBox) break;
+
+ boolean isOpen = ((byteValue >> bit) & 0x01) == 0x01;
+ statusMap.put(boxNum, isOpen);
+ }
+ }
+
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ }
+
+ return statusMap;
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/GpioUtils.java b/lib_face/src/main/java/com/sw/plate/utils/GpioUtils.java
new file mode 100644
index 0000000..ed6e82f
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/GpioUtils.java
@@ -0,0 +1,175 @@
+package com.sw.plate.utils;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.DataOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+
+/**
+ * Created by Administrator on 2018/11/9.
+ */
+public class GpioUtils {
+
+ private static final String TAG = "GpioUtils";
+
+ /*
+ 给export申请权限
+ */
+ public static void upgradeRootPermissionForExport() {
+ upgradeRootPermission("/sys/class/gpio/export");
+ }
+
+ /*
+ 配置一个gpio路径
+ */
+ public static boolean exportGpio(int gpio) {
+ return writeNode("/sys/class/gpio/export", "" + gpio);
+ }
+
+ /*
+ 给获取io口的状态的路径申请权限,该方法需要在exportGpio后调用
+ */
+ public static void upgradeRootPermissionForGpio(int gpio) {
+ upgradeRootPermission("/sys/class/gpio/gpio" + gpio + "/direction");
+ upgradeRootPermission("/sys/class/gpio/gpio" + gpio + "/value");
+ }
+
+
+ /*
+ 设置io口为输入或输出
+ */
+ public static boolean setGpioDirection(int gpio, int arg) {
+ String gpioDirection = "";
+ if (arg == 0) gpioDirection = "out";
+ else if (arg == 1) gpioDirection = "in";
+ else return false;
+ return writeNode("/sys/class/gpio/gpio" + gpio + "/direction", gpioDirection);
+ }
+
+ /*
+ 获取io口的状态为输出还是输入
+ */
+ public static String getGpioDirection(int gpio) {
+ String gpioDirection = "";
+ gpioDirection = readNode("/sys/class/gpio/gpio" + gpio + "/direction");
+ return gpioDirection;
+ }
+
+ /*
+ 给输出io口写值,高电平或低电平
+ */
+ public static boolean writeGpioValue(int gpio, String arg) {
+ return writeNode("/sys/class/gpio/gpio" + gpio + "/value", arg);
+ }
+
+ //获取当前gpio是高还是低
+ public static String getGpioValue(int gpio) {
+ return readNode("/sys/class/gpio/gpio" + gpio + "/value");
+ }
+
+
+ private static boolean upgradeRootPermission(String path) {
+ Process process = null;
+ DataOutputStream os = null;
+ try {
+ String cmd = "chmod 777 " + path;
+ process = Runtime.getRuntime().exec("su"); //切换到root帐号
+ os = new DataOutputStream(process.getOutputStream());
+ os.writeBytes(cmd + "\n");
+ os.writeBytes("exit\n");
+ os.flush();
+ process.waitFor();
+ } catch (Exception e) {
+ } finally {
+ try {
+ if (os != null) {
+ os.close();
+ }
+ process.destroy();
+ } catch (Exception e) {
+ }
+ }
+ try {
+ return process.waitFor() == 0;
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+
+ private static boolean writeNode(String path, String arg) {
+ Log.d(TAG, "Gpio_test set node path: " + path + " arg: " + arg);
+ if (path == null || arg == null) {
+ Log.e(TAG, "set node error");
+ return false;
+ }
+ FileWriter fileWriter = null;
+ BufferedWriter bufferedWriter = null;
+ try {
+ fileWriter = new FileWriter(path);
+ fileWriter.write(arg);
+ } catch (Exception e) {
+ Log.e(TAG, "Gpio_test write node error!! path" + path + " arg: " + arg);
+ e.printStackTrace();
+ return false;
+ } finally {
+ try {
+ if (fileWriter != null) {
+ fileWriter.close();
+ }
+ if (bufferedWriter != null) {
+ bufferedWriter.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return true;
+ }
+
+ private static long mTime = 0;
+ private static int mFailTimes = 0;
+
+ private static String readNode(String path) {
+ String result = "";
+
+ FileReader fread = null;
+ BufferedReader buffer = null;
+ try {
+ fread = new FileReader(path);
+ buffer = new BufferedReader(fread);
+ String str = null;
+ while ((str = buffer.readLine()) != null) {
+ result = str;
+ break;
+ }
+ mFailTimes = 0;
+ } catch (IOException e) {
+ Log.e(TAG, "IO Exception");
+ e.printStackTrace();
+ if (mTime == 0 || SystemClock.uptimeMillis() - mTime < 1000) {
+ mFailTimes++;
+ }
+ if (mFailTimes >= 3) {
+ Log.d(TAG, "read format node continuous failed three times, exist thread");
+ }
+ } finally {
+ try {
+ if (buffer != null) {
+ buffer.close();
+ }
+ if (fread != null) {
+ fread.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return result;
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/L.java b/lib_face/src/main/java/com/sw/plate/utils/L.java
new file mode 100644
index 0000000..4ce67fe
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/L.java
@@ -0,0 +1,57 @@
+package com.sw.plate.utils;
+
+/**
+ * Log统一管理类
+ */
+public class L {
+
+ private L() {
+ /* cannot be instantiated */
+ throw new UnsupportedOperationException("cannot be instantiated");
+ }
+
+ public static boolean isDebug = true;// 是否需要打印bug,可以在application的onCreate函数里面初始化
+ private static final String TAG = "mzf";
+
+ // 下面四个是默认tag的函数
+ public static void i(String msg) {
+ if (isDebug)
+ android.util.Log.i(TAG, msg);
+ }
+
+ public static void d(String msg) {
+ if (isDebug)
+ android.util.Log.d(TAG, msg);
+ }
+
+ public static void e(String msg) {
+ if (isDebug)
+ android.util.Log.e(TAG, msg);
+ }
+
+ public static void v(String msg) {
+ if (isDebug)
+ android.util.Log.v(TAG, msg);
+ }
+
+ // 下面是传入自定义tag的函数
+ public static void i(String tag, String msg) {
+ if (isDebug)
+ android.util.Log.i(tag, msg);
+ }
+
+ public static void d(String tag, String msg) {
+ if (isDebug)
+ android.util.Log.d(tag, msg);
+ }
+
+ public static void e(String tag, String msg) {
+ if (isDebug)
+ android.util.Log.e(tag, msg);
+ }
+
+ public static void v(String tag, String msg) {
+ if (isDebug)
+ android.util.Log.v(tag, msg);
+ }
+}
\ No newline at end of file
diff --git a/lib_face/src/main/java/com/sw/plate/utils/LightManager.java b/lib_face/src/main/java/com/sw/plate/utils/LightManager.java
new file mode 100644
index 0000000..be8c13e
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/LightManager.java
@@ -0,0 +1,25 @@
+package com.sw.plate.utils;
+
+public class LightManager {
+ private static final String TAG = "LightManager";
+
+ public static void openGreenLight() {
+ L.e(TAG, "openGreenLight");
+ GpioUtils.writeGpioValue(41, "1");
+ }
+
+ public static void closeGreenLight() {
+ L.e(TAG, "closeGreenLight");
+ GpioUtils.writeGpioValue(41, "0");
+ }
+
+ public static void openRedLight() {
+ L.e(TAG, "openRedLight");
+ GpioUtils.writeGpioValue(40, "1");
+ }
+
+ public static void closeRedLight() {
+ L.e(TAG, "closeRedLight");
+ GpioUtils.writeGpioValue(40, "0");
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/NV21ToBitmap.java b/lib_face/src/main/java/com/sw/plate/utils/NV21ToBitmap.java
new file mode 100644
index 0000000..871c1bc
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/NV21ToBitmap.java
@@ -0,0 +1,64 @@
+package com.sw.plate.utils;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.renderscript.Allocation;
+import android.renderscript.Element;
+import android.renderscript.RenderScript;
+import android.renderscript.ScriptIntrinsicYuvToRGB;
+import android.renderscript.Type;
+
+public class NV21ToBitmap {
+ private RenderScript rs;
+ private ScriptIntrinsicYuvToRGB yuvToRgbIntrinsic;
+ private Type.Builder yuvType, rgbaType;
+ private Allocation in, out;
+
+ public NV21ToBitmap(Context context) {
+ rs = RenderScript.create(context);
+ yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs));
+ }
+
+ public Bitmap nv21ToBitmap(byte[] nv21, int width, int height, int orientationDegree) {
+ if (yuvType == null) {
+ yuvType = new Type.Builder(rs, Element.U8(rs)).setX(nv21.length);
+ in = Allocation.createTyped(rs, yuvType.create(), Allocation.USAGE_SCRIPT);
+
+ rgbaType = new Type.Builder(rs, Element.RGBA_8888(rs)).setX(width).setY(height);
+ out = Allocation.createTyped(rs, rgbaType.create(), Allocation.USAGE_SCRIPT);
+ }
+
+ in.copyFrom(nv21);
+
+ yuvToRgbIntrinsic.setInput(in);
+ yuvToRgbIntrinsic.forEach(out);
+
+// Matrix m = new Matrix();
+// m.postScale(1,-1);
+// m.postRotate(90, (float) width / 2, (float) height / 2);
+// m.setRotate(90, (float) width / 2, (float) height / 2);
+
+ Bitmap bmpout = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ out.copyTo(bmpout);
+ if (orientationDegree == 0) {
+ return bmpout;
+ } else {
+ return adjustPhotoRotation(bmpout, orientationDegree);
+ }
+
+ }
+
+ Bitmap adjustPhotoRotation(Bitmap bm, final int orientationDegree) {
+ Matrix m = new Matrix();
+ m.setRotate(orientationDegree, (float) bm.getWidth() / 2, (float) bm.getHeight() / 2);
+ try {
+ Bitmap bm1 = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), m, true);
+ return bm1;
+ } catch (OutOfMemoryError ex) {
+ }
+ return null;
+ }
+
+
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/PrefUtils.java b/lib_face/src/main/java/com/sw/plate/utils/PrefUtils.java
new file mode 100644
index 0000000..f40ca2b
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/PrefUtils.java
@@ -0,0 +1,68 @@
+package com.sw.plate.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+public class PrefUtils {
+
+ public static final String PREF_NAME = "sw_selforder";
+
+ public static boolean getBoolean(Context ctx, String key,
+ boolean defaultValue) {
+ SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME,
+ Context.MODE_PRIVATE);
+ return sp.getBoolean(key, defaultValue);
+ }
+
+ public static void setBoolean(Context ctx, String key, boolean value) {
+ SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME,
+ Context.MODE_PRIVATE);
+ sp.edit().putBoolean(key, value).commit();
+ }
+
+ public static String getString(Context ctx, String key, String defaultValue) {
+ SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME,
+ Context.MODE_PRIVATE);
+ return sp.getString(key, defaultValue);
+ }
+
+ public static void setString(Context ctx, String key, String value) {
+ SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME,
+ Context.MODE_PRIVATE);
+ sp.edit().putString(key, value).commit();
+ }
+
+ public static int getInt(Context ctx, String key, int defaultValue) {
+ SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME,
+ Context.MODE_PRIVATE);
+ return sp.getInt(key, defaultValue);
+ }
+
+ public static void setInt(Context ctx, String key, int value) {
+ SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME,
+ Context.MODE_PRIVATE);
+ sp.edit().putInt(key, value).commit();
+ }
+
+ public static float getFloat(Context ctx, String key, float defaultValue) {
+ SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME,
+ Context.MODE_PRIVATE);
+ return sp.getFloat(key, defaultValue);
+ }
+
+ public static void setFloat(Context ctx, String key, float value) {
+ SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME,
+ Context.MODE_PRIVATE);
+ sp.edit().putFloat(key, value).commit();
+ }
+
+ public static void clearData(Context ctx, String key) {
+ SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+ sp.edit().remove(key).clear().commit();
+ }
+
+ public static void clearAllData(Context ctx) {
+ SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+ sp.edit().clear().commit();
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/ScanGunKeyEventHelper.java b/lib_face/src/main/java/com/sw/plate/utils/ScanGunKeyEventHelper.java
new file mode 100644
index 0000000..2b64fd0
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/ScanGunKeyEventHelper.java
@@ -0,0 +1,166 @@
+package com.sw.plate.utils;
+
+import static android.content.Context.INPUT_SERVICE;
+
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.view.KeyEvent;
+
+/**
+ * 扫描枪事件处理
+ */
+public class ScanGunKeyEventHelper {
+ private final static long MESSAGE_DELAY = 500; //延迟500ms,判断扫码是否完成。
+ private final StringBuffer mStringBufferResult; //扫码内容
+ private boolean mCaps; //大小写区分
+ private final Handler mHandler;
+ private final Runnable mScanningFishedRunnable;
+ private OnScanSuccessListener mOnScanSuccessListener;
+ private final Context mContext;
+
+ // private String mDeviceName = "TMC HIDKeyBoard";
+ private String mDeviceName = "Linux 3.4.35 with ak-hsudc Composite Gadget (ACM + HID)";
+ private String mDeviceName1 = "USBKey Chip USBKey Module";
+
+ public ScanGunKeyEventHelper(Context context, OnScanSuccessListener onScanSuccessListener) {
+
+ mContext = context;
+ mOnScanSuccessListener = onScanSuccessListener;
+ mStringBufferResult = new StringBuffer();
+ mHandler = new Handler();
+ mScanningFishedRunnable = this::performScanSuccess;
+ }
+
+ /**
+ * 返回扫码成功后的结果
+ */
+ private void performScanSuccess() {
+ String barcode = mStringBufferResult.toString();
+ if (mOnScanSuccessListener != null && !TextUtils.isEmpty(barcode))
+ mOnScanSuccessListener.onScanSuccess(barcode);
+ mStringBufferResult.setLength(0);
+ }
+
+ /**
+ * 扫码枪事件解析
+ *
+ * @param event
+ */
+ public void analysisKeyEvent(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ //字母大小写判断
+ checkLetterStatus(event);
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ char aChar = getInputCode(event);
+ if (aChar != 0) {
+ mStringBufferResult.append(aChar);
+ }
+ if (keyCode == KeyEvent.KEYCODE_ENTER) {
+ //若为回车键,直接返回
+ mHandler.removeCallbacks(mScanningFishedRunnable);
+ mHandler.post(mScanningFishedRunnable);
+ } else {
+ //延迟post,若500ms内,有其他事件
+ mHandler.removeCallbacks(mScanningFishedRunnable);
+ mHandler.postDelayed(mScanningFishedRunnable, MESSAGE_DELAY);
+ }
+
+ }
+ }
+
+ //检查shift键
+ private void checkLetterStatus(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ if (keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT || keyCode == KeyEvent.KEYCODE_SHIFT_LEFT) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ //按着shift键,表示大写
+ mCaps = true;
+ } else {
+ //松开shift键,表示小写
+ mCaps = false;
+ }
+ }
+ }
+
+ /**
+ * 获取扫描内容
+ *
+ * @param event
+ * @return
+ */
+ private char getInputCode(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ char aChar;
+ if (keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) {
+ //字母
+ aChar = (char) ((mCaps ? 'A' : 'a') + keyCode - KeyEvent.KEYCODE_A);
+ } else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
+ //数字
+ aChar = (char) ('0' + keyCode - KeyEvent.KEYCODE_0);
+ } else {
+ //其他符号
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_PERIOD:
+ aChar = '.';
+ break;
+ case KeyEvent.KEYCODE_MINUS:
+ aChar = mCaps ? '_' : '-';
+ break;
+ case KeyEvent.KEYCODE_SLASH:
+ aChar = '/';
+ break;
+ case KeyEvent.KEYCODE_BACKSLASH:
+ aChar = mCaps ? '|' : '\\';
+ break;
+ default:
+ aChar = 0;
+ break;
+ }
+ }
+ return aChar;
+ }
+
+ public interface OnScanSuccessListener {
+ void onScanSuccess(String barcode);
+ }
+
+ public void onDestroy() {
+ mHandler.removeCallbacks(mScanningFishedRunnable);
+ mOnScanSuccessListener = null;
+ }
+
+ /**
+ * 输入设备是否存在
+ *
+ * @param deviceName
+ * @return
+ */
+ public boolean isInputDeviceExist(String deviceName) {
+
+ InputManager inputManager = (InputManager) mContext.getSystemService(INPUT_SERVICE);
+ int[] deviceIds = inputManager.getInputDeviceIds();
+ for (int id : deviceIds) {
+ if (inputManager.getInputDevice(id).getName().equals(deviceName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 是否为扫码枪事件(部分机型KeyEvent获取的名字错误)
+ *
+ * @param event
+ * @return
+ */
+ public boolean isScanGunEvent(KeyEvent event) {
+ if (event == null || event.getDevice() == null) return false;
+ String deviceName = event.getDevice().getName();
+ L.e("event===" + deviceName +
+ "===Char===" + event.getCharacters() +
+ "===Action===" + event.getAction());
+ return deviceName.equals(mDeviceName) || deviceName.equals(mDeviceName1);
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/ThreadUtils.kt b/lib_face/src/main/java/com/sw/plate/utils/ThreadUtils.kt
new file mode 100644
index 0000000..caf51ed
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/ThreadUtils.kt
@@ -0,0 +1,124 @@
+package com.sw.plate.utils
+
+import android.os.Handler
+import android.os.Looper
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * 多功能线程工具类
+ * 结合协程、Handler和线程池实现线程切换
+ */
+object ThreadUtils : CoroutineScope {
+ // 主线程Handler
+ private val mainHandler by lazy { Handler(Looper.getMainLooper()) }
+
+ // 后台线程池(IO密集型任务)
+ private val ioThreadPool: ExecutorService by lazy {
+ Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2)
+ }
+
+ // CPU密集型线程池
+ private val cpuThreadPool: ExecutorService by lazy {
+ Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
+ }
+
+ // 协程Job管理
+ private val job = Job()
+
+ override val coroutineContext: CoroutineContext
+ get() = Dispatchers.Main + job
+
+ // ========== Handler相关方法 ==========
+
+ /**
+ * 在主线程执行任务
+ * @param delayMillis 延迟时间(毫秒)
+ */
+ fun runOnUiThread(delayMillis: Long = 0, block: () -> Unit) {
+ if (delayMillis > 0) {
+ mainHandler.postDelayed(block, delayMillis)
+ } else {
+ if (isOnMainThread()) {
+ block()
+ } else {
+ mainHandler.post(block)
+ }
+ }
+ }
+
+ /**
+ * 移除主线程任务
+ */
+ fun removeUiThreadTask(block: () -> Unit) {
+ mainHandler.removeCallbacks(block)
+ }
+
+ // ========== 线程池相关方法 ==========
+
+ /**
+ * 在IO线程执行任务
+ */
+ fun runOnIoThread(block: () -> Unit) {
+ ioThreadPool.execute(block)
+ }
+
+ /**
+ * 在CPU计算线程执行任务
+ */
+ fun runOnCpuThread(block: () -> Unit) {
+ cpuThreadPool.execute(block)
+ }
+
+ // ========== 协程相关方法 ==========
+
+ /**
+ * 启动协程(默认在主线程)
+ */
+ fun launch(block: suspend CoroutineScope.() -> Unit): Job {
+ return launch(coroutineContext, block = block)
+ }
+
+ /**
+ * 在IO线程启动协程
+ */
+ fun launchOnIo(block: suspend CoroutineScope.() -> Unit): Job {
+ return launch(Dispatchers.IO, block = block)
+ }
+
+ /**
+ * 切换到主线程(协程环境)
+ */
+ suspend fun switchToMain(block: suspend CoroutineScope.() -> T): T {
+ return withContext(Dispatchers.Main, block)
+ }
+
+ /**
+ * 切换到IO线程(协程环境)
+ */
+ suspend fun switchToIo(block: suspend CoroutineScope.() -> T): T {
+ return withContext(Dispatchers.IO, block)
+ }
+
+ /**
+ * 是否在主线程
+ */
+ fun isOnMainThread(): Boolean {
+ return Looper.myLooper() == Looper.getMainLooper()
+ }
+
+ /**
+ * 释放资源
+ */
+ fun release() {
+ job.cancel()
+ ioThreadPool.shutdown()
+ cpuThreadPool.shutdown()
+ }
+}
\ No newline at end of file
diff --git a/lib_face/src/main/java/com/sw/plate/utils/ToastUtils.java b/lib_face/src/main/java/com/sw/plate/utils/ToastUtils.java
new file mode 100644
index 0000000..88bdcfc
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/ToastUtils.java
@@ -0,0 +1,120 @@
+package com.sw.plate.utils;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.sw.plate.App;
+import com.sw.plate.R;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+
+/**
+ * The type Toast utils.
+ */
+public class ToastUtils {
+
+ private static Toast toast;
+ @SuppressLint("StaticFieldLeak")
+ private static TextView textCenterView;
+
+ /**
+ * Show center toast.
+ *
+ * @param text the text
+ */
+ public static void showToast(String text) {
+ if (TextUtils.isEmpty(text)) {
+ return;
+ }
+ runOnMainThread(()->{
+ Context context = App.getContext();
+ if (toast == null) {
+ View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
+ textCenterView = view.findViewById(R.id.toast_tv);
+ toast = new Toast(context);
+ toast.setGravity(Gravity.CENTER, 0, 20);
+ toast.setDuration(Toast.LENGTH_SHORT);
+ toast.setView(view);
+ }
+ textCenterView.setText(text);
+ toast.show();
+ });
+ }
+
+ public static void runOnMainThread(Runnable runnable) {
+ new Handler(Looper.getMainLooper()).post(runnable);
+ }
+
+ /**
+ * Show center toast.
+ *
+ * @param text the text
+ */
+ public static void showToast(String text, int duration) {
+ runOnMainThread(()-> {
+ Context context = App.getContext();
+ if (toast == null) {
+ View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
+ textCenterView = view.findViewById(R.id.toast_tv);
+ toast = new Toast(context);
+ toast.setGravity(Gravity.CENTER, 0, 20);
+ toast.setView(view);
+ }
+
+ textCenterView.setText(text);
+ toast.setDuration(duration);
+ toast.show();
+ });
+ }
+
+ public static void showLongToast(String text) {
+ runOnMainThread(()-> {
+ Context context = App.getContext();
+ if (toast == null) {
+ View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
+ textCenterView = view.findViewById(R.id.toast_tv);
+ toast = new Toast(context);
+ toast.setGravity(Gravity.TOP, 0, 20);
+ toast.setDuration(Toast.LENGTH_LONG);
+ toast.setView(view);
+ }
+ textCenterView.setText(text);
+ showMyToast(toast, 1000000 * 30);
+ });
+ }
+
+
+ //自定义Toast控件
+ private static void showMyToast(final Toast toast, final int cnt) {
+ final Timer timer = new Timer();
+ timer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ toast.show();
+ }
+ }, 0, Toast.LENGTH_LONG);
+ new Timer().schedule(new TimerTask() {
+ @Override
+ public void run() {
+ toast.cancel();
+ timer.cancel();
+ }
+ }, cnt);
+ }
+
+ public static void showSystemToast(String text) {
+ Context context = App.getContext();
+ Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
+ }
+
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/BindingUtil.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/BindingUtil.java
new file mode 100644
index 0000000..8a74e0a
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/BindingUtil.java
@@ -0,0 +1,42 @@
+package com.sw.plate.utils.arcface;
+
+import android.content.Context;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+import com.sw.plate.utils.arcface.face.model.CompareResult;
+
+import java.text.SimpleDateFormat;
+import java.util.List;
+
+
+public class BindingUtil {
+ public static void setImagePath(ImageView imageView, String path) {
+ Glide.with(imageView.getContext())
+ .load(path)
+ .into(imageView);
+ }
+
+ public static void setCompareResultList(RecyclerView recyclerView, List compareResultList) {
+ Context context = recyclerView.getContext();
+// FaceSearchResultAdapter adapter = new FaceSearchResultAdapter(compareResultList, context);
+// recyclerView.setAdapter(adapter);
+// DisplayMetrics dm = context.getResources().getDisplayMetrics();
+// int spanCount = dm.widthPixels /
+// (context.getResources().getDimensionPixelSize(R.dimen.item_head_image_padding) * 2 +
+// context.getResources().getDimensionPixelSize(R.dimen.item_image_size));
+// recyclerView.setLayoutManager(new GridLayoutManager(context, spanCount));
+// recyclerView.setItemAnimator(new DefaultItemAnimator());
+ }
+
+ private static final SimpleDateFormat REGISTER_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
+
+ public static void setDate(TextView textView, long date) {
+ synchronized (REGISTER_DATE_FORMAT) {
+ textView.setText(REGISTER_DATE_FORMAT.format(date));
+ }
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/ConfigUtil.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/ConfigUtil.java
new file mode 100644
index 0000000..7a2299c
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/ConfigUtil.java
@@ -0,0 +1,470 @@
+package com.sw.plate.utils.arcface;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import androidx.annotation.StringRes;
+
+import com.arcsoft.face.enums.DetectFaceOrientPriority;
+import com.sw.plate.AppConst;
+import com.sw.plate.R;
+
+/**
+ * 配置项设置,注意,{@link SharedPreferences}对象需要使用{@link PreferenceManager#getDefaultSharedPreferences(Context)},
+ * 以确保和{@link androidx.preference.PreferenceFragmentCompat}操作同一个xml。
+ */
+public class ConfigUtil {
+ /**
+ * 识别阈值
+ */
+ private static final float RECOMMEND_RECOGNIZE_THRESHOLD = 0.80f;
+ /**
+ * 遮挡阈值
+ */
+ private static final float RECOMMEND_SHELTER_THRESHOLD = 0.50f;
+ /**
+ * 眼睛开启阈值
+ */
+ private static final float RECOMMEND_EYE_OPEN_THRESHOLD = 0.50f;
+ /**
+ * 嘴巴闭合阈值
+ */
+ private static final float RECOMMEND_MOUTH_CLOSE_THRESHOLD = 0.50f;
+ /**
+ * 戴眼镜阈值
+ */
+ private static final float RECOMMEND_WEAR_GLASSES_THRESHOLD = 0.50f;
+ /**
+ * 可见光活体检测阈值
+ */
+ private static final float RECOMMEND_RGB_LIVENESS_THRESHOLD = 0.50f;
+ /**
+ * 红外活体检测阈值
+ */
+ private static final float RECOMMEND_IR_LIVENESS_THRESHOLD = 0.70f;
+ /**
+ * 活体 FQ 检测阈值
+ */
+ private static final float RECOMMEND_LIVENESS_FQ_THRESHOLD = 0.65f;
+ /**
+ * 可见光活体模型选择界限
+ */
+ private static final int RECOMMEND_RGB_LIVENESS_FACE_SIZE_THRESHOLD = 80;
+ /**
+ * 可见光活体模型选择界限
+ */
+ private static final int RECOMMEND_IR_LIVENESS_FACE_SIZE_THRESHOLD = 90;
+ /**
+ * 图像质量检测阈值:未戴口罩,且在人脸识别场景下
+ */
+ public static final float IMAGE_QUALITY_NO_MASK_RECOGNIZE_THRESHOLD = 0.49f;
+ /**
+ * 图像质量检测阈值:未戴口罩,且在人脸注册场景下
+ */
+ public static final float IMAGE_QUALITY_NO_MASK_REGISTER_THRESHOLD = 0.63f;
+ /**
+ * 图像质量检测阈值:戴口罩,且在人脸识别场景下
+ */
+ public static final float IMAGE_QUALITY_MASK_RECOGNIZE_THRESHOLD = 0.29f;
+
+ /**
+ * 人脸大小限制
+ */
+ private static final int RECOMMEND_FACE_SIZE_LIMIT = 360;
+ /**
+ * 上下帧人脸移动像素数限制
+ */
+ private static final int RECOMMEND_FACE_MOVE_LIMIT = 20;
+ /**
+ * 默认最大人脸检测数量
+ */
+ private static final int DEFAULT_MAX_DETECT_FACE_NUM = 1;
+ /**
+ * 默认人脸大小占比
+ */
+ private static final int DEFAULT_SCALE = 16;
+ /**
+ * 默认相机分辨率
+ */
+// private static final String DEFAULT_PREVIEW_SIZE = "1280x720";
+// private static final String DEFAULT_PREVIEW_SIZE = "1080x720";
+// private static final String DEFAULT_PREVIEW_SIZE = "720x1080";
+// private static final String DEFAULT_PREVIEW_SIZE = "720x1280";
+// private static final String DEFAULT_PREVIEW_SIZE = "1024x768";
+ private static final String DEFAULT_PREVIEW_SIZE = "1024x768";
+// private static final String DEFAULT_PREVIEW_SIZE = "800x600";
+// private static final String DEFAULT_PREVIEW_SIZE = "640x480";
+// private static final String DEFAULT_PREVIEW_SIZE = "640x480";
+
+
+ /**
+ * 获取String类型的preference
+ *
+ * @param context 上下文
+ * @param keyRes key的Id
+ * @param defaultValue 默认值
+ * @return preference值
+ */
+ private static String getString(Context context, @StringRes int keyRes, String defaultValue) {
+ if (context == null) {
+ return defaultValue;
+ }
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ String key = context.getString(keyRes);
+ return sharedPreferences.getString(key, defaultValue);
+ }
+
+ /**
+ * 获取boolean类型的preference
+ *
+ * @param context 上下文
+ * @param keyRes key的Id
+ * @param defaultValue 默认值
+ * @return preference值
+ */
+ private static boolean getBoolean(Context context, @StringRes int keyRes, boolean defaultValue) {
+ if (context == null) {
+ return defaultValue;
+ }
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ String key = context.getString(keyRes);
+ return sharedPreferences.getBoolean(key, defaultValue);
+ }
+
+ /**
+ * 获取int类型的preference
+ *
+ * @param context 上下文
+ * @param keyRes key的Id
+ * @param defaultValue 默认值
+ * @return preference值
+ */
+ private static int getInt(Context context, @StringRes int keyRes, int defaultValue) {
+ if (context == null) {
+ return defaultValue;
+ }
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ String key = context.getString(keyRes);
+ return sharedPreferences.getInt(key, defaultValue);
+ }
+
+ /**
+ * 获取float类型的preference
+ *
+ * @param context 上下文
+ * @param keyRes key的Id
+ * @param defaultValue 默认值
+ * @return preference值
+ */
+ private static float getFloat(Context context, @StringRes int keyRes, float defaultValue) {
+ if (context == null) {
+ return defaultValue;
+ }
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ String key = context.getString(keyRes);
+ return sharedPreferences.getFloat(key, defaultValue);
+ }
+
+ /**
+ * 保存int类型的preference
+ *
+ * @param context 上下文
+ * @param keyRes key的Id
+ * @param newValue key对应的value
+ * @return 是否保存成功
+ */
+ private static boolean commitInt(Context context, @StringRes int keyRes, int newValue) {
+ if (context == null) {
+ return false;
+ }
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ return sharedPreferences.edit()
+ .putInt(context.getString(keyRes), newValue)
+ .commit();
+ }
+
+ /**
+ * 保存String类型的preference
+ *
+ * @param context 上下文
+ * @param keyRes key的Id
+ * @param newValue key对应的value
+ * @return 是否保存成功
+ */
+ private static boolean commitString(Context context, @StringRes int keyRes, String newValue) {
+ if (context == null) {
+ return false;
+ }
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ return sharedPreferences.edit()
+ .putString(context.getString(keyRes), newValue)
+ .commit();
+ }
+
+ /**
+ * 设置截至目前已track到的人脸数
+ *
+ * @param context 上下文
+ * @param trackedFaceCount 截至目前已track到的人脸数
+ * @return 是否保存成功
+ */
+ public static boolean setTrackedFaceCount(Context context, int trackedFaceCount) {
+ if (context == null) {
+ return false;
+ }
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ return sharedPreferences.edit()
+ .putInt(context.getString(R.string.preference_track_face_count), trackedFaceCount)
+ .commit();
+ }
+
+ /**
+ * 获取到截至目前已track到的人脸数
+ *
+ * @param context 上下文
+ * @return 之前已track到的人脸数
+ */
+ public static int getTrackedFaceCount(Context context) {
+ return getInt(context, R.string.preference_track_face_count, 0);
+ }
+
+ /**
+ * 获取VIDEO模式人脸检测角度优先级
+ *
+ * @param context 上下文
+ * @return VIDEO模式人脸检测角度优先级
+ */
+ public static DetectFaceOrientPriority getFtOrient(Context context) {
+ if (context == null) {
+ return DetectFaceOrientPriority.ASF_OP_ALL_OUT;
+ }
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ return DetectFaceOrientPriority.valueOf(sharedPreferences.getString(context.getString(R.string.preference_choose_detect_degree), DetectFaceOrientPriority.ASF_OP_ALL_OUT.name()));
+ }
+
+ /**
+ * TODO: 该Demo基于单人脸识别实现,若想使用多人脸识别,请将 return true 改成 return getBoolean,并修改相关配置项的preference.xml和业务代码
+ *
+ * 获取识别界面是否保留最大人脸
+ *
+ * @param context 上下文
+ * @return 别界面是否保留最大人脸
+ */
+ public static boolean isKeepMaxFace(Context context) {
+// return getBoolean(context, R.string.preference_recognize_keep_max_face, false);
+ return true;
+ }
+
+ /**
+ * 获取是否限制人脸识别区域
+ *
+ * @param context 上下文
+ * @return 是否限制人脸识别区域
+ */
+ public static boolean isRecognizeAreaLimited(Context context) {
+ return getBoolean(context, R.string.preference_recognize_limit_recognize_area, false);
+ }
+
+ /**
+ * 视频人脸比对界面中,获取最大的人脸检测数量
+ *
+ * @param context 上下文
+ * @return 最大的人脸检测数量
+ */
+ public static int getRecognizeMaxDetectFaceNum(Context context) {
+ try {
+ return Integer.parseInt(getString(context, R.string.preference_recognize_max_detect_num, String.valueOf(DEFAULT_MAX_DETECT_FACE_NUM)));
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ }
+ return DEFAULT_MAX_DETECT_FACE_NUM;
+ }
+
+ /**
+ * 视频人脸比对界面中,获取预先设置的scale值
+ *
+ * @param context 上下文
+ * @return scale值
+ */
+ public static int getRecognizeScale(Context context) {
+ try {
+ return Integer.parseInt(getString(context, R.string.preference_recognize_scale_value, String.valueOf(DEFAULT_SCALE)));
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ }
+ return DEFAULT_SCALE;
+ }
+
+ /**
+ * 获取双目水平成像偏移量
+ *
+ * @param context 上下文
+ * @return 双目水平偏移量
+ */
+ public static int getDualCameraHorizontalOffset(Context context) {
+ return getInt(context, R.string.preference_dual_camera_offset_horizontal, 0);
+ }
+
+ /**
+ * 获取双目垂直成像偏移量
+ *
+ * @param context 上下文
+ * @return 双目水平偏移量
+ */
+ public static int getDualCameraVerticalOffset(Context context) {
+ return getInt(context, R.string.preference_dual_camera_offset_vertical, 0);
+ }
+
+ /**
+ * 视频人脸比对界面中,获取预先设置的识别阈值
+ *
+ * @param context 上下文
+ * @return 识别阈值
+ */
+ public static float getRecognizeThreshold(Context context) {
+ return Float.parseFloat(getString(context, R.string.preference_recognize_threshold, String.valueOf(RECOMMEND_RECOGNIZE_THRESHOLD)));
+ }
+
+ public static float getRecognizeShelterThreshold(Context context) {
+ return Float.parseFloat(getString(context, R.string.preference_shelter_threshold, String.valueOf(RECOMMEND_SHELTER_THRESHOLD)));
+ }
+
+ public static float getRecognizeEyeOpenThreshold(Context context) {
+ return Float.parseFloat(getString(context, R.string.preference_eye_open_threshold, String.valueOf(RECOMMEND_EYE_OPEN_THRESHOLD)));
+ }
+
+ public static float getRecognizeMouthCloseThreshold(Context context) {
+ return Float.parseFloat(getString(context, R.string.preference_mouth_close_threshold, String.valueOf(RECOMMEND_MOUTH_CLOSE_THRESHOLD)));
+ }
+
+ public static float getRecognizeWearGlassesThreshold(Context context) {
+ return Float.parseFloat(getString(context, R.string.preference_wear_glasses_threshold, String.valueOf(RECOMMEND_WEAR_GLASSES_THRESHOLD)));
+ }
+
+ public static float getRgbLivenessThreshold(Context context) {
+ return Float.parseFloat(getString(context, R.string.preference_rgb_liveness_threshold, String.valueOf(RECOMMEND_RGB_LIVENESS_THRESHOLD)));
+ }
+
+ public static float getIrLivenessThreshold(Context context) {
+ return Float.parseFloat(getString(context, R.string.preference_ir_liveness_threshold, String.valueOf(RECOMMEND_IR_LIVENESS_THRESHOLD)));
+ }
+
+ public static float getLivenessFqThreshold(Context context) {
+ return Float.parseFloat(getString(context, R.string.preference_liveness_fq_threshold, String.valueOf(RECOMMEND_LIVENESS_FQ_THRESHOLD)));
+ }
+
+ public static int getRgbLivenessFaceSizeThreshold(Context context) {
+ return Integer.parseInt(getString(context, R.string.preference_rgb_liveness_face_size_threshold, String.valueOf(RECOMMEND_RGB_LIVENESS_FACE_SIZE_THRESHOLD)));
+ }
+
+ public static int getIrLivenessFaceSizeThreshold(Context context) {
+ return Integer.parseInt(getString(context, R.string.preference_ir_liveness_face_size_threshold, String.valueOf(RECOMMEND_IR_LIVENESS_FACE_SIZE_THRESHOLD)));
+ }
+
+ public static float getImageQualityNoMaskRecognizeThreshold(Context context) {
+ return Float.parseFloat(getString(context, R.string.preference_image_quality_no_mask_recognize_threshold,
+ String.valueOf(IMAGE_QUALITY_NO_MASK_RECOGNIZE_THRESHOLD)));
+ }
+
+ public static float getImageQualityNoMaskRegisterThreshold(Context context) {
+ return Float.parseFloat(getString(context, R.string.preference_image_quality_no_mask_register_threshold,
+ String.valueOf(IMAGE_QUALITY_NO_MASK_REGISTER_THRESHOLD)));
+ }
+
+ public static float getImageQualityMaskRecognizeThreshold(Context context) {
+ return Float.parseFloat(getString(context, R.string.preference_image_quality_mask_recognize_threshold,
+ String.valueOf(IMAGE_QUALITY_MASK_RECOGNIZE_THRESHOLD)));
+ }
+
+ public static int getFaceSizeLimit(Context context) {
+ return Integer.parseInt(getString(context, R.string.preference_recognize_face_size_limit, String.valueOf(RECOMMEND_FACE_SIZE_LIMIT)));
+ }
+
+ public static int getFaceMoveLimit(Context context) {
+ return Integer.parseInt(getString(context, R.string.preference_recognize_move_pixel_limit, String.valueOf(RECOMMEND_FACE_MOVE_LIMIT)));
+ }
+
+ public static String getLivenessDetectType(Context context) {
+ return getString(context, R.string.preference_liveness_detect_type, context.getString(R.string.value_liveness_type_rgb));
+ }
+
+
+ public static boolean isEnableImageQualityDetect(Context context) {
+ return getBoolean(context, R.string.preference_enable_image_quality_detect, true);
+ }
+
+ public static boolean isEnableFaceSizeLimit(Context context) {
+ return getBoolean(context, R.string.preference_enable_face_size_limit, false);
+ }
+
+ public static boolean isEnableFaceMoveLimit(Context context) {
+ return getBoolean(context, R.string.preference_enable_face_move_limit, false);
+ }
+
+ public static boolean isSwitchCamera(Context context) {
+ return getBoolean(context, R.string.preference_switch_camera, false);
+ }
+
+ public static String getPreviewSize(Context context) {
+ return getString(context, R.string.preference_dual_camera_preview_size, DEFAULT_PREVIEW_SIZE);
+ }
+
+ public static String getRgbCameraAdditionalRotation(Context context) {
+ return getString(context, R.string.preference_rgb_camera_rotation, "0");
+ }
+
+ public static String getIrCameraAdditionalRotation(Context context) {
+ return getString(context, R.string.preference_ir_camera_rotation, "0");
+ }
+
+ public static String getAppId(Context context) {
+ return getString(context, R.string.preference_app_id, AppConst.ARCSOFT_APP_ID);
+ }
+
+ public static String getSdkKey(Context context) {
+ return getString(context, R.string.preference_sdk_key, AppConst.ARCSOFT_SDK_KEY);
+ }
+
+ public static String getActiveKey(Context context) {
+ return getString(context, R.string.preference_active_key, AppConst.ARCSOFT_ACTIVE_KEY);
+ }
+
+ public static boolean commitAppId(Context context, String appId) {
+ return commitString(context, R.string.preference_app_id, appId);
+ }
+
+ public static boolean commitSdkKey(Context context, String sdkKey) {
+ return commitString(context, R.string.preference_sdk_key, sdkKey);
+ }
+
+ public static boolean commitActiveKey(Context context, String activeKey) {
+ return commitString(context, R.string.preference_active_key, activeKey);
+ }
+
+
+ public static boolean isDrawRgbRectHorizontalMirror(Context context) {
+ return getBoolean(context, R.string.preference_draw_rgb_rect_horizontal_mirror, false);
+ }
+
+ public static boolean isDrawIrRectHorizontalMirror(Context context) {
+ return getBoolean(context, R.string.preference_draw_ir_rect_horizontal_mirror, false);
+ }
+
+ public static boolean isDrawRgbRectVerticalMirror(Context context) {
+ return getBoolean(context, R.string.preference_draw_rgb_rect_vertical_mirror, false);
+ }
+
+ public static boolean isDrawIrRectVerticalMirror(Context context) {
+ return getBoolean(context, R.string.preference_draw_ir_rect_vertical_mirror, false);
+ }
+
+ public static boolean isDrawRgbPreviewHorizontalMirror(Context context) {
+ return getBoolean(context, R.string.preference_rgb_preview_horizontal_mirror, false);
+ }
+
+ public static boolean isDrawIrPreviewHorizontalMirror(Context context) {
+ return getBoolean(context, R.string.preference_ir_preview_horizontal_mirror, false);
+ }
+
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/ErrorCodeUtil.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/ErrorCodeUtil.java
new file mode 100644
index 0000000..c8ba88a
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/ErrorCodeUtil.java
@@ -0,0 +1,51 @@
+package com.sw.plate.utils.arcface;
+
+import com.arcsoft.face.ErrorInfo;
+import com.arcsoft.imageutil.ArcSoftImageUtilError;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+
+public class ErrorCodeUtil {
+ /**
+ * 将ArcFace错误码转换为对应的错误码常量名,便于理解
+ * TODO:目前每次都遍历,如果使用频繁,建议将Field缓存处理,避免每次都反射
+ *
+ * @param code 错误码
+ * @return 错误码常量名
+ */
+ public static String arcFaceErrorCodeToFieldName(int code) {
+ Field[] declaredFields = ErrorInfo.class.getDeclaredFields();
+ for (Field declaredField : declaredFields) {
+ try {
+ if (Modifier.isFinal(declaredField.getModifiers()) && ((int) declaredField.get(ErrorInfo.class)) == code) {
+ return declaredField.getName();
+ }
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+ return "unknown error";
+ }
+
+ /**
+ * 将ArcSoftImageUtil错误码转换为对应的错误码常量名,便于理解
+ * TODO:目前每次都遍历,如果使用频繁,建议将Field缓存处理,避免每次都反射
+ *
+ * @param code 错误码
+ * @return 错误码常量名
+ */
+ public static String imageUtilErrorCodeToFieldName(int code) {
+ Field[] declaredFields = ArcSoftImageUtilError.class.getDeclaredFields();
+ for (Field declaredField : declaredFields) {
+ try {
+ if (Modifier.isFinal(declaredField.getModifiers()) && ((int) declaredField.get(ArcSoftImageUtilError.class)) == code) {
+ return declaredField.getName();
+ }
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+ return "unknown error";
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java
new file mode 100644
index 0000000..4b3753e
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java
@@ -0,0 +1,95 @@
+package com.sw.plate.utils.arcface;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.arcsoft.face.FaceEngine;
+import com.arcsoft.face.enums.RuntimeABI;
+import com.sw.plate.App;
+import com.sw.plate.utils.L;
+import com.sw.plate.utils.arcface.face.model.FacePreviewInfo;
+import com.sw.plate.utils.arcface.facedb.FaceDatabase;
+import com.sw.plate.utils.arcface.facedb.dao.FaceDao;
+import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
+
+import java.util.List;
+
+public class FaceApi {
+ private static final String TAG = "FaceApi";
+
+ public interface ActiveCallback {
+ void onSuccess(int code);
+
+ void onFail(Exception e);
+ }
+
+ /**
+ * 更新人脸数据
+ *
+ * @param index
+ * @param list
+ */
+ public void updateFaceData(int index, List list) {
+ Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size());
+ FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
+ if (index == 1) {
+ faceDao.deleteAll();
+ faceDao.resetId();
+ }
+ List insertIdList = faceDao.insert(list);
+ Log.d(TAG, "updateFaceData: count = " + insertIdList.size());
+ List queryList = faceDao.getAllFaces();
+ Log.d(TAG, "updateFaceData: queryList.size = " + queryList.size());
+ }
+
+ public void updateFaceData2(int index, List list) {
+ Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size());
+ FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
+ if (index == 0) {
+ faceDao.deleteAll();
+ faceDao.resetId();
+ }
+ faceDao.insert(list);
+ }
+
+ /**
+ * 激活arcsoft 人脸
+ *
+ * @param context
+ * @param arcsoftAppId
+ * @param arcsoftSdkKey
+ * @param arcsoftActiveKey
+ * @param callback
+ */
+ public void activeEngine(Context context, String arcsoftAppId,
+ String arcsoftSdkKey,
+ String arcsoftActiveKey, ActiveCallback callback) {
+ new Thread(() -> {
+ try {
+ RuntimeABI runtimeABI = FaceEngine.getRuntimeABI();
+ L.e("subscribe: getRuntimeABI() " + runtimeABI);
+
+ long start = System.currentTimeMillis();
+ int activeCode = FaceEngine.activeOnline(context, arcsoftActiveKey,
+ arcsoftAppId, arcsoftSdkKey);
+ L.e("subscribe cost: " + (System.currentTimeMillis() - start));
+ callback.onSuccess(activeCode);
+ } catch (Exception e) {
+ callback.onFail(e);
+ }
+ }).start();
+ }
+
+ /**
+ * 传入可见光相机预览数据
+ *
+ * @param nv21 可见光相机预览数据
+ * @param doRecognize 是否进行识别
+ * @return 当前帧的检测结果信息
+ */
+ public List onPreviewFrame(byte[] nv21, boolean doRecognize) {
+
+// List facePreviewInfoList = recognizeViewModel.onPreviewFrame(nv21, true);
+ return null;
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectTransformer.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectTransformer.java
new file mode 100644
index 0000000..7b34137
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectTransformer.java
@@ -0,0 +1,238 @@
+package com.sw.plate.utils.arcface;
+
+import android.graphics.Rect;
+import android.hardware.Camera;
+
+import com.sw.plate.utils.L;
+
+/**
+ * 将检测回传的人脸框(基于NV21数据)转换为View绘制(基于View)所需的人脸框
+ */
+public class FaceRectTransformer {
+ private int previewWidth, previewHeight, canvasWidth, canvasHeight, cameraDisplayOrientation, cameraId;
+ private boolean isMirror;
+ private boolean mirrorHorizontal = false, mirrorVertical = false;
+
+ /**
+ * 创建一个绘制辅助类对象,并且设置绘制相关的参数
+ *
+ * @param previewWidth 预览宽度
+ * @param previewHeight 预览高度
+ * @param canvasWidth 绘制控件的宽度
+ * @param canvasHeight 绘制控件的高度
+ * @param cameraDisplayOrientation 旋转角度
+ * @param cameraId 相机ID
+ * @param isMirror 是否水平镜像显示(若相机是镜像显示的,设为true,用于纠正)
+ * @param mirrorHorizontal 为兼容部分设备使用,水平再次镜像
+ * @param mirrorVertical 为兼容部分设备使用,垂直再次镜像
+ */
+ public FaceRectTransformer(int previewWidth, int previewHeight, int canvasWidth,
+ int canvasHeight, int cameraDisplayOrientation, int cameraId,
+ boolean isMirror, boolean mirrorHorizontal, boolean mirrorVertical) {
+ this.previewWidth = previewWidth;
+ this.previewHeight = previewHeight;
+ this.canvasWidth = canvasWidth;
+ this.canvasHeight = canvasHeight;
+ this.cameraDisplayOrientation = cameraDisplayOrientation;
+ this.cameraId = cameraId;
+ this.isMirror = isMirror;
+ this.mirrorHorizontal = mirrorHorizontal;
+ this.mirrorVertical = mirrorVertical;
+ }
+
+ /**
+ * 调整人脸框用来绘制
+ *
+ * @param ftRect FT人脸框
+ * @return 调整后的需要被绘制到View上的rect
+ */
+ public Rect adjustRect(Rect ftRect) {
+ int previewWidth = this.previewWidth;
+ int previewHeight = this.previewHeight;
+ int canvasWidth = this.canvasWidth;
+ int canvasHeight = this.canvasHeight;
+ int cameraDisplayOrientation = this.cameraDisplayOrientation;
+ int cameraId = this.cameraId;
+ boolean isMirror = this.isMirror;
+ boolean mirrorHorizontal = this.mirrorHorizontal;
+ boolean mirrorVertical = this.mirrorVertical;
+
+ if (ftRect == null) {
+ return null;
+ }
+
+ Rect rect = new Rect(ftRect);
+ float horizontalRatio;
+ float verticalRatio;
+ if (cameraDisplayOrientation % 180 == 0) {
+ horizontalRatio = (float) canvasWidth / (float) previewWidth;
+ verticalRatio = (float) canvasHeight / (float) previewHeight;
+ } else {
+ horizontalRatio = (float) canvasHeight / (float) previewWidth;
+ verticalRatio = (float) canvasWidth / (float) previewHeight;
+ }
+ rect.left *= horizontalRatio;
+ rect.right *= horizontalRatio;
+ rect.top *= verticalRatio;
+ rect.bottom *= verticalRatio;
+
+ Rect newRect = new Rect();
+// L.e("cameraDisplayOrientation " + cameraDisplayOrientation + " === " + cameraId);
+ switch (cameraDisplayOrientation) {
+ case 0:
+ if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ newRect.left = canvasWidth - rect.right;
+ newRect.right = canvasWidth - rect.left;
+
+// newRect.left = rect.left;
+// newRect.right = rect.right;
+ } else {
+ newRect.left = rect.left;
+ newRect.right = rect.right;
+
+// newRect.left = canvasWidth - rect.right;
+// newRect.right = canvasWidth - rect.left;
+ }
+ newRect.top = rect.top;
+ newRect.bottom = rect.bottom;
+ break;
+ case 90:
+ newRect.right = canvasWidth - rect.top;
+ newRect.left = canvasWidth - rect.bottom;
+ if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ newRect.top = canvasHeight - rect.right;
+ newRect.bottom = canvasHeight - rect.left;
+ } else {
+ newRect.top = rect.left;
+ newRect.bottom = rect.right;
+ }
+ break;
+ case 180:
+ newRect.top = canvasHeight - rect.bottom;
+ newRect.bottom = canvasHeight - rect.top;
+ if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ newRect.left = rect.left;
+ newRect.right = rect.right;
+ } else {
+ newRect.left = canvasWidth - rect.right;
+ newRect.right = canvasWidth - rect.left;
+
+// newRect.left = rect.left;
+// newRect.right = rect.right;
+ }
+ break;
+ case 270:
+// newRect.left = rect.top;
+// newRect.right = rect.bottom;
+
+ newRect.left = canvasWidth - rect.right;
+ newRect.right = canvasWidth - rect.left;
+
+ if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ newRect.top = canvasHeight - rect.right;
+ newRect.bottom = canvasHeight - rect.left;
+ } else {
+ newRect.top = rect.left;
+ newRect.bottom = rect.right;
+ }
+ break;
+ default:
+ break;
+ }
+
+ /**
+ * isMirror mirrorHorizontal finalIsMirrorHorizontal
+ * true true false
+ * false false false
+ * true false true
+ * false true true
+ *
+ * XOR
+ */
+ if (isMirror ^ mirrorHorizontal) {
+ int left = newRect.left;
+ int right = newRect.right;
+ newRect.left = canvasWidth - right;
+ newRect.right = canvasWidth - left;
+ }
+ if (mirrorVertical) {
+ int top = newRect.top;
+ int bottom = newRect.bottom;
+ newRect.top = canvasHeight - bottom;
+ newRect.bottom = canvasHeight - top;
+ }
+ return newRect;
+ }
+
+ public void setPreviewWidth(int previewWidth) {
+ this.previewWidth = previewWidth;
+ }
+
+ public void setPreviewHeight(int previewHeight) {
+ this.previewHeight = previewHeight;
+ }
+
+ public void setCanvasWidth(int canvasWidth) {
+ this.canvasWidth = canvasWidth;
+ }
+
+ public void setCanvasHeight(int canvasHeight) {
+ this.canvasHeight = canvasHeight;
+ }
+
+ public void setCameraDisplayOrientation(int cameraDisplayOrientation) {
+ this.cameraDisplayOrientation = cameraDisplayOrientation;
+ }
+
+ public void setCameraId(int cameraId) {
+ this.cameraId = cameraId;
+ }
+
+ public void setMirror(boolean mirror) {
+ isMirror = mirror;
+ }
+
+ public int getPreviewWidth() {
+ return previewWidth;
+ }
+
+ public int getPreviewHeight() {
+ return previewHeight;
+ }
+
+ public int getCanvasWidth() {
+ return canvasWidth;
+ }
+
+ public int getCanvasHeight() {
+ return canvasHeight;
+ }
+
+ public int getCameraDisplayOrientation() {
+ return cameraDisplayOrientation;
+ }
+
+ public int getCameraId() {
+ return cameraId;
+ }
+
+ public boolean isMirror() {
+ return isMirror;
+ }
+
+ public boolean isMirrorHorizontal() {
+ return mirrorHorizontal;
+ }
+
+ public void setMirrorHorizontal(boolean mirrorHorizontal) {
+ this.mirrorHorizontal = mirrorHorizontal;
+ }
+
+ public boolean isMirrorVertical() {
+ return mirrorVertical;
+ }
+
+ public void setMirrorVertical(boolean mirrorVertical) {
+ this.mirrorVertical = mirrorVertical;
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectView.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectView.java
new file mode 100644
index 0000000..c58ba6e
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectView.java
@@ -0,0 +1,298 @@
+package com.sw.plate.utils.arcface;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.arcsoft.face.FaceAttributeInfo;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * 用于显示人脸信息的控件
+ */
+public class FaceRectView extends View {
+ private CopyOnWriteArrayList drawInfoList = new CopyOnWriteArrayList<>();
+
+ // 画笔,复用
+ private Paint paint;
+
+ // 默认人脸框厚度
+ private static final int DEFAULT_FACE_RECT_THICKNESS = 6;
+
+ public FaceRectView(Context context) {
+ this(context, null);
+ }
+
+ public FaceRectView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ paint = new Paint();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (drawInfoList != null && drawInfoList.size() > 0) {
+ for (int i = 0; i < drawInfoList.size(); i++) {
+ drawFaceRect(canvas, drawInfoList.get(i), DEFAULT_FACE_RECT_THICKNESS, paint);
+ }
+ }
+ }
+
+ public void clearFaceInfo() {
+ drawInfoList.clear();
+ postInvalidate();
+ }
+
+ public void addFaceInfo(DrawInfo faceInfo) {
+ drawInfoList.add(faceInfo);
+ postInvalidate();
+ }
+
+ public void addFaceInfo(List faceInfoList) {
+ drawInfoList.addAll(faceInfoList);
+ postInvalidate();
+ }
+
+ public void drawRealtimeFaceInfo(List drawInfoList) {
+ clearFaceInfo();
+ if (drawInfoList == null || drawInfoList.size() == 0) {
+ return;
+ }
+ addFaceInfo(drawInfoList);
+ }
+
+ public static class DrawInfo {
+ private Rect rect;
+ private int sex;
+ private int age;
+ private int liveness;
+ private int color;
+ private int isWithinBoundary;
+ private String name = null;
+ private boolean drawRectInfo;
+ private Rect foreheadRect;
+ private FaceAttributeInfo faceAttributeInfo;
+ private boolean rgbRect;
+
+ public DrawInfo(Rect rect, int sex, int age, int liveness, int color, String name) {
+ this.rect = rect;
+ this.sex = sex;
+ this.age = age;
+ this.liveness = liveness;
+ this.color = color;
+ this.name = name;
+ }
+
+ public DrawInfo(Rect rect, int sex, int age, int liveness, int color, String name, int isWithinBoundary, Rect foreheadRect,
+ FaceAttributeInfo faceAttributeInfo, boolean drawRectInfo, boolean rgbRect) {
+ this.rect = rect;
+ this.sex = sex;
+ this.age = age;
+ this.liveness = liveness;
+ this.color = color;
+ this.name = name;
+ this.isWithinBoundary = isWithinBoundary;
+ this.drawRectInfo = drawRectInfo;
+ this.foreheadRect = foreheadRect;
+ this.faceAttributeInfo = faceAttributeInfo;
+ this.rgbRect = rgbRect;
+ }
+
+ public DrawInfo(DrawInfo drawInfo) {
+ if (drawInfo == null) {
+ return;
+ }
+ this.rect = drawInfo.rect;
+ this.sex = drawInfo.sex;
+ this.age = drawInfo.age;
+ this.liveness = drawInfo.liveness;
+ this.color = drawInfo.color;
+ this.name = drawInfo.name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Rect getRect() {
+ return rect;
+ }
+
+ public void setRect(Rect rect) {
+ this.rect = rect;
+ }
+
+ public int getSex() {
+ return sex;
+ }
+
+ public void setSex(int sex) {
+ this.sex = sex;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+
+ public int getLiveness() {
+ return liveness;
+ }
+
+ public void setLiveness(int liveness) {
+ this.liveness = liveness;
+ }
+
+ public int getColor() {
+ return color;
+ }
+
+ public void setColor(int color) {
+ this.color = color;
+ }
+
+ public boolean isDrawRectInfo() {
+ return drawRectInfo;
+ }
+
+ public void setDrawRectInfo(boolean drawRectInfo) {
+ this.drawRectInfo = drawRectInfo;
+ }
+
+ public Rect getForeheadRect() {
+ return foreheadRect;
+ }
+
+ public void setForeheadRect(Rect foreheadRect) {
+ this.foreheadRect = foreheadRect;
+ }
+
+ public FaceAttributeInfo getFaceAttributeInfo() {
+ return faceAttributeInfo;
+ }
+
+ public void setFaceAttributeInfo(FaceAttributeInfo faceAttributeInfo) {
+ this.faceAttributeInfo = faceAttributeInfo;
+ }
+
+ public int getIsWithinBoundary() {
+ return isWithinBoundary;
+ }
+
+ public void setIsWithinBoundary(int isWithinBoundary) {
+ this.isWithinBoundary = isWithinBoundary;
+ }
+ }
+
+
+ /**
+ * 绘制数据信息到view上,若 {@link DrawInfo#getName()} 不为null则绘制 {@link DrawInfo#getName()}
+ *
+ * @param canvas 需要被绘制的view的canvas
+ * @param drawInfo 绘制信息
+ * @param faceRectThickness 人脸框厚度
+ * @param paint 画笔
+ */
+ private static void drawFaceRect(Canvas canvas, DrawInfo drawInfo, int faceRectThickness, Paint paint) {
+ if (canvas == null || drawInfo == null) {
+ return;
+ }
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(faceRectThickness);
+ paint.setColor(drawInfo.getColor());
+// paint.setColor(Color.parseColor("#FF0000"));
+ paint.setAntiAlias(true);
+
+ Path mPath = new Path();
+ // 左上
+ Rect rect = drawInfo.getRect();
+ mPath.moveTo(rect.left, rect.top + rect.height() / 4);
+ mPath.lineTo(rect.left, rect.top);
+ mPath.lineTo(rect.left + rect.width() / 4, rect.top);
+ // 右上
+ mPath.moveTo(rect.right - rect.width() / 4, rect.top);
+ mPath.lineTo(rect.right, rect.top);
+ mPath.lineTo(rect.right, rect.top + rect.height() / 4);
+ // 右下
+ mPath.moveTo(rect.right, rect.bottom - rect.height() / 4);
+ mPath.lineTo(rect.right, rect.bottom);
+ mPath.lineTo(rect.right - rect.width() / 4, rect.bottom);
+ // 左下
+ mPath.moveTo(rect.left + rect.width() / 4, rect.bottom);
+ mPath.lineTo(rect.left, rect.bottom);
+ mPath.lineTo(rect.left, rect.bottom - rect.height() / 4);
+ canvas.drawPath(mPath, paint);
+
+ // 绘制文字,用最细的即可,避免在某些低像素设备上文字模糊
+// paint.setStrokeWidth(1);
+//
+// if (drawInfo.getName() == null) {
+// paint.setStyle(Paint.Style.FILL_AND_STROKE);
+// paint.setTextSize(rect.width() / 12);
+// String str = (drawInfo.getSex() == GenderInfo.MALE ? "MALE" : (drawInfo.getSex() == GenderInfo.FEMALE ? "FEMALE" : "UNKNOWN"))
+// + ","
+// + (drawInfo.getAge() == AgeInfo.UNKNOWN_AGE ? "UNKNOWN" : drawInfo.getAge())
+// + ","
+// + (drawInfo.getLiveness() == LivenessInfo.ALIVE ? "ALIVE" : (drawInfo.getLiveness() == LivenessInfo.NOT_ALIVE ? "NOT_ALIVE" : "UNKNOWN"));
+// canvas.drawText(str, rect.left, rect.top - 10, paint);
+// } else {
+// paint.setStyle(Paint.Style.FILL_AND_STROKE);
+// paint.setTextSize(rect.width() / 12);
+// canvas.drawText(drawInfo.getName(), rect.left, rect.top - 10, paint);
+// }
+
+// if (drawInfo.drawRectInfo && drawInfo.rgbRect) {
+// Rect foreRect = drawInfo.foreheadRect;
+// if (foreRect != null) {
+// Path forePath = new Path();
+// forePath.moveTo(foreRect.left, foreRect.top);
+// forePath.lineTo(foreRect.right, foreRect.top);
+// forePath.lineTo(foreRect.right, foreRect.bottom);
+// forePath.lineTo(foreRect.left, foreRect.bottom);
+// forePath.lineTo(foreRect.left, foreRect.top);
+// paint.setStyle(Paint.Style.STROKE);
+// paint.setStrokeWidth(3);
+// canvas.drawPath(forePath, paint);
+// }
+//
+// FaceAttributeInfo attributeInfo = drawInfo.getFaceAttributeInfo();
+// if (attributeInfo != null) {
+// paint.setStyle(Paint.Style.FILL_AND_STROKE);
+// int textSize = rect.width() / 8;
+// paint.setStrokeWidth(1);
+// paint.setTextSize(textSize);
+// int defX = rect.left;
+// int defY = rect.bottom + rect.width() / 8;
+//
+// String strInfo0 = "isWithinBoundary: " + drawInfo.getIsWithinBoundary();
+// canvas.drawText(strInfo0, defX, defY, paint);
+//
+// String strInfo1 = "WearGlasses: " + attributeInfo.getWearGlasses();
+// canvas.drawText(strInfo1, defX, defY + textSize, paint);
+//
+// String strInfo2 = "EyeOpen: [" + attributeInfo.getLeftEyeOpen() + "," + attributeInfo.getRightEyeOpen() + "]";
+// canvas.drawText(strInfo2, rect.left, defY + textSize * 2, paint);
+//
+// String strInfo3 = "MouseClose: " + attributeInfo.getMouthClose();
+// canvas.drawText(strInfo3, rect.left, defY + textSize * 3, paint);
+// }
+// }
+ }
+
+}
\ No newline at end of file
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/FileUtil.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/FileUtil.java
new file mode 100644
index 0000000..e0efc7e
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/FileUtil.java
@@ -0,0 +1,71 @@
+package com.sw.plate.utils.arcface;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class FileUtil {
+ /**
+ * 读取文件中的数据内容
+ *
+ * @param file 文件
+ * @return 二进制数据内容
+ */
+ public static byte[] fileToData(File file) {
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(file);
+ byte[] data = new byte[fis.available()];
+ fis.read(data);
+ fis.close();
+ return data;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static boolean saveDataToFile(byte[] data, File file, boolean append) {
+ if (data == null) {
+ return false;
+ }
+ File parentFile = file.getParentFile();
+ if (parentFile == null) {
+ return false;
+ }
+ if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
+ return false;
+ }
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(file, append);
+ int bufferSize = 1024;
+ int index = 0;
+ while (index < data.length) {
+ if (data.length - index < bufferSize) {
+ bufferSize = data.length - index;
+ }
+ fos.write(data, index, bufferSize);
+ index += bufferSize;
+ }
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ } finally {
+ try {
+ if (fos != null) {
+ fos.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public static boolean saveDataToFile(byte[] data, File file) {
+ return saveDataToFile(data, file, false);
+ }
+
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/ImageUtil.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/ImageUtil.java
new file mode 100644
index 0000000..0c8fcdd
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/ImageUtil.java
@@ -0,0 +1,229 @@
+package com.sw.plate.utils.arcface;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+import android.net.Uri;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ImageUtil {
+ public static final int DEFAULT_MAX_WIDTH = 1920;
+ public static final int DEFAULT_MAX_HEIGHT = 1080;
+
+
+ private static final int MASK_A = 0xFF000000;
+ private static final int MASK_R = 0x00FF0000;
+ private static final int MASK_G = 0x0000FF00;
+ private static final int MASK_B = 0x000000FF;
+
+ public static int rgbToY(int r, int g, int b) {
+ return (((66 * r + 129 * g + 25 * b + 128) >> 8) + 16);
+ }
+
+ public static int rgbToU(int r, int g, int b) {
+ return (((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128);
+ }
+
+ public static int rgbToV(int r, int g, int b) {
+ return (((112 * r - 94 * g - 18 * b + 128) >> 8) + 128);
+ }
+
+ public static void drawRectOnNv21(byte[] nv21, int width, int height, int color, int strokeWidth, Rect rect) {
+ if (rect == null || rect.isEmpty()) {
+ return;
+ }
+ drawRectOnNv21(nv21, width, height, color, strokeWidth, rect.left, rect.top, rect.right, rect.bottom);
+ }
+
+ public static void drawRectOnNv21(byte[] nv21, int width, int height, int color, int strokeWidth, int left, int top,
+ int right, int bottom) {
+ if ((strokeWidth & 1) == 1) {
+ strokeWidth += 1;
+ }
+ // 确保边界是4的倍数
+ left &= ~0b11;
+ top &= ~0b11;
+ right &= ~0b11;
+ bottom &= ~0b11;
+ // 对于溢出图像的边,不绘制
+ boolean drawLeft = true, drawTop = true, drawRight = true, drawBottom = true;
+ if (left <= 0) {
+ left = 0;
+ drawLeft = false;
+ }
+ if (top <= 0) {
+ top = 0;
+ drawTop = false;
+ }
+ if (right >= width) {
+ right = width;
+ drawRight = false;
+ }
+ if (bottom >= height) {
+ bottom = height;
+ drawBottom = false;
+ }
+
+ // 取出R G B的值,并转换为Y U V
+ int r = (color & MASK_R) >> 16;
+ int g = (color & MASK_G) >> 8;
+ int b = color & MASK_B;
+ int y = rgbToY(r, g, b);
+ int u = rgbToU(r, g, b);
+ int v = rgbToV(r, g, b);
+
+ // 根据边框的strokeWidth确定内边界
+ int innerTop = top + strokeWidth;
+ int innerBottom = bottom - strokeWidth;
+ int innerRight = right - strokeWidth;
+
+ int horizontalPixels = right - left;
+ int yStartIndex;
+ int uvStartIndex;
+ boolean drawUV;
+ if (drawTop) {
+ yStartIndex = top * width + left;
+ uvStartIndex = width * height + ((top / 2 * width) + left);
+ drawUV = false;
+ for (int i = top; i < innerTop; i++) {
+ for (int j = 0; j < horizontalPixels; j++) {
+ nv21[yStartIndex + j] = (byte) y;
+ }
+ yStartIndex += width;
+ if (drawUV = !drawUV) {
+ for (int j = 0; j < horizontalPixels; j += 2) {
+ nv21[uvStartIndex + j] = (byte) v;
+ nv21[uvStartIndex + j + 1] = (byte) u;
+ }
+ uvStartIndex += width;
+ }
+ }
+ }
+
+ if (drawLeft) {
+ //左边
+ yStartIndex = innerTop * width + left;
+ uvStartIndex = width * height + (innerTop / 2 * width + left);
+ drawUV = false;
+ for (int i = innerTop; i < innerBottom; i++) {
+ for (int j = 0; j < strokeWidth; j++) {
+ nv21[yStartIndex + j] = (byte) y;
+ }
+ yStartIndex += width;
+ if (drawUV = !drawUV) {
+ for (int j = 0; j < strokeWidth; j += 2) {
+ nv21[uvStartIndex + j] = (byte) v;
+ nv21[uvStartIndex + j + 1] = (byte) u;
+ }
+ uvStartIndex += width;
+ }
+ }
+ }
+ if (drawRight) {
+ //右边
+ yStartIndex = innerTop * width + innerRight;
+ uvStartIndex = width * height + (innerTop / 2 * width + innerRight);
+ drawUV = false;
+ for (int i = innerTop; i < innerBottom; i++) {
+ for (int j = 0; j < strokeWidth; j++) {
+ nv21[yStartIndex + j] = (byte) y;
+ }
+ yStartIndex += width;
+ if (drawUV = !drawUV) {
+ for (int j = 0; j < strokeWidth; j += 2) {
+ nv21[uvStartIndex + j] = (byte) v;
+ nv21[uvStartIndex + j + 1] = (byte) u;
+ }
+ uvStartIndex += width;
+ }
+ }
+ }
+
+ if (drawBottom) {
+ //下边
+ yStartIndex = innerBottom * width + left;
+ uvStartIndex = width * height + ((innerBottom / 2 * width) + left);
+ drawUV = false;
+ for (int i = innerBottom; i < bottom; i++) {
+ for (int j = 0; j < horizontalPixels; j++) {
+ nv21[yStartIndex + j] = (byte) y;
+ }
+ yStartIndex += width;
+ if (drawUV = !drawUV) {
+ for (int j = 0; j < horizontalPixels; j += 2) {
+ nv21[uvStartIndex + j] = (byte) v;
+ nv21[uvStartIndex + j + 1] = (byte) u;
+ }
+ uvStartIndex += width;
+ }
+ }
+ }
+ }
+
+ /**
+ * 缩放图像,如果需要缩放,就顺便把宽高对齐给做了
+ *
+ * @param bitmap 原图
+ * @param maxWidth 最大目标宽度
+ * @param maxHeight 最大目标高度
+ * @return 缩放后的图像
+ */
+ public static Bitmap scaleBitmap(Bitmap bitmap, int maxWidth, int maxHeight) {
+ float horizontalScale = ((float) bitmap.getWidth()) / maxWidth;
+ float verticalScale = ((float) bitmap.getHeight()) / maxHeight;
+ if (horizontalScale < 1 || verticalScale < 1) {
+ return bitmap;
+ }
+ float maxScale = Math.max(horizontalScale, verticalScale);
+ // 确保为4的倍数
+ int newWidth = (int) (bitmap.getWidth() / maxScale) & ~0b11;
+ int newHeight = (int) (bitmap.getHeight() / maxScale) & ~0b11;
+
+ return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
+ }
+
+ /**
+ * 将Uri转换为Bitmap,并限制最大宽高
+ */
+ public static Bitmap uriToScaledBitmap(Context context, Uri uri, int maxWidth, int maxHeight) {
+ ContentResolver contentResolver = context.getContentResolver();
+ byte[] data;
+ try {
+ InputStream input = null;
+ input = contentResolver.openInputStream(uri);
+ data = new byte[input.available()];
+ input.read(data);
+ input.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ return jpegToScaledBitmap(data, maxWidth, maxHeight);
+ }
+
+ /**
+ * 将jpeg形式的压缩图像转换为Bitmap,并限制最大宽高
+ *
+ * @param jpeg jpeg图像数据
+ * @param maxWidth 限制的最大宽度
+ * @param maxHeight 限制的最大高度
+ * @return 宽高小于限制值的Bitmap对象
+ */
+ public static Bitmap jpegToScaledBitmap(byte[] jpeg, int maxWidth, int maxHeight) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);
+
+ int inSampleSize = 1;
+ while (options.outWidth / inSampleSize > maxWidth || options.outHeight / inSampleSize > maxHeight) {
+ inSampleSize++;
+ }
+ options.inSampleSize = inSampleSize;
+ options.inJustDecodeBounds = false;
+ return BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/PreviewConfig.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/PreviewConfig.java
new file mode 100644
index 0000000..82098f1
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/PreviewConfig.java
@@ -0,0 +1,58 @@
+package com.sw.plate.utils.arcface;
+
+import android.hardware.Camera;
+
+public class PreviewConfig {
+ /**
+ * 默认的可见光相机ID
+ */
+ public static final int DEFAULT_RGB_CAMERA_ID = Camera.CameraInfo.CAMERA_FACING_BACK;
+ /**
+ * 默认的红外相机ID
+ */
+ public static final int DEFAULT_IR_CAMERA_ID = Camera.CameraInfo.CAMERA_FACING_FRONT;
+
+ private int rgbCameraId;
+ private int irCameraId;
+ private int rgbAdditionalDisplayOrientation;
+ private int irAdditionalDisplayOrientation;
+
+ public PreviewConfig(int rgbCameraId, int irCameraId, int rgbAdditionalDisplayOrientation, int irAdditionalDisplayOrientation) {
+ this.rgbCameraId = rgbCameraId;
+ this.irCameraId = irCameraId;
+ this.rgbAdditionalDisplayOrientation = rgbAdditionalDisplayOrientation;
+ this.irAdditionalDisplayOrientation = irAdditionalDisplayOrientation;
+ }
+
+ public int getRgbCameraId() {
+ return rgbCameraId;
+ }
+
+ public int getIrCameraId() {
+ return irCameraId;
+ }
+
+ public int getRgbAdditionalDisplayOrientation() {
+ return rgbAdditionalDisplayOrientation;
+ }
+
+ public int getIrAdditionalDisplayOrientation() {
+ return irAdditionalDisplayOrientation;
+ }
+
+ public void setRgbCameraId(int rgbCameraId) {
+ this.rgbCameraId = rgbCameraId;
+ }
+
+ public void setIrCameraId(int irCameraId) {
+ this.irCameraId = irCameraId;
+ }
+
+ public void setRgbAdditionalDisplayOrientation(int rgbAdditionalDisplayOrientation) {
+ this.rgbAdditionalDisplayOrientation = rgbAdditionalDisplayOrientation;
+ }
+
+ public void setIrAdditionalDisplayOrientation(int irAdditionalDisplayOrientation) {
+ this.irAdditionalDisplayOrientation = irAdditionalDisplayOrientation;
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/callback/BatchRegisterCallback.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/callback/BatchRegisterCallback.java
new file mode 100644
index 0000000..6290115
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/callback/BatchRegisterCallback.java
@@ -0,0 +1,25 @@
+package com.sw.plate.utils.arcface.callback;
+
+/**
+ * 批量注册的回调
+ */
+public interface BatchRegisterCallback {
+ /**
+ * 批量注册过程中的回调
+ *
+ * @param current 当前已处理的数量
+ * @param failed 处理失败的数量
+ * @param total 处理总数
+ */
+ void onProcess(int current, int failed, int total);
+
+ /**
+ * 批量注册结束的回调
+ *
+ * @param current 当前已处理的数量
+ * @param failed 处理失败的数量
+ * @param total 处理总数
+ * @param errMsg 错误消息
+ */
+ void onFinish(int current, int failed, int total, String errMsg);
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/callback/OnRegisterFinishedCallback.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/callback/OnRegisterFinishedCallback.java
new file mode 100644
index 0000000..a849a86
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/callback/OnRegisterFinishedCallback.java
@@ -0,0 +1,20 @@
+
+package com.sw.plate.utils.arcface.callback;
+
+
+import com.sw.plate.utils.arcface.face.model.FacePreviewInfo;
+import com.sw.plate.utils.arcface.model.UserFaceInfo;
+
+/**
+ * 实时注册的结果回调
+ */
+public interface OnRegisterFinishedCallback {
+ /**
+ * 注册结束的回调
+ *
+ * @param facePreviewInfo 注册的人脸信息
+ * @param userFaceInfo 是否成功
+ */
+// void onRegisterFinished(FacePreviewInfo facePreviewInfo, boolean success);
+ void onRegisterFinished(FacePreviewInfo facePreviewInfo, UserFaceInfo userFaceInfo);
+}
\ No newline at end of file
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/CameraHelper.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/CameraHelper.java
new file mode 100644
index 0000000..4acbd97
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/CameraHelper.java
@@ -0,0 +1,433 @@
+package com.sw.plate.utils.arcface.camera;
+
+import android.graphics.ImageFormat;
+import android.graphics.Point;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * 相机辅助类,和{@link CameraListener}共同使用,获取nv21数据等操作
+ */
+public class CameraHelper implements Camera.PreviewCallback {
+ private static final String TAG = "CameraHelper";
+ private volatile Camera mCamera;
+ private int mCameraId;
+ private Point previewViewSize;
+ private View previewDisplayView;
+ private Camera.Size previewSize;
+ private Point specificPreviewSize;
+ private int displayOrientation = 0;
+ private int rotation;
+ private int additionalRotation;
+ private boolean isMirror = false;
+
+ private Integer specificCameraId = null;
+ private CameraListener cameraListener;
+
+ private CameraHelper(Builder builder) {
+ previewDisplayView = builder.previewDisplayView;
+ specificCameraId = builder.specificCameraId;
+ cameraListener = builder.cameraListener;
+ rotation = builder.rotation;
+ additionalRotation = builder.additionalRotation;
+ previewViewSize = builder.previewViewSize;
+ specificPreviewSize = builder.previewSize;
+ if (builder.previewDisplayView instanceof TextureView) {
+ isMirror = builder.isMirror;
+ } else if (isMirror) {
+ throw new RuntimeException("mirror is effective only when the preview is on a textureView");
+ }
+ }
+
+ public void init() {
+ if (previewDisplayView instanceof TextureView) {
+ ((TextureView) this.previewDisplayView).setSurfaceTextureListener(textureListener);
+ } else if (previewDisplayView instanceof SurfaceView) {
+ ((SurfaceView) previewDisplayView).getHolder().addCallback(surfaceCallback);
+ }
+
+ if (isMirror) {
+ previewDisplayView.setScaleX(-1);
+ }
+ }
+
+ public int getSensorOrientation() {
+ Camera.CameraInfo info = new Camera.CameraInfo();
+ Camera.getCameraInfo(mCameraId, info);
+ return info.orientation;
+ }
+
+ public synchronized void start() {
+ if (mCamera != null) {
+ return;
+ }
+ //相机数量为2则打开1,1则打开0,相机ID 1为前置,0为后置
+ mCameraId = Camera.getNumberOfCameras() - 1;
+ //若指定了相机ID且该相机存在,则打开指定的相机
+ if (specificCameraId != null && specificCameraId <= mCameraId) {
+ mCameraId = specificCameraId;
+ }
+
+ //没有相机
+ if (mCameraId == -1) {
+ if (cameraListener != null) {
+ cameraListener.onCameraError(new Exception("camera not found"));
+ }
+ return;
+ }
+ if (mCamera == null) {
+ mCamera = Camera.open(mCameraId);
+ }
+
+ displayOrientation = getCameraOri(rotation);
+ mCamera.setDisplayOrientation(displayOrientation);
+ try {
+ Camera.Parameters parameters = mCamera.getParameters();
+ parameters.setPreviewFormat(ImageFormat.NV21);
+
+ // 预览大小设置
+ previewSize = parameters.getPreviewSize();
+ List supportedPreviewSizes = parameters.getSupportedPreviewSizes();
+ if (supportedPreviewSizes != null && supportedPreviewSizes.size() > 0) {
+ previewSize = getBestSupportedSize(supportedPreviewSizes, previewViewSize);
+ }
+ parameters.setPreviewSize(previewSize.width, previewSize.height);
+
+ // 对焦模式设置
+ List supportedFocusModes = parameters.getSupportedFocusModes();
+ if (supportedFocusModes != null && supportedFocusModes.size() > 0) {
+ if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
+ parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
+ } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
+ parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
+ } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
+ parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
+ }
+ }
+ mCamera.setParameters(parameters);
+ if (previewDisplayView instanceof TextureView) {
+ mCamera.setPreviewTexture(((TextureView) previewDisplayView).getSurfaceTexture());
+ } else {
+ mCamera.setPreviewDisplay(((SurfaceView) previewDisplayView).getHolder());
+ }
+ mCamera.setPreviewCallback(this);
+ mCamera.startPreview();
+ if (cameraListener != null) {
+ cameraListener.onCameraOpened(mCamera, mCameraId, displayOrientation, isMirror);
+ }
+ } catch (Exception e) {
+ if (cameraListener != null) {
+ cameraListener.onCameraError(e);
+ }
+ }
+ }
+
+ private int getCameraOri(int rotation) {
+ int degrees = rotation * 90;
+ switch (rotation) {
+ case Surface.ROTATION_0:
+ degrees = 0;
+ break;
+ case Surface.ROTATION_90:
+ degrees = 90;
+ break;
+ case Surface.ROTATION_180:
+ degrees = 180;
+ break;
+ case Surface.ROTATION_270:
+ degrees = 270;
+ break;
+ default:
+ break;
+ }
+ additionalRotation /= 90;
+ additionalRotation *= 90;
+ degrees += additionalRotation;
+ int result;
+ Camera.CameraInfo info = new Camera.CameraInfo();
+ Camera.getCameraInfo(mCameraId, info);
+ if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ result = (info.orientation + degrees) % 360;
+ result = (360 - result) % 360;
+ } else {
+ result = (info.orientation - degrees + 360) % 360;
+ }
+ return result;
+ }
+
+ public synchronized void stop() {
+ if (mCamera == null) {
+ return;
+ }
+ mCamera.setPreviewCallback(null);
+ mCamera.stopPreview();
+ mCamera.release();
+ mCamera = null;
+ if (cameraListener != null) {
+ cameraListener.onCameraClosed();
+ }
+ }
+
+ public synchronized boolean isStopped() {
+ return mCamera == null;
+ }
+
+ public void release() {
+ synchronized (this) {
+ stop();
+ previewDisplayView = null;
+ specificCameraId = null;
+ cameraListener = null;
+ previewViewSize = null;
+ specificPreviewSize = null;
+ previewSize = null;
+ }
+ }
+
+ private Camera.Size getBestSupportedSize(List sizes, Point previewViewSize) {
+ if (sizes == null || sizes.size() == 0) {
+ return mCamera.getParameters().getPreviewSize();
+ }
+ Camera.Size[] tempSizes = sizes.toArray(new Camera.Size[0]);
+ Arrays.sort(tempSizes, new Comparator() {
+ @Override
+ public int compare(Camera.Size o1, Camera.Size o2) {
+ if (o1.width > o2.width) {
+ return -1;
+ } else if (o1.width == o2.width) {
+ return o1.height > o2.height ? -1 : 1;
+ } else {
+ return 1;
+ }
+ }
+ });
+ sizes = Arrays.asList(tempSizes);
+
+ Camera.Size bestSize = sizes.get(0);
+ float previewViewRatio;
+ if (previewViewSize != null) {
+ previewViewRatio = (float) previewViewSize.x / (float) previewViewSize.y;
+ } else {
+ previewViewRatio = (float) bestSize.width / (float) bestSize.height;
+ }
+
+ if (previewViewRatio > 1) {
+ previewViewRatio = 1 / previewViewRatio;
+ }
+ boolean isNormalRotate = (additionalRotation % 180 == 0);
+ Log.i(TAG, "getBestSupportedSize previewViewSize: " + previewViewSize.toString());
+ for (Camera.Size s : sizes) {
+ if (specificPreviewSize != null && specificPreviewSize.x == s.width && specificPreviewSize.y == s.height) {
+ return s;
+ }
+ if (isNormalRotate) {
+ if (Math.abs((s.height / (float) s.width) - previewViewRatio) < Math.abs(bestSize.height / (float) bestSize.width - previewViewRatio)) {
+ bestSize = s;
+ }
+ } else {
+ if (Math.abs((s.width / (float) s.height) - previewViewRatio) < Math.abs(bestSize.width / (float) bestSize.height - previewViewRatio)) {
+ bestSize = s;
+ }
+ }
+ }
+ Log.i(TAG, "getBestSupportedSize bestSize: " + bestSize.width + "x" + bestSize.height);
+ return bestSize;
+ }
+
+ public List getSupportedPreviewSizes() {
+ if (mCamera == null) {
+ return null;
+ }
+ return mCamera.getParameters().getSupportedPreviewSizes();
+ }
+
+ public List getSupportedPictureSizes() {
+ if (mCamera == null) {
+ return null;
+ }
+ return mCamera.getParameters().getSupportedPictureSizes();
+ }
+
+
+ @Override
+ public void onPreviewFrame(byte[] nv21, Camera camera) {
+ if (cameraListener != null) {
+ cameraListener.onPreview(nv21, camera);
+ }
+ }
+
+ private TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener() {
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
+// start();
+ if (mCamera != null) {
+ try {
+ mCamera.setPreviewTexture(surfaceTexture);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+ stop();
+ return false;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+
+ }
+ };
+ private SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+// start();
+ if (mCamera != null) {
+ try {
+ mCamera.setPreviewDisplay(holder);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ stop();
+ }
+ };
+
+ public void changeDisplayOrientation(int rotation) {
+ if (mCamera != null) {
+ this.rotation = rotation;
+ displayOrientation = getCameraOri(rotation);
+ mCamera.setDisplayOrientation(displayOrientation);
+ if (cameraListener != null) {
+ cameraListener.onCameraConfigurationChanged(mCameraId, displayOrientation);
+ }
+ }
+ }
+
+ public static final class Builder {
+
+ /**
+ * 预览显示的view,目前仅支持surfaceView和textureView
+ */
+ private View previewDisplayView;
+
+ /**
+ * 是否镜像显示,只支持textureView
+ */
+ private boolean isMirror;
+ /**
+ * 指定的相机ID
+ */
+ private Integer specificCameraId;
+ /**
+ * 事件回调
+ */
+ private CameraListener cameraListener;
+ /**
+ * 屏幕的长宽,在选择最佳相机比例时用到
+ */
+ private Point previewViewSize;
+ /**
+ * 传入getWindowManager().getDefaultDisplay().getRotation()的值即可
+ */
+ private int rotation;
+ /**
+ * 指定的预览宽高,若系统支持则会以这个预览宽高进行预览
+ */
+ private Point previewSize;
+
+ /**
+ * 额外的旋转角度(用于适配一些定制设备)
+ */
+ private int additionalRotation;
+
+ public Builder() {
+ }
+
+
+ public Builder previewOn(View val) {
+ if (val instanceof SurfaceView || val instanceof TextureView) {
+ previewDisplayView = val;
+ return this;
+ } else {
+ throw new RuntimeException("you must preview on a textureView or a surfaceView");
+ }
+ }
+
+
+ public Builder isMirror(boolean val) {
+ isMirror = val;
+ return this;
+ }
+
+ public Builder previewSize(Point val) {
+ previewSize = val;
+ return this;
+ }
+
+ public Builder previewViewSize(Point val) {
+ previewViewSize = val;
+ return this;
+ }
+
+ public Builder rotation(int val) {
+ rotation = val;
+ return this;
+ }
+
+ public Builder additionalRotation(int val) {
+ additionalRotation = val;
+ return this;
+ }
+
+ public Builder specificCameraId(Integer val) {
+ specificCameraId = val;
+ return this;
+ }
+
+ public Builder cameraListener(CameraListener val) {
+ cameraListener = val;
+ return this;
+ }
+
+ public CameraHelper build() {
+ if (previewViewSize == null) {
+ Log.e(TAG, "previewViewSize is null, now use default previewSize");
+ }
+ if (cameraListener == null) {
+ Log.e(TAG, "cameraListener is null, callback will not be called");
+ }
+ if (previewDisplayView == null) {
+ throw new RuntimeException("you must preview on a textureView or a surfaceView");
+ }
+ return new CameraHelper(this);
+ }
+ }
+
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/CameraListener.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/CameraListener.java
new file mode 100644
index 0000000..2880feb
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/CameraListener.java
@@ -0,0 +1,44 @@
+package com.sw.plate.utils.arcface.camera;
+
+import android.hardware.Camera;
+
+
+public interface CameraListener {
+ /**
+ * 当打开时执行
+ *
+ * @param camera 相机实例
+ * @param cameraId 相机ID
+ * @param displayOrientation 相机预览旋转角度
+ * @param isMirror 是否镜像显示
+ */
+ void onCameraOpened(Camera camera, int cameraId, int displayOrientation, boolean isMirror);
+
+ /**
+ * 预览数据回调
+ *
+ * @param data 预览数据
+ * @param camera 相机实例
+ */
+ void onPreview(byte[] data, Camera camera);
+
+ /**
+ * 当相机关闭时执行
+ */
+ void onCameraClosed();
+
+ /**
+ * 当出现异常时执行
+ *
+ * @param e 相机相关异常
+ */
+ void onCameraError(Exception e);
+
+ /**
+ * 属性变化时调用
+ *
+ * @param cameraID 相机ID
+ * @param displayOrientation 相机旋转方向
+ */
+ void onCameraConfigurationChanged(int cameraID, int displayOrientation);
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/DualCameraHelper.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/DualCameraHelper.java
new file mode 100644
index 0000000..f8b5825
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/DualCameraHelper.java
@@ -0,0 +1,580 @@
+package com.sw.plate.utils.arcface.camera;
+
+import android.graphics.Bitmap;
+import android.graphics.ImageFormat;
+import android.graphics.Point;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * 打开两个相机的辅助类
+ *
+ * 由于IR摄像头和RGB摄像头的默认分辨率可能不同,为了让两者相同,该类做了以下操作:
+ * 1. 获取两者支持的分辨率列表到到静态变量{@link DualCameraHelper#rgbSupportedPreviewSizes}及{@link DualCameraHelper#irSupportedPreviewSizes}中,
+ * 2. 使用{@link DualCameraHelper#getCommonSupportedPreviewSize()}方法取分辨率的交集,
+ * 3. 使用{@link DualCameraHelper#getBestSupportedSize(List, Point)}取最佳分辨率使两个摄像头分辨率尽可能相同
+ */
+public class DualCameraHelper implements Camera.PreviewCallback {
+ private static List rgbSupportedPreviewSizes;
+ private static List irSupportedPreviewSizes;
+ private static final String TAG = "CameraHelper";
+ private Camera mCamera;
+ private int mCameraId;
+ private Point previewViewSize;
+ private View previewDisplayView;
+ private Camera.Size previewSize;
+ private Point specificPreviewSize;
+ private int displayOrientation = 0;
+ private int rotation;
+ private int additionalRotation;
+ private boolean isMirror = false;
+
+ private Integer specificCameraId = null;
+ private CameraListener cameraListener;
+// private static final int MIN_PREVIEW_WIDTH = 720;
+// private static final int MIN_PREVIEW_HEIGHT = 720;
+ private static final int MIN_PREVIEW_WIDTH = 800;
+ private static final int MIN_PREVIEW_HEIGHT = 600;
+
+ private DualCameraHelper(Builder builder) {
+ previewDisplayView = builder.previewDisplayView;
+ specificCameraId = builder.specificCameraId;
+ cameraListener = builder.cameraListener;
+ rotation = builder.rotation;
+ additionalRotation = builder.additionalRotation;
+ previewViewSize = builder.previewViewSize;
+ specificPreviewSize = builder.previewSize;
+ if (builder.previewDisplayView instanceof TextureView) {
+ isMirror = builder.isMirror;
+ } else if (isMirror) {
+ throw new RuntimeException("mirror is effective only when the preview is on a textureView");
+ }
+ }
+
+ public interface SurfaceFrameCallback {
+ void onFrameCallback(Bitmap frame);
+ }
+
+ private SurfaceFrameCallback surfaceFrameCallback;
+
+ public void setSurfaceFrameCallback(SurfaceFrameCallback callback) {
+ this.surfaceFrameCallback = callback;
+ }
+
+ public void init() {
+ if (previewDisplayView instanceof TextureView) {
+ ((TextureView) this.previewDisplayView).setSurfaceTextureListener(textureListener);
+ } else if (previewDisplayView instanceof SurfaceView) {
+ ((SurfaceView) previewDisplayView).getHolder().addCallback(surfaceCallback);
+ }
+
+ if (isMirror) {
+ previewDisplayView.setScaleX(-1);
+ }
+ }
+
+ public List getCommonSupportedPreviewSize() {
+ /**
+ * irSupportedPreviewSizes 和 rgbSupportedPreviewSizes 为null才去获取,
+ * 不为null就没必要获取了,而且此时有可能该camera已处于打开状态,无法打开camera
+ */
+ if (rgbSupportedPreviewSizes == null) {
+ Camera rgbCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
+// Camera rgbCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT);
+ rgbSupportedPreviewSizes = rgbCamera.getParameters().getSupportedPreviewSizes();
+ rgbCamera.release();
+ }
+ try {
+ if (irSupportedPreviewSizes == null) {
+ Camera irCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT);
+// Camera irCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
+ irSupportedPreviewSizes = irCamera.getParameters().getSupportedPreviewSizes();
+ irCamera.release();
+ }
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ irSupportedPreviewSizes = rgbSupportedPreviewSizes;
+ }
+ List commonPreviewSizes = new ArrayList<>();
+ for (Camera.Size rgbPreviewSize : rgbSupportedPreviewSizes) {
+ if (rgbPreviewSize.width < MIN_PREVIEW_WIDTH || rgbPreviewSize.height < MIN_PREVIEW_HEIGHT) {
+ continue;
+ }
+ for (Camera.Size irPreviewSize : irSupportedPreviewSizes) {
+ if (irPreviewSize.width == rgbPreviewSize.width && irPreviewSize.height == rgbPreviewSize.height) {
+ commonPreviewSizes.add(rgbPreviewSize);
+ }
+ }
+ }
+ return commonPreviewSizes;
+ }
+
+ /**
+ * 回传当前使用的cameraID,若当前没打开相机,回传-1
+ *
+ * @return cameraId,失败回传-1
+ */
+ public int getCurrentOpenedCameraId() {
+ if (mCamera == null) {
+ return -1;
+ }
+ return mCameraId;
+ }
+
+ public void start() {
+ synchronized (this) {
+ if (mCamera != null) {
+ return;
+ }
+ List supportedPreviewSize = getCommonSupportedPreviewSize();
+ StringBuilder stringBuilder = new StringBuilder();
+ for (Camera.Size size : supportedPreviewSize) {
+ stringBuilder.append("width=").append(size.width).append(",")
+ .append("height=").append(size.height).append(",");
+ }
+ Log.d(TAG, "start: "+stringBuilder);
+ //相机数量为2则打开1,1则打开0,相机ID 1为前置,0为后置
+ mCameraId = Camera.getNumberOfCameras() - 1;
+ //若指定了相机ID且该相机存在,则打开指定的相机
+// if (specificCameraId != null && specificCameraId <= mCameraId) {
+ if (specificCameraId != null) {
+ mCameraId = specificCameraId;
+ }
+
+ //没有相机
+ if (mCameraId == -1) {
+ if (cameraListener != null) {
+ cameraListener.onCameraError(new Exception("camera not found"));
+ }
+ return;
+ }
+ if (mCamera == null) {
+ mCamera = Camera.open(mCameraId);
+ }
+ displayOrientation = getCameraOri(rotation);
+ mCamera.setDisplayOrientation(displayOrientation);
+ try {
+ Camera.Parameters parameters = mCamera.getParameters();
+ parameters.setPreviewFormat(ImageFormat.NV21);
+
+ //预览大小设置
+ previewSize = parameters.getPreviewSize();
+ if (supportedPreviewSize != null && supportedPreviewSize.size() > 0) {
+ previewSize = getBestSupportedSize(supportedPreviewSize, previewViewSize);
+ }
+ Log.i(TAG, "start: " + previewSize.width + "x" + previewSize.height);
+ parameters.setPreviewSize(previewSize.width, previewSize.height);
+
+ //对焦模式设置
+ List supportedFocusModes = parameters.getSupportedFocusModes();
+ if (supportedFocusModes != null && supportedFocusModes.size() > 0) {
+ if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
+ parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
+ } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
+ parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
+ } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
+ parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
+ }
+ }
+ mCamera.setParameters(parameters);
+ if (previewDisplayView instanceof TextureView) {
+ mCamera.setPreviewTexture(((TextureView) previewDisplayView).getSurfaceTexture());
+ } else {
+ mCamera.setPreviewDisplay(((SurfaceView) previewDisplayView).getHolder());
+ }
+ mCamera.setPreviewCallback(this);
+ mCamera.startPreview();
+ if (cameraListener != null) {
+ cameraListener.onCameraOpened(mCamera, mCameraId, displayOrientation, isMirror);
+ }
+ } catch (Exception e) {
+ if (cameraListener != null) {
+ cameraListener.onCameraError(e);
+ }
+ }
+ }
+ }
+
+ public void switchCameraId() {
+ mCameraId = 1 - mCameraId;
+ if (specificCameraId != null) {
+ specificCameraId = 1 - specificCameraId;
+ }
+ }
+
+ private int getCameraOri(int rotation) {
+ int degrees = rotation * 90;
+ switch (rotation) {
+ case Surface.ROTATION_0:
+ degrees = 0;
+ break;
+ case Surface.ROTATION_90:
+ degrees = 90;
+ break;
+ case Surface.ROTATION_180:
+ degrees = 180;
+ break;
+ case Surface.ROTATION_270:
+ degrees = 270;
+ break;
+ default:
+ break;
+ }
+ additionalRotation /= 90;
+ additionalRotation *= 90;
+ degrees += additionalRotation;
+ int result;
+ Camera.CameraInfo info = new Camera.CameraInfo();
+ Camera.getCameraInfo(mCameraId, info);
+ if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ result = (info.orientation + degrees) % 360;
+ result = (360 - result) % 360;
+ } else {
+ result = (info.orientation - degrees + 360) % 360;
+ }
+ return result;
+ }
+
+ /**
+ * 停止预览
+ */
+ public void stop() {
+ synchronized (this) {
+ if (mCamera == null) {
+ return;
+ }
+ mCamera.setPreviewCallback(null);
+ mCamera.stopPreview();
+ mCamera.release();
+ mCamera = null;
+ if (cameraListener != null) {
+ cameraListener.onCameraClosed();
+ }
+ }
+ }
+
+ public boolean isStopped() {
+ synchronized (this) {
+ return mCamera == null;
+ }
+ }
+
+ /**
+ * 释放操作
+ */
+ public void release() {
+ synchronized (this) {
+ stop();
+ previewDisplayView = null;
+ specificCameraId = null;
+ cameraListener = null;
+ previewViewSize = null;
+ specificPreviewSize = null;
+ previewSize = null;
+ }
+ }
+
+ /**
+ * 获取候选分辨率列表中最接近预览view大小的分辨率
+ *
+ * @param sizes 支持的分辨率
+ * @param previewViewSize 预览view的大小
+ * @return 最接近预览view大小的分辨率
+ */
+ private Camera.Size getBestSupportedSize(List sizes, Point previewViewSize) {
+ if (sizes == null || sizes.size() == 0) {
+ return mCamera.getParameters().getPreviewSize();
+ }
+ Camera.Size[] tempSizes = sizes.toArray(new Camera.Size[0]);
+ Arrays.sort(tempSizes, new Comparator() {
+ @Override
+ public int compare(Camera.Size o1, Camera.Size o2) {
+ if (o1.width > o2.width) {
+ return -1;
+ } else if (o1.width == o2.width) {
+ return o1.height > o2.height ? -1 : 1;
+ } else {
+ return 1;
+ }
+ }
+ });
+ sizes = Arrays.asList(tempSizes);
+
+ Camera.Size bestSize = sizes.get(0);
+ float previewViewRatio;
+ if (previewViewSize != null) {
+ previewViewRatio = (float) previewViewSize.x / (float) previewViewSize.y;
+ } else {
+ previewViewRatio = (float) bestSize.width / (float) bestSize.height;
+ }
+
+ if (previewViewRatio > 1) {
+ previewViewRatio = 1 / previewViewRatio;
+ }
+ boolean isNormalRotate = (additionalRotation % 180 == 0);
+
+ for (Camera.Size s : sizes) {
+ if (specificPreviewSize != null && specificPreviewSize.x == s.width && specificPreviewSize.y == s.height) {
+ return s;
+ }
+ if (isNormalRotate) {
+ if (Math.abs((s.height / (float) s.width) - previewViewRatio) < Math.abs(bestSize.height / (float) bestSize.width - previewViewRatio)) {
+ bestSize = s;
+ }
+ } else {
+ if (Math.abs((s.width / (float) s.height) - previewViewRatio) < Math.abs(bestSize.width / (float) bestSize.height - previewViewRatio)) {
+ bestSize = s;
+ }
+ }
+ }
+ return bestSize;
+ }
+
+ public List getSupportedPreviewSizes() {
+ if (mCamera == null) {
+ return null;
+ }
+ return mCamera.getParameters().getSupportedPreviewSizes();
+ }
+
+ public List getSupportedPictureSizes() {
+ if (mCamera == null) {
+ return null;
+ }
+ return mCamera.getParameters().getSupportedPictureSizes();
+ }
+
+
+ @Override
+ public void onPreviewFrame(byte[] nv21, Camera camera) {
+ if (cameraListener != null) {
+ cameraListener.onPreview(nv21, camera);
+ }
+ }
+
+ private TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener() {
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
+// start();
+ if (mCamera != null) {
+ try {
+ mCamera.setPreviewTexture(surfaceTexture);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
+ Log.i(TAG, "onSurfaceTextureSizeChanged: " + width + " " + height);
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+ stop();
+ return false;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+ if (surfaceFrameCallback!=null) {
+ if (previewDisplayView instanceof TextureView) {
+ Bitmap frame = ((TextureView) previewDisplayView).getBitmap();
+ surfaceFrameCallback.onFrameCallback(frame);
+ }
+ }
+ }
+ };
+ private SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+// start();
+ if (mCamera != null) {
+ try {
+ mCamera.setPreviewDisplay(holder);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ stop();
+ }
+ };
+
+ public void changeDisplayOrientation(int rotation) {
+ if (mCamera != null) {
+ this.rotation = rotation;
+ displayOrientation = getCameraOri(rotation);
+ mCamera.setDisplayOrientation(displayOrientation);
+ if (cameraListener != null) {
+ cameraListener.onCameraConfigurationChanged(mCameraId, displayOrientation);
+ }
+ }
+ }
+
+ public static final class Builder {
+
+ /**
+ * 预览显示的view,目前仅支持surfaceView和textureView
+ */
+ private View previewDisplayView;
+
+ /**
+ * 是否镜像显示,只支持textureView
+ */
+ private boolean isMirror;
+ /**
+ * 指定的相机ID
+ */
+ private Integer specificCameraId;
+ /**
+ * 事件回调
+ */
+ private CameraListener cameraListener;
+ /**
+ * 屏幕的长宽,在选择最佳相机比例时用到
+ */
+ private Point previewViewSize;
+ /**
+ * 传入getWindowManager().getDefaultDisplay().getRotation()的值即可
+ */
+ private int rotation;
+ /**
+ * 指定的预览宽高,若系统支持则会以这个预览宽高进行预览
+ */
+ private Point previewSize;
+
+ /**
+ * 额外的旋转角度(用于适配一些定制设备)
+ */
+ private int additionalRotation;
+
+ public Builder() {
+ }
+
+
+ public Builder previewOn(View val) {
+ if (val instanceof SurfaceView || val instanceof TextureView) {
+ previewDisplayView = val;
+ return this;
+ } else {
+ throw new RuntimeException("you must preview on a textureView or a surfaceView");
+ }
+ }
+
+
+ public Builder isMirror(boolean val) {
+ isMirror = val;
+ return this;
+ }
+
+ public Builder previewSize(Point val) {
+ previewSize = val;
+ return this;
+ }
+
+ public Builder previewViewSize(Point val) {
+ previewViewSize = val;
+ return this;
+ }
+
+ public Builder rotation(int val) {
+ rotation = val;
+ return this;
+ }
+
+ public Builder additionalRotation(int val) {
+ additionalRotation = val;
+ return this;
+ }
+
+ public Builder specificCameraId(Integer val) {
+ specificCameraId = val;
+ return this;
+ }
+
+ public Builder cameraListener(CameraListener val) {
+ cameraListener = val;
+ return this;
+ }
+
+ public DualCameraHelper build() {
+ if (previewViewSize == null) {
+ Log.e(TAG, "previewViewSize is null, now use default previewSize");
+ }
+ if (cameraListener == null) {
+ Log.e(TAG, "cameraListener is null, callback will not be called");
+ }
+ if (previewDisplayView == null) {
+ throw new RuntimeException("you must preview on a textureView or a surfaceView");
+ }
+ return new DualCameraHelper(this);
+ }
+ }
+
+ /**
+ * 根据设置的额外旋转角度旋转
+ *
+ * @param additionalRotation 额外旋转角度
+ * @return 当前显示旋转角度
+ */
+ public int rotateAdditional(int additionalRotation) {
+ this.additionalRotation = additionalRotation;
+ int cameraOri = getCameraOri(rotation);
+ if (mCamera == null) {
+ start();
+ return cameraOri;
+ }
+ mCamera.setDisplayOrientation(cameraOri);
+ return cameraOri;
+ }
+
+ public void setSpecificPreviewSize(Point specificPreviewSize) {
+ this.specificPreviewSize = specificPreviewSize;
+ }
+
+ public static boolean hasDualCamera() {
+ return Camera.getNumberOfCameras() > 1;
+ }
+
+ public static boolean canOpenDualCamera() {
+ Camera camera0 = null;
+ Camera camera1 = null;
+ boolean can = true;
+ try {
+ camera0 = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
+ camera1 = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT);
+ } catch (Exception e) {
+ can = false;
+ }
+ if (camera0 != null) {
+ camera0.release();
+ }
+ if (camera1 != null) {
+ camera1.release();
+ }
+ return can;
+ }
+
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/CameraGLSurfaceView.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/CameraGLSurfaceView.java
new file mode 100644
index 0000000..245f66d
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/CameraGLSurfaceView.java
@@ -0,0 +1,98 @@
+package com.sw.plate.utils.arcface.camera.glsurface;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.opengl.GLES20;
+import android.opengl.GLSurfaceView;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+public class CameraGLSurfaceView extends GLSurfaceView {
+ private static final String TAG = "CameraGLSurfaceView";
+
+
+ YUVRenderer yuvRenderer;
+ NV21Drawer nv21Drawer;
+
+ public CameraGLSurfaceView(Context context) {
+ this(context, null);
+ }
+
+ public CameraGLSurfaceView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setEGLContextClientVersion(2);
+ // 设置Renderer到GLSurfaceView
+ yuvRenderer = new YUVRenderer();
+ nv21Drawer = new NV21Drawer();
+ setRenderer(yuvRenderer);
+ // 只有在绘制数据改变时才绘制view
+ setRenderMode(RENDERMODE_WHEN_DIRTY);
+ }
+
+ /**
+ * 设置不同的片段着色器代码以达到不同的预览效果
+ *
+ * @param fragmentShaderCode 片段着色器代码
+ */
+ public void setFragmentShaderCode(String fragmentShaderCode) {
+ nv21Drawer.setFragmentShaderCode(fragmentShaderCode);
+ }
+
+ public void init(boolean isMirror, int rotateDegree, int frameWidth, int frameHeight) {
+ nv21Drawer.init(isMirror, rotateDegree, frameWidth, frameHeight);
+
+ queueEvent(() -> yuvRenderer.initRenderer());
+ }
+
+ public class YUVRenderer implements Renderer {
+ private void initRenderer() {
+ boolean createSuccess = nv21Drawer.createGLProgram();
+ if (!createSuccess) {
+ Log.e(TAG, "initRenderer createGLProgram failed!");
+ }
+ }
+
+ @Override
+ public void onSurfaceCreated(GL10 unused, EGLConfig config) {
+ Log.i(TAG, "initRenderer onSurfaceCreated: ");
+ initRenderer();
+ }
+
+
+ @Override
+ public void onDrawFrame(GL10 gl) {
+ nv21Drawer.render();
+ }
+
+ @Override
+ public void onSurfaceChanged(GL10 unused, int width, int height) {
+ Log.i(TAG, "onSurfaceChanged: ");
+ GLES20.glViewport(0, 0, width, height);
+ }
+ }
+
+ /**
+ * 传入NV21刷新帧
+ *
+ * @param data NV21数据
+ */
+ public void renderNV21(byte[] data) {
+ nv21Drawer.updateNV21(data);
+ requestRender();
+ }
+
+
+ /**
+ * 传入NV21刷新帧,并同时绘制人脸框
+ *
+ * @param data NV21数据
+ * @param faceRect 人脸框
+ */
+ public void renderNV21WithFaceRect(byte[] data, Rect faceRect, int strokeWidth) {
+ nv21Drawer.updateNV21(data, faceRect, strokeWidth);
+ requestRender();
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/GLUtil.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/GLUtil.java
new file mode 100644
index 0000000..387e2a4
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/GLUtil.java
@@ -0,0 +1,266 @@
+package com.sw.plate.utils.arcface.camera.glsurface;
+
+import android.opengl.GLES20;
+import android.util.Log;
+
+import java.nio.IntBuffer;
+
+public class GLUtil {
+ private static final String TAG = "GLUtil";
+
+
+ /**
+ * 显示的顶点
+ */
+ static final float[] SQUARE_VERTICES = {
+ -1.0f, -1.0f,
+ 1.0f, -1.0f,
+ -1.0f, 1.0f,
+ 1.0f, 1.0f
+ };
+ /**
+ * 原数据显示
+ * 0,1***********1,1
+ * * *
+ * * *
+ * * *
+ * * *
+ * * *
+ * 0,0***********1,0
+ */
+ static final float[] COORD_VERTICES = {
+ 0.0f, 1.0f,
+ 1.0f, 1.0f,
+ 0.0f, 0.0f,
+ 1.0f, 0.0f
+ };
+
+ /**
+ * 逆时针旋转90度显示
+ * 1,1***********1,0
+ * * *
+ * * *
+ * * *
+ * * *
+ * * *
+ * 0,1***********0,0
+ */
+ static final float[] ROTATE_90_COORD_VERTICES = {
+ 1.0f, 1.0f,
+ 1.0f, 0.0f,
+ 0.0f, 1.0f,
+ 0.0f, 0.0f
+ };
+
+ /**
+ * 逆时针旋转180度显示
+ * 1,0***********0,0
+ * * *
+ * * *
+ * * *
+ * * *
+ * * *
+ * 1,1***********0,1
+ */
+ static final float[] ROTATE_180_COORD_VERTICES = {
+ 1.0f, 0.0f,
+ 0.0f, 0.0f,
+ 1.0f, 1.0f,
+ 0.0f, 1.0f
+ };
+
+ /**
+ * 逆时针旋转270度显示
+ * 0,0***********0,1
+ * * *
+ * * *
+ * * *
+ * * *
+ * * *
+ * 1,0***********1,1
+ */
+ static final float[] ROTATE_270_COORD_VERTICES = {
+ 0.0f, 0.0f,
+ 0.0f, 1.0f,
+ 1.0f, 0.0f,
+ 1.0f, 1.0f
+ };
+
+ /**
+ * 镜像显示
+ * 1,1***********0,1
+ * * *
+ * * *
+ * * *
+ * * *
+ * * *
+ * 1,0***********0,0
+ */
+ static final float[] MIRROR_COORD_VERTICES = {
+ 1.0f, 1.0f,
+ 0.0f, 1.0f,
+ 1.0f, 0.0f,
+ 0.0f, 0.0f
+ };
+
+ /**
+ * 镜像并逆时针旋转90度显示
+ * 0,1***********0,0
+ * * *
+ * * *
+ * * *
+ * * *
+ * * *
+ * 1,1***********1,0
+ */
+ static final float[] ROTATE_90_MIRROR_COORD_VERTICES = {
+ 0.0f, 1.0f,
+ 0.0f, 0.0f,
+ 1.0f, 1.0f,
+ 1.0f, 0.0f
+ };
+ /**
+ * 镜像并逆时针旋转180度显示
+ * 0,0***********1,0
+ * * *
+ * * *
+ * * *
+ * * *
+ * * *
+ * 0,1***********1,1
+ */
+ static final float[] ROTATE_180_MIRROR_COORD_VERTICES = {
+ 0.0f, 0.0f,
+ 1.0f, 0.0f,
+ 0.0f, 1.0f,
+ 1.0f, 1.0f
+ };
+ /**
+ * 镜像并逆时针旋转270度显示
+ * 1,0***********1,1
+ * * *
+ * * *
+ * * *
+ * * *
+ * * *
+ * 0,0***********0,1
+ */
+ static final float[] ROTATE_270_MIRROR_COORD_VERTICES = {
+ 1.0f, 0.0f,
+ 1.0f, 1.0f,
+ 0.0f, 0.0f,
+ 0.0f, 1.0f
+ };
+
+ /**
+ * 创建OpenGL Program,并链接
+ *
+ * @param fragmentShaderCode 片段着色器代码
+ * @param vertexShaderCode 顶点着色器代码
+ * @return OpenGL Program
+ */
+ static int createShaderProgram(String fragmentShaderCode, String vertexShaderCode) {
+ int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
+ int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
+ if (vertexShader == 0 || fragmentShader == 0) {
+ return 0;
+ }
+ int mProgram = GLES20.glCreateProgram();
+ GLES20.glAttachShader(mProgram, vertexShader);
+ GLES20.glAttachShader(mProgram, fragmentShader);
+ GLES20.glLinkProgram(mProgram);
+
+ IntBuffer linked = IntBuffer.allocate(1);
+ GLES20.glGetProgramiv(mProgram, GLES20.GL_LINK_STATUS, linked);
+ if (linked.get(0) == 0) {
+ return 0;
+ }
+ return mProgram;
+ }
+
+ /**
+ * 加载着色器
+ *
+ * @param shaderType 着色器类型,可以是片段着色器{@link GLES20#GL_FRAGMENT_SHADER}或顶点着色器{@link GLES20#GL_VERTEX_SHADER}
+ * @param source 着色器代码
+ * @return 着色器对象的引用,0代表失败
+ */
+ static int loadShader(int shaderType, String source) {
+ int shader = GLES20.glCreateShader(shaderType);
+ if (shader == 0) {
+ Log.e(TAG, "loadShader: failed to create shader");
+ checkGlErrorIfOccur("create shader " + shaderType);
+ return 0;
+ }
+ GLES20.glShaderSource(shader, source);
+ GLES20.glCompileShader(shader);
+ int[] compiled = new int[1];
+ GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
+ if (compiled[0] == 0) {
+ Log.e(TAG, "Could not compile shader " + shaderType + ":" + GLES20.glGetShaderInfoLog(shader));
+ GLES20.glDeleteShader(shader);
+ shader = 0;
+ checkGlErrorIfOccur("glGetShaderiv " + shaderType);
+ }
+ return shader;
+ }
+
+ /**
+ * 检查是否出现GLES错误
+ */
+ private static void checkGlErrorIfOccur(String op) {
+ int error = GLES20.glGetError();
+ if (error != GLES20.GL_NO_ERROR) {
+ String errorMsg = String.format("error 0x%h occurred: %s", error, op);
+ Log.e(TAG, errorMsg);
+ throw new RuntimeException(errorMsg);
+ }
+ }
+
+ /**
+ * 根据是否镜像和旋转角度选择合适的顶点坐标
+ *
+ * @param isMirror 是否镜像
+ * @param rotateDegree 旋转角度
+ * @return 顶点坐标
+ */
+ static float[] getCoordVerticesByPreviewParams(boolean isMirror, int rotateDegree) {
+ float[] coordVertice = GLUtil.COORD_VERTICES;
+ if (isMirror) {
+ switch (rotateDegree) {
+ case 0:
+ coordVertice = GLUtil.MIRROR_COORD_VERTICES;
+ break;
+ case 90:
+ coordVertice = GLUtil.ROTATE_90_MIRROR_COORD_VERTICES;
+ break;
+ case 180:
+ coordVertice = GLUtil.ROTATE_180_MIRROR_COORD_VERTICES;
+ break;
+ case 270:
+ coordVertice = GLUtil.ROTATE_270_MIRROR_COORD_VERTICES;
+ break;
+ default:
+ break;
+ }
+ } else {
+ switch (rotateDegree) {
+ case 0:
+ coordVertice = GLUtil.COORD_VERTICES;
+ break;
+ case 90:
+ coordVertice = GLUtil.ROTATE_90_COORD_VERTICES;
+ break;
+ case 180:
+ coordVertice = GLUtil.ROTATE_180_COORD_VERTICES;
+ break;
+ case 270:
+ coordVertice = GLUtil.ROTATE_270_COORD_VERTICES;
+ break;
+ default:
+ break;
+ }
+ }
+ return coordVertice.clone();
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/NV21Drawer.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/NV21Drawer.java
new file mode 100644
index 0000000..cb498fd
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/NV21Drawer.java
@@ -0,0 +1,297 @@
+package com.sw.plate.utils.arcface.camera.glsurface;
+
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.opengl.GLES20;
+import android.util.Log;
+
+import com.sw.plate.utils.arcface.ImageUtil;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.util.Arrays;
+
+/**
+ * 用于绘制NV21数据的封装类
+ */
+public class NV21Drawer {
+ private static final String TAG = "NV21Drawer";
+
+
+ // SQUARE_VERTICES每2个值作为一个顶点
+ private static final int COUNT_PER_SQUARE_VERTICE = 2;
+ // COORD_VERTICES每2个值作为一个顶点
+ private static final int COUNT_PER_COORD_VERTICES = 2;
+ // 一个FLOAT占4个字节,用于分配内存时的计算
+ private static final int FLOAT_SIZE_BYTES = 4;
+
+ /**
+ * 片段着色器,正常效果
+ */
+ public static final String FRAG_SHADER_NORMAL =
+ "precision mediump float;\n" +
+ " varying vec2 tc;\n" +
+ " uniform sampler2D ySampler;\n" +
+ " uniform sampler2D vuSampler;\n" +
+ " const mat3 yuvToRgbMat = mat3(1.0, 1.0, 1.0, 0, -0.344, 1.77, 1.403, -0.714,0);\n" +
+ " void main()\n" +
+ " {\n" +
+ " vec3 yuv;\n" +
+ " yuv.x = texture2D(ySampler, tc).r;\n" +
+ " vec4 vuVec = texture2D(vuSampler, tc);\n" +
+ " yuv.y = vuVec.a - 0.5;\n" +
+ " yuv.z = vuVec.r - 0.5;\n" +
+ " gl_FragColor = vec4(yuvToRgbMat * yuv, 1.0);\n" +
+ " }";
+ /**
+ * 片段着色器,灰度效果。R = G = B = Y
+ */
+ public static final String FRAG_SHADER_GRAY =
+ "precision mediump float;\n" +
+ " varying vec2 tc;\n" +
+ " uniform sampler2D ySampler;\n" +
+ " void main()\n" +
+ " {\n" +
+ " vec3 yuv;\n" +
+ " yuv.xyz = texture2D(ySampler, tc).rrr;\n" +
+ " gl_FragColor = vec4(yuv, 1.0);\n" +
+ " }";
+
+ /**
+ * 顶点着色器
+ */
+ private static final String VERTEX_SHADER =
+ " attribute vec4 attr_position;\n" +
+ " attribute vec2 attr_tc;\n" +
+ " varying vec2 tc;\n" +
+ " void main() {\n" +
+ " gl_Position = attr_position;\n" +
+ " tc = attr_tc;\n" +
+ " }";
+
+ // 源视频帧宽/高
+ private int frameWidth, frameHeight;
+ // 是否镜像
+ private boolean isMirror;
+ // 是否旋转
+ private int rotateDegree = 0;
+
+ // 用于画框并显示的NV21
+ private byte[] nv21WithRect;
+
+ private ByteBuffer yBuf = null, vuBuf = null;
+
+ // 纹理id
+ private int[] yTexture = new int[1];
+ private int[] vuTexture = new int[1];
+
+ private String fragmentShaderCode = FRAG_SHADER_NORMAL;
+
+ private FloatBuffer squareVertices = null;
+ private FloatBuffer coordVertices = null;
+
+ private int programHandle = 0;
+
+ // gl_attr
+ private int glPosition;
+ private int textureCoord;
+
+ /**
+ * 设置不同的片段着色器代码以达到不同的预览效果
+ *
+ * @param fragmentShaderCode 片段着色器代码
+ */
+ public void setFragmentShaderCode(String fragmentShaderCode) {
+ this.fragmentShaderCode = fragmentShaderCode;
+ }
+
+
+ public void init(boolean isMirror, int rotateDegree, int frameWidth, int frameHeight) {
+ if (this.frameWidth == frameWidth
+ && this.frameHeight == frameHeight
+ && this.rotateDegree == rotateDegree
+ && this.isMirror == isMirror) {
+ return;
+ }
+ this.frameWidth = frameWidth;
+ this.frameHeight = frameHeight;
+ this.rotateDegree = rotateDegree;
+ this.isMirror = isMirror;
+
+ int yFrameSize = this.frameHeight * this.frameWidth;
+ int vuFrameSize = yFrameSize / 2;
+ yBuf = ByteBuffer.allocateDirect(yFrameSize);
+ vuBuf = ByteBuffer.allocateDirect(vuFrameSize);
+
+ // TODO:这段代码可删除
+ // 这里的作用是为VU数据预先填上0x80,避免打开时的瞬间全是绿色
+ byte[] vu = new byte[vuFrameSize];
+ Arrays.fill(vu, (byte) 0x80);
+ vuBuf.put(vu);
+ vuBuf.position(0);
+
+ // 顶点坐标
+ squareVertices = ByteBuffer
+ .allocateDirect(GLUtil.SQUARE_VERTICES.length * FLOAT_SIZE_BYTES)
+ .order(ByteOrder.nativeOrder())
+ .asFloatBuffer();
+ squareVertices.put(GLUtil.SQUARE_VERTICES).position(0);
+
+ // 纹理坐标
+ float[] coordVertice = GLUtil.getCoordVerticesByPreviewParams(isMirror, rotateDegree);
+ // 显示多块数据
+// for (int i = 0; i < coordVertice.length; i++) {
+// coordVertice[i] *= 2;
+// }
+ coordVertices = ByteBuffer.allocateDirect(coordVertice.length * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer();
+ coordVertices.put(coordVertice).position(0);
+
+ }
+
+ private void createTexture(int width, int height, int format, int[] textureId) {
+
+ GLES20.glGenTextures(1, textureId, 0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[0]);
+
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
+
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
+
+ GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, format, width, height, 0, format, GLES20.GL_UNSIGNED_BYTE, null);
+ }
+
+ /**
+ * 创建OpenGL Program并关联shader代码中的变量
+ */
+ public boolean createGLProgram() {
+ if (squareVertices == null || coordVertices == null) {
+ return false;
+ }
+ programHandle = GLUtil.createShaderProgram(fragmentShaderCode, VERTEX_SHADER);
+ if (programHandle != 0) {
+
+ GLES20.glUseProgram(programHandle);
+
+ glPosition = GLES20.glGetAttribLocation(programHandle, "attr_position");
+ textureCoord = GLES20.glGetAttribLocation(programHandle, "attr_tc");
+
+ GLES20.glEnableVertexAttribArray(glPosition);
+ GLES20.glEnableVertexAttribArray(textureCoord);
+
+ squareVertices.position(0);
+ GLES20.glVertexAttribPointer(glPosition, COUNT_PER_SQUARE_VERTICE, GLES20.GL_FLOAT, false, 8, squareVertices);
+ coordVertices.position(0);
+ GLES20.glVertexAttribPointer(textureCoord, COUNT_PER_COORD_VERTICES, GLES20.GL_FLOAT, false, 8, coordVertices);
+
+
+ int ySampler = GLES20.glGetUniformLocation(programHandle, "ySampler");
+ int vuSampler = GLES20.glGetUniformLocation(programHandle, "vuSampler");
+
+ GLES20.glUniform1i(ySampler, 0);
+ GLES20.glUniform1i(vuSampler, 1);
+
+
+ //启用纹理
+ GLES20.glEnable(GLES20.GL_TEXTURE_2D);
+ //创建纹理
+ createTexture(frameWidth, frameHeight, GLES20.GL_LUMINANCE, yTexture);
+ createTexture(frameWidth / 2, frameHeight / 2, GLES20.GL_LUMINANCE_ALPHA, vuTexture);
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ boolean prepareDraw() {
+ if (programHandle != 0) {
+ GLES20.glUseProgram(programHandle);
+
+ GLES20.glEnableVertexAttribArray(glPosition);
+ GLES20.glEnableVertexAttribArray(textureCoord);
+
+ squareVertices.position(0);
+ GLES20.glVertexAttribPointer(glPosition, COUNT_PER_SQUARE_VERTICE, GLES20.GL_FLOAT, false, 8, squareVertices);
+ coordVertices.position(0);
+ GLES20.glVertexAttribPointer(textureCoord, COUNT_PER_COORD_VERTICES, GLES20.GL_FLOAT, false, 8, coordVertices);
+
+ return true;
+ } else {
+ Log.e(TAG, "program not created!");
+ return false;
+ }
+ }
+
+ synchronized boolean render() {
+ if (vuBuf != null && programHandle != 0) {
+
+ // y
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yTexture[0]);
+ GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D,
+ 0,
+ 0,
+ 0,
+ frameWidth,
+ frameHeight,
+ GLES20.GL_LUMINANCE,
+ GLES20.GL_UNSIGNED_BYTE,
+ yBuf);
+
+ // vu
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, vuTexture[0]);
+ GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D,
+ 0,
+ 0,
+ 0,
+ frameWidth / 2,
+ frameHeight / 2,
+ GLES20.GL_LUMINANCE_ALPHA,
+ GLES20.GL_UNSIGNED_BYTE,
+ vuBuf);
+
+ // 在数据绑定完成后进行绘制
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+ return true;
+ }
+ return false;
+ }
+
+ boolean updateNV21(byte[] data) {
+ if (vuBuf == null) {
+ return false;
+ }
+ int ySize = frameWidth * frameHeight;
+ int vuSize = ySize / 2;
+ synchronized (this) {
+ yBuf.put(data, 0, ySize).position(0);
+ vuBuf.put(data, ySize, vuSize).position(0);
+ }
+ return true;
+ }
+
+ boolean updateNV21(byte[] data, Rect faceRect, int strokeWidth) {
+ if (vuBuf == null) {
+ return false;
+ }
+ // 避免重复创建,频繁GC
+ if (nv21WithRect == null || nv21WithRect.length != data.length) {
+ nv21WithRect = new byte[data.length];
+ }
+ System.arraycopy(data, 0, nv21WithRect, 0, nv21WithRect.length);
+
+ ImageUtil.drawRectOnNv21(nv21WithRect, frameWidth, frameHeight, Color.YELLOW, strokeWidth, faceRect);
+ int ySize = frameWidth * frameHeight;
+ int vuSize = ySize / 2;
+
+ synchronized (this) {
+ yBuf.put(nv21WithRect, 0, ySize).position(0);
+ vuBuf.put(nv21WithRect, ySize, vuSize).position(0);
+ }
+ return true;
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/FaceHelper.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/FaceHelper.java
new file mode 100644
index 0000000..b3c8675
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/FaceHelper.java
@@ -0,0 +1,1137 @@
+package com.sw.plate.utils.arcface.face;
+
+import android.graphics.Rect;
+import android.hardware.Camera;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.arcsoft.face.ErrorInfo;
+import com.arcsoft.face.FaceEngine;
+import com.arcsoft.face.FaceFeature;
+import com.arcsoft.face.FaceInfo;
+import com.arcsoft.face.ImageQualitySimilar;
+import com.arcsoft.face.LivenessInfo;
+import com.arcsoft.face.MaskInfo;
+import com.arcsoft.face.enums.ExtractType;
+import com.sw.plate.utils.L;
+import com.sw.plate.utils.arcface.FaceRectTransformer;
+import com.sw.plate.utils.arcface.face.constants.LivenessType;
+import com.sw.plate.utils.arcface.face.constants.RequestFeatureStatus;
+import com.sw.plate.utils.arcface.face.constants.RequestLivenessStatus;
+import com.sw.plate.utils.arcface.face.facefilter.FaceMoveFilter;
+import com.sw.plate.utils.arcface.face.facefilter.FaceRecognizeAreaFilter;
+import com.sw.plate.utils.arcface.face.facefilter.FaceRecognizeFilter;
+import com.sw.plate.utils.arcface.face.facefilter.FaceSizeFilter;
+import com.sw.plate.utils.arcface.face.model.CompareResult;
+import com.sw.plate.utils.arcface.face.model.FacePreviewInfo;
+import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration;
+import com.sw.plate.utils.arcface.face.model.RecognizeInfo;
+import com.sw.plate.utils.arcface.faceserver.FaceServer;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.disposables.Disposable;
+
+/**
+ * 人脸操作辅助类
+ */
+public class FaceHelper implements FaceListener {
+
+ private static final String TAG = "FaceHelper";
+
+ /**
+ * 识别结果的回调
+ */
+ private RecognizeCallback recognizeCallback;
+
+
+ /**
+ * 用于记录人脸识别过程信息
+ */
+ private ConcurrentHashMap recognizeInfoMap = new ConcurrentHashMap<>();
+
+ private CompositeDisposable getFeatureDelayedDisposables = new CompositeDisposable();
+ private CompositeDisposable delayFaceTaskCompositeDisposable = new CompositeDisposable();
+ /**
+ * 转换方式,用于IR活体检测
+ */
+ private IDualCameraFaceInfoTransformer dualCameraFaceInfoTransformer;
+
+ /**
+ * 线程池正在处理任务
+ */
+ private static final int ERROR_BUSY = -1;
+ /**
+ * 特征提取引擎为空
+ */
+ private static final int ERROR_FR_ENGINE_IS_NULL = -2;
+ /**
+ * 活体检测引擎为空
+ */
+ private static final int ERROR_FL_ENGINE_IS_NULL = -3;
+ /**
+ * 人脸追踪引擎
+ */
+ private FaceEngine ftEngine;
+ /**
+ * 特征提取引擎
+ */
+ private FaceEngine frEngine;
+ /**
+ * 活体检测引擎
+ */
+ private FaceEngine flEngine;
+
+ private Camera.Size previewSize;
+
+ private List faceInfoList = new CopyOnWriteArrayList<>();
+ private List maskInfoList = new CopyOnWriteArrayList<>();
+ /**
+ * 特征提取线程池
+ */
+ private ExecutorService frExecutor;
+ /**
+ * 活体检测线程池
+ */
+ private ExecutorService flExecutor;
+ /**
+ * 特征提取线程队列
+ */
+ private LinkedBlockingQueue frThreadQueue;
+ /**
+ * 活体检测线程队列
+ */
+ private LinkedBlockingQueue flThreadQueue;
+
+ private FaceRectTransformer rgbFaceRectTransformer;
+ private FaceRectTransformer irFaceRectTransformer;
+ /**
+ * 控制可识别区域(相对于View),若未设置,则是全部区域
+ */
+ private Rect recognizeArea = new Rect(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
+
+ private List faceRecognizeFilterList = new ArrayList<>();
+ /**
+ * 上次应用退出时,记录的该App检测过的人脸数了
+ */
+ private int trackedFaceCount = 0;
+ /**
+ * 本次打开引擎后的最大faceId
+ */
+ private int currentMaxFaceId = 0;
+
+ /**
+ * 是否只检测活体
+ */
+ private boolean onlyDetectLiveness;
+
+ /**
+ * 是否需要更新faceData
+ */
+ private boolean needUpdateFaceData;
+
+ /**
+ * 识别的配置项
+ */
+ private RecognizeConfiguration recognizeConfiguration;
+ private List currentTrackIdList = new ArrayList<>();
+ private List facePreviewInfoList = new ArrayList<>();
+ private Disposable timerDisposable;
+
+ /**
+ * 请求获取活体检测结果,需要传入活体的参数,以下参数同
+ *
+ * @param nv21 NV21格式的图像数据
+ * @param faceInfo 人脸信息
+ * @param width 图像宽度
+ * @param height 图像高度
+ * @param format 图像格式
+ * @param livenessType 活体检测类型
+ * @param waitLock
+ */
+ public void requestFaceLiveness(byte[] nv21, FacePreviewInfo faceInfo, int width, int height, int format, LivenessType livenessType, Object waitLock) {
+ if (flEngine != null && flThreadQueue.remainingCapacity() > 0) {
+ flExecutor.execute(new FaceLivenessDetectRunnable(nv21, faceInfo, width, height, format, livenessType, waitLock));
+ } else {
+ onFaceLivenessInfoGet(null, faceInfo.getTrackId(), ERROR_BUSY);
+ }
+
+ }
+
+ private FaceHelper(Builder builder) {
+ needUpdateFaceData = builder.needUpdateFaceData;
+ onlyDetectLiveness = builder.onlyDetectLiveness;
+ ftEngine = builder.ftEngine;
+ trackedFaceCount = builder.trackedFaceCount;
+ previewSize = builder.previewSize;
+ frEngine = builder.frEngine;
+ flEngine = builder.flEngine;
+ recognizeCallback = builder.recognizeCallback;
+ recognizeConfiguration = builder.recognizeConfiguration;
+ dualCameraFaceInfoTransformer = builder.dualCameraFaceInfoTransformer;
+ /*
+ * fr 线程队列大小
+ */
+ int frQueueSize = recognizeConfiguration.getMaxDetectFaces();
+ if (builder.frQueueSize > 0) {
+ frQueueSize = builder.frQueueSize;
+ } else {
+ Log.e(TAG, "frThread num must > 0, now using default value:" + frQueueSize);
+ }
+ frThreadQueue = new LinkedBlockingQueue<>(frQueueSize);
+ frExecutor = new ThreadPoolExecutor(1, frQueueSize, 0, TimeUnit.MILLISECONDS, frThreadQueue, r -> {
+ Thread t = new Thread(r);
+ t.setName("frThread-" + t.getId());
+ return t;
+ });
+
+ /*
+ * fl 线程队列大小
+ */
+ int flQueueSize = recognizeConfiguration.getMaxDetectFaces();
+ if (builder.flQueueSize > 0) {
+ flQueueSize = builder.flQueueSize;
+ } else {
+ Log.e(TAG, "flThread num must > 0, now using default value:" + flQueueSize);
+ }
+ flThreadQueue = new LinkedBlockingQueue(flQueueSize);
+ flExecutor = new ThreadPoolExecutor(1, flQueueSize, 0, TimeUnit.MILLISECONDS, flThreadQueue, r -> {
+ Thread t = new Thread(r);
+ t.setName("flThread-" + t.getId());
+ return t;
+ });
+ if (previewSize == null) {
+ throw new RuntimeException("previewSize must be specified!");
+ }
+ if (recognizeConfiguration.isEnableFaceSizeLimit()) {
+ // 由于目前人脸框的宽高接近一致,所以在使用时horizontalSize和verticalSize的值设置成一样
+ faceRecognizeFilterList.add(new FaceSizeFilter(recognizeConfiguration.getFaceSizeLimit(), recognizeConfiguration.getFaceSizeLimit()));
+ }
+ if (recognizeConfiguration.isEnableFaceMoveLimit()) {
+ faceRecognizeFilterList.add(new FaceMoveFilter(recognizeConfiguration.getFaceMoveLimit()));
+ }
+ if (recognizeConfiguration.isEnableFaceAreaLimit()) {
+ faceRecognizeFilterList.add(new FaceRecognizeAreaFilter(recognizeArea));
+ }
+ }
+
+ /**
+ * 请求获取人脸特征数据
+ *
+ * @param nv21 图像数据
+ * @param facePreviewInfo 人脸信息
+ * @param width 图像宽度
+ * @param height 图像高度
+ * @param format 图像格式
+ */
+ public void requestFaceFeature(byte[] nv21, FacePreviewInfo facePreviewInfo, int width, int height, int format) {
+ L.e("requestFaceFeature===frThreadQueue.remainingCapacity()=" + frThreadQueue.remainingCapacity());
+ if (frEngine != null && frThreadQueue.remainingCapacity() > 0) {
+ frExecutor.execute(new FaceRecognizeRunnable(nv21, facePreviewInfo, width, height, format));
+ } else {
+ onFaceFeatureInfoGet(null, facePreviewInfo.getTrackId(), ERROR_BUSY);
+ }
+ }
+
+ public void clearFacePreviewInfoList() {
+ clearLeftFace(facePreviewInfoList);
+ if (recognizeInfoMap != null)
+ recognizeInfoMap.clear();
+ if (getFeatureDelayedDisposables != null) {
+ getFeatureDelayedDisposables.clear();
+ }
+ if (delayFaceTaskCompositeDisposable != null) {
+ delayFaceTaskCompositeDisposable.clear();
+ }
+ if (faceInfoList != null) {
+ faceInfoList.clear();
+ }
+ if (maskInfoList != null) {
+ maskInfoList.clear();
+ }
+ if (frThreadQueue != null) {
+ frThreadQueue.clear();
+ }
+ if (flThreadQueue != null) {
+ flThreadQueue.clear();
+ }
+ if (faceRecognizeFilterList != null) {
+ faceRecognizeFilterList.clear();
+ }
+
+ if (currentTrackIdList != null) {
+ currentTrackIdList.clear();
+ }
+ if (facePreviewInfoList != null) {
+ facePreviewInfoList.clear();
+ }
+ }
+
+ /**
+ * 释放对象
+ */
+ public void release() {
+ if (getFeatureDelayedDisposables != null) {
+ getFeatureDelayedDisposables.clear();
+ }
+ if (!frExecutor.isShutdown()) {
+ frExecutor.shutdownNow();
+ frThreadQueue.clear();
+ }
+ if (!flExecutor.isShutdown()) {
+ flExecutor.shutdownNow();
+ flThreadQueue.clear();
+ }
+ if (faceInfoList != null) {
+ faceInfoList.clear();
+ }
+ if (frThreadQueue != null) {
+ frThreadQueue.clear();
+ frThreadQueue = null;
+ }
+ if (flThreadQueue != null) {
+ flThreadQueue.clear();
+ flThreadQueue = null;
+ }
+ faceInfoList = null;
+ }
+
+ /**
+ * 处理帧数据
+ *
+ * @param rgbNv21 可见光相机预览回传的NV21数据
+ * @param irNv21 红外相机预览回传的NV21数据
+ * @param doRecognize 是否进行识别
+ * @return 实时人脸处理结果,封装添加了一个trackId,trackId的获取依赖于faceId,用于记录人脸序号并保存
+ */
+ public List onPreviewFrame(@NonNull byte[] rgbNv21, @Nullable byte[] irNv21, boolean doRecognize) {
+ if (ftEngine != null) {
+ faceInfoList.clear();
+ maskInfoList.clear();
+ facePreviewInfoList.clear();
+ int code = ftEngine.detectFaces(rgbNv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList);
+ if (code != ErrorInfo.MOK) {
+ onFail(new Exception("detectFaces failed,code is " + code));
+ return facePreviewInfoList;
+ }
+ if (recognizeConfiguration.isKeepMaxFace()) {
+ keepMaxFace(faceInfoList);
+ }
+ refreshTrackId(faceInfoList);
+ if (faceInfoList.isEmpty()) {
+ return facePreviewInfoList;
+ }
+ if (!onlyDetectLiveness) {
+ code = ftEngine.process(rgbNv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList,
+ FaceEngine.ASF_MASK_DETECT);
+ if (code == ErrorInfo.MOK) {
+ code = ftEngine.getMask(maskInfoList);
+ if (code != ErrorInfo.MOK) {
+ onFail(new Exception("process getMask failed,code is " + code));
+ return facePreviewInfoList;
+ }
+ } else {
+ onFail(new Exception("process mask failed,code is " + code));
+ return facePreviewInfoList;
+ }
+ }
+
+ for (int i = 0; i < faceInfoList.size(); i++) {
+ FacePreviewInfo facePreviewInfo = new FacePreviewInfo(faceInfoList.get(i), currentTrackIdList.get(i));
+ if (!maskInfoList.isEmpty()) {
+ MaskInfo maskInfo = maskInfoList.get(i);
+ facePreviewInfo.setMask(maskInfo.getMask());
+ }
+ if (rgbFaceRectTransformer != null && recognizeArea != null) {
+ Rect rect = rgbFaceRectTransformer.adjustRect(faceInfoList.get(i).getRect());
+ Rect foreRect = rgbFaceRectTransformer.adjustRect(faceInfoList.get(i).getForeheadRect());
+ facePreviewInfo.setRgbTransformedRect(rect);
+ facePreviewInfo.setForeRect(foreRect);
+ }
+ if (irFaceRectTransformer != null) {
+ FaceInfo faceInfo = faceInfoList.get(i);
+ if (dualCameraFaceInfoTransformer != null) {
+ faceInfo = dualCameraFaceInfoTransformer.transformFaceInfo(faceInfo);
+ }
+ facePreviewInfo.setFaceInfoIr(faceInfo);
+ facePreviewInfo.setIrTransformedRect(irFaceRectTransformer.adjustRect(faceInfo.getRect()));
+ }
+ facePreviewInfoList.add(facePreviewInfo);
+ }
+ clearLeftFace(facePreviewInfoList);
+ if (doRecognize) {
+ doRecognize(rgbNv21, irNv21, facePreviewInfoList);
+ }
+ } else {
+ facePreviewInfoList.clear();
+ }
+ return facePreviewInfoList;
+ }
+
+ public void setRgbFaceRectTransformer(FaceRectTransformer rgbFaceRectTransformer) {
+ this.rgbFaceRectTransformer = rgbFaceRectTransformer;
+ }
+
+ public void setIrFaceRectTransformer(FaceRectTransformer irFaceRectTransformer) {
+ this.irFaceRectTransformer = irFaceRectTransformer;
+ }
+
+
+ /**
+ * 删除已经离开的人脸
+ *
+ * @param facePreviewInfoList 人脸和trackId列表
+ */
+ private void clearLeftFace(List facePreviewInfoList) {
+ if (facePreviewInfoList == null || facePreviewInfoList.size() == 0) {
+ if (getFeatureDelayedDisposables != null) {
+ getFeatureDelayedDisposables.clear();
+ }
+ }
+ Enumeration keys = recognizeInfoMap.keys();
+ while (keys.hasMoreElements()) {
+ int key = keys.nextElement();
+ boolean contained = false;
+ for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) {
+ if (facePreviewInfo.getTrackId() == key) {
+ contained = true;
+ break;
+ }
+ }
+ if (!contained) {
+ RecognizeInfo recognizeInfo = recognizeInfoMap.remove(key);
+ if (recognizeInfo != null) {
+ recognizeCallback.onNoticeChanged("leave");
+ // 人脸离开时,通知特征提取线程,避免一直等待活体结果
+ synchronized (recognizeInfo.getWaitLock()) {
+ recognizeInfo.getWaitLock().notifyAll();
+ }
+ }
+ }
+ }
+ }
+
+ private void doRecognize(byte[] rgbNv21, byte[] irNv21, List facePreviewInfoList) {
+ if (facePreviewInfoList != null && !facePreviewInfoList.isEmpty() && previewSize != null) {
+ for (FaceRecognizeFilter faceRecognizeFilter : faceRecognizeFilterList) {
+ faceRecognizeFilter.filter(facePreviewInfoList);
+ }
+ for (int i = 0; i < facePreviewInfoList.size(); i++) {
+ FacePreviewInfo facePreviewInfo = facePreviewInfoList.get(i);
+ if (!facePreviewInfo.isQualityPass()) {
+ continue;
+ }
+ //跳过mask值为MaskInfo.UNKNOWN的人脸
+ if (!onlyDetectLiveness && facePreviewInfo.getMask() == MaskInfo.UNKNOWN) {
+ continue;
+ }
+ RecognizeInfo recognizeInfo = getRecognizeInfo(recognizeInfoMap, facePreviewInfo.getTrackId());
+ int status = recognizeInfo.getRecognizeStatus();
+ /*
+ * 在活体检测开启,在人脸识别状态不为成功或人脸活体状态不为处理中(ANALYZING)且不为处理完成(ALIVE、NOT_ALIVE)时重新进行活体检测
+ */
+ if (recognizeConfiguration.isEnableLiveness() && status != RequestFeatureStatus.SUCCEED) {
+ int liveness = recognizeInfo.getLiveness();
+ if (liveness != LivenessInfo.ALIVE && liveness != LivenessInfo.NOT_ALIVE && liveness != RequestLivenessStatus.ANALYZING
+ || status == RequestFeatureStatus.FAILED) {
+ changeLiveness(facePreviewInfo.getTrackId(), RequestLivenessStatus.ANALYZING);
+ requestFaceLiveness(
+ irNv21 == null ? rgbNv21 : irNv21,
+ facePreviewInfo,
+ previewSize.width,
+ previewSize.height,
+ FaceEngine.CP_PAF_NV21,
+ irNv21 == null ? LivenessType.RGB : LivenessType.IR,
+ recognizeInfo.getWaitLock()
+ );
+ }
+ }
+ /*
+ * 对于每个人脸,若状态为空或者为失败,则请求特征提取(可根据需要添加其他判断以限制特征提取次数),
+ * 特征提取回传的人脸特征结果在{@link FaceListener#onFaceFeatureInfoGet(FaceFeature, Integer, Integer)}中回传
+ */
+ if (status == RequestFeatureStatus.TO_RETRY) {
+ changeRecognizeStatus(facePreviewInfo.getTrackId(), RequestFeatureStatus.SEARCHING);
+ requestFaceFeature(
+ rgbNv21, facePreviewInfo,
+ previewSize.width,
+ previewSize.height,
+ FaceEngine.CP_PAF_NV21
+ );
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onFail(Exception e) {
+ Log.e(TAG, "onFail:" + e.getMessage());
+ }
+
+ /**
+ * 获取识别信息,识别信息为空则创建一个新的
+ *
+ * @param recognizeInfoMap 存放识别信息的map
+ * @param trackId 人脸唯一标识
+ * @return 识别信息
+ */
+ public RecognizeInfo getRecognizeInfo(Map recognizeInfoMap, int trackId) {
+ RecognizeInfo recognizeInfo = recognizeInfoMap.get(trackId);
+ if (recognizeInfo == null) {
+ recognizeInfo = new RecognizeInfo();
+ recognizeInfoMap.put(trackId, recognizeInfo);
+ }
+ return recognizeInfo;
+ }
+
+ @Override
+ public void onFaceFeatureInfoGet(@Nullable FaceFeature faceFeature, Integer trackId, Integer errorCode) {
+ //FR成功
+ RecognizeInfo recognizeInfo = getRecognizeInfo(recognizeInfoMap, trackId);
+ if (faceFeature != null) {
+ // 人脸已离开,不用处理
+ if (recognizeInfo == null) {
+ return;
+ }
+ //不做活体检测的情况,直接搜索
+ if (!recognizeConfiguration.isEnableLiveness()) {
+ searchFace(faceFeature, trackId);
+ }
+ //活体检测通过,搜索特征
+ else if (recognizeInfo.getLiveness() == LivenessInfo.ALIVE) {
+ searchFace(faceFeature, trackId);
+ }
+ //活体检测未出结果,或者非活体,等待
+ else {
+ synchronized (recognizeInfo.getWaitLock()) {
+ try {
+ recognizeInfo.getWaitLock().wait();
+ if (recognizeInfoMap.containsKey(trackId)) {
+ onFaceFeatureInfoGet(faceFeature, trackId, errorCode);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "onFaceFeatureInfoGet: 等待活体结果时退出界面会执行,正常现象,可注释异常代码块");
+ e.printStackTrace();
+ }
+ }
+ }
+
+ }
+ //特征提取失败时,为了及时提示做个UI反馈,将name修改为"ExtractCode:${errorCode}",再重置状态
+ else {
+ if (recognizeInfo.increaseAndGetExtractErrorRetryCount() > recognizeConfiguration.getExtractRetryCount()) {
+ // 在尝试最大次数后,特征提取仍然失败,则认为识别未通过
+ recognizeInfo.setExtractErrorRetryCount(0);
+ retryRecognizeDelayed(trackId);
+ } else {
+ changeRecognizeStatus(trackId, RequestFeatureStatus.TO_RETRY);
+ }
+ }
+ }
+
+
+ // 定义一个Handler,用于处理延迟任务
+ private Handler delayHandler = new Handler(Looper.getMainLooper());
+
+ /**
+ * 延迟 {@link RecognizeConfiguration#getLivenessFailedRetryInterval()}后,重新进行活体检测
+ *
+ * @param trackId 人脸ID
+ */
+ private void retryLivenessDetectDelayed(final Integer trackId) {
+ Log.d(TAG, "retryLivenessDetectDelayed: trackId = " + trackId + ", threadName =" + Thread.currentThread().getName());
+ Log.d(TAG, "retryLivenessDetectDelayed: Interval = " + recognizeConfiguration.getLivenessFailedRetryInterval());
+
+ // 创建延迟任务
+ Runnable delayTask = new Runnable() {
+ @Override
+ public void run() {
+ // 将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测
+ changeLiveness(trackId, LivenessInfo.UNKNOWN);
+ // 从集合中移除这个任务
+ delayFaceTaskList.remove(this);
+ }
+ };
+
+ // 将任务添加到集合中管理
+ delayFaceTaskList.add(delayTask);
+
+ // 发送延迟任务
+ delayHandler.postDelayed(delayTask, recognizeConfiguration.getLivenessFailedRetryInterval());
+ }
+
+ /**
+ * 延迟 {@link RecognizeConfiguration#getRecognizeFailedRetryInterval()}后,重新进行人脸识别
+ *
+ * @param trackId 人脸ID
+ */
+ private void retryRecognizeDelayed(final Integer trackId) {
+ Log.d(TAG, "retryRecognizeDelayed: trackId = " + trackId);
+ changeRecognizeStatus(trackId, RequestFeatureStatus.FAILED);
+
+ // 创建延迟任务
+ Runnable delayTask = new Runnable() {
+ @Override
+ public void run() {
+ // 将该人脸特征提取状态置为FAILED,帧回调处理时会重新进行活体检测
+ changeRecognizeStatus(trackId, RequestFeatureStatus.TO_RETRY);
+ // 从集合中移除这个任务
+ delayFaceTaskList.remove(this);
+ }
+ };
+
+ // 将任务添加到集合中管理
+ delayFaceTaskList.add(delayTask);
+
+ // 发送延迟任务
+ delayHandler.postDelayed(delayTask, recognizeConfiguration.getRecognizeFailedRetryInterval());
+ }
+
+ // 需要在类中定义一个集合来管理所有的延迟任务,替代原来的CompositeDisposable
+ private List delayFaceTaskList = new ArrayList<>();
+
+ @Override
+ public void onFaceLivenessInfoGet(@Nullable LivenessInfo livenessInfo, Integer trackId, Integer errorCode) {
+ Log.d(TAG, "onFaceLivenessInfoGet: trackId = " + trackId);
+ if (livenessInfo != null) {
+ int liveness = livenessInfo.getLiveness();
+ Log.i(TAG, "onFaceLivenessInfoGet liveness:" + liveness);
+ changeLiveness(trackId, liveness);
+ // 非活体,重试
+ if (liveness != LivenessInfo.ALIVE) {
+ noticeCurrentStatus("活体检测未通过");
+ // 延迟 FAIL_RETRY_INTERVAL 后,将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测
+ retryLivenessDetectDelayed(trackId);
+ }
+ } else {
+ RecognizeInfo recognizeInfo = getRecognizeInfo(recognizeInfoMap, trackId);
+ // 连续多次活体检测失败(接口调用回传值非0),将活体检测值重置为未知,会在帧回调中重新进行活体检测
+ if (recognizeInfo.increaseAndGetLivenessErrorRetryCount() > recognizeConfiguration.getLivenessRetryCount()) {
+ recognizeInfo.setLivenessErrorRetryCount(0);
+ retryLivenessDetectDelayed(trackId);
+ } else {
+ changeLiveness(trackId, LivenessInfo.UNKNOWN);
+ }
+ }
+ }
+
+ private void noticeCurrentStatus(String notice) {
+ if (recognizeCallback != null) {
+ recognizeCallback.onNoticeChanged(notice);
+ }
+ if (timerDisposable != null && !timerDisposable.isDisposed()) {
+ timerDisposable.dispose();
+ }
+ timerDisposable = Observable.timer(1500, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(aLong -> {
+ if (recognizeCallback != null) {
+ recognizeCallback.onNoticeChanged("");
+ }
+ });
+ }
+
+ private void searchFace(final FaceFeature faceFeature, final Integer trackId) {
+ CompareResult compareResult = FaceServer.getInstance().searchFaceFeature(faceFeature, frEngine);
+ if (compareResult == null || compareResult.getFaceEntity() == null) {
+ retryRecognizeDelayed(trackId);
+ return;
+ }
+ compareResult.setTrackId(trackId);
+ boolean pass = compareResult.getSimilar() > recognizeConfiguration.getSimilarThreshold();
+ recognizeCallback.onRecognized(compareResult, getRecognizeInfo(recognizeInfoMap, trackId).getLiveness(), pass);
+ if (pass) {
+ setName(trackId, "识别通过");
+ noticeCurrentStatus("识别通过");
+ changeRecognizeStatus(trackId, RequestFeatureStatus.SUCCEED);
+ } else {
+ noticeCurrentStatus("未通过:NOT_REGISTERED");
+ retryRecognizeDelayed(trackId);
+ }
+ }
+
+ /**
+ * 人脸特征提取线程
+ */
+ public class FaceRecognizeRunnable implements Runnable {
+ private FaceInfo faceInfo;
+ private int width;
+ private int height;
+ private int format;
+ private Integer trackId;
+ private byte[] nv21Data;
+ private int isMask;
+
+ /**
+ * 异步特征提取任务的构造函数
+ *
+ * @param nv21Data 可见光图像数据
+ * @param facePreviewInfo 人脸信息
+ * @param width 图像宽度
+ * @param height 图像高度
+ * @param format 图像格式
+ */
+ private FaceRecognizeRunnable(byte[] nv21Data, FacePreviewInfo facePreviewInfo, int width, int height, int format) {
+ if (nv21Data == null) {
+ return;
+ }
+ this.nv21Data = nv21Data;
+ this.faceInfo = new FaceInfo(facePreviewInfo.getFaceInfoRgb());
+ this.width = width;
+ this.height = height;
+ this.format = format;
+ this.trackId = facePreviewInfo.getTrackId();
+ this.isMask = facePreviewInfo.getMask();
+ }
+
+ @Override
+ public void run() {
+ if (nv21Data != null) {
+ if (frEngine != null) {
+ if (recognizeConfiguration.isEnableImageQuality()) {
+ /*
+ * 开启人脸质量检测
+ */
+ ImageQualitySimilar qualitySimilar = new ImageQualitySimilar();
+ int iqCode;
+ long iqStartTime = System.currentTimeMillis();
+ synchronized (frEngine) {
+ iqCode = frEngine.imageQualityDetect(nv21Data, width, height, format, faceInfo, isMask, qualitySimilar);
+ }
+ Log.i(TAG, "fr iqTime:" + (System.currentTimeMillis() - iqStartTime) + "ms");
+ if (iqCode == ErrorInfo.MOK) {
+ float quality = qualitySimilar.getScore();
+ float destQuality = isMask == MaskInfo.WORN ? recognizeConfiguration.getImageQualityMaskRecognizeThreshold() :
+ recognizeConfiguration.getImageQualityNoMaskRecognizeThreshold();
+ if (quality >= destQuality) {
+ extractFace();
+ } else {
+ onFaceFail(iqCode, "fr imageQualityDetect score invalid");
+ }
+ } else {
+ onFaceFail(iqCode, "fr imageQuality failed errorCode is " + iqCode);
+ }
+ } else {
+ extractFace();
+ }
+ } else {
+ onFaceFail(ERROR_FR_ENGINE_IS_NULL, "fr failed ,frEngine is null");
+ }
+ }
+ nv21Data = null;
+ }
+
+ /**
+ * 对人脸图像进行特征提取
+ */
+ private void extractFace() {
+ long irStartTime = System.currentTimeMillis();
+ FaceFeature faceFeature = new FaceFeature();
+ int frCode;
+ synchronized (frEngine) {
+ /*
+ * 该场景为识别场景,所以参数“ExtractType”值为ExtractType.RECOGNIZE,且参数“mask”值为实际检测到的值,即isMask
+ */
+ frCode = frEngine.extractFaceFeature(nv21Data, width, height, format, faceInfo, ExtractType.RECOGNIZE, isMask, faceFeature);
+ }
+ Log.i(TAG, "frTime:" + (System.currentTimeMillis() - irStartTime) + "ms");
+ if (frCode == ErrorInfo.MOK) {
+ onFaceFeatureInfoGet(faceFeature, trackId, frCode);
+ } else {
+ onFaceFail(frCode, "fr failed errorCode is " + frCode);
+ }
+ }
+
+ private void onFaceFail(int code, String errorMsg) {
+ onFaceFeatureInfoGet(null, trackId, code);
+ onFail(new Exception(errorMsg));
+ }
+ }
+
+ /**
+ * 活体检测的线程
+ */
+ public class FaceLivenessDetectRunnable implements Runnable {
+ private FaceInfo faceInfo;
+ private int width;
+ private int height;
+ private int format;
+ private Integer trackId;
+ private byte[] nv21Data;
+ private LivenessType livenessType;
+ private Object waitLock;
+
+ /**
+ * 异步活体任务的构造函数
+ *
+ * @param nv21Data 可见光或红外图像数据
+ * @param faceInfo 可见光人脸检测得到的人脸信息
+ * @param width 图像宽度
+ * @param height 图像高度
+ * @param format 图像格式
+ * @param livenessType 活体检测类型,可以是可见光活体检测{@link LivenessType#RGB}或红外活体检测{@link LivenessType#IR}
+ * @param waitLock 活体检测通过后,调用该对象的notifyAll函数,通知识别线程活体已通过
+ */
+ private FaceLivenessDetectRunnable(byte[] nv21Data, FacePreviewInfo faceInfo, int width, int height, int format, LivenessType livenessType, Object waitLock) {
+ if (nv21Data == null) {
+ return;
+ }
+ this.nv21Data = nv21Data;
+ this.faceInfo = new FaceInfo(faceInfo.getFaceInfoRgb());
+ this.width = width;
+ this.height = height;
+ this.format = format;
+ this.trackId = faceInfo.getTrackId();
+ this.livenessType = livenessType;
+ this.waitLock = waitLock;
+ }
+
+ @Override
+ public void run() {
+ if (nv21Data != null) {
+ if (flEngine != null) {
+ processLiveness();
+ } else {
+ onProcessFail(ERROR_FL_ENGINE_IS_NULL, "fl failed ,frEngine is null");
+ }
+ }
+ nv21Data = null;
+ }
+
+ /**
+ * 执行活体检测
+ */
+ private void processLiveness() {
+ List livenessInfoList = new ArrayList<>();
+ int flCode = -1;
+ synchronized (flEngine) {
+ long flStartTime = System.currentTimeMillis();
+ if (livenessType == LivenessType.RGB) {
+ // RGB活体检测
+ flCode = flEngine.process(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_LIVENESS);
+ } else {
+ // IR活体检测,若有设置双目偏移,则先进行人脸框映射
+ if (dualCameraFaceInfoTransformer != null) {
+ faceInfo = dualCameraFaceInfoTransformer.transformFaceInfo(faceInfo);
+ }
+ List faceInfoList = new ArrayList<>();
+ int fdCode = flEngine.detectFaces(nv21Data, width, height, format, faceInfoList);
+ boolean isFaceExists = isFaceExists(faceInfoList, faceInfo);
+ if (fdCode == ErrorInfo.MOK && isFaceExists) {
+ if (needUpdateFaceData) {
+ /*
+ * 若IR人脸框有偏移,则需要对IR的人脸数据进行updateFaceData处理,再将处理后的FaceInfo信息传输给活体检测接口
+ */
+ flCode = flEngine.updateFaceData(nv21Data, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21,
+ new ArrayList<>(Collections.singletonList(faceInfo)));
+ if (flCode == ErrorInfo.MOK) {
+ flCode = flEngine.processIr(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_IR_LIVENESS);
+ }
+ } else {
+ flCode = flEngine.processIr(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_IR_LIVENESS);
+ }
+ } else {
+ onFail(new Exception("ir detectFaces failed fdCode:" + fdCode + ",isFaceExists:" + isFaceExists));
+ }
+ }
+ Log.i(TAG, "flTime:" + (System.currentTimeMillis() - flStartTime) + "ms");
+ }
+ if (flCode == ErrorInfo.MOK) {
+ if (livenessType == LivenessType.RGB) {
+ flCode = flEngine.getLiveness(livenessInfoList);
+ } else {
+ flCode = flEngine.getIrLiveness(livenessInfoList);
+ }
+ }
+
+ if (flCode == ErrorInfo.MOK && !livenessInfoList.isEmpty()) {
+ onFaceLivenessInfoGet(livenessInfoList.get(0), trackId, flCode);
+ if (livenessInfoList.get(0).getLiveness() == LivenessInfo.ALIVE) {
+ synchronized (waitLock) {
+ waitLock.notifyAll();
+ }
+ }
+ } else {
+ onProcessFail(flCode, "fl failed errorCode is " + flCode);
+ }
+ }
+
+ private void onProcessFail(int code, String msg) {
+ onFaceLivenessInfoGet(null, trackId, code);
+ onFail(new Exception(msg));
+ }
+ }
+
+ /**
+ * 如果人脸列表中有一个人脸和faceInfo相交,则认为该faceInfo可信
+ *
+ * @param faceInfoList 人脸信息列表
+ * @param faceInfo 人脸信息
+ * @return 人脸信息列表中是否有人脸和传入的人脸信息相交
+ */
+ public static boolean isFaceExists(List faceInfoList, FaceInfo faceInfo) {
+ if (faceInfoList == null || faceInfoList.isEmpty() || faceInfo == null) {
+ return false;
+ }
+ for (FaceInfo info : faceInfoList) {
+ if (Rect.intersects(faceInfo.getRect(), info.getRect())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * 刷新trackId
+ *
+ * @param ftFaceList 传入的人脸列表
+ */
+ private void refreshTrackId(List ftFaceList) {
+ currentTrackIdList.clear();
+ for (FaceInfo faceInfo : ftFaceList) {
+ currentTrackIdList.add(faceInfo.getFaceId() + trackedFaceCount);
+ }
+ if (!ftFaceList.isEmpty()) {
+ currentMaxFaceId = ftFaceList.get(ftFaceList.size() - 1).getFaceId();
+ }
+ }
+
+ /**
+ * 获取当前的最大trackID,可用于退出时保存
+ *
+ * @return 当前trackId
+ */
+ public int getTrackedFaceCount() {
+ // 引擎的人脸下标从0开始,因此需要+1
+ return trackedFaceCount + currentMaxFaceId + 1;
+ }
+
+ /**
+ * 新增搜索成功的人脸
+ *
+ * @param trackId 指定的trackId
+ * @param name trackId对应的人脸
+ */
+ public void setName(int trackId, String name) {
+ RecognizeInfo recognizeInfo = recognizeInfoMap.get(trackId);
+ if (recognizeInfo != null) {
+ recognizeInfo.setName(name);
+ }
+ }
+
+
+ /**
+ * 设置转换方式,用于IR活体检测
+ *
+ * @param transformer 转换方式
+ */
+ public void setDualCameraFaceInfoTransformer(IDualCameraFaceInfoTransformer transformer) {
+ this.dualCameraFaceInfoTransformer = transformer;
+ }
+
+
+ public String getName(int trackId) {
+ RecognizeInfo recognizeInfo = recognizeInfoMap.get(trackId);
+ return recognizeInfo == null ? null : recognizeInfo.getName();
+ }
+
+
+ /**
+ * 设置可识别区域(相对于View)
+ *
+ * @param recognizeArea 可识别区域
+ */
+ public void setRecognizeArea(Rect recognizeArea) {
+ if (recognizeArea != null) {
+ this.recognizeArea.set(recognizeArea);
+ }
+ }
+
+ @IntDef(value = {
+ RequestFeatureStatus.FAILED,
+ RequestFeatureStatus.SEARCHING,
+ RequestFeatureStatus.SUCCEED,
+ RequestFeatureStatus.TO_RETRY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface RequestFaceFeatureStatus {
+ }
+
+ @IntDef(value = {
+ LivenessInfo.ALIVE,
+ LivenessInfo.NOT_ALIVE,
+ LivenessInfo.UNKNOWN,
+ LivenessInfo.FACE_NUM_MORE_THAN_ONE,
+ LivenessInfo.FACE_TOO_SMALL,
+ LivenessInfo.FACE_ANGLE_TOO_LARGE,
+ LivenessInfo.FACE_BEYOND_BOUNDARY,
+ RequestLivenessStatus.ANALYZING
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface RequestFaceLivenessStatus {
+ }
+
+ /**
+ * 修改人脸识别的状态
+ *
+ * @param trackId 根据VIDEO模式人脸检测获取的人脸的唯一标识
+ * @param newStatus 新的识别状态,详见{@link RequestFeatureStatus}中的定义
+ */
+ public void changeRecognizeStatus(int trackId, @RequestFaceFeatureStatus int newStatus) {
+ getRecognizeInfo(recognizeInfoMap, trackId).setRecognizeStatus(newStatus);
+ }
+
+ /**
+ * 修改活体活体值或活体检测状态
+ *
+ * @param trackId 根据VIDEO模式人脸检测获取的人脸的唯一标识
+ * @param newLiveness 新的活体值或活体检测状态
+ */
+ public void changeLiveness(int trackId, @RequestFaceLivenessStatus int newLiveness) {
+ getRecognizeInfo(recognizeInfoMap, trackId).setLiveness(newLiveness);
+ }
+
+ /**
+ * 获取活体值或活体检测状态
+ *
+ * @param trackId 根据VIDEO模式人脸检测获取的人脸的唯一标识
+ * @return 活体值或活体检测状态
+ */
+ public Integer getLiveness(int trackId) {
+ return getRecognizeInfo(recognizeInfoMap, trackId).getLiveness();
+ }
+
+ /**
+ * 获取人脸识别状态
+ *
+ * @param trackId 根据VIDEO模式人脸检测获取的人脸的唯一标识
+ * @return 人脸识别状态
+ */
+ public Integer getRecognizeStatus(int trackId) {
+ return getRecognizeInfo(recognizeInfoMap, trackId).getRecognizeStatus();
+ }
+
+ /**
+ * 保留ftFaceList中最大的人脸
+ *
+ * @param ftFaceList 人脸追踪时,一帧数据的人脸信息
+ */
+ private static void keepMaxFace(List ftFaceList) {
+ if (ftFaceList == null || ftFaceList.size() <= 1) {
+ return;
+ }
+ FaceInfo maxFaceInfo = ftFaceList.get(0);
+ for (FaceInfo faceInfo : ftFaceList) {
+ if (faceInfo.getRect().width() > maxFaceInfo.getRect().width()) {
+ maxFaceInfo = faceInfo;
+ }
+ }
+ ftFaceList.clear();
+ ftFaceList.add(maxFaceInfo);
+ }
+
+
+ public static final class Builder {
+ private FaceEngine ftEngine;
+ private FaceEngine frEngine;
+ private FaceEngine flEngine;
+ private Camera.Size previewSize;
+ private boolean onlyDetectLiveness;
+ private boolean needUpdateFaceData;
+ private RecognizeConfiguration recognizeConfiguration;
+ private RecognizeCallback recognizeCallback;
+ private IDualCameraFaceInfoTransformer dualCameraFaceInfoTransformer;
+ private int frQueueSize;
+ private int flQueueSize;
+ private int trackedFaceCount;
+
+ public Builder() {
+ }
+
+ public Builder recognizeConfiguration(RecognizeConfiguration val) {
+ recognizeConfiguration = val;
+ return this;
+ }
+
+ public Builder dualCameraFaceInfoTransformer(IDualCameraFaceInfoTransformer val) {
+ dualCameraFaceInfoTransformer = val;
+ return this;
+ }
+
+ public Builder recognizeCallback(RecognizeCallback val) {
+ recognizeCallback = val;
+ return this;
+ }
+
+ public Builder ftEngine(FaceEngine val) {
+ ftEngine = val;
+ return this;
+ }
+
+ public Builder frEngine(FaceEngine val) {
+ frEngine = val;
+ return this;
+ }
+
+ public Builder flEngine(FaceEngine val) {
+ flEngine = val;
+ return this;
+ }
+
+ public Builder previewSize(Camera.Size val) {
+ previewSize = val;
+ return this;
+ }
+
+ public Builder frQueueSize(int val) {
+ frQueueSize = val;
+ return this;
+ }
+
+ public Builder flQueueSize(int val) {
+ flQueueSize = val;
+ return this;
+ }
+
+ public Builder trackedFaceCount(int val) {
+ trackedFaceCount = val;
+ return this;
+ }
+
+ public Builder onlyDetectLiveness(boolean val) {
+ onlyDetectLiveness = val;
+ return this;
+ }
+
+ public Builder needUpdateFaceData(boolean val) {
+ needUpdateFaceData = val;
+ return this;
+ }
+
+ public FaceHelper build() {
+ return new FaceHelper(this);
+ }
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/FaceListener.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/FaceListener.java
new file mode 100644
index 0000000..e3080d9
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/FaceListener.java
@@ -0,0 +1,37 @@
+package com.sw.plate.utils.arcface.face;
+
+import androidx.annotation.Nullable;
+
+import com.arcsoft.face.FaceFeature;
+import com.arcsoft.face.LivenessInfo;
+
+/**
+ * 人脸处理回调
+ */
+public interface FaceListener {
+ /**
+ * 当出现异常时执行
+ *
+ * @param e 异常信息
+ */
+ void onFail(Exception e);
+
+
+ /**
+ * 请求人脸特征后的回调
+ *
+ * @param faceFeature 人脸特征数据
+ * @param trackId 人脸Id(相当于请求码)
+ * @param errorCode 错误码
+ */
+ void onFaceFeatureInfoGet(@Nullable FaceFeature faceFeature, Integer trackId, Integer errorCode);
+
+ /**
+ * 请求活体检测后的回调
+ *
+ * @param livenessInfo 活体检测结果
+ * @param trackId 人脸Id(相当于请求码)
+ * @param errorCode 错误码
+ */
+ void onFaceLivenessInfoGet(@Nullable LivenessInfo livenessInfo, Integer trackId, Integer errorCode);
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/IDualCameraFaceInfoTransformer.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/IDualCameraFaceInfoTransformer.java
new file mode 100644
index 0000000..55394fb
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/IDualCameraFaceInfoTransformer.java
@@ -0,0 +1,16 @@
+package com.sw.plate.utils.arcface.face;
+
+import com.arcsoft.face.FaceInfo;
+
+/**
+ * 设置双目识别时,将RGB Camera帧数据检测到的人脸信息用于IR Camera帧数据活体检测时的转换方式
+ */
+public interface IDualCameraFaceInfoTransformer {
+ /**
+ * 将RGB Camera帧数据检测到的人脸信息用于IR Camera帧数据活体检测时的转换方式
+ *
+ * @param faceInfo RGB Camera帧数据检测到的人脸信息
+ * @return 转换后,用于IR活体检测的FaceInfo
+ */
+ FaceInfo transformFaceInfo(FaceInfo faceInfo);
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/RecognizeCallback.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/RecognizeCallback.java
new file mode 100644
index 0000000..1694616
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/RecognizeCallback.java
@@ -0,0 +1,20 @@
+package com.sw.plate.utils.arcface.face;
+
+
+import com.sw.plate.utils.arcface.face.model.CompareResult;
+
+public interface RecognizeCallback {
+ /**
+ * 识别结果回调
+ *
+ * @param compareResult 比对结果
+ * @param liveness 活体值
+ * @param similarPass 是否通过(依据设置的阈值)
+ */
+ void onRecognized(CompareResult compareResult, Integer liveness, boolean similarPass);
+
+ /**
+ * 提示文字变更的回调
+ */
+ void onNoticeChanged(String notice);
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/Test.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/Test.java
new file mode 100644
index 0000000..535f2f8
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/Test.java
@@ -0,0 +1,49 @@
+package com.sw.plate.utils.arcface.face;
+
+
+import android.util.Log;
+
+import java.util.concurrent.TimeUnit;
+
+import io.reactivex.Observable;
+import io.reactivex.Observer;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.disposables.Disposable;
+
+public class Test {
+
+ private static final String TAG = "Test";
+ private CompositeDisposable delayFaceTaskCompositeDisposable = new CompositeDisposable();
+
+ public void test1() {
+ Log.d(TAG, "test1: ");
+ Observable.timer(10, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(new Observer() {
+ Disposable disposable;
+
+ @Override
+ public void onSubscribe(Disposable d) {
+ disposable = d;
+ delayFaceTaskCompositeDisposable.add(disposable);
+ }
+
+ @Override
+ public void onNext(Long value) {
+
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ e.printStackTrace();
+ }
+
+ @Override
+ public void onComplete() {
+ Log.d(TAG, "onComplete: ");
+ delayFaceTaskCompositeDisposable.remove(disposable);
+ }
+ });
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/LivenessType.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/LivenessType.java
new file mode 100644
index 0000000..58ba754
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/LivenessType.java
@@ -0,0 +1,15 @@
+package com.sw.plate.utils.arcface.face.constants;
+
+/**
+ * 活体检测类型
+ */
+public enum LivenessType {
+ /**
+ * RGB活体检测
+ */
+ RGB,
+ /**
+ * 红外活体检测
+ */
+ IR
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RecognizeColor.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RecognizeColor.java
new file mode 100644
index 0000000..98d0497
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RecognizeColor.java
@@ -0,0 +1,22 @@
+package com.sw.plate.utils.arcface.face.constants;
+
+import android.graphics.Color;
+
+/**
+ * 识别过程中人脸框的颜色
+ */
+public class RecognizeColor {
+ /**
+ * 未知情况的颜色
+ */
+ public static final int COLOR_UNKNOWN = Color.YELLOW;
+ /**
+ * 成功的颜色
+ */
+ public static final int COLOR_SUCCESS = Color.GREEN;
+ /**
+ * 失败的颜色
+ */
+ public static final int COLOR_FAILED = Color.YELLOW;
+
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RequestFeatureStatus.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RequestFeatureStatus.java
new file mode 100644
index 0000000..08137d4
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RequestFeatureStatus.java
@@ -0,0 +1,29 @@
+package com.sw.plate.utils.arcface.face.constants;
+
+/**
+ * 人脸识别中可能出现的状态
+ *
+ * @author
+ */
+public @interface RequestFeatureStatus {
+ /**
+ * 默认状态
+ */
+ int DEFAULT = -1;
+ /**
+ * 处理中
+ */
+ int SEARCHING = 0;
+ /**
+ * 识别成功
+ */
+ int SUCCEED = 1;
+ /**
+ * 待重试
+ */
+ int TO_RETRY = 2;
+ /**
+ * 识别失败
+ */
+ int FAILED = 3;
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RequestLivenessStatus.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RequestLivenessStatus.java
new file mode 100644
index 0000000..9ed3eb9
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RequestLivenessStatus.java
@@ -0,0 +1,5 @@
+package com.sw.plate.utils.arcface.face.constants;
+
+public class RequestLivenessStatus {
+ public static final int ANALYZING = 10;
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceMoveFilter.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceMoveFilter.java
new file mode 100644
index 0000000..3fef1f3
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceMoveFilter.java
@@ -0,0 +1,93 @@
+package com.sw.plate.utils.arcface.face.facefilter;
+
+import android.graphics.Rect;
+
+import com.sw.plate.utils.arcface.face.model.FacePreviewInfo;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.LinkedBlockingDeque;
+
+/**
+ * 人脸移动过滤器:
+ * 仅保留在{@link FaceMoveFilter#CHECK_QUEUE_SIZE}帧数内,每一帧人脸的移动大小都小于{@link FaceMoveFilter#movePixels}的人脸
+ */
+public class FaceMoveFilter implements FaceRecognizeFilter {
+ private static final String TAG = "FaceMoveFilter";
+ private Map> facePositionQueueMap = new ConcurrentHashMap<>();
+ private static final int CHECK_QUEUE_SIZE = 5;
+ private double movePixels;
+
+ public FaceMoveFilter(double movePixels) {
+ this.movePixels = movePixels;
+ }
+
+ @Override
+ public void filter(List facePreviewInfoList) {
+ clearFacesNotInPreview(facePreviewInfoList);
+ for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) {
+ LinkedBlockingDeque rectDeque = facePositionQueueMap.get(facePreviewInfo.getTrackId());
+ if (rectDeque == null) {
+ rectDeque = new LinkedBlockingDeque<>(CHECK_QUEUE_SIZE);
+ facePositionQueueMap.put(facePreviewInfo.getTrackId(), rectDeque);
+ }
+ if (rectDeque.remainingCapacity() == 0) {
+ rectDeque.removeLast();
+ }
+ rectDeque.push(facePreviewInfo.getFaceInfoRgb().getRect());
+
+ if (!facePreviewInfo.isQualityPass()) {
+ continue;
+ }
+
+ boolean qualityPass = false;
+ if (rectDeque.size() == CHECK_QUEUE_SIZE) {
+ qualityPass = true;
+ Iterator iterator = rectDeque.iterator();
+ Rect previous = iterator.next();
+ while (iterator.hasNext()) {
+ Rect current = iterator.next();
+ double distance = getDistance(current, previous);
+ previous = current;
+ if (distance > movePixels) {
+ qualityPass = false;
+ break;
+ }
+ }
+ }
+ facePreviewInfo.setQualityPass(qualityPass);
+ }
+ }
+
+ private void clearFacesNotInPreview(List facePreviewInfo) {
+ Set trackIdSet = facePositionQueueMap.keySet();
+ for (Integer trackId : trackIdSet) {
+ boolean contains = false;
+ for (FacePreviewInfo previewInfo : facePreviewInfo) {
+ if (previewInfo.getTrackId() == trackId) {
+ contains = true;
+ break;
+ }
+ }
+ if (!contains) {
+ facePositionQueueMap.remove(trackId);
+ }
+ }
+ }
+
+ public static double getDistance(Rect first, Rect second) {
+ int firstX = first.centerX();
+ int firstY = first.centerY();
+
+ int secondX = second.centerX();
+ int secondY = second.centerY();
+
+ int distanceX = secondX - firstX;
+ int distanceY = secondY - firstY;
+
+ return Math.sqrt(distanceX * distanceX + distanceY * distanceY);
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceRecognizeAreaFilter.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceRecognizeAreaFilter.java
new file mode 100644
index 0000000..e713432
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceRecognizeAreaFilter.java
@@ -0,0 +1,31 @@
+package com.sw.plate.utils.arcface.face.facefilter;
+
+
+import android.graphics.Rect;
+
+import com.sw.plate.utils.arcface.face.model.FacePreviewInfo;
+
+import java.util.List;
+
+/**
+ * 人脸识别区域过滤器:
+ * 仅保留人脸区域在{@link FaceRecognizeAreaFilter#validArea}中的人脸。(基于View位置判断)
+ */
+public class FaceRecognizeAreaFilter implements FaceRecognizeFilter {
+ private static final String TAG = "FaceRecognizeAreaFilter";
+ private Rect validArea;
+
+ public FaceRecognizeAreaFilter(Rect validArea) {
+ this.validArea = validArea;
+ }
+
+ @Override
+ public void filter(List facePreviewInfoList) {
+ for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) {
+ if (!facePreviewInfo.isQualityPass()) {
+ continue;
+ }
+ facePreviewInfo.setQualityPass(validArea.contains(facePreviewInfo.getRgbTransformedRect()));
+ }
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceRecognizeFilter.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceRecognizeFilter.java
new file mode 100644
index 0000000..0c80f22
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceRecognizeFilter.java
@@ -0,0 +1,13 @@
+package com.sw.plate.utils.arcface.face.facefilter;
+
+
+import com.sw.plate.utils.arcface.face.model.FacePreviewInfo;
+
+import java.util.List;
+
+/**
+ * 人脸识别过滤器,仅保留满足条件的人脸,(只有满足条件的人脸才进行后续的活体检测、人脸识别操作)
+ */
+public interface FaceRecognizeFilter {
+ void filter(List facePreviewInfoList);
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceSizeFilter.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceSizeFilter.java
new file mode 100644
index 0000000..ecebdab
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceSizeFilter.java
@@ -0,0 +1,39 @@
+package com.sw.plate.utils.arcface.face.facefilter;
+
+import android.graphics.Rect;
+
+import com.sw.plate.utils.arcface.face.model.FacePreviewInfo;
+
+import java.util.List;
+
+/**
+ * 人脸尺寸过滤器:
+ * 仅保留人脸宽度大于{@link FaceSizeFilter#horizontalSize},且人脸高度大于{@link FaceSizeFilter#verticalSize}的人脸。
+ */
+public class FaceSizeFilter implements FaceRecognizeFilter {
+ private int horizontalSize;
+ private int verticalSize;
+
+ private static final String TAG = "FaceSizeFilter";
+
+ public FaceSizeFilter(int horizontalSize, int verticalSize) {
+ this.horizontalSize = horizontalSize;
+ this.verticalSize = verticalSize;
+ }
+
+ @Override
+ public void filter(List facePreviewInfoList) {
+ for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) {
+ if (!facePreviewInfo.isQualityPass()) {
+ continue;
+ }
+ if (facePreviewInfo.getFaceInfoRgb() != null) {
+ Rect rgbRect = facePreviewInfo.getFaceInfoRgb().getRect();
+ Rect irRect = facePreviewInfo.getFaceInfoIr() == null ? null : facePreviewInfo.getFaceInfoIr().getRect();
+ boolean rgbRectValid = rgbRect == null || (rgbRect.width() > horizontalSize && rgbRect.height() > verticalSize);
+ boolean irRectValid = irRect == null || (irRect.width() > horizontalSize && irRect.height() > verticalSize);
+ facePreviewInfo.setQualityPass(rgbRectValid && irRectValid);
+ }
+ }
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/CompareResult.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/CompareResult.java
new file mode 100644
index 0000000..2d33f8b
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/CompareResult.java
@@ -0,0 +1,64 @@
+package com.sw.plate.utils.arcface.face.model;
+
+
+import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
+
+public class CompareResult {
+ private FaceEntity faceEntity;
+ private float similar;
+ private int trackId;
+ private int compareCode;
+ private long cost;
+
+ public CompareResult(FaceEntity faceEntity, float similar) {
+ this.faceEntity = faceEntity;
+ this.similar = similar;
+ }
+
+ public CompareResult(FaceEntity faceEntity, float similar, int compareCode, long cost) {
+ this.faceEntity = faceEntity;
+ this.similar = similar;
+ this.compareCode = compareCode;
+ this.cost = cost;
+ }
+
+ public FaceEntity getFaceEntity() {
+ return faceEntity;
+ }
+
+ public void setFaceEntity(FaceEntity faceEntity) {
+ this.faceEntity = faceEntity;
+ }
+
+ public float getSimilar() {
+ return similar;
+ }
+
+ public void setSimilar(float similar) {
+ this.similar = similar;
+ }
+
+ public int getTrackId() {
+ return trackId;
+ }
+
+ public void setTrackId(int trackId) {
+ this.trackId = trackId;
+ }
+
+ public int getCompareCode() {
+ return compareCode;
+ }
+
+ public void setCompareCode(int compareCode) {
+ this.compareCode = compareCode;
+ }
+
+ public long getCost() {
+ return cost;
+ }
+
+ public void setCost(long cost) {
+ this.cost = cost;
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/FacePreviewInfo.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/FacePreviewInfo.java
new file mode 100644
index 0000000..8f0b045
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/FacePreviewInfo.java
@@ -0,0 +1,152 @@
+package com.sw.plate.utils.arcface.face.model;
+
+import android.graphics.Rect;
+
+import com.arcsoft.face.FaceInfo;
+import com.arcsoft.face.LivenessInfo;
+
+/**
+ * 人脸追踪时的信息
+ */
+public class FacePreviewInfo {
+ /**
+ * RGB人脸信息,包括人脸框和人脸角度
+ */
+ private FaceInfo faceInfoRgb;
+ /**
+ * IR人脸信息,包括人脸框和人脸角度
+ */
+ private FaceInfo faceInfoIr;
+ /**
+ * 可见光成像对应的用于FaceRectView绘制的Rect
+ */
+ private Rect rgbTransformedRect;
+ /**
+ * 红外成像对应的用于FaceRectView绘制的Rect
+ */
+ private Rect irTransformedRect;
+ private int rgbLiveness = LivenessInfo.UNKNOWN;
+ private int irLiveness = LivenessInfo.UNKNOWN;
+ private float imageQuality = 0f;
+ /**
+ * 识别区域是否合法
+ */
+ private boolean recognizeAreaValid;
+ /**
+ * 基于{@link FaceInfo#getFaceId()}的一个偏移值,可理解为SDK截至目前检测到的人次,唯一性同faceId
+ */
+ private int trackId;
+ /**
+ * 整体质量是否通过,包括人脸大小、角度、移动速度等
+ */
+ private boolean qualityPass = true;
+
+ /**
+ * 是否戴口罩
+ */
+ private int mask;
+
+ private Rect foreRect;
+
+ public Rect getForeRect() {
+ return foreRect;
+ }
+
+ public void setForeRect(Rect foreRect) {
+ this.foreRect = foreRect;
+ }
+
+ public FacePreviewInfo(FaceInfo faceInfoRgb, int trackId) {
+ this.faceInfoRgb = faceInfoRgb;
+ this.trackId = trackId;
+ }
+
+ public FaceInfo getFaceInfoRgb() {
+ return faceInfoRgb;
+ }
+
+ public void setFaceInfoRgb(FaceInfo faceInfoRgb) {
+ this.faceInfoRgb = faceInfoRgb;
+ }
+
+
+ public int getTrackId() {
+ return trackId;
+ }
+
+ public void setTrackId(int trackId) {
+ this.trackId = trackId;
+ }
+
+ public void setRgbTransformedRect(Rect rgbTransformedRect) {
+ this.rgbTransformedRect = rgbTransformedRect;
+ }
+
+ public void setIrTransformedRect(Rect irTransformedRect) {
+ this.irTransformedRect = irTransformedRect;
+ }
+
+ public Rect getRgbTransformedRect() {
+ return rgbTransformedRect;
+ }
+
+ public Rect getIrTransformedRect() {
+ return irTransformedRect;
+ }
+
+ public boolean isRecognizeAreaValid() {
+ return recognizeAreaValid;
+ }
+
+ public void setRecognizeAreaValid(boolean recognizeAreaValid) {
+ this.recognizeAreaValid = recognizeAreaValid;
+ }
+
+ public void setFaceInfoIr(FaceInfo faceInfoIr) {
+ this.faceInfoIr = faceInfoIr;
+ }
+
+ public FaceInfo getFaceInfoIr() {
+ return faceInfoIr;
+ }
+
+ public int getRgbLiveness() {
+ return rgbLiveness;
+ }
+
+ public void setRgbLiveness(int rgbLiveness) {
+ this.rgbLiveness = rgbLiveness;
+ }
+
+ public int getIrLiveness() {
+ return irLiveness;
+ }
+
+ public void setIrLiveness(int irLiveness) {
+ this.irLiveness = irLiveness;
+ }
+
+ public void setImageQuality(float imageQuality) {
+ this.imageQuality = imageQuality;
+ }
+
+ public float getImageQuality() {
+ return imageQuality;
+ }
+
+ public boolean isQualityPass() {
+ return qualityPass;
+ }
+
+ public void setQualityPass(boolean qualityPass) {
+ this.qualityPass = qualityPass;
+ }
+
+ public int getMask() {
+ return mask;
+ }
+
+ public void setMask(int mask) {
+ this.mask = mask;
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/RecognizeConfiguration.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/RecognizeConfiguration.java
new file mode 100644
index 0000000..ee22a9b
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/RecognizeConfiguration.java
@@ -0,0 +1,302 @@
+package com.sw.plate.utils.arcface.face.model;
+
+import com.arcsoft.face.LivenessParam;
+import com.sw.plate.utils.arcface.ConfigUtil;
+
+/**
+ * 识别相关的配置项
+ */
+public class RecognizeConfiguration {
+ /**
+ * 产生特征提取失败示语的特征提取次数(小于该值不提示)
+ */
+ private int extractRetryCount;
+ /**
+ * 产生活体检测失败示语的活体检测次数(小于该值不提示)
+ */
+ private int livenessRetryCount;
+ /**
+ * 最大人脸检测数量
+ */
+ private int maxDetectFaces;
+ /**
+ * 识别阈值
+ */
+ private float similarThreshold;
+ /**
+ * 图像质量检测阈值:适用于不戴口罩且人脸识别场景
+ */
+ private float imageQualityNoMaskRecognizeThreshold;
+ /**
+ * 图像质量检测阈值:适用于戴口罩且人脸识别场景
+ */
+ private float imageQualityMaskRecognizeThreshold;
+ /**
+ * 识别失败重试间隔
+ */
+ private int recognizeFailedRetryInterval;
+ /**
+ * 活体检测未通过重试间隔
+ */
+ private int livenessFailedRetryInterval;
+ /**
+ * 启用活体
+ */
+ private boolean enableLiveness;
+ /**
+ * 启用图像质量检测
+ */
+ private boolean enableImageQuality;
+ /**
+ * 识别区域限制
+ */
+ private boolean enableFaceAreaLimit;
+ /**
+ * 仅识别最大人脸
+ */
+ private boolean keepMaxFace;
+ /**
+ * 活体阈值设置
+ */
+ private LivenessParam livenessParam;
+
+
+ /**
+ * 启用人脸边长限制
+ */
+ private boolean enableFaceSizeLimit = false;
+ /**
+ * 启用人脸移动限制
+ */
+ private boolean enableFaceMoveLimit = false;
+ /**
+ * 人脸边长限制值
+ */
+ private int faceSizeLimit = 0;
+ /**
+ * 人脸上下针移动限制值
+ */
+ private int faceMoveLimit = 0;
+
+
+ public RecognizeConfiguration(Builder builder) {
+ this.extractRetryCount = builder.extractRetryCount;
+ this.livenessRetryCount = builder.livenessRetryCount;
+ this.livenessFailedRetryInterval = builder.livenessFailedRetryInterval;
+ this.maxDetectFaces = builder.maxDetectFaces;
+ this.similarThreshold = builder.similarThreshold;
+ this.imageQualityNoMaskRecognizeThreshold = builder.imageQualityNoMaskRecognizeThreshold;
+ this.imageQualityMaskRecognizeThreshold = builder.imageQualityMaskRecognizeThreshold;
+ this.enableLiveness = builder.enableLiveness;
+ this.enableImageQuality = builder.enableImageQuality;
+ this.enableFaceAreaLimit = builder.enableFaceAreaLimit;
+ this.keepMaxFace = builder.keepMaxFace;
+ this.recognizeFailedRetryInterval = builder.recognizeFailedRetryInterval;
+ this.livenessParam = builder.livenessParam;
+ this.enableFaceSizeLimit = builder.enableFaceSizeLimit;
+ this.enableFaceMoveLimit = builder.enableFaceMoveLimit;
+ this.faceSizeLimit = builder.faceSizeLimit;
+ this.faceMoveLimit = builder.faceMoveLimit;
+ }
+
+ //TODO: demo不实现所有配置,若以下项也需要进行自定义配置,可参考其他配置项实现
+ public static class Builder {
+ private int extractRetryCount = 3;
+ private int livenessRetryCount = 3;
+ private int maxDetectFaces = 3;
+ private int recognizeFailedRetryInterval = 0;
+ private int livenessFailedRetryInterval = 0;
+ private float similarThreshold = 0.8f;
+ private float imageQualityNoMaskRecognizeThreshold = ConfigUtil.IMAGE_QUALITY_NO_MASK_RECOGNIZE_THRESHOLD;
+ private float imageQualityMaskRecognizeThreshold = ConfigUtil.IMAGE_QUALITY_MASK_RECOGNIZE_THRESHOLD;
+ private boolean enableLiveness = false;
+ private boolean enableFaceAreaLimit = false;
+ private boolean enableImageQuality = false;
+ private boolean enableFaceSizeLimit = false;
+ private boolean enableFaceMoveLimit = false;
+ private int faceSizeLimit = 0;
+ private int faceMoveLimit = 0;
+ private boolean keepMaxFace = false;
+ private LivenessParam livenessParam;
+
+ public Builder recognizeFailedRetryInterval(int val) {
+ this.recognizeFailedRetryInterval = val;
+ return this;
+ }
+
+ public Builder livenessFailedRetryInterval(int val) {
+ this.livenessFailedRetryInterval = val;
+ return this;
+ }
+
+ public Builder extractRetryCount(int val) {
+ this.extractRetryCount = val;
+ return this;
+ }
+
+ public Builder livenessRetryCount(int val) {
+ this.livenessRetryCount = val;
+ return this;
+ }
+
+ public Builder maxDetectFaces(int val) {
+ this.maxDetectFaces = val;
+ return this;
+ }
+
+ public Builder similarThreshold(float val) {
+ this.similarThreshold = val;
+ return this;
+ }
+
+ public Builder imageQualityNoMaskRecognizeThreshold(float val) {
+ this.imageQualityNoMaskRecognizeThreshold = val;
+ return this;
+ }
+
+ public Builder imageQualityMaskRecognizeThreshold(float val) {
+ this.imageQualityMaskRecognizeThreshold = val;
+ return this;
+ }
+
+ public Builder enableLiveness(boolean val) {
+ this.enableLiveness = val;
+ return this;
+ }
+
+
+ public Builder enableImageQuality(boolean val) {
+ this.enableImageQuality = val;
+ return this;
+ }
+
+ public Builder enableFaceAreaLimit(boolean val) {
+ this.enableFaceAreaLimit = val;
+ return this;
+ }
+
+ public Builder enableFaceSizeLimit(boolean val) {
+ this.enableFaceSizeLimit = val;
+ return this;
+ }
+
+ public Builder enableFaceMoveLimit(boolean val) {
+ this.enableFaceMoveLimit = val;
+ return this;
+ }
+
+ public Builder faceSizeLimit(int val) {
+ this.faceSizeLimit = val;
+ return this;
+ }
+
+ public Builder faceMoveLimit(int val) {
+ this.faceMoveLimit = val;
+ return this;
+ }
+
+ public Builder keepMaxFace(boolean val) {
+ this.keepMaxFace = val;
+ return this;
+ }
+
+ public Builder livenessParam(LivenessParam val) {
+ this.livenessParam = val;
+ return this;
+ }
+
+
+ public RecognizeConfiguration build() {
+ return new RecognizeConfiguration(this);
+ }
+ }
+
+ public float getImageQualityNoMaskRecognizeThreshold() {
+ return imageQualityNoMaskRecognizeThreshold;
+ }
+
+ public float getImageQualityMaskRecognizeThreshold() {
+ return imageQualityMaskRecognizeThreshold;
+ }
+
+ public boolean isEnableImageQuality() {
+ return enableImageQuality;
+ }
+
+ public boolean isEnableFaceAreaLimit() {
+ return enableFaceAreaLimit;
+ }
+
+ public LivenessParam getLivenessParam() {
+ return livenessParam;
+ }
+
+ public int getExtractRetryCount() {
+ return extractRetryCount;
+ }
+
+ public int getLivenessRetryCount() {
+ return livenessRetryCount;
+ }
+
+ public int getMaxDetectFaces() {
+ return maxDetectFaces;
+ }
+
+ public float getSimilarThreshold() {
+ return similarThreshold;
+ }
+
+ public boolean isEnableLiveness() {
+ return enableLiveness;
+ }
+
+
+ public int getRecognizeFailedRetryInterval() {
+ return recognizeFailedRetryInterval;
+ }
+
+ public int getLivenessFailedRetryInterval() {
+ return livenessFailedRetryInterval;
+ }
+
+ public boolean isKeepMaxFace() {
+ return keepMaxFace;
+ }
+
+ public boolean isEnableFaceSizeLimit() {
+ return enableFaceSizeLimit;
+ }
+
+ public boolean isEnableFaceMoveLimit() {
+ return enableFaceMoveLimit;
+ }
+
+ public int getFaceSizeLimit() {
+ return faceSizeLimit;
+ }
+
+ public int getFaceMoveLimit() {
+ return faceMoveLimit;
+ }
+
+ @Override
+ public String toString() {
+ return
+ "extractRetryCount: " + extractRetryCount + "\r\n" +
+ "similarThreshold: " + similarThreshold + "\r\n" +
+ "recognizeFailedRetryInterval: " + recognizeFailedRetryInterval + "\r\n" +
+
+ "keepMaxFace: " + keepMaxFace + "\r\n" +
+ "maxDetectFaces: " + maxDetectFaces + "\r\n" +
+
+ "enableImageQuality: " + enableImageQuality + "\r\n" +
+ "imageQualityNoMaskRecognizeThreshold: " + imageQualityNoMaskRecognizeThreshold + "\r\n" +
+ "imageQualityMaskRecognizeThreshold: " + imageQualityMaskRecognizeThreshold + "\r\n" +
+
+ "enableLiveness: " + enableLiveness + "\r\n" +
+ "livenessRetryCount: " + livenessRetryCount + "\r\n" +
+ "livenessParams: " + (livenessParam == null ? null : (livenessParam.getRgbThreshold() + "," + livenessParam.getIrThreshold()));
+
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/RecognizeInfo.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/RecognizeInfo.java
new file mode 100644
index 0000000..b49e421
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/RecognizeInfo.java
@@ -0,0 +1,86 @@
+package com.sw.plate.utils.arcface.face.model;
+
+import com.arcsoft.face.LivenessInfo;
+import com.sw.plate.utils.arcface.face.constants.RequestFeatureStatus;
+
+/**
+ * 单个人脸(faceId)识别过程中的信息
+ */
+public class RecognizeInfo {
+ /**
+ * 用于记录人脸识别相关状态
+ */
+ private int recognizeStatus = RequestFeatureStatus.TO_RETRY;
+ /**
+ * 用于记录人脸特征提取出错重试次数
+ */
+ private int extractErrorRetryCount;
+ /**
+ * 用于存储活体值
+ */
+ private int liveness = LivenessInfo.UNKNOWN;
+ /**
+ * 用于存储活体检测出错重试次数
+ */
+ private int livenessErrorRetryCount;
+ /**
+ * 用户姓名,用于显示
+ */
+ private String name;
+ /**
+ * 特征等活体的lock
+ */
+ private Object waitLock = new Object();
+
+ public int getRecognizeStatus() {
+ return recognizeStatus;
+ }
+
+ public void setRecognizeStatus(int recognizeStatus) {
+ this.recognizeStatus = recognizeStatus;
+ }
+
+ public void setLiveness(int liveness) {
+ this.liveness = liveness;
+ }
+
+ public int increaseAndGetExtractErrorRetryCount() {
+ return ++extractErrorRetryCount;
+ }
+
+ public int getLiveness() {
+ return liveness;
+ }
+
+ public int increaseAndGetLivenessErrorRetryCount() {
+ return ++livenessErrorRetryCount;
+ }
+
+ public void setExtractErrorRetryCount(int extractErrorRetryCount) {
+ this.extractErrorRetryCount = extractErrorRetryCount;
+ }
+
+ public void setLivenessErrorRetryCount(int livenessErrorRetryCount) {
+ this.livenessErrorRetryCount = livenessErrorRetryCount;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Object getWaitLock() {
+ return waitLock;
+ }
+
+ public int getExtractErrorRetryCount() {
+ return extractErrorRetryCount;
+ }
+
+ public int getLivenessErrorRetryCount() {
+ return livenessErrorRetryCount;
+ }
+}
\ No newline at end of file
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/FaceDatabase.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/FaceDatabase.java
new file mode 100644
index 0000000..ddb985f
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/FaceDatabase.java
@@ -0,0 +1,31 @@
+package com.sw.plate.utils.arcface.facedb;
+
+import android.content.Context;
+
+import androidx.room.Database;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+
+import com.sw.plate.utils.arcface.facedb.dao.FaceDao;
+import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
+
+@Database(entities = {FaceEntity.class}, version = 1, exportSchema = false)
+public abstract class FaceDatabase extends RoomDatabase {
+ public abstract FaceDao faceDao();
+
+ private static volatile FaceDatabase faceDatabase = null;
+
+ public static FaceDatabase getInstance(Context context) {
+ if (faceDatabase == null) {
+ synchronized (FaceDatabase.class) {
+ if (faceDatabase == null) {
+ faceDatabase = Room.databaseBuilder(context, FaceDatabase.class,
+ context.getDatabasePath("faceDB.db").getPath()
+// context.getExternalFilesDir("database") + File.separator + "faceDB.db"
+ ).build();
+ }
+ }
+ }
+ return faceDatabase;
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/dao/FaceDao.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/dao/FaceDao.java
new file mode 100644
index 0000000..4b3a682
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/dao/FaceDao.java
@@ -0,0 +1,91 @@
+package com.sw.plate.utils.arcface.facedb.dao;
+
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+import androidx.room.Update;
+
+import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
+
+import java.util.List;
+
+@Dao
+public interface FaceDao {
+ /**
+ * 获取库中所有已注册人脸
+ *
+ * @return 所有已注册人脸
+ */
+ @Query("SELECT * FROM face")
+ List getAllFaces();
+
+ /**
+ * 分页获取库中的人脸
+ *
+ * @param start 起始下标
+ * @param size 单次获取的长度
+ * @return 从下标为start开始的size个已注册人脸
+ */
+ @Query("SELECT * FROM face order by faceId desc limit :start,:size ")
+ List getFaces(int start, int size);
+
+ /**
+ * 更新已注册的人脸信息
+ *
+ * @param faceEntity 已注册的人脸信息
+ * @return
+ */
+ @Update
+ int updateFaceEntity(FaceEntity faceEntity);
+
+ /**
+ * 删除人脸
+ *
+ * @param faceEntity 已注册的人脸信息
+ * @return
+ */
+ @Delete
+ int deleteFace(FaceEntity faceEntity);
+
+ /**
+ * @return 该用户已注册人脸
+ */
+ @Query("DELETE from face WHERE user_name = :userName")
+ int deleteFaceById(String userName);
+
+ /**
+ * 删除所有已注册的人脸
+ *
+ * @return
+ */
+ @Query("DELETE from face")
+ int deleteAll();
+
+ /**
+ * 插入一个人脸入库
+ *
+ * @param faceEntity
+ * @return
+ */
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ Long insert(FaceEntity faceEntity);
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ List insert(List items);
+
+ /**
+ * 获取已注册的人脸数
+ *
+ * @return
+ */
+ @Query("SELECT COUNT(1) from face")
+ int getFaceCount();
+
+ @Query("SELECT * FROM face WHERE faceId = :faceId limit 1")
+ FaceEntity queryByFaceId(int faceId);
+
+ @Query("UPDATE sqlite_sequence SET seq = 0 WHERE name ='face'")
+ void resetId();
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java
new file mode 100644
index 0000000..aac56cb
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java
@@ -0,0 +1,159 @@
+package com.sw.plate.utils.arcface.facedb.entity;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * 人脸库中的单挑人脸记录
+ */
+@Entity(
+ tableName = "face"
+)
+public class FaceEntity implements Parcelable {
+ /**
+ * 人脸id,主键
+ */
+ @PrimaryKey(autoGenerate = true)
+ private long faceId;
+ /**
+ * 用户名称
+ */
+ @ColumnInfo(name = "user_name")
+ private String userName;
+ /**
+ * 图片路径
+ */
+ @ColumnInfo(name = "image_path")
+ private String imagePath;
+ /**
+ * 人脸特征数据
+ */
+ @ColumnInfo(name = "feature_data")
+ private byte[] featureData;
+ /**
+ * 注册时间
+ */
+ @ColumnInfo(name = "register_time")
+ private long registerTime;
+
+
+ public FaceEntity(String userName, String imagePath, byte[] featureData) {
+ this.userName = userName;
+ this.imagePath = imagePath;
+ this.featureData = featureData;
+ registerTime = System.currentTimeMillis();
+ }
+
+ public FaceEntity(FaceEntity faceEntity) {
+ this.faceId = faceEntity.faceId;
+ this.userName = faceEntity.userName;
+ this.imagePath = faceEntity.imagePath;
+ this.featureData = faceEntity.featureData;
+ this.registerTime = faceEntity.registerTime;
+ }
+
+
+ protected FaceEntity(Parcel in) {
+ faceId = in.readLong();
+ registerTime = in.readLong();
+ userName = in.readString();
+ imagePath = in.readString();
+ featureData = in.createByteArray();
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public FaceEntity createFromParcel(Parcel in) {
+ return new FaceEntity(in);
+ }
+
+ @Override
+ public FaceEntity[] newArray(int size) {
+ return new FaceEntity[size];
+ }
+ };
+
+ public long getFaceId() {
+ return faceId;
+ }
+
+ public void setFaceId(long faceId) {
+ this.faceId = faceId;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public void setUserName(String userName) {
+ this.userName = userName;
+ }
+
+ public String getImagePath() {
+ return imagePath;
+ }
+
+ public void setImagePath(String imagePath) {
+ this.imagePath = imagePath;
+ }
+
+ public byte[] getFeatureData() {
+ return featureData;
+ }
+
+ public void setFeatureData(byte[] featureData) {
+ this.featureData = featureData;
+ }
+
+ public long getRegisterTime() {
+ return registerTime;
+ }
+
+ public void setRegisterTime(long registerTime) {
+ this.registerTime = registerTime;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(faceId);
+ dest.writeLong(registerTime);
+ dest.writeString(userName);
+ dest.writeString(imagePath);
+ dest.writeByteArray(featureData);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ FaceEntity that = (FaceEntity) o;
+ return faceId == that.faceId &&
+ registerTime == that.registerTime &&
+ userName.equals(that.userName) &&
+ imagePath.equals(that.imagePath) &&
+ Arrays.equals(featureData, that.featureData);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(faceId, registerTime, userName, imagePath);
+ result = 31 * result + Arrays.hashCode(featureData);
+ return result;
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/faceserver/FaceServer.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/faceserver/FaceServer.java
new file mode 100644
index 0000000..da36a0e
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/faceserver/FaceServer.java
@@ -0,0 +1,624 @@
+package com.sw.plate.utils.arcface.faceserver;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.util.Log;
+
+import com.arcsoft.face.ErrorInfo;
+import com.arcsoft.face.FaceEngine;
+import com.arcsoft.face.FaceFeature;
+import com.arcsoft.face.FaceFeatureInfo;
+import com.arcsoft.face.FaceInfo;
+import com.arcsoft.face.MaskInfo;
+import com.arcsoft.face.SearchResult;
+import com.arcsoft.face.enums.DetectFaceOrientPriority;
+import com.arcsoft.face.enums.DetectMode;
+import com.arcsoft.face.enums.ExtractType;
+import com.arcsoft.imageutil.ArcSoftImageFormat;
+import com.arcsoft.imageutil.ArcSoftImageUtil;
+import com.arcsoft.imageutil.ArcSoftImageUtilError;
+import com.arcsoft.imageutil.ArcSoftRotateDegree;
+import com.sw.plate.App;
+import com.sw.plate.utils.arcface.ErrorCodeUtil;
+import com.sw.plate.utils.arcface.ImageUtil;
+import com.sw.plate.utils.arcface.face.model.CompareResult;
+import com.sw.plate.utils.arcface.face.model.FacePreviewInfo;
+import com.sw.plate.utils.arcface.facedb.FaceDatabase;
+import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
+import com.sw.plate.utils.arcface.model.UserFaceInfo;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+
+/**
+ * 人脸库操作类,包含注册和搜索
+ */
+public class FaceServer {
+ private static final String TAG = "FaceServer";
+ private static FaceEngine faceEngine = null;
+ private static volatile FaceServer faceServer = null;
+ private List faceRegisterInfoList;
+ private String imageRootPath;
+ /**
+ * 最大注册人脸数
+ */
+ private static final int MAX_REGISTER_FACE_COUNT = 30000;
+
+ private FaceServer() {
+ faceRegisterInfoList = new ArrayList<>();
+ }
+
+ public static FaceServer getInstance() {
+ if (faceServer == null) {
+ synchronized (FaceServer.class) {
+ if (faceServer == null) {
+ faceServer = new FaceServer();
+ }
+ }
+ }
+ return faceServer;
+ }
+
+ public interface OnInitFinishedCallback {
+ void onFinished(int faceCount);
+ }
+
+ public void init(Context context) {
+ init(context, null);
+ }
+
+ public synchronized void init(Context context, OnInitFinishedCallback onInitFinishedCallback) {
+ if (faceEngine == null && context != null) {
+ faceEngine = new FaceEngine();
+ int engineCode = faceEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_ALL_OUT,
+ 1, FaceEngine.ASF_FACE_RECOGNITION | FaceEngine.ASF_FACE_DETECT | FaceEngine.ASF_MASK_DETECT);
+ if (engineCode == ErrorInfo.MOK) {
+ initFaceList(context, null, onInitFinishedCallback, false);
+ } else {
+ faceEngine = null;
+ Log.e(TAG, "init: failed! code = " + engineCode);
+ }
+ }
+ if (faceRegisterInfoList != null && onInitFinishedCallback != null) {
+ onInitFinishedCallback.onFinished(faceRegisterInfoList.size());
+ }
+ }
+
+ /**
+ * 销毁
+ */
+ public synchronized void release() {
+ if (faceRegisterInfoList != null) {
+ faceRegisterInfoList.clear();
+ faceRegisterInfoList = null;
+ }
+ if (faceEngine != null) {
+ synchronized (faceEngine) {
+ faceEngine.unInit();
+ }
+ faceEngine = null;
+ }
+ faceServer = null;
+ }
+
+ /**
+ * 初始化人脸特征数据以及人脸特征数据对应的注册图
+ *
+ * @param context 上下文对象
+ * @param faceEngine 指定FaceEngine
+ * @param onInitFinishedCallback 加载完成的回调
+ * @param recognize 是否处于人脸识别流程
+ */
+ public void initFaceList(final Context context, FaceEngine faceEngine, final OnInitFinishedCallback onInitFinishedCallback, boolean recognize) {
+ Disposable disposable = Observable.create((ObservableOnSubscribe) emitter -> {
+ if (recognize) {
+ List faceEntityList = FaceDatabase.getInstance(context).faceDao().getAllFaces();
+ registerFaceFeatureInfoListFromDb(faceEngine, faceEntityList);
+ emitter.onNext(faceEntityList.size());
+ } else {
+ faceRegisterInfoList = FaceDatabase.getInstance(context).faceDao().getAllFaces();
+ emitter.onNext(faceRegisterInfoList == null ? 0 : faceRegisterInfoList.size());
+ }
+ emitter.onComplete();
+ }).subscribeOn(Schedulers.io())
+ .unsubscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(size -> {
+ if (onInitFinishedCallback != null) {
+ onInitFinishedCallback.onFinished(size);
+ }
+ });
+ }
+
+ public synchronized void removeOneFace(FaceEntity faceEntity) {
+ if (faceRegisterInfoList != null) {
+ faceRegisterInfoList.remove(faceEntity);
+ }
+ }
+
+
+ public synchronized void removeFaceById(String id) {
+ Iterator iterator = faceRegisterInfoList.iterator();
+ while (iterator.hasNext()) {
+ FaceEntity next = iterator.next();
+ if (id.equals(next.getUserName())) {
+ iterator.remove();
+ }
+ }
+ }
+
+ public synchronized void addUserFace(FaceEntity faceEntity) {
+ faceRegisterInfoList.add(faceEntity);
+ }
+
+
+ @SuppressLint("CheckResult")
+ public synchronized int clearAllFaces() {
+ if (faceRegisterInfoList != null) {
+ faceRegisterInfoList.clear();
+ }
+ if (faceEngine != null) {
+ faceEngine.removeFaceFeature(-1);
+ }
+ Context applicationContext = App.getContext();
+ int deleteSize = FaceDatabase.getInstance(applicationContext).faceDao().deleteAll();
+ File imgDir = new File(getImageDir());
+ File[] files = imgDir.listFiles();
+ if (files != null && files.length > 0) {
+ for (File file : files) {
+ file.delete();
+ }
+ }
+ return deleteSize;
+ }
+
+ /**
+ * 用于预览时注册人脸
+ *
+ * @param context 上下文对象
+ * @param nv21 NV21数据
+ * @param width NV21宽度
+ * @param height NV21高度
+ * @param faceInfo {@link FaceEngine#detectFaces(byte[], int, int, int, List)}获取的人脸信息
+ * @param name 保存的名字,若为空则使用时间戳
+ * @param frEngine 添加人脸数据,用于后续{@link FaceEngine#searchFaceFeature(FaceFeature)}
+ * @param registerFaceEngine 用于{@link FaceEngine#extractFaceFeature(byte[], int, int, int, FaceInfo, ExtractType, int, FaceFeature)}注册人脸到本地数据库
+ * @return 是否注册成功
+ */
+ public boolean registerNv21(Context context, byte[] nv21, int width, int height, FacePreviewInfo faceInfo, String name,
+ FaceEngine frEngine, FaceEngine registerFaceEngine) {
+ if (registerFaceEngine == null || context == null || nv21 == null || width % 4 != 0 || nv21.length != width * height * 3 / 2) {
+ Log.e(TAG, "registerNv21: invalid params");
+ return false;
+ }
+ FaceFeature faceFeature = new FaceFeature();
+ int code;
+ /*
+ * 特征提取,注册人脸时extractType值为ExtractType.REGISTER,mask的值为MaskInfo.NOT_WORN
+ */
+ synchronized (registerFaceEngine) {
+ code = registerFaceEngine.extractFaceFeature(nv21, width, height, FaceEngine.CP_PAF_NV21, faceInfo.getFaceInfoRgb(),
+ ExtractType.REGISTER, MaskInfo.NOT_WORN, faceFeature);
+ }
+ if (code != ErrorInfo.MOK) {
+ Log.e(TAG, "registerNv21: extractFaceFeature failed , code is " + code);
+ return false;
+ } else {
+ /*
+ * 1.保存注册结果(注册图、特征数据)
+ * 2.为了美观,扩大rect截取注册图
+ */
+ Rect cropRect = getBestRect(width, height, faceInfo.getFaceInfoRgb().getRect());
+ if (cropRect == null) {
+ Log.e(TAG, "registerNv21: cropRect is null!");
+ return false;
+ }
+
+ cropRect.left &= ~3;
+ cropRect.top &= ~3;
+ cropRect.right &= ~3;
+ cropRect.bottom &= ~3;
+
+ // 创建一个头像的Bitmap,存放旋转结果图
+ Bitmap headBmp = getHeadImage(nv21, width, height, faceInfo.getFaceInfoRgb().getOrient(), cropRect, ArcSoftImageFormat.NV21);
+ String imgPath = getImagePath(name);
+ try {
+ FileOutputStream fos = new FileOutputStream(imgPath);
+ headBmp.compress(Bitmap.CompressFormat.JPEG, 100, fos);
+ fos.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ FaceEntity faceEntity = new FaceEntity(name, imgPath, faceFeature.getFeatureData());
+ long faceId = FaceDatabase.getInstance(context).faceDao().insert(faceEntity);
+ faceEntity.setFaceId(faceId);
+ registerFaceFeatureInfoFromDb(faceEntity, frEngine);
+ return true;
+ }
+ }
+
+
+ public UserFaceInfo getUserInfo(Context context, byte[] nv21, int width, int height, FacePreviewInfo faceInfo, String name,
+ FaceEngine frEngine, FaceEngine registerFaceEngine) {
+ if (registerFaceEngine == null || context == null || nv21 == null || width % 4 != 0 || nv21.length != width * height * 3 / 2) {
+ Log.e(TAG, "registerNv21: invalid params");
+ return null;
+ }
+ FaceFeature faceFeature = new FaceFeature();
+ int code;
+ /*
+ * 特征提取,注册人脸时extractType值为ExtractType.REGISTER,mask的值为MaskInfo.NOT_WORN
+ */
+ synchronized (registerFaceEngine) {
+ code = registerFaceEngine.extractFaceFeature(nv21, width, height, FaceEngine.CP_PAF_NV21, faceInfo.getFaceInfoRgb(),
+ ExtractType.REGISTER, MaskInfo.NOT_WORN, faceFeature);
+ }
+ if (code != ErrorInfo.MOK) {
+ Log.e(TAG, "registerNv21: extractFaceFeature failed , code is " + code);
+ return null;
+ } else {
+ /*
+ * 1.保存注册结果(注册图、特征数据)
+ * 2.为了美观,扩大rect截取注册图
+ */
+ Rect cropRect = getBestRect(width, height, faceInfo.getFaceInfoRgb().getRect());
+ if (cropRect == null) {
+ Log.e(TAG, "registerNv21: cropRect is null!");
+ return null;
+ }
+
+ cropRect.left &= ~3;
+ cropRect.top &= ~3;
+ cropRect.right &= ~3;
+ cropRect.bottom &= ~3;
+
+ // 创建一个头像的Bitmap,存放旋转结果图
+ Bitmap headBmp = getHeadImage(nv21, width, height, faceInfo.getFaceInfoRgb().getOrient(), cropRect, ArcSoftImageFormat.NV21);
+// String imgPath = getImagePath(name);
+// try {
+// FileOutputStream fos = new FileOutputStream(imgPath);
+// headBmp.compress(Bitmap.CompressFormat.JPEG, 100, fos);
+// fos.close();
+// } catch (IOException e) {
+// e.printStackTrace();
+// return null;
+// }
+// FaceEntity faceEntity = new FaceEntity(name, imgPath, faceFeature.getFeatureData());
+// long faceId = FaceDatabase.getInstance(context).faceDao().insert(faceEntity);
+// faceEntity.setFaceId(faceId);
+// registerFaceFeatureInfoFromDb(faceEntity, frEngine);
+
+
+ UserFaceInfo userFaceInfo = new UserFaceInfo();
+ userFaceInfo.setFaceFeature(faceFeature);
+ userFaceInfo.setHeadBmp(headBmp);
+ userFaceInfo.setFrEngine(frEngine);
+
+ return userFaceInfo;
+ }
+
+ }
+
+ /**
+ * 通过FaceEngine注册多个人脸数据
+ *
+ * @param faceEngine 指定FaceEngine
+ * @param faceEntityList 人脸数据集
+ */
+ private void registerFaceFeatureInfoListFromDb(FaceEngine faceEngine, List faceEntityList) {
+ List faceFeatureInfoList = new ArrayList<>();
+ for (FaceEntity faceEntity : faceEntityList) {
+ FaceFeatureInfo faceFeatureInfo = new FaceFeatureInfo((int) faceEntity.getFaceId(), faceEntity.getFeatureData());
+ faceFeatureInfoList.add(faceFeatureInfo);
+ }
+ if (faceEngine != null) {
+ //首先清除FaceEngine中所有人脸数据,再添加新的人脸数据
+ faceEngine.removeFaceFeature(-1);
+ int res = faceEngine.registerFaceFeature(faceFeatureInfoList);
+ Log.i(TAG, "registerFaceFeature:" + res);
+ }
+ }
+
+ /**
+ * 通过FaceEngine注册单个人脸数据
+ *
+ * @param faceEngine 指定FaceEngine
+ * @param faceEntity 指定人脸数据
+ */
+ public void registerFaceFeatureInfoFromDb(FaceEntity faceEntity, FaceEngine faceEngine) {
+ if (faceEntity != null && faceEngine != null) {
+ FaceFeatureInfo faceFeatureInfo = new FaceFeatureInfo((int) faceEntity.getFaceId(), faceEntity.getFeatureData());
+ int res = faceEngine.registerFaceFeature(faceFeatureInfo);
+ Log.i(TAG, "registerFaceFeature:" + res);
+ }
+ }
+
+ /**
+ * 获取存放注册照的文件夹路径
+ *
+ * @return 存放注册照的文件夹路径
+ */
+ private String getImageDir() {
+// return App.getContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)
+// + File.separator + "faceDB" + File.separator + "registerFaces";
+ return App.getContext().getFilesDir()
+ + File.separator + "faceDB"
+ + File.separator + "registerFaces";
+ }
+
+ /**
+ * 根据用户名获取注册图保存路径
+ *
+ * @param name 用户名
+ * @return 图片保存地址
+ */
+ private String getImagePath(String name) {
+ if (imageRootPath == null) {
+ imageRootPath = getImageDir();
+ File dir = new File(imageRootPath);
+ if (!dir.exists() && !dir.mkdirs()) {
+ return null;
+ }
+ }
+ return imageRootPath + File.separator + name + "_" + System.currentTimeMillis() + ".jpg";
+ }
+
+ /**
+ * 注册一个jpg数据
+ *
+ * @param context
+ * @param jpeg
+ * @param name
+ * @return
+ */
+ public FaceEntity registerJpeg(Context context, byte[] jpeg, String name) throws RegisterFailedException {
+ if (faceRegisterInfoList != null && faceRegisterInfoList.size() >= MAX_REGISTER_FACE_COUNT) {
+ Log.e(TAG, "registerJpeg: registered face count limited " + faceRegisterInfoList.size());
+ // 已达注册上限,超过该值会影响识别率
+ throw new RegisterFailedException("registered face count limited");
+ }
+ Bitmap bitmap = ImageUtil.jpegToScaledBitmap(jpeg, ImageUtil.DEFAULT_MAX_WIDTH, ImageUtil.DEFAULT_MAX_HEIGHT);
+ bitmap = ArcSoftImageUtil.getAlignedBitmap(bitmap, true);
+ byte[] imageData = ArcSoftImageUtil.createImageData(bitmap.getWidth(), bitmap.getHeight(), ArcSoftImageFormat.BGR24);
+ int code = ArcSoftImageUtil.bitmapToImageData(bitmap, imageData, ArcSoftImageFormat.BGR24);
+ if (code != ArcSoftImageUtilError.CODE_SUCCESS) {
+ throw new RuntimeException("bitmapToImageData failed, code is " + code);
+ }
+ return registerBgr24(context, imageData, bitmap.getWidth(), bitmap.getHeight(), name);
+ }
+
+ /**
+ * 用于注册照片人脸
+ *
+ * @param context 上下文对象
+ * @param bgr24 bgr24数据
+ * @param width bgr24宽度
+ * @param height bgr24高度
+ * @param name 保存的名字,若为空则使用时间戳
+ * @return 注册成功后的人脸信息
+ */
+ public FaceEntity registerBgr24(Context context, byte[] bgr24, int width, int height, String name) {
+ if (faceEngine == null || context == null || bgr24 == null || width % 4 != 0 || bgr24.length != width * height * 3) {
+ Log.e(TAG, "registerBgr24: invalid params");
+ return null;
+ }
+ //人脸检测
+ List faceInfoList = new ArrayList<>();
+ int code;
+ synchronized (faceEngine) {
+ code = faceEngine.detectFaces(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList);
+ }
+ if (code == ErrorInfo.MOK && !faceInfoList.isEmpty()) {
+ code = faceEngine.process(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList,
+ FaceEngine.ASF_MASK_DETECT);
+ if (code == ErrorInfo.MOK) {
+ List maskInfoList = new ArrayList<>();
+ faceEngine.getMask(maskInfoList);
+ if (!maskInfoList.isEmpty()) {
+ int isMask = maskInfoList.get(0).getMask();
+ if (isMask == MaskInfo.WORN) {
+ /*
+ * 注册照要求不戴口罩
+ */
+ Log.e(TAG, "registerBgr24: maskInfo is worn");
+ return null;
+ }
+ }
+ }
+
+ FaceFeature faceFeature = new FaceFeature();
+ /*
+ * 特征提取,注册人脸时参数extractType值为ExtractType.REGISTER,参数mask的值为MaskInfo.NOT_WORN
+ */
+ synchronized (faceEngine) {
+ code = faceEngine.extractFaceFeature(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList.get(0),
+ ExtractType.REGISTER, MaskInfo.NOT_WORN, faceFeature);
+ }
+ String userName = name == null ? String.valueOf(System.currentTimeMillis()) : name;
+
+ //保存注册结果(注册图、特征数据)
+ if (code == ErrorInfo.MOK) {
+ //为了美观,扩大rect截取注册图
+ Rect cropRect = getBestRect(width, height, faceInfoList.get(0).getRect());
+ if (cropRect == null) {
+ Log.e(TAG, "registerBgr24: cropRect is null");
+ return null;
+ }
+
+ cropRect.left &= ~3;
+ cropRect.top &= ~3;
+ cropRect.right &= ~3;
+ cropRect.bottom &= ~3;
+
+ String imgPath = getImagePath(userName);
+
+ // 创建一个头像的Bitmap,存放旋转结果图
+ Bitmap headBmp = getHeadImage(bgr24, width, height, faceInfoList.get(0).getOrient(), cropRect, ArcSoftImageFormat.BGR24);
+
+ try {
+ FileOutputStream fos = new FileOutputStream(imgPath);
+ headBmp.compress(Bitmap.CompressFormat.JPEG, 100, fos);
+ fos.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+
+ // 内存中的数据同步
+ if (faceRegisterInfoList == null) {
+ faceRegisterInfoList = new ArrayList<>();
+ }
+ FaceEntity faceEntity = new FaceEntity(name, imgPath, faceFeature.getFeatureData());
+ long faceId = FaceDatabase.getInstance(context).faceDao().insert(faceEntity);
+ faceEntity.setFaceId(faceId);
+ faceRegisterInfoList.add(faceEntity);
+ return faceEntity;
+ } else {
+ Log.e(TAG, "registerBgr24: extract face feature failed, code is " + code);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "registerBgr24: no face detected, code is " + code);
+ return null;
+ }
+ }
+
+ /**
+ * 截取合适的头像并旋转,保存为注册头像
+ *
+ * @param originImageData 原始的BGR24数据
+ * @param width BGR24图像宽度
+ * @param height BGR24图像高度
+ * @param orient 人脸角度
+ * @param cropRect 裁剪的位置
+ * @param imageFormat 图像格式
+ * @return 头像的图像数据
+ */
+ private Bitmap getHeadImage(byte[] originImageData, int width, int height, int orient, Rect cropRect, ArcSoftImageFormat imageFormat) {
+ byte[] headImageData = ArcSoftImageUtil.createImageData(cropRect.width(), cropRect.height(), imageFormat);
+ int cropCode = ArcSoftImageUtil.cropImage(originImageData, headImageData, width, height, cropRect, imageFormat);
+ if (cropCode != ArcSoftImageUtilError.CODE_SUCCESS) {
+ throw new RuntimeException("crop image failed, code is " + cropCode);
+ }
+
+ //判断人脸旋转角度,若不为0度则旋转注册图
+ byte[] rotateHeadImageData = null;
+ int cropImageWidth;
+ int cropImageHeight;
+ // 90度或270度的情况,需要宽高互换
+ if (orient == FaceEngine.ASF_OC_90 || orient == FaceEngine.ASF_OC_270) {
+ cropImageWidth = cropRect.height();
+ cropImageHeight = cropRect.width();
+ } else {
+ cropImageWidth = cropRect.width();
+ cropImageHeight = cropRect.height();
+ }
+ ArcSoftRotateDegree rotateDegree = null;
+ switch (orient) {
+ case FaceEngine.ASF_OC_90:
+ rotateDegree = ArcSoftRotateDegree.DEGREE_270;
+ break;
+ case FaceEngine.ASF_OC_180:
+ rotateDegree = ArcSoftRotateDegree.DEGREE_180;
+ break;
+ case FaceEngine.ASF_OC_270:
+ rotateDegree = ArcSoftRotateDegree.DEGREE_90;
+ break;
+ case FaceEngine.ASF_OC_0:
+ default:
+ rotateHeadImageData = headImageData;
+ break;
+ }
+ // 非0度的情况,旋转图像
+ if (rotateDegree != null) {
+ rotateHeadImageData = new byte[headImageData.length];
+ int rotateCode = ArcSoftImageUtil.rotateImage(headImageData, rotateHeadImageData, cropRect.width(), cropRect.height(), rotateDegree, imageFormat);
+ if (rotateCode != ArcSoftImageUtilError.CODE_SUCCESS) {
+ throw new RuntimeException("rotate image failed, code is : " + rotateCode + ", code description is : " + ErrorCodeUtil.imageUtilErrorCodeToFieldName(rotateCode));
+ }
+ }
+ // 将创建一个Bitmap,并将图像数据存放到Bitmap中
+ Bitmap headBmp = Bitmap.createBitmap(cropImageWidth, cropImageHeight, Bitmap.Config.RGB_565);
+ int imageDataToBitmapCode = ArcSoftImageUtil.imageDataToBitmap(rotateHeadImageData, headBmp, imageFormat);
+ if (imageDataToBitmapCode != ArcSoftImageUtilError.CODE_SUCCESS) {
+ throw new RuntimeException("failed to transform image data to bitmap, code is : " + imageDataToBitmapCode
+ + ", code description is : " + ErrorCodeUtil.imageUtilErrorCodeToFieldName(imageDataToBitmapCode));
+ }
+ return headBmp;
+ }
+
+ /**
+ * 在特征库中搜索
+ *
+ * @param faceFeature 传入特征数据
+ * @param faceEngine 指定FaceEngine
+ * @return 比对结果
+ */
+ public CompareResult searchFaceFeature(FaceFeature faceFeature, FaceEngine faceEngine) {
+ if (faceEngine == null || faceFeature == null) {
+ return null;
+ }
+ long start = System.currentTimeMillis();
+ SearchResult searchResult;
+ try {
+ long searchStart = System.currentTimeMillis();
+ searchResult = faceEngine.searchFaceFeature(faceFeature);
+ Log.i(TAG, "searchCost:" + (System.currentTimeMillis() - searchStart) + "ms");
+ if (searchResult != null) {
+ FaceFeatureInfo faceFeatureInfo = searchResult.getFaceFeatureInfo();
+ FaceEntity faceEntity = FaceDatabase.getInstance(App.getContext()).faceDao().queryByFaceId(faceFeatureInfo.getSearchId());
+ if (faceEntity != null) {
+ return new CompareResult(faceEntity, searchResult.getMaxSimilar(), ErrorInfo.MOK, System.currentTimeMillis() - start);
+ }
+ }
+ } catch (IllegalArgumentException exception) {
+ Log.i(TAG, "searchFaceFeature exception:" + exception.getMessage());
+ }
+ return null;
+ }
+
+ /**
+ * 将图像中需要截取的Rect向外扩张一倍,若扩张一倍会溢出,则扩张到边界,若Rect已溢出,则收缩到边界
+ *
+ * @param width 图像宽度
+ * @param height 图像高度
+ * @param srcRect 原Rect
+ * @return 调整后的Rect
+ */
+ private static Rect getBestRect(int width, int height, Rect srcRect) {
+ if (srcRect == null) {
+ return null;
+ }
+ Rect rect = new Rect(srcRect);
+
+ // 原rect边界已溢出宽高的情况
+ int maxOverFlow = Math.max(-rect.left, Math.max(-rect.top, Math.max(rect.right - width, rect.bottom - height)));
+ if (maxOverFlow >= 0) {
+ rect.inset(maxOverFlow, maxOverFlow);
+ return rect;
+ }
+
+ // 原rect边界未溢出宽高的情况
+ int padding = rect.height() / 2;
+
+ // 若以此padding扩张rect会溢出,取最大padding为四个边距的最小值
+ if (!(rect.left - padding > 0 && rect.right + padding < width && rect.top - padding > 0 && rect.bottom + padding < height)) {
+ padding = Math.min(Math.min(Math.min(rect.left, width - rect.right), height - rect.bottom), rect.top);
+ }
+ rect.inset(-padding, -padding);
+ return rect;
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/faceserver/RegisterFailedException.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/faceserver/RegisterFailedException.java
new file mode 100644
index 0000000..bf115eb
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/faceserver/RegisterFailedException.java
@@ -0,0 +1,7 @@
+package com.sw.plate.utils.arcface.faceserver;
+
+public class RegisterFailedException extends Exception {
+ public RegisterFailedException(String message) {
+ super(message);
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/model/UserFaceInfo.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/model/UserFaceInfo.java
new file mode 100644
index 0000000..94eb7fe
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/model/UserFaceInfo.java
@@ -0,0 +1,37 @@
+package com.sw.plate.utils.arcface.model;
+
+import android.graphics.Bitmap;
+
+import com.arcsoft.face.FaceEngine;
+import com.arcsoft.face.FaceFeature;
+
+public class UserFaceInfo {
+ private FaceEngine frEngine;
+ private Bitmap headBmp;
+ private FaceFeature faceFeature;
+
+ public FaceEngine getFrEngine() {
+ return frEngine;
+ }
+
+ public void setFrEngine(FaceEngine frEngine) {
+ this.frEngine = frEngine;
+ }
+
+ public Bitmap getHeadBmp() {
+ return headBmp;
+ }
+
+ public void setHeadBmp(Bitmap headBmp) {
+ this.headBmp = headBmp;
+ }
+
+ public FaceFeature getFaceFeature() {
+ return faceFeature;
+ }
+
+ public void setFaceFeature(FaceFeature faceFeature) {
+ this.faceFeature = faceFeature;
+ }
+
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/view/RecognizeAreaView.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/view/RecognizeAreaView.java
new file mode 100644
index 0000000..6d707e3
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/view/RecognizeAreaView.java
@@ -0,0 +1,189 @@
+package com.sw.plate.utils.arcface.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+
+import com.sw.plate.R;
+import com.sw.plate.utils.arcface.FaceRectView;
+
+
+/**
+ * 控制可识别区域的控件,中间的镂空区域为可识别区域。
+ *
+ * 结合{@link FaceRectView}和{@link com.arcsoft.arcfacedemo.util.FaceRectTransformer}使用,可判断人脸是否显示在镂空区域
+ *
+ * 注意:需要保证人脸框绘制正确,识别区域的控制才有效。
+ *
+ * 实际使用中建议不要实现onTouch
+ */
+public class RecognizeAreaView extends View implements View.OnTouchListener {
+ /**
+ * 限制的识别区域
+ */
+ private RectF limitArea;
+
+ /**
+ * 不可识别区域的颜色
+ */
+ private int shadowColor;
+
+ /**
+ * 触摸点到当前识别区域的4个顶点距离的平方
+ * 0:左上角
+ * 1:右上角
+ * 2:左下角
+ * 3:右下角
+ */
+ private double[] distanceSquares = new double[4];
+
+ /**
+ * 识别区域发生变更的回调
+ */
+ public interface OnRecognizeAreaChangedListener {
+ /**
+ * 当识别区域发生变更时执行
+ *
+ * @param recognizeArea 新的识别区域(相对于View,而非图像数据)
+ */
+ void onRecognizeAreaChanged(Rect recognizeArea);
+ }
+
+ OnRecognizeAreaChangedListener onRecognizeAreaChangedListener;
+
+ /**
+ * 设置识别区域发生变更的回调
+ *
+ * @param onRecognizeAreaChangedListener 识别区域发生变更的回调
+ */
+ public void setOnRecognizeAreaChangedListener(OnRecognizeAreaChangedListener onRecognizeAreaChangedListener) {
+ this.onRecognizeAreaChangedListener = onRecognizeAreaChangedListener;
+ }
+
+ public RecognizeAreaView(Context context) {
+ this(context, null);
+ }
+
+ public RecognizeAreaView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ shadowColor = ContextCompat.getColor(context, R.color.color_bg_notification);
+ setOnTouchListener(this);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+ limitArea = new RectF(0, 0, width, height);
+ if (onRecognizeAreaChangedListener != null) {
+ onRecognizeAreaChangedListener.onRecognizeAreaChanged(
+ new Rect(((int) limitArea.left), ((int) limitArea.top),
+ ((int) limitArea.right), ((int) limitArea.bottom))
+ );
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (limitArea == null) {
+ return;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ canvas.clipOutRect(limitArea);
+ } else {
+ canvas.clipRect(limitArea, Region.Op.DIFFERENCE);
+ }
+ canvas.drawColor(shadowColor);
+ }
+
+ /**
+ * 根据最近的触摸点,刷新识别区域
+ *
+ * @param x 触摸点的横坐标
+ * @param y 触摸点的纵坐标
+ */
+ private void updateRecognizeArea(float x, float y) {
+ /*
+ 0:左上角
+ 1:右上角
+ 2:左下角
+ 3:右下角
+ */
+ distanceSquares[0] = getDistanceSquare(x, y, limitArea.left, limitArea.top);
+ distanceSquares[1] = getDistanceSquare(x, y, limitArea.right, limitArea.top);
+ distanceSquares[2] = getDistanceSquare(x, y, limitArea.left, limitArea.bottom);
+ distanceSquares[3] = getDistanceSquare(x, y, limitArea.right, limitArea.bottom);
+
+ int closestIndex = 0;
+ double closestDistance = distanceSquares[0];
+ for (int i = 1; i < distanceSquares.length; i++) {
+ double distance = distanceSquares[i];
+ if (closestDistance > distance) {
+ closestDistance = distance;
+ closestIndex = i;
+ }
+ }
+ switch (closestIndex) {
+ case 0:
+ limitArea.left = x;
+ limitArea.top = y;
+ break;
+ case 1:
+ limitArea.right = x;
+ limitArea.top = y;
+ break;
+ case 2:
+ limitArea.left = x;
+ limitArea.bottom = y;
+ break;
+ case 3:
+ limitArea.right = x;
+ limitArea.bottom = y;
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * 获取两点距离的平方(由于只是为了大小比较,所以没必要开根号,减少运算)
+ *
+ * @param x1 第一个点的横坐标
+ * @param y1 第一个点的纵坐标
+ * @param x2 第二个点的横坐标
+ * @param y2 第二个点的纵坐标
+ * @return 距离的平方
+ */
+ private double getDistanceSquare(float x1, float y1, float x2, float y2) {
+ float deltaHorizontal = x1 - x2;
+ float deltaVertical = y1 - y2;
+ return deltaHorizontal * deltaHorizontal + deltaVertical * deltaVertical;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ int pointerCount = event.getPointerCount();
+ for (int i = 0; i < pointerCount; i++) {
+ updateRecognizeArea(event.getX(i), event.getY(i));
+ }
+ if (onRecognizeAreaChangedListener != null) {
+ onRecognizeAreaChangedListener.onRecognizeAreaChanged(
+ new Rect(((int) limitArea.left), ((int) limitArea.top),
+ ((int) limitArea.right), ((int) limitArea.bottom))
+ );
+ }
+ invalidate();
+ return true;
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/viewmodel/ActiveViewModel.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/viewmodel/ActiveViewModel.java
new file mode 100644
index 0000000..04c3082
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/viewmodel/ActiveViewModel.java
@@ -0,0 +1,71 @@
+package com.sw.plate.utils.arcface.viewmodel;
+
+import android.content.Context;
+import android.os.Environment;
+
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import com.arcsoft.face.FaceEngine;
+import com.sw.plate.AppConst;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Properties;
+
+public class ActiveViewModel extends ViewModel {
+ private MutableLiveData activeResult = new MutableLiveData<>();
+
+ public void activeOnline(Context context, String activeKey, String appId, String sdkKey) {
+ activeResult.postValue(FaceEngine.activeOnline(context, activeKey, appId, sdkKey));
+ }
+
+ public void activeOffline(Context context, String path) {
+ activeResult.postValue(FaceEngine.activeOffline(context, path));
+ }
+
+ private static final int ACTIVE_KEY_EFFECTIVE_LENGTH = 16;
+
+ public String formatActiveKey(String activeKey) {
+ String rawActiveKey = activeKey.replace("-", "").toUpperCase();
+ StringBuilder newActiveKey = new StringBuilder();
+ if (rawActiveKey.length() == ACTIVE_KEY_EFFECTIVE_LENGTH) {
+ for (int i = 0; i < 4; i++) {
+ newActiveKey.append(rawActiveKey.substring(i * 4, i * 4 + 4)).append("-");
+ }
+ newActiveKey.deleteCharAt(newActiveKey.length() - 1);
+ return newActiveKey.toString();
+ } else {
+ return activeKey;
+ }
+
+ }
+
+ public MutableLiveData getActiveResult() {
+ return activeResult;
+ }
+
+
+ public Properties loadProperties() {
+ Properties properties = new Properties();
+ FileInputStream fis = null;
+ File configFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + AppConst.ACTIVE_CONFIG_FILE_NAME);
+ try {
+ fis = new FileInputStream(configFile);
+ properties.load(fis);
+ return properties;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ } finally {
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/viewmodel/RecognizeViewModel.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/viewmodel/RecognizeViewModel.java
new file mode 100644
index 0000000..7fe5798
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/viewmodel/RecognizeViewModel.java
@@ -0,0 +1,668 @@
+package com.sw.plate.utils.arcface.viewmodel;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.Camera;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import com.arcsoft.face.AgeInfo;
+import com.arcsoft.face.ErrorInfo;
+import com.arcsoft.face.FaceAttributeParam;
+import com.arcsoft.face.FaceEngine;
+import com.arcsoft.face.FaceInfo;
+import com.arcsoft.face.GenderInfo;
+import com.arcsoft.face.LivenessInfo;
+import com.arcsoft.face.LivenessParam;
+import com.arcsoft.face.MaskInfo;
+import com.arcsoft.face.enums.DetectFaceOrientPriority;
+import com.arcsoft.face.enums.DetectMode;
+import com.sw.plate.App;
+import com.sw.plate.R;
+import com.sw.plate.utils.arcface.ConfigUtil;
+import com.sw.plate.utils.arcface.FaceRectTransformer;
+import com.sw.plate.utils.arcface.FaceRectView;
+import com.sw.plate.utils.arcface.PreviewConfig;
+import com.sw.plate.utils.arcface.callback.OnRegisterFinishedCallback;
+import com.sw.plate.utils.arcface.face.FaceHelper;
+import com.sw.plate.utils.arcface.face.RecognizeCallback;
+import com.sw.plate.utils.arcface.face.constants.LivenessType;
+import com.sw.plate.utils.arcface.face.constants.RecognizeColor;
+import com.sw.plate.utils.arcface.face.constants.RequestFeatureStatus;
+import com.sw.plate.utils.arcface.face.model.CompareResult;
+import com.sw.plate.utils.arcface.face.model.FacePreviewInfo;
+import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration;
+import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
+import com.sw.plate.utils.arcface.faceserver.FaceServer;
+import com.sw.plate.utils.arcface.model.UserFaceInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.observers.DisposableObserver;
+import io.reactivex.schedulers.Schedulers;
+
+public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
+ /**
+ * 人脸识别过程中数据的更新类型
+ */
+ public enum EventType {
+ /**
+ * 人脸插入
+ */
+ INSERTED,
+ /**
+ * 人脸移除
+ */
+ REMOVED
+ }
+
+ public static class FaceItemEvent {
+ private int index;
+ private EventType eventType;
+
+ public FaceItemEvent(int index, EventType eventType) {
+ this.index = index;
+ this.eventType = eventType;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public void setIndex(int index) {
+ this.index = index;
+ }
+
+ public EventType getEventType() {
+ return eventType;
+ }
+
+ public void setEventType(EventType eventType) {
+ this.eventType = eventType;
+ }
+ }
+
+ private static final String TAG = "RecognizeViewModel";
+
+
+ private OnRegisterFinishedCallback onRegisterFinishedCallback;
+
+ /**
+ * 注册人脸状态码,准备注册
+ */
+ public static final int REGISTER_STATUS_READY = 0;
+ /**
+ * 注册人脸状态码,注册中
+ */
+ public static final int REGISTER_STATUS_PROCESSING = 1;
+ /**
+ * 注册人脸状态码,注册结束(无论成功失败)
+ */
+ public static final int REGISTER_STATUS_DONE = 2;
+
+ /**
+ * 人脸识别的状态,预设值为:已结束
+ */
+ private int registerStatus = REGISTER_STATUS_DONE;
+
+ private static final int MAX_DETECT_NUM = 10;
+ /**
+ * 相机预览的分辨率
+ */
+ private Camera.Size previewSize;
+ /**
+ * 用于头像RecyclerView显示的信息
+ */
+ private MutableLiveData> compareResultList;
+
+ private MutableLiveData faceItemEventMutableLiveData = new MutableLiveData<>();
+
+ /**
+ * 各个引擎初始化的错误码
+ */
+ private MutableLiveData ftInitCode = new MutableLiveData<>();
+ private MutableLiveData frInitCode = new MutableLiveData<>();
+ private MutableLiveData flInitCode = new MutableLiveData<>();
+
+ /**
+ * 人脸操作辅助类,推帧即可,内部会进行特征提取、识别
+ */
+ private FaceHelper faceHelper;
+ /**
+ * VIDEO模式人脸检测引擎,用于预览帧人脸追踪及图像质量检测
+ */
+ private FaceEngine ftEngine;
+ /**
+ * 用于特征提取的引擎
+ */
+ private FaceEngine frEngine;
+ /**
+ * IMAGE模式活体检测引擎,用于预览帧人脸活体检测
+ */
+ private FaceEngine flEngine;
+
+ private PreviewConfig previewConfig;
+
+ private MutableLiveData recognizeConfiguration = new MutableLiveData<>();
+
+ private MutableLiveData recognizeNotice = new MutableLiveData<>();
+
+ private MutableLiveData drawRectInfoText = new MutableLiveData<>();
+
+ private MutableLiveData recognizeUserId = new MutableLiveData<>();
+
+ /**
+ * 检测ir活体前,是否需要更新faceData
+ */
+ private boolean needUpdateFaceData;
+ /**
+ * 当前活体检测的检测类型
+ */
+ private LivenessType livenessType;
+
+ /**
+ * IR活体数据
+ */
+ private byte[] irNV21 = null;
+
+ /**
+ * 人脸库数据加载完成
+ */
+ private boolean loadFaceList;
+
+ private Disposable registerNv21Disposable;
+
+ public void refreshIrPreviewData(byte[] irPreviewData) {
+ irNV21 = irPreviewData;
+ }
+
+ /**
+ * 设置当前活体检测的检测类型
+ *
+ * @param liveType 活体检测的检测类型
+ */
+ public void setLiveType(LivenessType liveType) {
+ this.livenessType = liveType;
+ }
+
+ public void setRgbFaceRectTransformer(FaceRectTransformer rgbFaceRectTransformer) {
+ faceHelper.setRgbFaceRectTransformer(rgbFaceRectTransformer);
+ }
+
+ public void setIrFaceRectTransformer(FaceRectTransformer irFaceRectTransformer) {
+ faceHelper.setIrFaceRectTransformer(irFaceRectTransformer);
+ }
+
+
+ /**
+ * 注册实时NV21数据
+ *
+ * @param nv21 实时相机预览的NV21数据
+ * @param facePreviewInfo 人脸信息
+ */
+ private void registerFace(final byte[] nv21, FacePreviewInfo facePreviewInfo) {
+ updateRegisterStatus(REGISTER_STATUS_PROCESSING);
+ registerNv21Disposable = Observable.create((ObservableOnSubscribe) emitter -> {
+ FaceEngine registerEngine = new FaceEngine();
+ int res = registerEngine.init(App.getContext(), DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_0_ONLY,
+ 1, FaceEngine.ASF_FACE_RECOGNITION);
+ if (res == ErrorInfo.MOK) {
+// boolean success = FaceServer.getInstance().registerNv21(App.getContext(), nv21.clone(), previewSize.width,
+// previewSize.height, facePreviewInfo, "registered_" + faceHelper.getTrackedFaceCount(), frEngine, registerEngine);
+
+ UserFaceInfo userFaceInfo = FaceServer.getInstance().getUserInfo(App.getContext(), nv21.clone(), previewSize.width,
+ previewSize.height, facePreviewInfo, "registered_" + faceHelper.getTrackedFaceCount(), frEngine, registerEngine);
+ registerEngine.unInit();
+ emitter.onNext(userFaceInfo);
+ } else {
+ emitter.onNext(null);
+ }
+ emitter.onComplete();
+ })
+ .subscribeOn(Schedulers.computation())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribeWith(new DisposableObserver() {
+
+ @Override
+ public void onNext(UserFaceInfo success) {
+ if (onRegisterFinishedCallback != null) {
+ onRegisterFinishedCallback.onRegisterFinished(facePreviewInfo, success);
+ }
+
+ updateRegisterStatus(REGISTER_STATUS_DONE);
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ e.printStackTrace();
+ if (onRegisterFinishedCallback != null) {
+ onRegisterFinishedCallback.onRegisterFinished(facePreviewInfo, null);
+ }
+ updateRegisterStatus(REGISTER_STATUS_DONE);
+ }
+
+ @Override
+ public void onComplete() {
+ }
+ });
+
+ }
+
+ public MutableLiveData> getCompareResultList() {
+ if (compareResultList == null) {
+ compareResultList = new MutableLiveData<>();
+ compareResultList.setValue(new ArrayList<>());
+ }
+ return compareResultList;
+ }
+
+ /**
+ * 初始化引擎
+ */
+ public void init(PreviewConfig previewConfig1) {
+ Context context = App.getContext();
+ if (previewConfig1 != null) {
+ previewConfig = previewConfig1;
+ } else {
+ boolean switchCamera = ConfigUtil.isSwitchCamera(context);
+ previewConfig = new PreviewConfig(
+ switchCamera ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK,
+ switchCamera ? Camera.CameraInfo.CAMERA_FACING_BACK : Camera.CameraInfo.CAMERA_FACING_FRONT,
+ Integer.parseInt(ConfigUtil.getRgbCameraAdditionalRotation(context)),
+ Integer.parseInt(ConfigUtil.getIrCameraAdditionalRotation(context))
+ );
+ }
+ // 填入在设置界面设置好的配置信息
+ boolean enableLive = !ConfigUtil.getLivenessDetectType(context).equals(context.getString(R.string.value_liveness_type_disable));
+// enableLive = false;
+ boolean enableFaceQualityDetect = ConfigUtil.isEnableImageQualityDetect(context);
+ boolean enableFaceMoveLimit = ConfigUtil.isEnableFaceMoveLimit(context);
+ boolean enableFaceSizeLimit = ConfigUtil.isEnableFaceSizeLimit(context);
+ RecognizeConfiguration configuration = new RecognizeConfiguration.Builder()
+ .enableFaceMoveLimit(enableFaceMoveLimit)
+ .enableFaceSizeLimit(enableFaceSizeLimit)
+ .faceSizeLimit(ConfigUtil.getFaceSizeLimit(context))
+ .faceMoveLimit(ConfigUtil.getFaceMoveLimit(context))
+ .enableLiveness(enableLive)
+ .enableImageQuality(enableFaceQualityDetect)
+ .maxDetectFaces(ConfigUtil.getRecognizeMaxDetectFaceNum(context))
+ .keepMaxFace(ConfigUtil.isKeepMaxFace(context))
+ .similarThreshold(ConfigUtil.getRecognizeThreshold(context))
+ .imageQualityNoMaskRecognizeThreshold(ConfigUtil.getImageQualityNoMaskRecognizeThreshold(context))
+ .imageQualityMaskRecognizeThreshold(ConfigUtil.getImageQualityMaskRecognizeThreshold(context))
+ .livenessParam(new LivenessParam(ConfigUtil.getRgbLivenessThreshold(context), ConfigUtil.getIrLivenessThreshold(context),
+ ConfigUtil.getLivenessFqThreshold(context)))
+ .build();
+ int cameraOffsetX = ConfigUtil.getDualCameraHorizontalOffset(context);
+ int cameraOffsetY = ConfigUtil.getDualCameraVerticalOffset(context);
+ needUpdateFaceData = (livenessType == LivenessType.IR && (cameraOffsetX != 0 || cameraOffsetY != 0));
+
+ ftEngine = new FaceEngine();
+ int ftEngineMask = FaceEngine.ASF_FACE_DETECT | FaceEngine.ASF_MASK_DETECT;
+ ftInitCode.postValue(ftEngine.init(context, DetectMode.ASF_DETECT_MODE_VIDEO, ConfigUtil.getFtOrient(context),
+ ConfigUtil.getRecognizeMaxDetectFaceNum(context), ftEngineMask));
+ FaceAttributeParam attributeParam = new FaceAttributeParam(
+ ConfigUtil.getRecognizeEyeOpenThreshold(context), ConfigUtil.getRecognizeMouthCloseThreshold(context),
+ ConfigUtil.getRecognizeWearGlassesThreshold(context));
+ ftEngine.setFaceAttributeParam(attributeParam);
+
+ frEngine = new FaceEngine();
+ int frEngineMask = FaceEngine.ASF_FACE_RECOGNITION;
+ if (enableFaceQualityDetect) {
+ frEngineMask |= FaceEngine.ASF_IMAGEQUALITY;
+ }
+ frInitCode.postValue(frEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_0_ONLY,
+ 10, frEngineMask));
+ FaceServer.getInstance().initFaceList(context, frEngine, faceCount -> loadFaceList = true, true);
+
+ //启用活体检测时,才初始化活体引擎
+ if (enableLive) {
+ flEngine = new FaceEngine();
+ int flEngineMask = (livenessType == LivenessType.RGB ? FaceEngine.ASF_LIVENESS : (FaceEngine.ASF_IR_LIVENESS | FaceEngine.ASF_FACE_DETECT));
+ if (needUpdateFaceData) {
+ flEngineMask |= FaceEngine.ASF_UPDATE_FACEDATA;
+ }
+ flInitCode.postValue(flEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE,
+ DetectFaceOrientPriority.ASF_OP_ALL_OUT, 10, flEngineMask));
+ LivenessParam livenessParam = new LivenessParam(ConfigUtil.getRgbLivenessThreshold(context), ConfigUtil.getIrLivenessThreshold(context), ConfigUtil.getLivenessFqThreshold(context));
+ flEngine.setLivenessParam(livenessParam);
+ }
+
+ recognizeConfiguration.setValue(configuration);
+ }
+
+ public void addFace(FaceEntity faceEntity) {
+ if (frEngine != null)
+ FaceServer.getInstance().registerFaceFeatureInfoFromDb(faceEntity, frEngine);
+ }
+
+ public void refreshFaceList() {
+ FaceServer.getInstance().initFaceList(App.getContext(), frEngine, faceCount -> loadFaceList = true, true);
+ }
+
+ /**
+ * 销毁引擎,faceHelper中可能会有特征提取耗时操作仍在执行,加锁防止crash
+ */
+ private void unInit() {
+ if (ftEngine != null) {
+ synchronized (ftEngine) {
+ int ftUnInitCode = ftEngine.unInit();
+ Log.i(TAG, "unInitEngine: " + ftUnInitCode);
+ }
+ }
+ if (frEngine != null) {
+ synchronized (frEngine) {
+ int frUnInitCode = frEngine.unInit();
+ Log.i(TAG, "unInitEngine: " + frUnInitCode);
+ }
+ }
+ if (flEngine != null) {
+ synchronized (flEngine) {
+ int flUnInitCode = flEngine.unInit();
+ Log.i(TAG, "unInitEngine: " + flUnInitCode);
+ }
+ }
+ }
+
+ /**
+ * 删除已经离开的人脸
+ *
+ * @param facePreviewInfoList 人脸和trackId列表
+ */
+ public void clearLeftFace(List facePreviewInfoList) {
+ List compareResults = compareResultList.getValue();
+ if (compareResults != null) {
+ for (int i = compareResults.size() - 1; i >= 0; i--) {
+ boolean contains = false;
+ for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) {
+ if (facePreviewInfo.getTrackId() == compareResults.get(i).getTrackId()) {
+ contains = true;
+ break;
+ }
+ }
+ if (!contains) {
+ compareResults.remove(i);
+ getFaceItemEventMutableLiveData().postValue(new FaceItemEvent(i, EventType.REMOVED));
+ }
+ }
+ }
+ }
+
+ /**
+ * 释放操作
+ */
+ public void destroy() {
+ unInit();
+ if (faceHelper != null) {
+ ConfigUtil.setTrackedFaceCount(App.getContext(), faceHelper.getTrackedFaceCount());
+ faceHelper.release();
+ faceHelper = null;
+ }
+ FaceServer.getInstance().release();
+ if (registerNv21Disposable != null) {
+ registerNv21Disposable.dispose();
+ registerNv21Disposable = null;
+ }
+ }
+
+ /**
+ * 当相机打开时由activity调用,进行一些初始化操作
+ *
+ * @param camera 相机实例
+ */
+ public void onRgbCameraOpened(Camera camera) {
+ Camera.Size lastPreviewSize = previewSize;
+ previewSize = camera.getParameters().getPreviewSize();
+ // 切换相机的时候可能会导致预览尺寸发生变化
+ initFaceHelper(lastPreviewSize);
+ }
+
+ /**
+ * 当相机打开时由activity调用,进行一些初始化操作
+ *
+ * @param camera 相机实例
+ */
+ public void onIrCameraOpened(Camera camera) {
+ Camera.Size lastPreviewSize = previewSize;
+ previewSize = camera.getParameters().getPreviewSize();
+ // 切换相机的时候可能会导致预览尺寸发生变化
+ initFaceHelper(lastPreviewSize);
+ }
+
+ private void initFaceHelper(Camera.Size lastPreviewSize) {
+ if (faceHelper == null || lastPreviewSize == null ||
+ lastPreviewSize.width != previewSize.width || lastPreviewSize.height != previewSize.height) {
+ Integer trackedFaceCount = null;
+ // 记录切换时的人脸序号
+ if (faceHelper != null) {
+ trackedFaceCount = faceHelper.getTrackedFaceCount();
+ faceHelper.release();
+ }
+ Context context = App.getContext();
+ int horizontalOffset = ConfigUtil.getDualCameraHorizontalOffset(context);
+ int verticalOffset = ConfigUtil.getDualCameraVerticalOffset(context);
+ int maxDetectFaceNum = ConfigUtil.getRecognizeMaxDetectFaceNum(context);
+ faceHelper = new FaceHelper.Builder()
+ .ftEngine(ftEngine)
+ .frEngine(frEngine)
+ .flEngine(flEngine)
+ .needUpdateFaceData(needUpdateFaceData)
+ .frQueueSize(maxDetectFaceNum)
+ .flQueueSize(maxDetectFaceNum)
+ .previewSize(previewSize)
+ .recognizeCallback(this)
+ .recognizeConfiguration(recognizeConfiguration.getValue())
+ .trackedFaceCount(trackedFaceCount == null ? ConfigUtil.getTrackedFaceCount(context) : trackedFaceCount)
+ .dualCameraFaceInfoTransformer(faceInfo -> {
+ FaceInfo irFaceInfo = new FaceInfo(faceInfo);
+ irFaceInfo.getRect().offset(horizontalOffset, verticalOffset);
+ return irFaceInfo;
+ })
+ .build();
+ }
+ }
+
+ @Override
+ public void onRecognized(CompareResult compareResult, Integer live, boolean similarPass) {
+ Disposable disposable = Observable.just(true).observeOn(AndroidSchedulers.mainThread()).subscribe(aBoolean -> {
+ if (similarPass) {
+ if (recognizeUserId != null) {
+ recognizeUserId.postValue(compareResult);
+ }
+ boolean isAdded = false;
+ List compareResults = compareResultList.getValue();
+ if (compareResults != null && !compareResults.isEmpty()) {
+ for (CompareResult compareResult1 : compareResults) {
+ if (compareResult1.getTrackId() == compareResult.getTrackId()) {
+ isAdded = true;
+ break;
+ }
+ }
+ }
+ if (!isAdded) {
+ //对于多人脸搜索,假如最大显示数量为 MAX_DETECT_NUM 且有新的人脸进入,则以队列的形式移除
+ if (compareResults != null && compareResults.size() >= MAX_DETECT_NUM) {
+ compareResults.remove(0);
+ getFaceItemEventMutableLiveData().postValue(new FaceItemEvent(0, EventType.REMOVED));
+ }
+ if (compareResults != null) {
+ compareResults.add(compareResult);
+ getFaceItemEventMutableLiveData().postValue(new FaceItemEvent(compareResults.size() - 1, EventType.INSERTED));
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onNoticeChanged(String notice) {
+ if (recognizeNotice != null) {
+ recognizeNotice.postValue(notice);
+ }
+ }
+
+ public void setDrawRectInfoTextValue(boolean openDrawRect) {
+ String stringDrawText = openDrawRect ? "关闭绘制" : "开启绘制";
+ if (drawRectInfoText != null) {
+ drawRectInfoText.postValue(stringDrawText);
+ }
+ }
+
+ /**
+ * 设置实时注册的结果回调
+ *
+ * @param onRegisterFinishedCallback 实时注册的结果回调
+ */
+ public void setOnRegisterFinishedCallback(OnRegisterFinishedCallback onRegisterFinishedCallback) {
+ this.onRegisterFinishedCallback = onRegisterFinishedCallback;
+ }
+
+ public MutableLiveData getFtInitCode() {
+ return ftInitCode;
+ }
+
+ public MutableLiveData getFrInitCode() {
+ return frInitCode;
+ }
+
+ public MutableLiveData getFlInitCode() {
+ return flInitCode;
+ }
+
+ public MutableLiveData getRecognizeNotice() {
+ return recognizeNotice;
+ }
+
+ public MutableLiveData getRecognizeUserId() {
+ return recognizeUserId;
+ }
+
+ public MutableLiveData getDrawRectInfoText() {
+ return drawRectInfoText;
+ }
+
+ public MutableLiveData getFaceItemEventMutableLiveData() {
+ return faceItemEventMutableLiveData;
+ }
+
+ /**
+ * 准备注册,将注册的状态值修改为待注册
+ */
+ public void prepareRegister() {
+ if (registerStatus == REGISTER_STATUS_DONE) {
+ updateRegisterStatus(REGISTER_STATUS_READY);
+ }
+ }
+
+ public void updateRegisterStatus(int status) {
+ registerStatus = status;
+ }
+
+ /**
+ * 根据预览信息生成绘制信息
+ *
+ * @param facePreviewInfoList 预览信息
+ * @return 绘制信息
+ */
+ public List getDrawInfo(List facePreviewInfoList, LivenessType livenessType, boolean drawRectInfo) {
+ List drawInfoList = new ArrayList<>();
+ for (int i = 0; i < facePreviewInfoList.size(); i++) {
+ int trackId = facePreviewInfoList.get(i).getTrackId();
+ String name = faceHelper.getName(trackId);
+ Integer liveness = faceHelper.getLiveness(trackId);
+ Integer recognizeStatus = faceHelper.getRecognizeStatus(trackId);
+
+ // 根据识别结果和活体结果设置颜色
+ int color = RecognizeColor.COLOR_UNKNOWN;
+ if (recognizeStatus != null) {
+ if (recognizeStatus == RequestFeatureStatus.FAILED) {
+ color = RecognizeColor.COLOR_FAILED;
+ }else if (recognizeStatus == RequestFeatureStatus.SUCCEED) {
+ color = RecognizeColor.COLOR_SUCCESS;
+ } else if (recognizeStatus == RequestFeatureStatus.TO_RETRY) {
+ color = RecognizeColor.COLOR_UNKNOWN;
+ //需要重试
+// FaceEntity faceEntity = new FaceEntity("2",null, null);
+// CompareResult result = new CompareResult(faceEntity, 0.0f);
+// recognizeUserId.postValue(result);
+ }
+ }
+ if (liveness != null && liveness == LivenessInfo.NOT_ALIVE) {
+ color = RecognizeColor.COLOR_FAILED;
+ }
+
+ drawInfoList.add(new FaceRectView.DrawInfo(
+ livenessType == LivenessType.RGB ? facePreviewInfoList.get(i).getRgbTransformedRect() : facePreviewInfoList.get(i).getIrTransformedRect(),
+ GenderInfo.UNKNOWN, AgeInfo.UNKNOWN_AGE, liveness == null ? LivenessInfo.UNKNOWN : liveness, color,
+ name == null ? "" : name, facePreviewInfoList.get(i).getFaceInfoRgb().getIsWithinBoundary(),
+ facePreviewInfoList.get(i).getForeRect(), facePreviewInfoList.get(i).getFaceInfoRgb().getFaceAttributeInfo(), drawRectInfo,
+ livenessType == LivenessType.RGB));
+ }
+ return drawInfoList;
+ }
+
+
+ /**
+ * 传入可见光相机预览数据
+ *
+ * @param nv21 可见光相机预览数据
+ * @param doRecognize 是否进行识别
+ * @return 当前帧的检测结果信息
+ */
+ public List onPreviewFrame(byte[] nv21, boolean doRecognize) {
+ if (faceHelper != null) {
+ if (!loadFaceList) {
+ return null;
+ }
+ if (livenessType == LivenessType.IR && irNV21 == null) {
+ return null;
+ }
+ List facePreviewInfoList = faceHelper.onPreviewFrame(nv21, irNV21, doRecognize);
+ if (registerStatus == REGISTER_STATUS_READY && !facePreviewInfoList.isEmpty()) {
+ FacePreviewInfo facePreviewInfo = facePreviewInfoList.get(0);
+ if (facePreviewInfo.getMask() != MaskInfo.WORN) {
+ registerFace(nv21, facePreviewInfoList.get(0));
+ } else {
+ Toast.makeText(App.getContext(), "注册照要求不戴口罩", Toast.LENGTH_SHORT).show();
+ updateRegisterStatus(REGISTER_STATUS_DONE);
+ }
+ }
+ return facePreviewInfoList;
+ }
+ return null;
+ }
+
+ /**
+ * 设置可识别区域(相对于View)
+ *
+ * @param recognizeArea 可识别区域
+ */
+ public void setRecognizeArea(Rect recognizeArea) {
+ if (faceHelper != null) {
+ faceHelper.setRecognizeArea(recognizeArea);
+ }
+ }
+
+ public MutableLiveData getRecognizeConfiguration() {
+ return recognizeConfiguration;
+ }
+
+ public PreviewConfig getPreviewConfig() {
+ return previewConfig;
+ }
+
+ public Point loadPreviewSize() {
+ String[] size = ConfigUtil.getPreviewSize(App.getContext()).split("x");
+ return new Point(Integer.parseInt(size[0]), Integer.parseInt(size[1]));
+ }
+}
diff --git a/lib_face/src/main/res/layout/toast_bg.xml b/lib_face/src/main/res/layout/toast_bg.xml
new file mode 100644
index 0000000..8c87595
--- /dev/null
+++ b/lib_face/src/main/res/layout/toast_bg.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lib_face/src/main/res/values/colors.xml b/lib_face/src/main/res/values/colors.xml
new file mode 100644
index 0000000..706e518
--- /dev/null
+++ b/lib_face/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #80000000
+ #000000
+
\ No newline at end of file
diff --git a/lib_face/src/main/res/values/strings.xml b/lib_face/src/main/res/values/strings.xml
new file mode 100644
index 0000000..47286d5
--- /dev/null
+++ b/lib_face/src/main/res/values/strings.xml
@@ -0,0 +1,49 @@
+
+
+
+ rgb_liveness
+ ir_liveness
+ disable_liveness
+
+ track_face_count
+ choose_detect_degree
+ max_detect_num
+ limit_recognize_area
+ scale_value
+ dual_camera_offset_horizontal
+ dual_camera_offset_vertical
+ recognize_threshold
+ shelter_threshold
+ eye_open_threshold
+ mouth_close_threshold
+ wear_glasses_threshold
+ recognize_face_size_limit
+ recognize_move_pixel_limit
+ rgb_liveness_threshold
+ ir_liveness_threshold
+ liveness_fq_threshold
+ rgb_liveness_face_size_threshold
+ ir_liveness_face_size_threshold
+ dual_camera_preview_size
+ app_id
+ sdk_key
+ active_key
+ enable_image_quality_detect
+ enable_face_size_limit
+ enable_face_move_limit
+ image_quality_no_mask_recognize_threshold
+ image_quality_no_mask_register_threshold
+ image_quality_mask_recognize_threshold
+
+ switch_camera
+ draw_rgb_rect_horizontal_mirror
+ draw_rgb_rect_vertical_mirror
+ draw_ir_rect_horizontal_mirror
+ draw_ir_rect_vertical_mirror
+ rgb_preview_horizontal_mirror
+ ir_preview_horizontal_mirror
+
+ liveness_detect_type
+ rgb_camera_rotation
+ ir_camera_rotation
+
\ No newline at end of file
diff --git a/lib_face/src/test/java/com/sw/plate/ExampleUnitTest.java b/lib_face/src/test/java/com/sw/plate/ExampleUnitTest.java
new file mode 100644
index 0000000..44a803e
--- /dev/null
+++ b/lib_face/src/test/java/com/sw/plate/ExampleUnitTest.java
@@ -0,0 +1,19 @@
+package com.sw.plate;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ int i1 = (int) (0.5f * 280);
+ System.err.println("");
+ }
+}
\ No newline at end of file
diff --git a/local.properties b/local.properties
new file mode 100644
index 0000000..62c4a0c
--- /dev/null
+++ b/local.properties
@@ -0,0 +1,10 @@
+## This file is automatically generated by Android Studio.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file should *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+#
+# Location of the SDK. This is only used by Gradle.
+# For customization when using a Version Control System, please read the
+# header note.
+sdk.dir=D\:\\Android\\sdk
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..162e0bf
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,36 @@
+@file:Suppress("UnstableApiUsage")
+
+pluginManagement {
+ repositories {
+ maven { url = uri("https://maven.aliyun.com/repository/public/") }
+ maven { url = uri("https://maven.aliyun.com/repository/central") }
+ maven { url = uri("https://maven.aliyun.com/repository/google") }
+ maven { url = uri("https://maven.aliyun.com/repository/jcenter") }
+ maven { url = uri("https://jitpack.io") }
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ maven { url = uri("https://maven.aliyun.com/repository/public/") }
+ maven { url = uri("https://maven.aliyun.com/repository/central") }
+ maven { url = uri("https://maven.aliyun.com/repository/google") }
+ maven { url = uri("https://maven.aliyun.com/repository/jcenter") }
+ maven { url = uri("https://jitpack.io") }
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "FaceCollect"
+include(":app")
+include(":lib_face")