1、集成车牌识别模型,支持车牌检测与识别

2、新增 Milvus 身份验证支持
3、目标检测功能升级:可指定类别及topk
4、支持自定义线程池线程数量
This commit is contained in:
dengwenjie
2025-07-28 12:04:02 +08:00
parent 1bd74d1bb8
commit 1d45bc597d
117 changed files with 3490 additions and 437 deletions

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.smartjavaai</groupId>
<artifactId>smartjavaai-parent</artifactId>
<version>1.0.20</version>
<version>1.0.22</version>
</parent>
<name>smartjavaai-common</name>

View File

@@ -29,6 +29,11 @@ public class ModelConfig {
*/
private String batchifier;
/**
* 模型预测器池大小(默认为cpu核心数)
*/
private int predictorPoolSize;
/**
* 个性化配置(按模型类型动态解析)
*/

View File

@@ -57,6 +57,7 @@ public class R<T> {
NO_FACE_DETECTED(3, "未检测到人脸"),
PARAM_ERROR(4, "参数错误"),
INVALID_VIDEO(5, "视频无效"),
NO_OBJECT_DETECTED(6, "未检测到目标"),
Unknown(-1, "未知错误");
private final int code;

View File

@@ -3,6 +3,7 @@ package cn.smartjavaai.common.pool;
import ai.djl.inference.Predictor;
import ai.djl.repository.zoo.ZooModel;
import ai.djl.translate.Translator;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
@@ -12,6 +13,7 @@ import org.apache.commons.pool2.impl.DefaultPooledObject;
* @author dwj
* @date 2025/4/8
*/
@Slf4j
public class PredictorFactory<I, O> extends BasePooledObjectFactory<Predictor<I, O>> {
private final ZooModel<I, O> model;
@@ -21,6 +23,7 @@ public class PredictorFactory<I, O> extends BasePooledObjectFactory<Predictor<I,
@Override
public Predictor<I, O> create() {
log.debug("create predictor");
return model.newPredictor();
}
@@ -31,6 +34,7 @@ public class PredictorFactory<I, O> extends BasePooledObjectFactory<Predictor<I,
@Override
public void destroyObject(PooledObject<Predictor<I, O>> p) {
log.debug("close predictor");
p.getObject().close();
}
}

View File

@@ -8,6 +8,9 @@ import ai.djl.ndarray.NDArray;
import cn.smartjavaai.common.entity.DetectionRectangle;
import cn.smartjavaai.common.entity.DetectionResponse;
import org.opencv.core.Mat;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.imgproc.Imgproc;
import javax.imageio.ImageIO;
import java.awt.*;
@@ -364,6 +367,7 @@ public class ImageUtils {
int width = metrics.stringWidth(text) + padding * 2 - stroke / 2;
int height = metrics.getHeight() + metrics.getDescent();
int ascent = metrics.getAscent();
y = Math.max(0, y - height);
java.awt.Rectangle background = new java.awt.Rectangle(x, y, width, height);
g.fill(background);
g.setPaint(Color.WHITE);
@@ -468,6 +472,33 @@ public class ImageUtils {
return true;
}
/**
* 在图像上绘制带白色背景、黑色文字的文本
*/
public static void putTextWithBackground(Mat image, String text, org.opencv.core.Point origin, Scalar textColor, Scalar backgroundColor, int padding) {
// 默认字体
int font = Imgproc.FONT_HERSHEY_SCRIPT_SIMPLEX;
// 默认字体缩放大小
double fontScale = 1.0;
//线条粗细
int thickness = 2;
//获取文字大小
int[] baseLine = new int[1];
Size textSize = Imgproc.getTextSize(text, font, fontScale, thickness, baseLine);
int textWidth = (int) textSize.width;
int textHeight = (int) textSize.height;
//计算带padding的背景框
org.opencv.core.Point bgTopLeft = new org.opencv.core.Point(origin.x - padding, origin.y - textHeight - padding);
org.opencv.core.Point bgBottomRight = new org.opencv.core.Point(origin.x + textWidth + padding, origin.y + baseLine[0] + padding);
//绘制背景矩形
Imgproc.rectangle(image, bgTopLeft, bgBottomRight, backgroundColor, Imgproc.FILLED);
//绘制文字(黑色)
Imgproc.putText(image, text, origin, font, fontScale, textColor, thickness);
}
}

View File

@@ -0,0 +1,124 @@
package cn.smartjavaai.common.utils;
import ai.djl.modality.cv.util.NDImageUtils;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDManager;
import ai.djl.ndarray.index.NDIndex;
import ai.djl.ndarray.types.DataType;
import ai.djl.ndarray.types.Shape;
import java.util.Arrays;
/**
* 按比例缩放,剩余空间用指定颜色填充
* @author dwj
*/
public class LetterBoxUtils {
public enum PaddingPosition {
CENTER, LEFT_TOP, RIGHT_BOTTOM
}
public static class ResizeResult {
public NDArray image;
public float r;
public int left;
public int top;
}
public static ResizeResult letterboxWithMeta(NDArray paddingImg, float r, int left, int top) {
// ... letterbox 逻辑不变
ResizeResult result = new ResizeResult();
result.image = paddingImg;
result.r = r;
result.left = left;
result.top = top;
return result;
}
/**
* 按比例缩放 + padding
*
* @param img 原图 NDArray HWC
* @param targetW 目标宽度
* @param targetH 目标高度
* @param padColor padding 填充颜色RGB 归一化 0-1
* @param position padding 位置CENTER / LEFT_TOP / RIGHT_BOTTOM
* @return 处理后的 NDArray
*/
public static ResizeResult letterbox(NDManager manager, NDArray img, int targetW, int targetH, float padColor, PaddingPosition position) {
long origH = img.getShape().get(0);
long origW = img.getShape().get(1);
float r = Math.min(targetW / (float) origW, targetH / (float) origH);
int newW = Math.round(origW * r);
int newH = Math.round(origH * r);
img = NDImageUtils.resize(img, newW, newH); // HWC 0~1
// NDArray paddingImg = manager
// .full(new Shape(targetW, targetH, 3), padColor, DataType.UINT8);
NDArray paddingImg = manager.zeros(new Shape(targetW, targetH, 3), DataType.FLOAT32);
paddingImg = paddingImg.add(114);
int padW = targetW - newW;
int padH = targetH - newH;
int top = 0, left = 0;
switch (position) {
case CENTER:
left = padW / 2;
top = padH / 2;
break;
case LEFT_TOP:
left = 0;
top = 0;
break;
case RIGHT_BOTTOM:
left = padW;
top = padH;
break;
}
paddingImg.set(new NDIndex(String.format("%d:%d,%d:%d", top, top + newH, left, left + newW)), img);
return letterboxWithMeta(paddingImg, r, left, top);
}
/**
* 恢复缩放后的 box
* @param boxes
* @param scaleRatio
* @param left
* @param top
* @param keypointStart
* @param keypointDim
* @return
*/
public static NDArray restoreBox(NDArray boxes, float scaleRatio, float left, float top, int keypointStart, int keypointDim) {
// 处理 bbox
NDArray x1 = boxes.get(":, 0").sub(left).div(scaleRatio);
NDArray y1 = boxes.get(":, 1").sub(top).div(scaleRatio);
NDArray x2 = boxes.get(":, 2").sub(left).div(scaleRatio);
NDArray y2 = boxes.get(":, 3").sub(top).div(scaleRatio);
boxes.set(new NDIndex(":, 0"), x1);
boxes.set(new NDIndex(":, 1"), y1);
boxes.set(new NDIndex(":, 2"), x2);
boxes.set(new NDIndex(":, 3"), y2);
if (keypointDim > 0) {
for (int i = 0; i < keypointDim; i += 2) {
int xIdx = keypointStart + i;
int yIdx = keypointStart + i + 1;
NDArray keyX = boxes.get(":, " + xIdx).sub(left).div(scaleRatio);
NDArray keyY = boxes.get(":, " + yIdx).sub(top).div(scaleRatio);
boxes.set(new NDIndex(":, " + xIdx), keyX);
boxes.set(new NDIndex(":, " + yIdx), keyY);
}
}
return boxes;
}
}

View File

@@ -0,0 +1,67 @@
package cn.smartjavaai.common.utils;
import ai.djl.ndarray.NDArray;
import java.util.ArrayList;
import java.util.List;
/**
* @author dwj
* @date 2025/7/23
*/
public class NMSUtils {
/**
* 通用 NMS 方法,输入 NDArray 形式的 boxes 和 scores返回保留的索引列表
*
* @param boxes NDArray 形状为 (N, 4),格式为 [x1, y1, x2, y2]
* @param scores NDArray 形状为 (N,) 或 (N,1),每个 box 的置信度
* @param iouThreshold IOU 阈值,超过该阈值则认为有重叠
* @return 保留框的索引列表
*/
public static int[] nms(NDArray boxes, NDArray scores, float iouThreshold) {
if (boxes.isEmpty()) {
return new int[0];
}
NDArray x1 = boxes.get(":, 0");
NDArray y1 = boxes.get(":, 1");
NDArray x2 = boxes.get(":, 2");
NDArray y2 = boxes.get(":, 3");
NDArray areas = x2.sub(x1).add(1).mul(y2.sub(y1).add(1));
// 按照置信度降序排序
NDArray order = scores.argSort().flip(0);
List<Integer> keep = new ArrayList<>();
while (order.size() > 0) {
int idx = (int)order.getLong(0);
keep.add(idx);
if (order.size() == 1) break;
NDArray currentBox = boxes.get(idx);
NDArray others = boxes.get(order);
NDArray xx1 = x1.get(order).maximum(x1.get(idx));
NDArray yy1 = y1.get(order).maximum(y1.get(idx));
NDArray xx2 = x2.get(order).minimum(x2.get(idx));
NDArray yy2 = y2.get(order).minimum(y2.get(idx));
NDArray w = xx2.sub(xx1).add(1).maximum(0);
NDArray h = yy2.sub(yy1).add(1).maximum(0);
NDArray inter = w.mul(h);
NDArray remAreas = areas.get(order);
NDArray union = remAreas.add(areas.get(idx)).sub(inter);
NDArray iou = inter.div(union);
NDArray mask = iou.lte(iouThreshold);
order = order.get(mask);
}
return keep.stream().mapToInt(i -> i).toArray();
}
}

View File

@@ -124,4 +124,20 @@ public class OpenCVUtils {
mat.put(0, 0, data);
return mat;
}
/**
* 透视变换
*
* @param src
* @param srcPoints
* @param dstPoints
* @return
*/
public static Mat perspectiveTransform(Mat src, Mat srcPoints, Mat dstPoints) {
Mat dst = src.clone();
Mat warp_mat = Imgproc.getPerspectiveTransform(srcPoints, dstPoints);
Imgproc.warpPerspective(src, dst, warp_mat, dst.size());
warp_mat.release();
return dst;
}
}

View File

@@ -0,0 +1,70 @@
package cn.smartjavaai.common.utils;
import ai.djl.modality.cv.output.Point;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
/**
* @author dwj
*/
public class PointUtils {
/**
* 对 4 个关键点进行排序,顺序为:
* 左上、右上、右下、左下
*/
public static List<Point> orderPoints(List<Point> points) {
if (points == null || points.size() != 4) {
throw new IllegalArgumentException("必须提供 4 个点");
}
// 按 X 坐标升序排列
points.sort(Comparator.comparingDouble(Point::getX));
List<Point> left = points.subList(0, 2);
List<Point> right = points.subList(2, 4);
// 左侧两点按 Y 排序:上为 tl下为 bl
Point tl = left.get(0).getY() < left.get(1).getY() ? left.get(0) : left.get(1);
Point bl = left.get(0).getY() >= left.get(1).getY() ? left.get(0) : left.get(1);
// 右侧两点按 Y 排序:上为 tr下为 br
Point tr = right.get(0).getY() < right.get(1).getY() ? right.get(0) : right.get(1);
Point br = right.get(0).getY() >= right.get(1).getY() ? right.get(0) : right.get(1);
return Arrays.asList(tl, tr, br, bl);
}
/**
* 欧式距离计算
*
* @param point1
* @param point2
* @return
*/
public static float distance(float[] point1, float[] point2) {
float disX = point1[0] - point2[0];
float disY = point1[1] - point2[1];
float dis = (float) Math.sqrt(disX * disX + disY * disY);
return dis;
}
/**
* 欧式距离计算
*
* @param point1
* @param point2
* @return
*/
public static float distance(Point point1, Point point2) {
double disX = point1.getX() - point2.getX();
double disY = point1.getY() - point2.getY();
float dis = (float) Math.sqrt(disX * disX + disY * disY);
return dis;
}
}