提交修改
This commit is contained in:
parent
21c6e3f820
commit
bdd3f16cd6
@ -1,41 +1,504 @@
|
|||||||
package com.heyu.api.controller.car;
|
package com.heyu.api.controller.car;
|
||||||
|
|
||||||
import com.heyu.api.baidu.handle.traffic.BLicensePlateHandle;
|
import com.heyu.api.controller.AbstractRecognizeController;
|
||||||
import com.heyu.api.baidu.request.traffic.BLicensePlateRequest;
|
import com.heyu.api.controller.BaiduOcrError;
|
||||||
import com.heyu.api.controller.BaseController;
|
|
||||||
import com.heyu.api.controller.ocr.BaiduOcrResult;
|
|
||||||
import com.heyu.api.data.annotation.EbAuthentication;
|
import com.heyu.api.data.annotation.EbAuthentication;
|
||||||
import com.heyu.api.data.annotation.NotIntercept;
|
import com.heyu.api.data.annotation.NotIntercept;
|
||||||
import com.heyu.api.data.constants.ApiConstants;
|
import com.heyu.api.data.constants.ApiConstants;
|
||||||
|
import com.heyu.api.data.utils.ImageInputUtils;
|
||||||
|
import com.heyu.api.data.utils.ImageInputUtils.ResolvedImageInput;
|
||||||
import com.heyu.api.data.utils.R;
|
import com.heyu.api.data.utils.R;
|
||||||
|
import com.heyu.api.data.utils.StringUtils;
|
||||||
import com.heyu.api.request.car.LicensePlateRecognizeRequest;
|
import com.heyu.api.request.car.LicensePlateRecognizeRequest;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import com.heyu.api.resp.car.RecognizeLicensePlateResp;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 车牌识别控制器
|
||||||
|
* <p>
|
||||||
|
* <b>接口背景</b>:用于交通运输、停车场、物流车队等场景,将照片中的车牌号码自动识别为结构化文本,
|
||||||
|
* 替代人工录入。底层对接百度智能云文字识别「车牌识别」能力,由本服务完成参数校验、请求转发与字段映射。
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* 支持识别:中国大陆机动车蓝牌、黄牌(单双行)、绿牌、大型新能源(黄绿)、领使馆车牌、警牌、武警牌(单双行)、
|
||||||
|
* 军牌(单双行)、港澳出入境车牌、农用车牌、民航车牌。
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h3>百度官方文档</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>产品文档:<a href="https://cloud.baidu.com/doc/OCR/s/ck3h7y191">车牌识别</a></li>
|
||||||
|
* <li>接口地址:{@code POST https://aip.baidubce.com/rest/2.0/ocr/v1/license_plate}</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>本服务接口</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>路径:{@code POST /car/license/plate/recognize}</li>
|
||||||
|
* <li>Content-Type:{@code application/json}({@code @RequestBody})</li>
|
||||||
|
* <li>鉴权:{@link EbAuthentication}(Tencent 鉴权头,见项目网关配置)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>请求参数({@link LicensePlateRecognizeRequest})</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>imageUrlOrBase64:HTTP(S) 链接或 Base64 字符串,服务端自动识别(支持 jpg/jpeg/png/bmp)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>响应结构({@link RecognizeLicensePlateResp})</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>plateNumber 车牌号码 / confidence 号码置信度</li>
|
||||||
|
* <li>plateType 车牌类型(小型汽车 / 新能源车 / 大型汽车 / 警车 / ...)</li>
|
||||||
|
* <li>plateTypeConfidence 类型置信度(由号码置信度推导)</li>
|
||||||
|
* <li>plateColor 车牌颜色(蓝 / 绿 / 黄 / 黄绿 / 白 / 黑)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>返回约定(重要)</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>一律 {@code R.ok()};入参校验失败、平台异常、识别结果为空等情况,将说明写入 {@code msg},
|
||||||
|
* {@code data} 可能为空或字段不全(对外不暴露「百度」字样,日志内可排查)</li>
|
||||||
|
* <li>入参校验失败时<strong>未调用百度、不计费</strong></li>
|
||||||
|
* <li>多车牌场景默认取置信度最高的"主车牌"返回</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author heyu
|
||||||
|
* @since 1.0.0
|
||||||
|
* @see LicensePlateRecognizeRequest
|
||||||
|
* @see RecognizeLicensePlateResp
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/car/license/plate")
|
@RequestMapping("/car/license/plate")
|
||||||
@NotIntercept
|
@NotIntercept
|
||||||
public class RecognizeLicensePlateController extends BaseController {
|
public class RecognizeLicensePlateController extends AbstractRecognizeController {
|
||||||
|
|
||||||
@Autowired
|
/** 百度车牌识别 API 路径,完整地址见类注释 */
|
||||||
private BLicensePlateHandle bLicensePlateHandle;
|
private static final String LICENSE_PLATE_URI = "/rest/2.0/ocr/v1/license_plate";
|
||||||
|
|
||||||
@PostMapping("/recognize")
|
/** 车牌识别无 side 概念,统一用此占位符填充上下文,便于日志格式与父类一致 */
|
||||||
|
private static final String SIDE_PLACEHOLDER = "plate";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 车牌识别
|
||||||
|
*
|
||||||
|
* @param request 车牌识别请求,字段见类注释「请求参数」
|
||||||
|
* @return {@link RecognizeLicensePlateResp}
|
||||||
|
*/
|
||||||
@EbAuthentication(tencent = ApiConstants.TENCENT_AUTH)
|
@EbAuthentication(tencent = ApiConstants.TENCENT_AUTH)
|
||||||
|
@PostMapping("/recognize")
|
||||||
public R recognize(@RequestBody LicensePlateRecognizeRequest request) {
|
public R recognize(@RequestBody LicensePlateRecognizeRequest request) {
|
||||||
return BaiduOcrResult.raw(bLicensePlateHandle.handle(toBaiduRequest(request)));
|
long start = System.currentTimeMillis();
|
||||||
|
RecognizeContext ctx = null;
|
||||||
|
try {
|
||||||
|
// ---------- 步骤①:参数校验(不调百度、不扣费) ----------
|
||||||
|
String validateError = validateRequest(request);
|
||||||
|
if (validateError != null) {
|
||||||
|
ResolvedImageInput validateImage = request != null
|
||||||
|
? ImageInputUtils.resolve(request.getImageUrlOrBase64()) : null;
|
||||||
|
log.info("车牌识别:参数检查没通过,接口仍返回成功并附带提示(还没调识别、不扣费)。{} 返回给客户:{}",
|
||||||
|
buildInputLogContext(request, validateImage),
|
||||||
|
abbreviate(validateError, 120));
|
||||||
|
return okResult(SIDE_PLACEHOLDER, validateError, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 步骤②:解析影像入参 & 构建上下文 ----------
|
||||||
|
ResolvedImageInput imageInput = ImageInputUtils.resolve(request.getImageUrlOrBase64());
|
||||||
|
ctx = new RecognizeContext(
|
||||||
|
SIDE_PLACEHOLDER,
|
||||||
|
imageInput,
|
||||||
|
buildInputLogContext(request, imageInput));
|
||||||
|
|
||||||
|
// ---------- 步骤③:组装百度 API 请求体 ----------
|
||||||
|
String content = buildRequestContent(imageInput);
|
||||||
|
if (isBlank(content)) {
|
||||||
|
log.error("车牌识别:组装请求失败,请求里没带有效图片。{}。{}",
|
||||||
|
ctx.imageInput.getType().getDesc(), ctx.inputLog);
|
||||||
|
return okResult(SIDE_PLACEHOLDER, formatHint(
|
||||||
|
"报文组装异常",
|
||||||
|
"识别请求体在序列化后未包含任何影像载荷,平台侧无法受理本次识别",
|
||||||
|
"请核对 imageUrlOrBase64 是否非空且为有效 Base64 或 HTTP(S) 链接",
|
||||||
|
"影像模式=" + ctx.imageInput.getType().name()
|
||||||
|
), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 步骤④:调用百度平台识别 ----------
|
||||||
|
Map<String, Object> platformResult = callPlatform(content, ctx);
|
||||||
|
if (platformResult == null) {
|
||||||
|
return okResult(SIDE_PLACEHOLDER, formatHint(
|
||||||
|
"服务无回执",
|
||||||
|
"识别指令已下发,但在约定时间内未收到平台可解析的 JSON 回执",
|
||||||
|
"建议间隔 3~5 秒重试;若连续失败,请记录 traceId、调用时刻并联系技术支持排查链路",
|
||||||
|
"影像模式=" + ctx.imageInput.getType().name()
|
||||||
|
), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 步骤⑤:解析平台结果 → 构建车牌响应 ----------
|
||||||
|
Map<String, Object> primary = pickPrimaryPlate(platformResult);
|
||||||
|
RecognizeLicensePlateResp data = buildPlateResp(primary);
|
||||||
|
String hint = resolvePlatformHint(platformResult, primary, ctx, data);
|
||||||
|
|
||||||
|
// ---------- 步骤⑥:日志记录 & 返回 ----------
|
||||||
|
logRecognizeResult(ctx, platformResult, data, hint, start);
|
||||||
|
return okResult(SIDE_PLACEHOLDER, hint, data);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
long cost = System.currentTimeMillis() - start;
|
||||||
|
ResolvedImageInput fallbackImage = request != null
|
||||||
|
? ImageInputUtils.resolve(request.getImageUrlOrBase64()) : null;
|
||||||
|
String mode = ctx != null ? ctx.imageInput.getType().getDesc()
|
||||||
|
: (fallbackImage != null ? fallbackImage.getType().getDesc() : "未知");
|
||||||
|
log.error("车牌识别:程序运行出错,耗时 {} ms。{}。客户传的:{}。异常:{} - {}",
|
||||||
|
cost, mode, buildInputLogContext(request, fallbackImage),
|
||||||
|
e.getClass().getSimpleName(),
|
||||||
|
e.getMessage() != null ? e.getMessage() : "无具体说明", e);
|
||||||
|
return okResult(SIDE_PLACEHOLDER, formatHint(
|
||||||
|
"运行时故障",
|
||||||
|
"服务端在处理识别流程时抛出未预期异常,识别结果不可用",
|
||||||
|
"请勿重复高频重试;请保存 traceId、异常发生时间,由技术支持结合堆栈进一步定位",
|
||||||
|
"异常类型=" + e.getClass().getSimpleName()
|
||||||
|
+ (e.getMessage() != null ? ",摘要=" + e.getMessage() : "")
|
||||||
|
), null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private BLicensePlateRequest toBaiduRequest(LicensePlateRecognizeRequest request) {
|
/**
|
||||||
BLicensePlateRequest bRequest = new BLicensePlateRequest();
|
* 子类钩子:车牌识别只有一种响应类型,无 side 区分。
|
||||||
if (request == null) {
|
*/
|
||||||
return bRequest;
|
@Override
|
||||||
|
protected Object defaultEmptyResp(String side) {
|
||||||
|
return new RecognizeLicensePlateResp();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 流程拆分方法 =====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用百度云车牌识别接口。
|
||||||
|
*
|
||||||
|
* @param content 已拼装好的 POST 请求体
|
||||||
|
* @param ctx 上下文(影像模式、入参摘要)
|
||||||
|
* @return 平台返回的 JSON 解析后的 Map;超时/鉴权/解析失败时返回 null
|
||||||
|
*/
|
||||||
|
private Map<String, Object> callPlatform(String content, RecognizeContext ctx) {
|
||||||
|
int len = content.length();
|
||||||
|
log.info("车牌识别:开始调用平台识别。{},请求大小约 {} 字节。{}",
|
||||||
|
ctx.imageInput.getType().getDesc(), len, ctx.inputLog);
|
||||||
|
Map<String, Object> result = requestBaidu(LICENSE_PLATE_URI, content);
|
||||||
|
if (result == null) {
|
||||||
|
log.error("车牌识别:平台没有返回结果(可能网络超时、鉴权失败或响应无法解析)。{},请求约 {} 字节。{}",
|
||||||
|
ctx.imageInput.getType().getDesc(), len, ctx.inputLog);
|
||||||
}
|
}
|
||||||
bRequest.setImageBase64(request.getImageBase64());
|
return result;
|
||||||
bRequest.setImageUrl(request.getImageUrl());
|
}
|
||||||
return bRequest;
|
|
||||||
|
/**
|
||||||
|
* 解析平台返回结果,判断是否需要向客户返回提示信息。
|
||||||
|
* <p>判断顺序:</p>
|
||||||
|
* <ol>
|
||||||
|
* <li>error_code 存在 → 平台业务拒绝</li>
|
||||||
|
* <li>words_result 为空 / 非数组 → 平台未识别出车牌</li>
|
||||||
|
* <li>响应对象所有字段均为空 → 字段映射失败</li>
|
||||||
|
* <li>以上均不命中 → 返回 null(识别正常)</li>
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
private String resolvePlatformHint(Map<String, Object> platformResult,
|
||||||
|
Map<String, Object> primary,
|
||||||
|
RecognizeContext ctx,
|
||||||
|
RecognizeLicensePlateResp data) {
|
||||||
|
Object errorCodeObj = platformResult.get("error_code");
|
||||||
|
if (errorCodeObj != null) {
|
||||||
|
String errorCode = String.valueOf(errorCodeObj);
|
||||||
|
Object errorMsgObj = platformResult.get("error_msg");
|
||||||
|
String errorMsg = errorMsgObj != null ? errorMsgObj.toString() : "平台未返回文字描述";
|
||||||
|
String category = BaiduOcrError.categoryOf(errorCode);
|
||||||
|
log.error("车牌识别:平台拒绝了本次识别。[{}] 错误码 {},原因:{}。{}。客户传的:{}。{}",
|
||||||
|
category, errorCode, errorMsg,
|
||||||
|
ctx.imageInput.getType().getDesc(),
|
||||||
|
ctx.inputLog, buildPlatformReceiptSummary(platformResult));
|
||||||
|
return formatHint(
|
||||||
|
category,
|
||||||
|
BaiduOcrError.reasonOf(errorCode),
|
||||||
|
BaiduOcrError.suggestionOf(errorCode, "车牌"),
|
||||||
|
"错误码=" + errorCode + ",错误描述=" + errorMsg
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (primary == null) {
|
||||||
|
log.info("车牌识别:平台返回了,但 words_result 为空或非数组(可能不是车牌、画面无车牌、或图片不清晰)。{}。客户传的:{}。{}",
|
||||||
|
ctx.imageInput.getType().getDesc(),
|
||||||
|
ctx.inputLog, buildPlatformReceiptSummary(platformResult));
|
||||||
|
return formatHint(
|
||||||
|
"结构化结果缺失",
|
||||||
|
"平台回执未包含可解析的车牌识别结果,无法进入字段映射环节",
|
||||||
|
"优先排查:① 画面是否包含完整车牌;② 车牌是否被遮挡 / 反光 / 严重倾斜;"
|
||||||
|
+ "③ 使用 URL 时确保平台抓取节点可访问且无 403/302 拦截",
|
||||||
|
"解析状态=结果集为空"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isAllStringFieldsBlank(data)) {
|
||||||
|
log.info("车牌识别:平台有返回,但 plateNumber / color 等字段一个都没识别出来。{}。客户传的:{}。平台回执:{}",
|
||||||
|
ctx.imageInput.getType().getDesc(),
|
||||||
|
ctx.inputLog, buildPlatformReceiptSummary(platformResult));
|
||||||
|
return formatHint(
|
||||||
|
"字段映射为空",
|
||||||
|
"平台回执已通过结构校验,但车牌号码、颜色等结构化字段均未命中",
|
||||||
|
"请确认上传图片为车辆照片且车牌占画面比例合理(建议占画面 1/8 以上);"
|
||||||
|
+ "推荐重拍:垂直拍摄、避开反光、保证四角完整",
|
||||||
|
"已映射字段数=0"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录本次识别的最终结果日志(含耗时、识别字段数量)。
|
||||||
|
*/
|
||||||
|
private void logRecognizeResult(RecognizeContext ctx, Map<String, Object> platformResult,
|
||||||
|
Object data, String hint, long start) {
|
||||||
|
long cost = System.currentTimeMillis() - start;
|
||||||
|
int mapped = countNonBlankStringFields(data);
|
||||||
|
if (StringUtils.isNotBlank(hint)) {
|
||||||
|
log.info("车牌识别:处理结束(接口仍返回成功,但带了提示信息)。耗时 {} ms,识别出 {} 个字段。{}。客户传的:{}。平台回执:{}。给客户的提示:{}",
|
||||||
|
cost, mapped, ctx.imageInput.getType().getDesc(),
|
||||||
|
ctx.inputLog, buildPlatformReceiptSummary(platformResult), abbreviate(hint, 120));
|
||||||
|
} else {
|
||||||
|
log.info("车牌识别:识别成功。耗时 {} ms,共识别出 {} 个字段。{}。客户传的:{}。平台回执:{}",
|
||||||
|
cost, mapped, ctx.imageInput.getType().getDesc(),
|
||||||
|
ctx.inputLog, buildPlatformReceiptSummary(platformResult));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 校验 =====================
|
||||||
|
|
||||||
|
private String validateRequest(LicensePlateRecognizeRequest request) {
|
||||||
|
if (request == null) {
|
||||||
|
log.info("车牌识别:没收到任何请求参数(表单未绑定成功),直接拒绝,未调用识别、不扣费");
|
||||||
|
return formatHint(
|
||||||
|
"入参绑定失败",
|
||||||
|
"控制器未接收到可绑定的请求对象,所有业务字段均为空",
|
||||||
|
"请确认使用 POST 提交;Content-Type 为 application/json;"
|
||||||
|
+ "字段名与接口文档一致(imageUrlOrBase64)",
|
||||||
|
"绑定结果=LicensePlateRecognizeRequest 为 null"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ImageInputUtils.ValidationResult imageValidation =
|
||||||
|
ImageInputUtils.validate(request.getImageUrlOrBase64());
|
||||||
|
if (!imageValidation.isValid()) {
|
||||||
|
log.info("车牌识别:imageUrlOrBase64 校验未通过({}),未调用识别、不扣费。原因:{}。{}",
|
||||||
|
imageValidation.getCategory(),
|
||||||
|
imageValidation.getReason(),
|
||||||
|
buildInputLogContext(request, null));
|
||||||
|
return formatHint(
|
||||||
|
imageValidation.getCategory(),
|
||||||
|
imageValidation.getReason(),
|
||||||
|
imageValidation.getSuggestion(),
|
||||||
|
imageValidation.getDetail()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 请求构造 =====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造调用百度车牌识别的 POST 请求体(application/x-www-form-urlencoded 格式)。
|
||||||
|
* <p>本接口默认走单车牌识别,不开启遮挡 / PS / 多张检测开关。</p>
|
||||||
|
*/
|
||||||
|
private String buildRequestContent(ResolvedImageInput imageInput) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (imageInput == null || StringUtils.isBlank(imageInput.getFormValue())) {
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
if (ImageInputUtils.ImageInputType.URL == imageInput.getType()) {
|
||||||
|
sb.append("&url=").append(imageInput.getFormValue());
|
||||||
|
} else {
|
||||||
|
sb.append("&image=").append(imageInput.getFormValue());
|
||||||
|
}
|
||||||
|
sb.append("&multi_detect=false");
|
||||||
|
sb.append("&multi_scale=false");
|
||||||
|
sb.append("&detect_complete=false");
|
||||||
|
sb.append("&detect_risk=false");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 平台结果映射 =====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从百度返回的 {@code words_result} 数组中挑出"主车牌"。
|
||||||
|
* <p>策略:</p>
|
||||||
|
* <ol>
|
||||||
|
* <li>无 words_result 或为空 → 返回 null</li>
|
||||||
|
* <li>只有一条 → 直接返回</li>
|
||||||
|
* <li>多条 → 取号码非空且 probability 平均值最高的一条</li>
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Map<String, Object> pickPrimaryPlate(Map<String, Object> platformResult) {
|
||||||
|
Object wordsResult = platformResult.get("words_result");
|
||||||
|
if (!(wordsResult instanceof List)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<Map<String, Object>> list = (List<Map<String, Object>>) wordsResult;
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Map<String, Object> best = null;
|
||||||
|
double bestScore = -1;
|
||||||
|
for (Map<String, Object> item : list) {
|
||||||
|
if (item == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Object number = item.get("number");
|
||||||
|
if (number == null || StringUtils.isBlank(number.toString())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double score = averageProbability(item);
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
best = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将"主车牌"映射为对外响应对象。
|
||||||
|
*/
|
||||||
|
private RecognizeLicensePlateResp buildPlateResp(Map<String, Object> primary) {
|
||||||
|
RecognizeLicensePlateResp resp = new RecognizeLicensePlateResp();
|
||||||
|
if (primary == null) {
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
Object number = primary.get("number");
|
||||||
|
if (number != null) {
|
||||||
|
resp.setPlateNumber(number.toString());
|
||||||
|
}
|
||||||
|
double avgConfidence = averageProbability(primary);
|
||||||
|
if (avgConfidence >= 0) {
|
||||||
|
resp.setConfidence((float) avgConfidence);
|
||||||
|
resp.setPlateTypeConfidence((float) avgConfidence);
|
||||||
|
}
|
||||||
|
Object color = primary.get("color");
|
||||||
|
if (color != null) {
|
||||||
|
String colorRaw = color.toString();
|
||||||
|
resp.setPlateColor(translateColor(colorRaw));
|
||||||
|
resp.setPlateType(inferPlateType(colorRaw, resp.getPlateNumber()));
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把百度返回的颜色英文标识翻译为中文(对外不暴露英文 enum)。
|
||||||
|
*/
|
||||||
|
private String translateColor(String colorRaw) {
|
||||||
|
if (StringUtils.isBlank(colorRaw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Map<String, String> map = colorTranslationMap();
|
||||||
|
String hit = map.get(colorRaw.trim().toLowerCase());
|
||||||
|
return hit != null ? hit : colorRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于颜色 + 号码长度推断车牌类型,对齐阿里云 RecognizeLicensePlate 规范。
|
||||||
|
*/
|
||||||
|
private String inferPlateType(String colorRaw, String plateNumber) {
|
||||||
|
if (StringUtils.isBlank(colorRaw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String c = colorRaw.trim().toLowerCase();
|
||||||
|
switch (c) {
|
||||||
|
case "blue":
|
||||||
|
return "小型汽车";
|
||||||
|
case "yellow":
|
||||||
|
return "大型汽车";
|
||||||
|
case "green":
|
||||||
|
case "gradual_green":
|
||||||
|
return "新能源车";
|
||||||
|
case "yellow_green":
|
||||||
|
return "大型新能源车";
|
||||||
|
case "white":
|
||||||
|
if (plateNumber != null && plateNumber.contains("警")) {
|
||||||
|
return "警车";
|
||||||
|
}
|
||||||
|
if (plateNumber != null && (plateNumber.startsWith("WJ") || plateNumber.contains("WJ"))) {
|
||||||
|
return "武警车";
|
||||||
|
}
|
||||||
|
return "军车";
|
||||||
|
case "black":
|
||||||
|
if (plateNumber != null && (plateNumber.startsWith("使") || plateNumber.startsWith("领"))) {
|
||||||
|
return "使领馆车";
|
||||||
|
}
|
||||||
|
return "港澳车";
|
||||||
|
default:
|
||||||
|
return "其他";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> colorTranslationMap() {
|
||||||
|
Map<String, String> m = new HashMap<>();
|
||||||
|
m.put("blue", "蓝");
|
||||||
|
m.put("yellow", "黄");
|
||||||
|
m.put("green", "绿");
|
||||||
|
m.put("gradual_green", "渐变绿");
|
||||||
|
m.put("yellow_green", "黄绿");
|
||||||
|
m.put("white", "白");
|
||||||
|
m.put("black", "黑");
|
||||||
|
return Collections.unmodifiableMap(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算百度 {@code probability} 数组的平均值;数组缺失或非数字时返回 -1。
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private double averageProbability(Map<String, Object> item) {
|
||||||
|
Object probObj = item.get("probability");
|
||||||
|
if (!(probObj instanceof List)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
List<Object> list = (List<Object>) probObj;
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
double sum = 0;
|
||||||
|
int count = 0;
|
||||||
|
for (Object o : list) {
|
||||||
|
if (o instanceof Number) {
|
||||||
|
sum += ((Number) o).doubleValue();
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count == 0 ? -1 : sum / count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 私有工具 =====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化为统一的提示文案。
|
||||||
|
*/
|
||||||
|
private String formatHint(String category, String reason, String suggestion, String detail) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("【车牌识别·").append(category).append("】").append(reason);
|
||||||
|
if (StringUtils.isNotBlank(detail)) {
|
||||||
|
sb.append("|定位信息:").append(detail);
|
||||||
|
}
|
||||||
|
sb.append("|处置指引:").append(suggestion);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 入参日志说明(不打印影像正文,只说类型与长度) */
|
||||||
|
private String buildInputLogContext(LicensePlateRecognizeRequest request, ResolvedImageInput imageInput) {
|
||||||
|
if (request == null) {
|
||||||
|
return "未收到请求体";
|
||||||
|
}
|
||||||
|
String modeDesc = imageInput != null ? imageInput.getType().getDesc() : "影像未解析";
|
||||||
|
int rawLen = imageInput != null
|
||||||
|
? textLength(imageInput.getRawValue())
|
||||||
|
: textLength(request.getImageUrlOrBase64());
|
||||||
|
return String.format("%s,影像原始长度 %d 字符", modeDesc, rawLen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,53 +1,46 @@
|
|||||||
package com.heyu.api.resp.car;
|
package com.heyu.api.resp.car;
|
||||||
|
|
||||||
|
|
||||||
import com.heyu.api.baidu.response.convert.BTextreviewQueryResp;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/***
|
/**
|
||||||
* https://next.api.aliyun.com/api/ocr/2019-12-30/RecognizeLicensePlate?useCommon=true&tab=DOC&lang=JAVA&sdkStyle=dara
|
* 车牌识别响应
|
||||||
*
|
*
|
||||||
* 车牌识别
|
* 字段对齐阿里云 RecognizeLicensePlate 规范:
|
||||||
|
* https://next.api.aliyun.com/api/ocr/2019-12-30/RecognizeLicensePlate
|
||||||
*
|
*
|
||||||
* RecognizeLicensePlate
|
* 仅返回画面中"主车牌"(百度多车牌场景下默认取第一个,按置信度降序)。
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class RecognizeLicensePlateResp {
|
public class RecognizeLicensePlateResp {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 车牌号码。
|
||||||
|
* 示例值:粤BP57E7
|
||||||
|
*/
|
||||||
|
private String plateNumber;
|
||||||
|
|
||||||
/***
|
/**
|
||||||
|
* 车牌号码的置信度,取值范围 0~1。
|
||||||
|
* 示例值:0.997
|
||||||
|
*/
|
||||||
|
private Float confidence;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 车牌类型。包括:小型汽车、新能源车、大型汽车、挂车、教练车、警车、军车、使领馆车、港澳车。
|
||||||
|
* 示例值:小型汽车
|
||||||
|
*/
|
||||||
|
private String plateType;
|
||||||
|
|
||||||
|
/**
|
||||||
* 车牌类型的置信度,取值范围 0~1。
|
* 车牌类型的置信度,取值范围 0~1。
|
||||||
*
|
* 示例值:0.95
|
||||||
* 示例值:
|
|
||||||
* 1
|
|
||||||
*/
|
*/
|
||||||
private Float plateTypeConfidence;
|
private Float plateTypeConfidence;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 车牌类型。包括:小型汽车、新能源车、大型汽车、挂车、教练车、警车、军车、使领馆车、港澳车。
|
* 车牌颜色(蓝 / 绿 / 黄 / 黄绿 / 白 / 黑)。
|
||||||
*
|
* 示例值:蓝
|
||||||
* 示例值:
|
|
||||||
* 小型汽车
|
|
||||||
*/
|
*/
|
||||||
private String plateType;
|
private String plateColor;
|
||||||
|
|
||||||
|
|
||||||
/***
|
|
||||||
* 车牌号码的置信度,取值范围 0~1。
|
|
||||||
*
|
|
||||||
* 示例值:
|
|
||||||
* 0.99745339155197144
|
|
||||||
*/
|
|
||||||
private Float confidence;
|
|
||||||
|
|
||||||
|
|
||||||
/***
|
|
||||||
* 车牌号码。
|
|
||||||
*
|
|
||||||
* 示例值:
|
|
||||||
* 粤BP57E7
|
|
||||||
*/
|
|
||||||
private String plateNumber;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user