一、最终效果(红框为标定,支持标定多个复选框)

二、代码(通过4个点位进行绘制)
[{"x":0.296094,"y":0.509722},{"x":0.508594,"y":0.509722},{"x":0.508594,"y":0.816667},{"x":0.296094,"y":0.816667}]
package com.jeesite.modules.inspectionDayReport.service;import cn.hutool.json.ObjectMapper;
import com.alibaba.fastjson.TypeReference;
import com.jeesite.modules.config.DeviceTypeConstants;
import com.jeesite.modules.hk.entity.inspection.HkInspectionDialIndicatorParam;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.*;
import java.util.List;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;/*** 图片坐标绘制服务类* 用于在图片上绘制标定线条并返回处理后的图片路径*/
@Service
public class drawCoordinatesOnImageService {private static final Logger log = LoggerFactory.getLogger(drawCoordinatesOnImageService.class);/*** 在图片上绘制标定线条*/public String drawCoordinatesFile(String originalImgPath,List<List<Map<String, Double>>> coordinatesList,  // 多区域坐标(每个子列表为一个区域)List<String> labelsList                          // 区域标签(与坐标区域数量匹配)) throws IOException {// 1. 验证输入参数if (originalImgPath == null || originalImgPath.isEmpty()) {throw new IllegalArgumentException("原始图片路径不能为空");}if (coordinatesList == null || coordinatesList.isEmpty()) {throw new IllegalArgumentException("坐标区域列表不能为空");}// 修改2:新增标签列表校验(非空且与坐标区域数量一致)if (labelsList == null || labelsList.size() != coordinatesList.size()) {throw new IllegalArgumentException("标签列表为空或与坐标区域数量不匹配");}// 2. 读取原始图片File originalFile = new File(originalImgPath);if (!originalFile.exists()) {throw new IOException("原始图片不存在: " + originalImgPath);}BufferedImage image = ImageIO.read(originalFile);// 3. 获取图片尺寸并创建绘图上下文int imageWidth = image.getWidth();int imageHeight = image.getHeight();Graphics2D g2d = image.createGraphics();g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿// 4. 设置绘制参数(线条颜色、宽度、文字样式)g2d.setColor(Color.RED);g2d.setStroke(new BasicStroke(3.0f));// 修改3:设置文字样式(支持中文、大小20px)g2d.setFont(new Font("SimHei", Font.PLAIN, 20));  // 中文显示字体g2d.setColor(Color.RED);  // 文字颜色与线条一致// 5. 循环绘制每个区域及标签(修改为多区域循环)for (int i = 0; i < coordinatesList.size(); i++) {List<Map<String, Double>> coordinates = coordinatesList.get(i);  // 当前区域坐标String label = labelsList.get(i);  // 当前区域标签if (coordinates.size() == 1) {// ====== 补充:单个点绘制圆形的核心变量定义 ======Map<String, Double> centerPoint = coordinates.get(0);  // 获取圆心相对坐标// 计算圆心像素坐标(相对坐标转像素坐标)int centerX = (int) (centerPoint.get("x") * imageWidth);int centerY = (int) (centerPoint.get("y") * imageHeight);// 计算半径(相对半径取图片宽度的5%)double relativeRadius = 0.05;  // 可根据需求调整int pixelRadius = (int) (relativeRadius * imageWidth);  // 像素半径// 绘制圆形g2d.drawOval(centerX - pixelRadius, centerY - pixelRadius, 1 * pixelRadius, 1 * pixelRadius);// ====== 文字坐标计算(原逻辑保留)======int textX = centerX - pixelRadius;  // 文字X坐标(圆形左对齐)int textY = centerY - pixelRadius - 10;  // 文字Y坐标(圆形上方10px)g2d.drawString(label, textX, textY);}  else {// ... 现有多边形绘制逻辑 ...for (int j = 0; j < coordinates.size(); j++) {Map<String, Double> currentPoint = coordinates.get(j);Map<String, Double> nextPoint = coordinates.get((j + 1) % coordinates.size());int x1 = (int) (currentPoint.get("x") * imageWidth);int y1 = (int) (currentPoint.get("y") * imageHeight);int x2 = (int) (nextPoint.get("x") * imageWidth);int y2 = (int) (nextPoint.get("y") * imageHeight);g2d.drawLine(x1, y1, x2, y2);}// ====== 新增:在多边形左上角绘制文字 ======Map<String, Double> firstPoint = coordinates.get(0);  // 取第一个点作为文字基准int textX = (int) (firstPoint.get("x") * imageWidth);int textY = (int) (firstPoint.get("y") * imageHeight) - 10;  // 多边形上方10像素g2d.drawString(label, textX, textY);}}// 6. 释放绘图资源g2d.dispose();// 7. 生成指定目录保存处理后的图片String outputDir = "D:\\111\\photo\\ss"; // 生产环境图片保存目录File dir = new File(outputDir);if (!dir.exists()) {dir.mkdirs(); // 自动创建多级目录(若不存在)}// 获取原始图片文件名(保留扩展名)String originalFileName = new File(originalImgPath).getName();File outputFile = new File(dir,  "marked_" + originalFileName );ImageIO.write(image, "jpg", outputFile);// 8. 返回处理后的图片路径return outputFile.getAbsolutePath();}/*** 在图片上绘制标定线条*/public  InputStream drawCoordinatesInputStream(String originalImgPath, List<HkInspectionDialIndicatorParam> hkInspectionDialIndicatorParamList) {try {// 1. 验证输入参数if (originalImgPath == null || originalImgPath.isEmpty()) {throw new IllegalArgumentException("原始图片路径不能为空");}// 2. 读取原始图片File originalFile = new File(originalImgPath);if (!originalFile.exists()) {throw new IOException("原始图片不存在: " + originalImgPath);}BufferedImage image = ImageIO.read(originalFile);// 3. 获取图片尺寸并创建绘图上下文int imageWidth = image.getWidth();int imageHeight = image.getHeight();Graphics2D g2d = image.createGraphics();g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿// 4. 设置绘制参数(红色线条,3px宽度)g2d.setColor(Color.RED);g2d.setStroke(new BasicStroke(3.0f));List<List<Map<String, Double>>> coordinatesList = new ArrayList<>();List<String> labelsList = new ArrayList<>();  // 存储每个区域的文字说明for (int i = 0; i < hkInspectionDialIndicatorParamList.size(); i++) {HkInspectionDialIndicatorParam param = hkInspectionDialIndicatorParamList.get(i);List<Map<String, Double>> dwMap = new ArrayList<>();String targetRegion =  param.getTargetRegion();JSONArray jsonArray = JSONUtil.parseArray(targetRegion);for (Object obj : jsonArray) {JSONObject jsonObj = (JSONObject) obj;Map<String, Double> coord = new HashMap<>();// 提取 x/y 坐标并转换为 Doublecoord.put("x", jsonObj.getDouble("x"));coord.put("y", jsonObj.getDouble("y"));dwMap.add(coord); // 添加到当前边框坐标列表}coordinatesList.add(dwMap);// 生成标签:使用检测类型名称(如"指针类"、"数字类")String readingType = param.getReadingType();String label = DeviceTypeConstants.getTypeName(readingType);labelsList.add(StringUtils.isNotBlank(label) ? label + (i+1) : "未知区域");  // 默认为"未知区域"}// 5. 循环绘制每个边框(新增外层循环处理多个边框)for (int i = 0; i < coordinatesList.size(); i++) {List<Map<String, Double>> coordinates = coordinatesList.get(i);String label = labelsList.get(i);  // 获取当前区域的文字说明// ====== 新增:设置文字样式(字体、颜色、大小)======g2d.setFont(new Font("SimHei", Font.PLAIN, 20));  // 支持中文的字体g2d.setColor(Color.RED);  // 文字颜色(黑色醒目)if (coordinates.size() == 1) {// 单个点:绘制圆形Map<String, Double> centerPoint = coordinates.get(0);int centerX = (int) (centerPoint.get("x") * imageWidth);int centerY = (int) (centerPoint.get("y") * imageHeight);double relativeRadius = 0.05;int pixelRadius = (int) (relativeRadius * imageWidth);g2d.drawOval(centerX - pixelRadius, centerY - pixelRadius, 1 * pixelRadius, 1 * pixelRadius);// ====== 新增:在圆形上方绘制文字 ======int textX = centerX - pixelRadius;  // 文字X坐标(与圆形左对齐)int textY = centerY - pixelRadius - 10;  // 文字Y坐标(圆形上方10像素)g2d.drawString(label, textX, textY);  // 绘制文字} else if (coordinates.size() == 2) {// 两个点:绘制圆形Map<String, Double> point1 = coordinates.get(0);Map<String, Double> point2 = coordinates.get(1);int x1 = (int) (point1.get("x") * imageWidth);int y1 = (int) (point1.get("y") * imageHeight);int x2 = (int) (point2.get("x") * imageWidth);int y2 = (int) (point2.get("y") * imageHeight);int centerX = (x1 + x2) / 2;int centerY = (y1 + y2) / 2;double distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));int radius = (int) (distance / 2);g2d.drawOval(centerX - radius, centerY - radius, 2 * radius, 2 * radius);// ====== 新增:在圆形上方绘制文字 ======int textX = centerX - radius;int textY = centerY - radius - 10;g2d.drawString(label, textX, textY);} else {// 多点:绘制多边形for (int j = 0; j < coordinates.size(); j++) {Map<String, Double> currentPoint = coordinates.get(j);Map<String, Double> nextPoint = coordinates.get((j + 1) % coordinates.size());int x1 = (int) (currentPoint.get("x") * imageWidth);int y1 = (int) (currentPoint.get("y") * imageHeight);int x2 = (int) (nextPoint.get("x") * imageWidth);int y2 = (int) (nextPoint.get("y") * imageHeight);g2d.drawLine(x1, y1, x2, y2);}// ====== 新增:在多边形左上角绘制文字 ======Map<String, Double> firstPoint = coordinates.get(0);  // 取第一个点作为文字基准int textX = (int) (firstPoint.get("x") * imageWidth);int textY = (int) (firstPoint.get("y") * imageHeight) - 10;  // 多边形上方10像素g2d.drawString(label, textX, textY);}}// 6. 释放绘图资源g2d.dispose();// 7. 将处理后的图片写入字节数组输出流(替换文件保存逻辑)ByteArrayOutputStream baos = new ByteArrayOutputStream();ImageIO.write(image, "jpg", baos);// 8. 转换为字节数组输入流并返回return new ByteArrayInputStream(baos.toByteArray());}catch (Exception e){log.error("绘图失败:" + e);}return null;}@Testpublic void test() throws IOException {List<List<Map<String, Double>>> coordinatesList  = new ArrayList<>();// 示例坐标:矩形区域(Java 8兼容版本)List<Map<String, Double>> coordinates = new ArrayList<>();// 添加第一个坐标点Map<String, Double> point1 = new HashMap<>();point1.put("x", 0.020312);point1.put("y", 0.141667);coordinates.add(point1);// 添加第二个坐标点Map<String, Double> point2 = new HashMap<>();point2.put("x", 0.167187);point2.put("y", 0.141667);coordinates.add(point2);// 添加第三个坐标点Map<String, Double> point3 = new HashMap<>();point3.put("x", 0.167187);point3.put("y", 0.390278);coordinates.add(point3);// 添加第四个坐标点Map<String, Double> point4 = new HashMap<>();point4.put("x", 0.020312);point4.put("y", 0.390278);coordinates.add(point4);coordinatesList.add( coordinates);List<String> labelsList = new ArrayList<>();labelsList.add("标定1");// 调用绘制服务获取处理后的图片路径drawCoordinatesFile("D:\\111\\photo\\snapshot_20251020143453692.jpg", coordinatesList,labelsList);System.out.println("图片处理完成!");}
}