You've already forked FrameTour-BE
feat(face): 增加人脸识别详情与人工调整功能
- 新增人脸识别详情接口,返回系统采纳与被过滤的样本信息 - 新增人工调整识别结果接口,支持用户手动选择或排除样本 - 引入样本过滤原因枚举,用于记录和展示过滤原因 - 重构样本过滤逻辑,增加过滤轨迹追踪功能 - 优化时间范围与设备照片数量限制的过滤实现 - 在搜索结果中增加过滤轨迹信息,便于前端展示 - 添加人脸识别详情VO和样本VO,丰富返回数据结构 - 完善人脸识别相关的请求与响应模型定义
This commit is contained in:
@@ -3,6 +3,7 @@ package com.ycwl.basic.service.task;
|
||||
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||
import com.ycwl.basic.model.task.resp.SampleFilterTrace;
|
||||
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@@ -25,7 +26,19 @@ public interface TaskFaceService {
|
||||
* @param scenicConfig 景区配置管理器
|
||||
* @return 筛选后的样本ID列表
|
||||
*/
|
||||
List<Long> applySampleFilters(List<Long> acceptedSampleIds,
|
||||
List<FaceSampleEntity> allFaceSampleList,
|
||||
List<Long> applySampleFilters(List<Long> acceptedSampleIds,
|
||||
List<FaceSampleEntity> allFaceSampleList,
|
||||
ScenicConfigManager scenicConfig);
|
||||
|
||||
/**
|
||||
* 带过滤轨迹的样本筛选逻辑。
|
||||
*
|
||||
* @param acceptedSampleIds 已接受的样本ID列表
|
||||
* @param allFaceSampleList 所有人脸样本实体列表
|
||||
* @param scenicConfig 景区配置
|
||||
* @return 包含最终样本及过滤原因的轨迹对象
|
||||
*/
|
||||
SampleFilterTrace applySampleFiltersWithTrace(List<Long> acceptedSampleIds,
|
||||
List<FaceSampleEntity> allFaceSampleList,
|
||||
ScenicConfigManager scenicConfig);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
|
||||
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
|
||||
import com.ycwl.basic.model.pc.face.enums.FaceRecognitionFilterReason;
|
||||
import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLog;
|
||||
import com.ycwl.basic.model.pc.faceDetectLog.resp.MatchLocalRecord;
|
||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||
@@ -30,6 +31,7 @@ import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
|
||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||
import com.ycwl.basic.model.task.resp.SampleFilterTrace;
|
||||
import com.ycwl.basic.repository.DeviceRepository;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
@@ -52,7 +54,10 @@ import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -264,7 +269,18 @@ public class TaskFaceServiceImpl implements TaskFaceService {
|
||||
idIndexMap.put(allFaceSampleIds.get(i), i);
|
||||
}
|
||||
allFaceSampleList.sort(Comparator.comparing(sample -> idIndexMap.get(sample.getId())));
|
||||
acceptFaceSampleIds = applySampleFilters(acceptFaceSampleIds, allFaceSampleList, scenicConfig);
|
||||
SampleFilterTrace filterTrace = applySampleFiltersWithTrace(acceptFaceSampleIds, allFaceSampleList, scenicConfig);
|
||||
List<Long> finalAcceptedSampleIds = filterTrace.getAcceptedSampleIds() == null
|
||||
? Collections.emptyList()
|
||||
: filterTrace.getAcceptedSampleIds();
|
||||
Set<Long> initialAcceptedSet = acceptFaceSampleIds == null
|
||||
? Collections.emptySet()
|
||||
: new HashSet<>(acceptFaceSampleIds);
|
||||
for (Long sampleId : allFaceSampleIds) {
|
||||
if (!finalAcceptedSampleIds.contains(sampleId) && !initialAcceptedSet.contains(sampleId)) {
|
||||
filterTrace.addReason(sampleId, FaceRecognitionFilterReason.SCORE_BELOW_THRESHOLD);
|
||||
}
|
||||
}
|
||||
List<MatchLocalRecord> collect = new ArrayList<>();
|
||||
for (SearchFaceResultItem item : records) {
|
||||
MatchLocalRecord record = new MatchLocalRecord();
|
||||
@@ -277,7 +293,7 @@ public class TaskFaceServiceImpl implements TaskFaceService {
|
||||
if (device != null) {
|
||||
record.setDeviceName(device.getName());
|
||||
}
|
||||
record.setAccept(acceptFaceSampleIds.contains(optionalFse.get().getId()));
|
||||
record.setAccept(finalAcceptedSampleIds.contains(optionalFse.get().getId()));
|
||||
record.setFaceUrl(optionalFse.get().getFaceUrl());
|
||||
record.setShotDate(optionalFse.get().getCreateAt());
|
||||
}
|
||||
@@ -289,13 +305,14 @@ public class TaskFaceServiceImpl implements TaskFaceService {
|
||||
collect.add(record);
|
||||
}
|
||||
logEntity.setMatchLocalRecord(JacksonUtil.toJSONString(collect));
|
||||
if (acceptFaceSampleIds.isEmpty()) {
|
||||
if (finalAcceptedSampleIds.isEmpty()) {
|
||||
respVo.setFirstMatchRate(0f);
|
||||
respVo.setSampleListIds(Collections.emptyList());
|
||||
return respVo;
|
||||
}
|
||||
respVo.setFirstMatchRate(response.getFirstMatchRate());
|
||||
respVo.setSampleListIds(acceptFaceSampleIds);
|
||||
respVo.setSampleListIds(finalAcceptedSampleIds);
|
||||
respVo.setFilterTrace(filterTrace);
|
||||
return respVo;
|
||||
} catch (Exception e) {
|
||||
logEntity.setMatchRawResult("识别错误,错误为:["+e.getLocalizedMessage()+"]");
|
||||
@@ -389,158 +406,136 @@ public class TaskFaceServiceImpl implements TaskFaceService {
|
||||
*/
|
||||
@Override
|
||||
public List<Long> applySampleFilters(List<Long> acceptedSampleIds,
|
||||
List<FaceSampleEntity> allFaceSampleList,
|
||||
ScenicConfigManager scenicConfig) {
|
||||
List<FaceSampleEntity> allFaceSampleList,
|
||||
ScenicConfigManager scenicConfig) {
|
||||
SampleFilterTrace trace = applySampleFiltersWithTrace(acceptedSampleIds, allFaceSampleList, scenicConfig);
|
||||
List<Long> result = trace.getAcceptedSampleIds();
|
||||
return result == null ? Collections.emptyList() : result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleFilterTrace applySampleFiltersWithTrace(List<Long> acceptedSampleIds,
|
||||
List<FaceSampleEntity> allFaceSampleList,
|
||||
ScenicConfigManager scenicConfig) {
|
||||
SampleFilterTrace trace = new SampleFilterTrace();
|
||||
if (acceptedSampleIds == null || acceptedSampleIds.isEmpty()) {
|
||||
return acceptedSampleIds;
|
||||
trace.setAcceptedSampleIds(acceptedSampleIds == null ? Collections.emptyList() : new ArrayList<>(acceptedSampleIds));
|
||||
return trace;
|
||||
}
|
||||
if (scenicConfig == null) {
|
||||
// 没有配置,不管
|
||||
return acceptedSampleIds;
|
||||
if (allFaceSampleList == null || allFaceSampleList.isEmpty()) {
|
||||
trace.setAcceptedSampleIds(new ArrayList<>(acceptedSampleIds));
|
||||
return trace;
|
||||
}
|
||||
|
||||
// 1. 找到最高匹配的样本(按创建时间倒序排列的第一个)
|
||||
Optional<FaceSampleEntity> firstFaceSample = allFaceSampleList.stream()
|
||||
.filter(faceSample -> acceptedSampleIds.contains(faceSample.getId())).max(Comparator.comparing(FaceSampleEntity::getCreateAt));
|
||||
Map<Long, FaceSampleEntity> sampleMap = allFaceSampleList.stream()
|
||||
.collect(Collectors.toMap(FaceSampleEntity::getId, sample -> sample, (a, b) -> a));
|
||||
|
||||
if (firstFaceSample.isEmpty()) {
|
||||
log.warn("样本筛选逻辑:未找到匹配的人脸样本,acceptedIds: {}", acceptedSampleIds);
|
||||
return acceptedSampleIds;
|
||||
List<Long> workingList = acceptedSampleIds.stream()
|
||||
.filter(sampleMap::containsKey)
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
|
||||
if (workingList.isEmpty()) {
|
||||
trace.setAcceptedSampleIds(Collections.emptyList());
|
||||
return trace;
|
||||
}
|
||||
|
||||
FaceSampleEntity topMatchSample = firstFaceSample.get();
|
||||
log.debug("样本筛选逻辑:找到最高匹配样本 ID={}, 创建时间={}",
|
||||
topMatchSample.getId(), topMatchSample.getCreateAt());
|
||||
|
||||
List<Long> filteredIds = acceptedSampleIds;
|
||||
|
||||
// 2. 应用时间范围筛选(基于景区配置)
|
||||
if (scenicConfig.getInteger("tour_time", 0) > 0) {
|
||||
filteredIds = filterSampleIdsByTimeRange(filteredIds, topMatchSample, scenicConfig.getInteger("tour_time"));
|
||||
log.debug("应用时间范围筛选:游览时间限制={}分钟", scenicConfig.getInteger("tour_time"));
|
||||
Integer tourMinutes = scenicConfig != null ? scenicConfig.getInteger("tour_time") : null;
|
||||
if (tourMinutes != null && tourMinutes > 0) {
|
||||
workingList = filterSampleIdsByTimeRangeWithTrace(workingList, sampleMap, tourMinutes, trace);
|
||||
log.debug("应用时间范围筛选:游览时间限制={}分钟,过滤后数量={}", tourMinutes, workingList.size());
|
||||
} else {
|
||||
log.debug("时间范围逻辑:景区未设置游览时间限制");
|
||||
}
|
||||
|
||||
workingList = applyDevicePhotoLimitWithTrace(workingList, sampleMap, trace);
|
||||
trace.setAcceptedSampleIds(new ArrayList<>(workingList));
|
||||
|
||||
|
||||
// 3. 应用设备照片数量限制筛选
|
||||
filteredIds = applyDevicePhotoLimit(filteredIds, allFaceSampleList);
|
||||
log.debug("应用设备照片数量限制筛选完成");
|
||||
|
||||
// 4. TODO: 基于景区配置的其他筛选策略
|
||||
// 可以根据 scenicConfig 中的配置来决定是否启用特定筛选
|
||||
// 示例:未来可能的筛选策略
|
||||
// if (scenicConfig.getEnableLocationFilter() != null && scenicConfig.getEnableLocationFilter()) {
|
||||
// filteredIds = applyLocationFilter(filteredIds, allFaceSampleList, scenicConfig);
|
||||
// }
|
||||
// if (scenicConfig.getEnableQualityFilter() != null && scenicConfig.getEnableQualityFilter()) {
|
||||
// filteredIds = applyQualityFilter(filteredIds, allFaceSampleList, scenicConfig);
|
||||
// }
|
||||
// if (scenicConfig.getMaxSampleCount() != null) {
|
||||
// filteredIds = applySampleCountLimit(filteredIds, scenicConfig.getMaxSampleCount());
|
||||
// }
|
||||
|
||||
log.debug("样本筛选完成:原始数量={}, 最终数量={}",
|
||||
acceptedSampleIds.size(), filteredIds.size());
|
||||
return filteredIds;
|
||||
log.debug("样本筛选完成:原始数量={}, 最终数量={}", acceptedSampleIds.size(), workingList.size());
|
||||
return trace;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据时间范围过滤人脸样本ID
|
||||
* 基于最高匹配样本的时间,过滤出指定时间范围内的样本ID
|
||||
*/
|
||||
private List<Long> filterSampleIdsByTimeRange(List<Long> acceptedSampleIds,
|
||||
FaceSampleEntity firstMatch,
|
||||
int tourMinutes) {
|
||||
if (acceptedSampleIds == null || acceptedSampleIds.isEmpty() ||
|
||||
firstMatch == null || tourMinutes <= 0) {
|
||||
return acceptedSampleIds;
|
||||
}
|
||||
|
||||
List<FaceSampleEntity> acceptFaceSampleList = faceSampleMapper.listByIds(acceptedSampleIds);
|
||||
if (acceptFaceSampleList.isEmpty()) {
|
||||
return acceptedSampleIds;
|
||||
}
|
||||
|
||||
Date startDate = DateUtil.offsetMinute(firstMatch.getCreateAt(), -tourMinutes);
|
||||
Date endDate = DateUtil.offsetMinute(firstMatch.getCreateAt(), 1);
|
||||
|
||||
List<Long> filteredIds = acceptFaceSampleList.stream()
|
||||
.filter(faceSample -> faceSample.getCreateAt().after(startDate) &&
|
||||
faceSample.getCreateAt().before(endDate))
|
||||
.map(FaceSampleEntity::getId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
log.info("时间范围逻辑:最高匹配:{},时间范围:{}~{},原样本数:{},过滤后样本数:{}",
|
||||
firstMatch.getId(), startDate, endDate, acceptedSampleIds.size(), filteredIds.size());
|
||||
|
||||
return filteredIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备配置的limit_photo值限制每个设备的照片数量
|
||||
*
|
||||
* @param acceptedSampleIds 已接受的样本ID列表
|
||||
* @param allFaceSampleList 所有人脸样本实体列表
|
||||
* @return 应用设备照片数量限制后的样本ID列表
|
||||
*/
|
||||
private List<Long> applyDevicePhotoLimit(List<Long> acceptedSampleIds,
|
||||
List<FaceSampleEntity> allFaceSampleList) {
|
||||
private List<Long> filterSampleIdsByTimeRangeWithTrace(List<Long> acceptedSampleIds,
|
||||
Map<Long, FaceSampleEntity> sampleMap,
|
||||
int tourMinutes,
|
||||
SampleFilterTrace trace) {
|
||||
if (acceptedSampleIds == null || acceptedSampleIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
FaceSampleEntity topMatchSample = acceptedSampleIds.stream()
|
||||
.map(sampleMap::get)
|
||||
.filter(Objects::nonNull)
|
||||
.max(Comparator.comparing(FaceSampleEntity::getCreateAt))
|
||||
.orElse(null);
|
||||
if (topMatchSample == null || topMatchSample.getCreateAt() == null) {
|
||||
log.warn("样本筛选逻辑:未找到匹配的人脸样本,acceptedIds: {}", acceptedSampleIds);
|
||||
return acceptedSampleIds;
|
||||
}
|
||||
|
||||
// 获取过滤后的样本列表
|
||||
List<FaceSampleEntity> filteredSamples = allFaceSampleList.stream()
|
||||
.filter(sample -> acceptedSampleIds.contains(sample.getId()))
|
||||
.collect(Collectors.toList());
|
||||
Date startDate = DateUtil.offsetMinute(topMatchSample.getCreateAt(), -tourMinutes);
|
||||
Date endDate = DateUtil.offsetMinute(topMatchSample.getCreateAt(), 1);
|
||||
|
||||
// 按设备ID分组
|
||||
Map<Long, List<FaceSampleEntity>> samplesByDevice = filteredSamples.stream()
|
||||
.collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId));
|
||||
|
||||
List<Long> resultIds = new ArrayList<>();
|
||||
|
||||
// 处理每个设备的样本
|
||||
for (Map.Entry<Long, List<FaceSampleEntity>> entry : samplesByDevice.entrySet()) {
|
||||
Long deviceId = entry.getKey();
|
||||
List<FaceSampleEntity> deviceSamples = entry.getValue();
|
||||
|
||||
// 获取设备配置
|
||||
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(deviceId);
|
||||
Integer limitPhoto = null;
|
||||
if (deviceConfig != null) {
|
||||
limitPhoto = deviceConfig.getInteger("limit_photo");
|
||||
List<Long> result = new ArrayList<>();
|
||||
for (Long sampleId : acceptedSampleIds) {
|
||||
FaceSampleEntity sample = sampleMap.get(sampleId);
|
||||
if (sample == null || sample.getCreateAt() == null) {
|
||||
result.add(sampleId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果没有配置或配置为0,不限制
|
||||
if (limitPhoto == null || limitPhoto <= 0) {
|
||||
List<Long> deviceSampleIds = deviceSamples.stream()
|
||||
.map(FaceSampleEntity::getId)
|
||||
.collect(Collectors.toList());
|
||||
resultIds.addAll(deviceSampleIds);
|
||||
log.debug("设备照片限制:设备ID={}, 无限制,保留{}张照片",
|
||||
deviceId, deviceSampleIds.size());
|
||||
Date createAt = sample.getCreateAt();
|
||||
if (createAt.after(startDate) && createAt.before(endDate)) {
|
||||
result.add(sampleId);
|
||||
} else {
|
||||
// 取前N张
|
||||
List<FaceSampleEntity> limitedSamples = deviceSamples.stream()
|
||||
.limit(limitPhoto)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<Long> limitedIds = limitedSamples.stream()
|
||||
.map(FaceSampleEntity::getId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
resultIds.addAll(limitedIds);
|
||||
log.debug("设备照片限制:设备ID={}, 限制={}张, 原始{}张,最终{}张",
|
||||
deviceId, limitPhoto, deviceSamples.size(), limitedIds.size());
|
||||
trace.addReason(sampleId, FaceRecognitionFilterReason.OUT_OF_TIME_RANGE);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("设备照片数量限制筛选:原始样本数量={}, 筛选后数量={}",
|
||||
acceptedSampleIds.size(), resultIds.size());
|
||||
|
||||
return resultIds;
|
||||
log.info("时间范围逻辑:最高匹配:{},时间范围:{}~{},原样本数:{},过滤后样本数:{}",
|
||||
topMatchSample.getId(), startDate, endDate, acceptedSampleIds.size(), result.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<Long> applyDevicePhotoLimitWithTrace(List<Long> acceptedSampleIds,
|
||||
Map<Long, FaceSampleEntity> sampleMap,
|
||||
SampleFilterTrace trace) {
|
||||
if (acceptedSampleIds == null || acceptedSampleIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Map<Long, Integer> usedCount = new HashMap<>();
|
||||
Map<Long, Integer> limitCache = new HashMap<>();
|
||||
List<Long> result = new ArrayList<>();
|
||||
|
||||
for (Long sampleId : acceptedSampleIds) {
|
||||
FaceSampleEntity sample = sampleMap.get(sampleId);
|
||||
if (sample == null) {
|
||||
result.add(sampleId);
|
||||
continue;
|
||||
}
|
||||
Long deviceId = sample.getDeviceId();
|
||||
if (deviceId == null) {
|
||||
result.add(sampleId);
|
||||
continue;
|
||||
}
|
||||
Integer limitPhoto = limitCache.computeIfAbsent(deviceId, id -> {
|
||||
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(id);
|
||||
return deviceConfig != null ? deviceConfig.getInteger("limit_photo") : null;
|
||||
});
|
||||
if (limitPhoto == null || limitPhoto <= 0) {
|
||||
result.add(sampleId);
|
||||
continue;
|
||||
}
|
||||
int used = usedCount.getOrDefault(deviceId, 0);
|
||||
if (used < limitPhoto) {
|
||||
result.add(sampleId);
|
||||
usedCount.put(deviceId, used + 1);
|
||||
} else {
|
||||
trace.addReason(sampleId, FaceRecognitionFilterReason.DEVICE_PHOTO_LIMIT);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("设备照片数量限制筛选:原始样本数量={}, 筛选后数量={}",
|
||||
acceptedSampleIds.size(), result.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user