技术栈
前端: React+face-api
后端: Java+Spring Boot+OpenCV
前言
- 本文是调研阶段代码未经处理,但是结果是成功的,只不过使用时按照项目逻辑更改代码就好了
- OpenCV的Jar包在Maven库中没有,需要自行去官网下载
- react使用webpeak构建工具,如果使用Vite导入model文件容易出现问题
效果图
后端逻辑
1、配置OpenCV库
1:新建一个project项目,在resources包下创建lib(OpenCV存放位置)和static(xml处理文件存放位置)文件夹,
OpenCV下载位置----------opencv.org/releases/
XML处理文件下载位置 github.com/opencv/open…
2:创建启动类和接口文件
3:配置yml文件,解除上传文件限制
2、导入相应xml处理文件和Jar包
pom文件导入
<dependency>
<groupId>org</groupId>
<artifactId>opencv</artifactId>
<version>4.8.1</version>
<scope>system</scope>
<systemPath>${project.basedir}\src\main\resources\lib\opencv-481.jar</systemPath>
</dependency>
MyImage文件初始化以及引入
static CascadeClassifier faceDetector;
static {
// 加载OpenCV本地库
System.setProperty("java.awt.headless", "false");
URL url = ClassLoader.getSystemResource("lib/opencv_java481.dll");
System.load(url.getPath());
String classifierPath = "C:\Users\liu35\Desktop\haarcascade_frontalface_alt.xml";
faceDetector = new CascadeClassifier(classifierPath);
}
3、处理图片
(1)灰度化图片
public static Mat conv_Mat(String img) {
// 读取图像
Mat mat1 = Imgcodecs.imread(img);
if (mat1.empty()) {
System.err.println("Image not loaded properly.");
return null; // 或者抛出异常
}
Mat mat2 = new Mat();
// 灰度化:将图像从一种颜色空间转换为另一种颜色空间
Imgproc.cvtColor(mat1, mat2, Imgproc.COLOR_BGR2GRAY);
// 探测人脸:检测到的对象作为矩形列表返回
MatOfRect faceDetections = new MatOfRect();
List<Rect> faces = faceDetections.toList();
for (Rect face : faces) {
System.out.println(face.x+"----"+face.y+"----"+face.width+"----"+face.height);
}
if (faces.isEmpty()) {
System.out.println("No face detected.");
// 可以考虑返回原图、空白图或null,并在后续使用时妥善处理
// 这里以返回原图为例
System.out.println(mat2);
return mat2;
}
Rect rect = faces.get(0); // 假设只取第一个人脸
Mat face = new Mat(mat1, rect);
return face;
}
(2)人脸识别比对
public static double faceRecognitionComparison(String image1, String image2) {
Mat mat1 = conv_Mat(image1);
Mat mat2 = conv_Mat(image2);
Mat mat3 = new Mat();
Mat mat4 = new Mat();
// 颜色范围
MatOfFloat ranges = new MatOfFloat(0f, 256f);
// 直方图大小, 越大匹配越精确 (越慢)
MatOfInt histSize = new MatOfInt(1000);
Imgproc.calcHist(Arrays.asList(mat1), new MatOfInt(0), new Mat(), mat3, histSize, ranges);
Imgproc.calcHist(Arrays.asList(mat2), new MatOfInt(0), new Mat(), mat4, histSize, ranges);
// 比较两个密集或两个稀疏直方图
return Imgproc.compareHist(mat3, mat4, Imgproc.CV_COMP_CORREL);
}
(3)主执行函数
public static void main(String[] args) {
// 比对图片的绝对地址
String path="C:\Users\liu35\Desktop\sucai\3.jpg";
String path1="C:\Users\liu35\Desktop\sucai\2.jpg";
double comparison = faceRecognitionComparison(path, path1);
System.out.println("对比结果:" + comparison);
if (comparison > 0.80) {
System.out.println("人脸匹配成功");
} else {
System.out.println("人脸不匹配识别");
}
}
至此就可以执行main函数,就可以得到人脸比对结果。
4、处理视频
(1)导入文件
static CascadeClassifier faceDetector;
static {
// 加载OpenCV本地库
System.setProperty("java.awt.headless", "false");
URL url = ClassLoader.getSystemResource("lib/opencv_java481.dll");
System.load(url.getPath());
String classifierPath = "C:\Users\liu35\Desktop\haarcascade_frontalface_alt.xml";
faceDetector = new CascadeClassifier(classifierPath);
}
(3)从视频帧中识别人脸
public static Mat getFace(Mat image) {
MatOfRect face = new MatOfRect();
// 检测输入图像中不同大小的对象。检测到的对象作为矩形列表返回。
faceDetector.detectMultiScale(image, face);
Rect[] rects = face.toArray();
if (rects.length > 0 && Math.random() * 10 > 8) {
System.out.println("识别人脸个数: " + rects.length);
// Imgcodecs.imwrite("C:\Users\liu35\Desktop\sucai\" + UUID.randomUUID() + ".png", image);
}
if (rects != null && rects.length >= 1) {
// 为每张识别到的人脸画一个圈
for (int i = 0; i < rects.length; i++) {
/**
* 绘制一个简单的、粗的或填充的直角矩形
*
* img 图像
* pt1 - 矩形的顶点
* pt2 - 与 pt1 相对的矩形的顶点
* color – 矩形颜色或亮度(灰度图像)意味着该函数必须绘制一个填充的矩形。
*/
Imgproc.rectangle(image, new Point(rects[i].x, rects[i].y), new Point(rects[i].x + rects[i].width, rects[i].y + rects[i].height), new Scalar(0, 255, 0));
/**
* 绘制一个文本字符串,放在识别人脸框上
*
* img -- 图像
* text -- 要绘制的文本字符串
* org – 图像中文本字符串的左下角
* fontFace – 字体类型,请参阅#HersheyFonts
* fontScale – 字体比例因子乘以特定字体的基本大小
* color - 文本颜色
* thickness ——用于绘制文本的线条粗细
* lineType – 线型
* bottomLeftOrigin – 当为 true 时,图像数据原点位于左下角。否则,它位于左上角
*/
Imgproc.putText(image, "test", new Point(rects[i].x, rects[i].y), Imgproc.FONT_HERSHEY_SCRIPT_SIMPLEX, 1.0, new Scalar(0, 255, 0), 1, Imgproc.LINE_AA, false);
}
}
return image;
}
(4) 灰度化图片
public static Mat conv_Mat(String img) {
// 读取图像
Mat mat1 = Imgcodecs.imread(img);
if (mat1.empty()) {
System.err.println("Image not loaded properly.");
return null; // 或者抛出异常
}
Mat mat2 = new Mat();
// 灰度化:将图像从一种颜色空间转换为另一种颜色空间
Imgproc.cvtColor(mat1, mat2, Imgproc.COLOR_BGR2GRAY);
// 探测人脸:检测到的对象作为矩形列表返回
MatOfRect faceDetections = new MatOfRect();
List<Rect> faces = faceDetections.toList();
for (Rect face : faces) {
System.out.println(face.x+"----"+face.y+"----"+face.width+"----"+face.height);
}
if (faces.isEmpty()) {
System.out.println("No face detected.");
// 可以考虑返回原图、空白图或null,并在后续使用时妥善处理
// 这里以返回原图为例
System.out.println(mat2);
return mat2;
}
Rect rect = faces.get(0); // 假设只取第一个人脸
Mat face = new Mat(mat1, rect);
return face;
}
(5)比较直方图
public static double faceRecognitionComparison(Mat mat1, String image2) {
Mat mat2 = conv_Mat(image2);
Mat mat3 = new Mat();
Mat mat4 = new Mat();
// 颜色范围
MatOfFloat ranges = new MatOfFloat(0f, 256f);
// 直方图大小, 越大匹配越精确 (越慢)
MatOfInt histSize = new MatOfInt(100000);
Imgproc.calcHist(Arrays.asList(mat1), new MatOfInt(0), new Mat(), mat3, histSize, ranges);
Imgproc.calcHist(Arrays.asList(mat2), new MatOfInt(0), new Mat(), mat4, histSize, ranges);
// 比较两个密集或两个稀疏直方图
return Imgproc.compareHist(mat3, mat4, Imgproc.CV_COMP_CORREL);
}
(6) 识别人脸
public static void videoFaceRecognition() {
// 读取视频文件
VideoCapture capture = new VideoCapture();
String path="C:\Users\liu35\Desktop\sucai\video.webm";
capture.open(path);
if (!capture.isOpened()) {
throw new RuntimeException("读取视频文件失败");
}
Mat video = new Mat();
int index = 0;
while (capture.isOpened()) {
// 抓取、解码并返回下一个视频帧写入Mat对象中
capture.read(video);
// 显示从视频中识别的人脸图像
Mat face = getFace(video);
//比对的图片地址
String path1="C:\Users\liu35\Desktop\sucai\2.jpg";
double comparison = faceRecognitionComparison(face, path1);
System.out.println("对比结果:" + comparison);
if (comparison > 0.60) {
System.out.println("人脸匹配成功");
} else {
System.out.println("人脸不匹配识别");
}
return;
}
}
执行main
public static void main(String[] args) {
videoFaceRecognition();
}
5、逻辑书写
艺术已成,接下来搭建接口用于前后端操作 代码和上文基本一致,换一种执行方式罢了,所以我就直接展示全部代码
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
public class face {
static CascadeClassifier faceDetector;
static {
// 加载OpenCV本地库
System.setProperty("java.awt.headless", "false");
URL url = ClassLoader.getSystemResource("lib/opencv_java481.dll");
System.load(url.getPath());
// String classifierPath = "C:\Users\liu35\Desktop\haarcascade_frontalface_alt.xml";
// faceDetector = new CascadeClassifier(classifierPath);
// 加载XML分类器文件
InputStream is = face.class.getClassLoader().getResourceAsStream("static/haarcascade_frontalface_alt.xml");
File tempFile = new File(System.getProperty("java.io.tmpdir"), "haarcascade_frontalface_alt.xml");
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
// 处理异常
}
String classifierPath = tempFile.getAbsolutePath();
faceDetector = new CascadeClassifier(classifierPath);
}
@PostMapping("/login")
public ResponseEntity<?> faceLogin(@RequestBody MultipartFile image )throws IOException {
if (!image.isEmpty() ) {
// 将MultipartFile转换为临时文件
File tempFile = convertToFile(image);
try {
// 使用临时文件的路径进行处理
double comparison = faceRecognitionComparison(tempFile.getAbsolutePath(),
"C:\Users\liu35\Desktop\sucai\3.jpg");
System.out.println("对比结果:" + comparison);
if (comparison > 0.70) {
System.out.println("人脸匹配成功");
HashMap<String, Object> obj = new HashMap<>();
obj.put("token","eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoidXNlciIsIm5hbWUiOiJiaWFvIiwiZXhwIjoxNzE1MzMxOTg5fQ.aS9y7bxC7-7xroLscn9xa--OcmlbQAitobx2kwC3C8c");
obj.put("name","Liu Biao");
obj.put("id","1");
return ResponseEntity.ok(obj);
} else {
System.out.println("人脸不匹配识别");
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
}
} finally {
// 清理:删除临时文件(根据需要,也可以选择保留)
Files.deleteIfExists(tempFile.toPath());
}
} else {
return ResponseEntity.badRequest().body("No image file provided.");
}
}
private File convertToFile(MultipartFile file) throws IOException {
File tempFile = Files.createTempFile("temp", file.getOriginalFilename()).toFile();
file.transferTo(tempFile);
return tempFile;
}
/**
* 人脸识别比对
*/
public static double faceRecognitionComparison(String image1, String image2) {
Mat mat1 = conv_Mat(image1);
Mat mat2 = conv_Mat(image2);
Mat mat3 = new Mat();
Mat mat4 = new Mat();
// 颜色范围
MatOfFloat ranges = new MatOfFloat(0f, 256f);
// 直方图大小, 越大匹配越精确 (越慢)
MatOfInt histSize = new MatOfInt(100000);
Imgproc.calcHist(Arrays.asList(mat1), new MatOfInt(0), new Mat(), mat3, histSize, ranges);
Imgproc.calcHist(Arrays.asList(mat2), new MatOfInt(0), new Mat(), mat4, histSize, ranges);
// 比较两个密集或两个稀疏直方图
return Imgproc.compareHist(mat3, mat4, Imgproc.CV_COMP_CORREL);
}
/**
* 灰度化人脸
*/
public static Mat conv_Mat(String img) {
// 读取图像
Mat mat1 = Imgcodecs.imread(img);
if (mat1.empty()) {
System.err.println("Image not loaded properly.");
return null; // 或者抛出异常
}
Mat mat2 = new Mat();
// 灰度化:将图像从一种颜色空间转换为另一种颜色空间
Imgproc.cvtColor(mat1, mat2, Imgproc.COLOR_BGR2GRAY);
// 探测人脸:检测到的对象作为矩形列表返回
MatOfRect faceDetections = new MatOfRect();
List<Rect> faces = faceDetections.toList();
for (Rect face : faces) {
System.out.println(face.x+"----"+face.y+"----"+face.width+"----"+face.height);
}
if (faces.isEmpty()) {
System.out.println("No face detected.");
// 可以考虑返回原图、空白图或null,并在后续使用时妥善处理
// 这里以返回原图为例
System.out.println(mat2);
return mat2;
}
Rect rect = faces.get(0); // 假设只取第一个人脸
Mat face = new Mat(mat1, rect);
return face;
}
}
前端逻辑
1、搭建框架和静态文件
记得安装face-api
"face-api.js": "^0.22.2",
我这边使用的creat-react-app命令搭建的,然后把下载的face-api的处理文件放在public/models中
引入处理文件和定义状态
const navigate = useNavigate();
const videoRef = useRef(null);
const canvasRef = useRef(null);
const [isStreaming, setIsStreaming] = useState(false);
const [isModelLoaded, setIsModelLoaded] = useState(false);
const [faceResult, setFaceResult] = useState(false);
// 添加 timeoutId 状态
const [timeoutId, setTimeoutId] = useState(null);
useEffect(() => {
try {
Promise.all([
faceapi.nets.tinyFaceDetector.loadFromUri("/models"),
faceapi.nets.faceLandmark68Net.loadFromUri("/models"),
faceapi.nets.faceRecognitionNet.loadFromUri("/models"),
])
.then(() => {
setIsModelLoaded(true);
})
.catch((err) => {
console.error(err);
});
} catch {}
}, []);
2、书写静态页面
外页面自定义样式就行,我这里直接展示摄像头处理页面的代码
<div>
<div>
<video autoPlay muted ref={videoRef} />
<canvas className="canvas" ref={canvasRef} />
</div>
{!isStreaming && !isModelLoaded && <p>Loading models...</p>}
</div>
3、调用摄像头并截取画面图像
// 访问摄像头
useEffect(() => {
if (!isModelLoaded) return;
navigator.mediaDevices
.getUserMedia({ video: true })
.then((stream) => {
if (videoRef.current) {
// 添加这个检查
videoRef.current.srcObject = stream;
setIsStreaming(true);
} else {
console.warn("videoRef is not ready yet");
}
})
.catch((err) => {
console.error("Something went wrong!");
});
return () => {
if (videoRef.current) {
videoRef.current.srcObject.getTracks().forEach((track) => track.stop());
}
};
}, [isModelLoaded]);
4、后端验证并保存token
// 捕获图片并发送
const captureAndSendImage = async () => {
if (!isStreaming) return;
const canvas = canvasRef.current;
const context = canvas.getContext("2d");
// 设置canvas大小与视频相同
canvas.width = videoRef.current.videoWidth;
canvas.height = videoRef.current.videoHeight;
// 绘制视频帧到canvas
context.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
// 转换canvas为图片数据
canvas.toBlob(async (blob) => {
// 在这里添加人脸检测逻辑(如果需要)
// 假设我们有一个sendImageToBackend函数来发送图片到后端
// sendImageToBackend(blob);
// 示例:使用fetch API发送图片(作为FormData)
const formData = new FormData();
formData.append("image", blob);
fetch("http://localhost:8080/login", {
method: "POST",
body: formData,
})
.then((response) => response.json())
.then((data) => {
console.log("Response from server:", data);
setFaceResult(true);
// 停止视频流
if (videoRef.current && videoRef.current.srcObject) {
videoRef.current.srcObject
.getTracks()
.forEach((track) => track.stop());
}
// 清理定时器
if (timeoutId) {
clearTimeout(timeoutId);
setTimeoutId(null); // 清理后重置状态
}
// setIsStreaming
navigate("/charging");
return data;
})
.catch((error) => {
// 停止视频流
if (videoRef.current && videoRef.current.srcObject) {
videoRef.current.srcObject
.getTracks()
.forEach((track) => track.stop());
}
console.error("Error:", error);
navigate("/login");
});
});
};
5、写一个定时器方便看效果
useEffect(() => {
let id;
if (isStreaming) {
id = setTimeout(() => {
captureAndSendImage();
}, 1000);
}
setTimeoutId(id); // 存储定时器ID到状态中
return () => {
if (id) {
clearTimeout(id);
}
};
}, [isStreaming]);
结语
本文是对比图像相似图,也可以实现对象人像面部特征哦!总之调研还算顺利,可以正常使用,只不过逻辑要运用到项目中是不行的,可以自行处理相应的项目逻辑实现同样效果。