Kaynağa Gözat

微信端人脸照片采集接口开发

baiyun 1 yıl önce
ebeveyn
işleme
4fadd05241

+ 9 - 0
ruoyi-modules/ruoyi-backstage/pom.xml

@@ -160,6 +160,15 @@
             <artifactId>pay-java-wx</artifactId>
             <version>2.13.1</version>
         </dependency>
+
+        <!-- 人脸识别 -->
+        <dependency>
+            <groupId>com.arcsoft.face</groupId>
+            <artifactId>arcsoft-sdk-face</artifactId>
+            <version>3.0.0.0</version>
+        </dependency>
+
+
     </dependencies>
 
     <build>

+ 68 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/config/ArcFaceConfig.java

@@ -0,0 +1,68 @@
+package org.dromara.backstage.config;
+
+
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author flysheep
+ * @date 2020/7/31 0031
+ * @time 16:31
+ */
+@Configuration
+@ConfigurationProperties(prefix = "arcconfig.arcface-sdk")
+@ConditionalOnProperty(value = { "arcConfig.arcface-sdk.enable" },matchIfMissing = false)
+public class ArcFaceConfig {
+    //获取配置参数
+    public String sdkLibPath="";
+
+    public String appId = "";
+
+    public String sdkKey = "";
+
+    public Integer detectPooSize = 5;
+
+    public Integer comparePooSize = 5;
+
+    public String getSdkLibPath() {
+        return sdkLibPath;
+    }
+
+    public void setSdkLibPath(String sdkLibPath) {
+        this.sdkLibPath = sdkLibPath;
+    }
+
+    public String getAppId() {
+        return appId;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
+
+    public String getSdkKey() {
+        return sdkKey;
+    }
+
+    public void setSdkKey(String sdkKey) {
+        this.sdkKey = sdkKey;
+    }
+
+    public Integer getDetectPooSize() {
+        return detectPooSize;
+    }
+
+    public void setDetectPooSize(Integer detectPooSize) {
+        this.detectPooSize = detectPooSize;
+    }
+
+    public Integer getComparePooSize() {
+        return comparePooSize;
+    }
+
+    public void setComparePooSize(Integer comparePooSize) {
+        this.comparePooSize = comparePooSize;
+    }
+}

+ 64 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/config/FaceEngineFactory.java

@@ -0,0 +1,64 @@
+package org.dromara.backstage.config;
+
+import com.arcsoft.face.EngineConfiguration;
+import com.arcsoft.face.FaceEngine;
+import com.arcsoft.face.enums.ErrorInfo;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.pool2.BasePooledObjectFactory;
+import org.apache.commons.pool2.PooledObject;
+import org.apache.commons.pool2.impl.DefaultPooledObject;
+import org.dromara.common.core.exception.ServiceException;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+
+/**
+ * @author flysheep
+ * @date 2020/7/27 0027
+ * @time 15:31
+ */
+@Slf4j
+@ConditionalOnProperty(value = { "arcConfig.arcface-sdk.enable" }, matchIfMissing = false)
+public class FaceEngineFactory extends BasePooledObjectFactory<FaceEngine> {
+	private String libPath;
+	private String appId;
+	private String sdkKey;
+	private String activeKey;
+	private EngineConfiguration engineConfiguration;
+
+	public FaceEngineFactory(String libPath, String appId, String sdkKey, String activeKey,
+                             EngineConfiguration engineConfiguration) {
+		this.appId = appId;
+		this.sdkKey = sdkKey;
+		this.activeKey = activeKey;
+		this.libPath = libPath;
+		this.engineConfiguration = engineConfiguration;
+	}
+
+	@Override
+	public FaceEngine create() {
+
+		FaceEngine faceEngine = new FaceEngine(libPath);
+		int activeCode = faceEngine.activeOnline(appId, sdkKey);
+		if (activeCode != ErrorInfo.MOK.getValue() && activeCode != ErrorInfo.MERR_ASF_ALREADY_ACTIVATED.getValue()) {
+			log.error("引擎激活失败" + activeCode);
+            throw new ServiceException("引擎激活失败: "+ activeCode);
+		}
+		int initCode = faceEngine.init(engineConfiguration);
+		if (initCode != ErrorInfo.MOK.getValue()) {
+			log.error("引擎初始化失败" + initCode);
+            throw new ServiceException("引擎初始化失败: "+ initCode);
+		}
+		return faceEngine;
+	}
+
+	@Override
+	public PooledObject<FaceEngine> wrap(FaceEngine faceEngine) {
+		return new DefaultPooledObject<>(faceEngine);
+	}
+
+	@Override
+	public void destroyObject(PooledObject<FaceEngine> pool) throws Exception {
+		FaceEngine faceEngine = pool.getObject();
+		int result = faceEngine.unInit();
+		super.destroyObject(pool);
+	}
+}

+ 24 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/wx/service/FaceEngineService.java

@@ -0,0 +1,24 @@
+package org.dromara.backstage.wx.service;
+
+import com.arcsoft.face.FaceInfo;
+import com.arcsoft.face.toolkit.ImageInfo;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+
+import java.util.List;
+
+
+/**
+ * @author flysheep
+ * @date 2020/7/27 0027
+ * @time 15:37
+ */
+@ConditionalOnProperty(value = { "arcConfig.arcface-sdk.enable" }, matchIfMissing = false)
+public interface FaceEngineService {
+
+	// 检测脸部信息
+	List<FaceInfo> detectFaces(ImageInfo imageInfo);
+
+	// 生成特征码
+	String createFeatureData(ImageInfo imageInfo);
+
+}

+ 3 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/wx/service/IWxService.java

@@ -2,6 +2,7 @@ package org.dromara.backstage.wx.service;
 
 import org.dromara.backstage.payment.domain.vo.PtUserAccountVo;
 import org.dromara.backstage.wx.domain.vo.WxCreditAccountVo;
+import org.dromara.common.core.domain.R;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 
@@ -18,4 +19,6 @@ public interface IWxService {
     TableDataInfo<WxCreditAccountVo> findCreditAccount(Long userId, String startTime, String endTime, PageQuery pageQuery);
 
     boolean updateCardStatus(Long userId, String cardStatus);
+
+    R<String> uploadUserPhoto(Long userId, String imgData);
 }

+ 133 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/wx/service/impl/FaceEngineServiceImpl.java

@@ -0,0 +1,133 @@
+package org.dromara.backstage.wx.service.impl;
+
+import cn.hutool.core.codec.Base64;
+import com.arcsoft.face.*;
+import com.arcsoft.face.enums.DetectMode;
+import com.arcsoft.face.enums.DetectOrient;
+import com.arcsoft.face.toolkit.ImageInfo;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.pool2.impl.GenericObjectPool;
+import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
+import org.dromara.backstage.config.ArcFaceConfig;
+import org.dromara.backstage.config.FaceEngineFactory;
+import org.dromara.backstage.wx.service.FaceEngineService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author flysheep
+ * @date 2020/7/27 0027
+ * @time 15:40
+ */
+@Service
+@Slf4j
+@ConditionalOnProperty(value = { "arcConfig.arcface-sdk.enable" }, matchIfMissing = false)
+public class FaceEngineServiceImpl implements FaceEngineService {
+	@Autowired
+	private ArcFaceConfig arcFaceConfig;
+
+	public final static Logger logger = LoggerFactory.getLogger(FaceEngineServiceImpl.class);
+
+	// 通用人脸识别引擎池
+	private GenericObjectPool<FaceEngine> faceEngineGeneralPool;
+
+	// 引擎配置
+	@PostConstruct
+	public void init() {
+		GenericObjectPoolConfig detectPoolConfig = new GenericObjectPoolConfig();
+		detectPoolConfig.setMaxIdle(arcFaceConfig.getDetectPooSize());
+		detectPoolConfig.setMaxTotal(arcFaceConfig.getDetectPooSize());
+		detectPoolConfig.setMinIdle(arcFaceConfig.getDetectPooSize());
+		detectPoolConfig.setLifo(false);
+		EngineConfiguration detectCfg = new EngineConfiguration();
+		FunctionConfiguration detectFunctionCfg = new FunctionConfiguration();
+		detectFunctionCfg.setSupportFaceDetect(true);// 开启人脸检测功能
+		detectFunctionCfg.setSupportFaceRecognition(true);// 开启人脸识别功能
+		detectFunctionCfg.setSupportAge(true);// 开启年龄检测功能
+	    detectFunctionCfg.setSupportGender(true);// 开启性别检测功能
+		detectFunctionCfg.setSupportLiveness(true);// 开启活体检测功能
+
+		detectCfg.setFunctionConfiguration(detectFunctionCfg);
+		detectCfg.setDetectMode(DetectMode.ASF_DETECT_MODE_IMAGE);// 图片检测模式,如果是连续帧的视频流图片,那么改成VIDEO模式
+		detectCfg.setDetectFaceOrientPriority(DetectOrient.ASF_OP_0_ONLY);// 人脸旋转角度
+		faceEngineGeneralPool = new GenericObjectPool(new FaceEngineFactory(arcFaceConfig.getSdkLibPath(),
+				arcFaceConfig.getAppId(), arcFaceConfig.getSdkKey(), null, detectCfg), detectPoolConfig);// 底层库算法对象池
+
+	}
+
+	// 人脸检测
+	@Override
+	public List<FaceInfo> detectFaces(ImageInfo imageInfo) {
+		// 参数判断
+		if (imageInfo == null)
+			return null;
+		// FaceEngine人脸引擎类
+		FaceEngine faceEngine = null;
+		try {
+			// 这里进行获取
+			faceEngine = faceEngineGeneralPool.borrowObject();
+			if (faceEngine == null) {
+				return null;
+			}
+			List<FaceInfo> faceInfoList = new ArrayList<>();
+			// 我们进行人脸检测
+
+			int errorCode = faceEngine.detectFaces(imageInfo.getImageData(), imageInfo.getWidth(),
+					imageInfo.getHeight(), imageInfo.getImageFormat(), faceInfoList);
+			if (errorCode == 0) {
+				return faceInfoList;
+			}
+		} catch (Exception e) {
+			logger.error(e.getMessage());
+			logger.error(Arrays.toString(e.getStackTrace()));
+		} finally {
+			if (faceEngine != null) {
+				// 释放引擎对象
+				faceEngineGeneralPool.returnObject(faceEngine);
+			}
+		}
+		return null;
+	}
+
+	// 生成特征码
+	// 这里还需要改,关于图片处理
+	@Override
+	public String createFeatureData(ImageInfo imageInfo) {
+		// 获取脸部算法
+		// FaceEngine人脸引擎类
+		FaceEngine faceEngine = null;
+		try {
+			// 这里进行获取人脸引擎
+			faceEngine = faceEngineGeneralPool.borrowObject();
+			List<FaceInfo> faceInfoList = new ArrayList<FaceInfo>();
+			// 人脸检测返回值
+			int code = faceEngine.detectFaces(imageInfo.getImageData(), imageInfo.getWidth(), imageInfo.getHeight(),
+					imageInfo.getImageFormat(), faceInfoList);
+			if (code == 0 && faceInfoList.size() > 0) {
+				FaceFeature faceFeature = new FaceFeature();
+				faceEngine.extractFaceFeature(imageInfo.getImageData(), imageInfo.getWidth(), imageInfo.getHeight(),
+						imageInfo.getImageFormat(), faceInfoList.get(0), faceFeature);
+				String featureData = Base64.encode(faceFeature.getFeatureData());
+				return featureData;
+			}
+		} catch (Exception e) {
+			logger.error(e.getMessage());
+			logger.error(Arrays.toString(e.getStackTrace()));
+		} finally {
+			if (faceEngine != null) {
+				// 释放引擎对象
+				faceEngineGeneralPool.returnObject(faceEngine);
+			}
+		}
+		return null;
+	}
+
+}

+ 199 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/wx/service/impl/WxServiceImpl.java

@@ -1,36 +1,71 @@
 package org.dromara.backstage.wx.service.impl;
 
+import cn.hutool.core.codec.Base64;
 import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.img.Img;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.crypto.digest.MD5;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONObject;
+import com.arcsoft.face.FaceInfo;
+import com.arcsoft.face.toolkit.ImageInfo;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.dromara.backstage.cardCenter.domain.PtCard;
 import org.dromara.backstage.cardCenter.mapper.PtCardMapper;
 import org.dromara.backstage.consumption.mapper.XfCreditAccountMapper;
+import org.dromara.backstage.payment.domain.PtUserAccount;
+import org.dromara.backstage.payment.domain.bo.PtUserAccountBo;
 import org.dromara.backstage.payment.domain.vo.PtUserAccountVo;
 import org.dromara.backstage.payment.mapper.PtUserAccountMapper;
 import org.dromara.backstage.wx.domain.vo.WxCreditAccountVo;
+import org.dromara.backstage.wx.service.FaceEngineService;
 import org.dromara.backstage.wx.service.IWxService;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.system.api.RemoteDictService;
 import org.dromara.system.api.domain.vo.RemoteDictDataVo;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.List;
 import java.util.Map;
+import java.util.TreeMap;
 import java.util.stream.Collectors;
 
+import static com.arcsoft.face.toolkit.ImageFactory.getRGBData;
+
 /**
  * 微信Service业务层处理
  *
  */
 @RequiredArgsConstructor
 @Service
+@Slf4j
 public class WxServiceImpl implements IWxService {
 
     private final PtUserAccountMapper accountMapper;
     private final XfCreditAccountMapper creditAccountMapper;
     private final PtCardMapper cardMapper;
     private final RemoteDictService dictService;
+    private FaceEngineService faceEngineService;
+
+    @Value("${upload.upload-path}/")    // 文件上传路径
+    private String uploadPath;
+    @Value("${upload.image.user}/")     // 用户头像路径
+    private String userPath;
+    @Value("${dzbp.sync-img.url}/")     // 电子班牌照片推送接口
+    private String syncImgToDzbpUrl;
 
     @Override
     public PtUserAccountVo getUserInfoByUserId(Long userId) {
@@ -57,4 +92,168 @@ public class WxServiceImpl implements IWxService {
             .eq(PtCard::getUserId, userId));
         return count > 0;
     }
+
+    @Override
+    public R<String> uploadUserPhoto(Long userId, String imgData) {
+        //1.查询用户信息,用于判断身份
+        PtUserAccountVo vo = accountMapper.selectVoById(userId);
+        if (ObjectUtil.isEmpty(vo)) {
+            throw new RuntimeException("用户不存在");
+        }
+        //2.调用虹软,识别人脸是否正确
+        int strIndex = imgData.indexOf(";base64,");
+        if (strIndex > 0) {
+            imgData = imgData.substring(strIndex + 8); // 过滤掉data:image/jpg;base64,字符串
+        }
+
+        byte[] imageBytes = Base64.decode(imgData);
+        ImageInfo imageInfo = getRGBData(imageBytes);
+        List<FaceInfo> faceInfosList = faceEngineService.detectFaces(imageInfo);
+        if (faceInfosList == null || faceInfosList.size() == 0) {
+            return R.fail("人脸识别不成功!");
+        }
+
+        try {
+            //3.图片压缩处理
+            imageBytes = imgCompression(imageBytes);
+
+            //4.更新账户表人脸照片地址,上传照片到服务器
+            uploadUserPhoto(userId, imageBytes);
+
+            //5.如果用户是学员身份,则将照片同步给电子班牌
+            if("2".equals(vo.getCategory())){
+                syncImgToDZBP(vo, imgData);
+            }
+        }catch (ServiceException e){
+            return R.fail(e.getMessage());
+        }
+       return R.ok("上传图片成功");
+    }
+
+    /**
+     * 同步照片到电子班牌
+     * @param vo
+     * @param imgData
+     */
+    private void syncImgToDZBP(PtUserAccountVo vo, String imgData){
+        String timestamp = Long.toString(System.currentTimeMillis());
+        TreeMap<String, Object> params = new TreeMap<String, Object>();
+        params.put("pid", "hnswdx");
+        params.put("random", RandomUtil.randomString(16));
+        params.put("timestamp", timestamp);
+        StringBuilder s1 = new StringBuilder();
+        for (String key : params.keySet()) {
+            s1.append(key).append("=").append(params.get(key)).append("&");
+        }
+        s1.append("key=").append("WPDRUiVRo0KIKf4s20hhbyk1GgMmibtT");
+        String sign = MD5.create().digestHex(s1.toString()).toUpperCase();
+        params.put("sign", sign);
+
+        s1 = new StringBuilder();
+        for (String key : params.keySet()) {
+            s1.append(key).append("=").append(params.get(key)).append("&");
+        }
+        s1.deleteCharAt(s1.length() - 1);
+
+        JSONObject face = new JSONObject();
+        face.put("type", "face");
+        JSONArray array = new JSONArray();
+        JSONObject data = new JSONObject();
+        data.put("p_key", vo.getOtherId());
+        data.put("no", vo.getOtherId());
+        data.put("face_image", imgData);
+        data.put("update_date", System.currentTimeMillis());
+        array.put(data);
+        face.put("data", array);
+
+        String res = HttpRequest.post(syncImgToDzbpUrl + "?" + s1.toString())
+            .body(face.toString())
+            .execute().body();
+        log.info("同步照片到电子班牌返回结果:{}", res);
+    }
+    private void uploadUserPhoto(Long userId, byte[] imageBytes) {
+        //1.上传照片到指定目录
+        ByteArrayInputStream bis = new ByteArrayInputStream(imageBytes);
+        byte[] bytes = new byte[1024];
+        int index;
+
+        String localFileName = uploadPath + userPath + userId + ".jpg";
+        FileOutputStream downloadFile = null;
+        try {
+            downloadFile = new FileOutputStream(localFileName);
+            while ((index = bis.read(bytes)) != -1) {
+                downloadFile.write(bytes, 0, index);
+                downloadFile.flush();
+            }
+        } catch (IOException e) {
+            log.error("图片处理失败,请稍后重试!", e);
+            throw new ServiceException("图片处理失败,请稍后重试!");
+        } finally {
+            try {
+                bis.close();
+            } catch (IOException e) {
+                log.error("inputStream关闭失败!", e);
+            }
+            if (downloadFile != null) {
+                try {
+                    downloadFile.close();
+                } catch (IOException e) {
+                    log.error("downloadFile关闭失败!", e);
+                }
+            }
+        }
+
+        //2.保存图片路径到数据库
+        String photoUrl = userPath + userId + ".jpg";
+        PtUserAccountBo bo = PtUserAccountBo.builder().userId(userId).photo(photoUrl).build();
+        accountMapper.updateById(MapstructUtils.convert(bo, PtUserAccount.class));
+    }
+    /**
+     * 图片压缩
+     * @param imageBytes
+     * @return
+     */
+    private static byte[] imgCompression(byte[] imageBytes) {
+        // 小于1M就不进行压缩里,浪费执行时间
+        float quality = 0f;
+        if (imageBytes.length > 1024 * 1024 * 10) { // 大于10M
+            quality = 0.1f;
+        } else if (imageBytes.length > 1024 * 1024 * 5) { // 大于5M
+            quality = 0.2f;
+        } else if (imageBytes.length > 1024 * 1024 * 1) {// 大于1M
+            quality = 0.5f;
+        }
+
+        if (quality != 0) {
+            ByteArrayInputStream bis = null;
+            ByteArrayOutputStream bos = null;
+            try {
+                bis = new ByteArrayInputStream(imageBytes);
+                bos = new ByteArrayOutputStream();
+                Img.from(bis).setQuality(quality).write(bos);
+                imageBytes =  bos.toByteArray();
+            } catch (Exception e) {
+                throw new ServiceException("图片处理失败,请稍后重试!");
+            } finally {
+                if (bis != null) {
+                    try {
+                        bis.close();
+                    } catch (IOException e) {
+                        // TODO Auto-generated catch block
+                        e.printStackTrace();
+                    }
+                }
+                if (bos != null) {
+                    try {
+                        bos.close();
+                    } catch (IOException e) {
+                        // TODO Auto-generated catch block
+                        e.printStackTrace();
+                    }
+                }
+            }
+        }
+        return imageBytes;
+    }
+
 }