像CAD制图一样,使用Java绘图标注图片的瑕疵

关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言

老程序员已经做过使用Java自己编写图片验证码的项目,然而随着技术的发展,三方库的增强。滑动验证码、旋转验证码等更加方便的验证码出现,逐渐取代了传统的验证码。尤其当前12306图片验证码为了防止黄牛刷票,热搜一个接一个......

当然也不乏有很多成熟的传统验证码工具,已经很少自己去写验证码了,重复造轮子完全没有必要。验证码可能不需要了写了,但是这项画图的技术依然有他的用武之地。

我们一起来看看今天的需求!

02 契机

这几天一直忙着紧急项目,都没有时间更文。这段时间就是研究了画图这个玩意,主要用来标记图片上的瑕疵点。瑕疵点过多,标注的内容还不能重叠,真实让人想破了头。我们先看看标注的结果:

怎么来画出这样的标注图呢?我们一步步拆解。

03 步骤拆解

我们需要用到的API类:

  • BufferedImage
  • Graphics2D
  • Font
  • FontMetrics

其实说白了就是数学问题,三角函数问题。

3.1 瑕疵点花圆

主要用来标注目标点的位置,以坐标点为圆心,画一个适合半径的圆。我的图片尺寸是1600*1200,我取的半径是15。

伪代码:

java 复制代码
BufferedImage image = ImageIO.read();
Graphics2D g2d = image.createGraphics();

// 设置画笔颜色
g2d.setColor(Color.ORANGE);
// 以目标点(x,y)为圆心画一个指定半径的圆饼填充
g2d.fillOval(x - radius, y - radius, radius * 2, radius * 2);

fillOval()用来填充图形。

参数为左上角的坐标,所以这里都要减去半径才是画笔落笔的地方。宽度和高度分别指直径。

3.2 箭头辅线

剑斗分为两种:带辅助线和不带辅助线。我们以带辅助线的为例。

带辅助线的箭头如图上的瑕疵点02。箭头其实也是线,我们需要确定箭头的角度大小,而箭头的方向取决于辅助线的方向。为了计算的方便,我们设定为的辅助线的角度为45°,这样对应的X轴和Y轴的值都相等。

画辅助线,我们需要知道两个坐标即可。标记点坐标(x,y),向右上方45°延伸,位于第一象限。假如延伸的距离为x0,那么辅助线终点的位置(x+x0, y-x0)。Y轴为什么要减,是因为画布的坐标(0,0)位于左上角,Y轴朝下为正。

绘制辅助线

java 复制代码
g2d.draw(new java.awt.geom.Line2D.Double(x, y, x+x0, y-x0));

计算箭头的坐标

java 复制代码
double angle = Math.atan2(y1 - y2, x1 - x2);
double x3 = x1 - arrowSize * Math.cos(angle - Math.PI / 12);
double y3 = y1 - arrowSize * Math.sin(angle - Math.PI / 12);
double x4 = x1 - arrowSize * Math.cos(angle + Math.PI / 12);
double y4 = y1 - arrowSize * Math.sin(angle + Math.PI / 12);

这里是利用三角函数计算箭头的相对坐标。arrowSize箭头大小,而Math.PI / 12表示15°,因为Math.PI等于180°,表示箭头与辅助线的夹角。

绘制箭头

java 复制代码
// 绘制箭头
g2d.draw(new java.awt.geom.Line2D.Double(x1, y1, x3, y3));
g2d.draw(new java.awt.geom.Line2D.Double(x1, y1, x4, y4));

填充箭头

java 复制代码
// 箭头三个点的坐标
Polygon arrowHead = new Polygon();
arrowHead.addPoint((int) x1, (int) y1);
arrowHead.addPoint((int) x3, (int) y3);
arrowHead.addPoint((int) x4, (int) y4);
// 划线填充
g2d.fill(arrowHead);

3.3 底色

绘制内容的底色,防止图片颜色造成干扰。

java 复制代码
// 标注的内容增加底色
g2d.setColor(Color.GRAY);
g2d.fillRect((int)dx, (int)dy, Math.abs(contentWidth), fontHeight);

我们填充矩形,(dx, dy)依然是左上角的位置。

3.4 标注内容

标注内容我们也需要画上底线,我们以辅助线的末端作为起点(x2,y2),contentWidth为内容的宽度。

java 复制代码
g2d.draw(new java.awt.geom.Line2D.Double(x2, y2, x2 + contentWidth, y2));

g2d.setColor(Color.WHITE);
g2d.setFont(font);
g2d.drawString(content, ltx, lty);

(ltx,lty)同样为左上角的位置。

04 完整代码

这里的代码是我封装的一个小工具。里面简单的画图可以实现了,但是还缺少碰撞检测以及优化避让。先分享给大家。

java 复制代码
public class FastDraw {

    /** 图片源 */
    private BufferedImage image;
    private Font font;

    /** 画布 */
    private Graphics2D g2d;
    /** 画布/边界宽高 */
    private int imageWidth;
    private int imageHeight;

    /** 字体信息和高度 */
    private FontMetrics fontMetrics;
    private int fontHeight;

    /** 标记点半径 */
    private int radius = 15;

    public FastDraw(BufferedImage image, Font font) {
        Assert.notNull(image, "image is null");
        Assert.notNull(font, "font is null");

        this.image = image;
        this.font = font;

        this.imageWidth = image.getWidth();
        this.imageHeight = image.getHeight();

        this.g2d = image.createGraphics();
        this.fontMetrics = g2d.getFontMetrics(font);
        this.fontHeight = fontMetrics.getHeight();
    }

    /**
     * 绘制标注内容:Callout Content
     *
     * x1, y1 : 起点坐标
     * x0 : 辅助斜线X轴长度
     * content : 标注内容
     * arrowSize : 箭头大小
     * quadrant : 象限(默认第一象限:逆时针)
     *
     * 返回值: 辅助线末端坐标
     **/
    public void drawArrow(double x, double y, double x0, double arrowSize, String content, int quadrant) {
        double x1 = initBoundaryX(x);
        double y1 = initBoundaryY(y);

        // 绘制箭头的圆点
        g2d.setColor(Color.ORANGE);
        // 以损伤点为中心画一个radius为半径的圆
        g2d.fillOval((int) x1 - radius, (int) y1 - radius, radius * 2, radius * 2);

        // 绘制箭头使用白色
        g2d.setColor(Color.WHITE);
        int contentWidth = getWordWidth(content);
        // 绘制箭头直线
        double x2 = 0.0, y2 = 0.0;
        // 绘制标注
        int ltx = 0;
        switch (quadrant) {
            case 1:
                x2 = x1 + x0;
                y2 = y1 - x0;
                ltx = (int)x2;
                break;
            case 2:
                x2 = x1 - x0;
                y2 = y1 - x0;
                contentWidth = -contentWidth;
                ltx = (int)x2 + contentWidth;
                break;
            case 3:
                x2 = x1 - x0;
                y2 = y1 + x0;
                contentWidth = -contentWidth;
                ltx = (int)x2 + contentWidth;
                break;
            case 4:
                x2 = x1 + x0;
                y2 = y1 + x0;
                ltx = (int)x2;
        }
        // 左上y:内容向上字体高度的1/4
        int lty = (int) y2 - fontHeight/4;
        // 底色起点坐标
        double dx = ltx;
        double dy = (int) y2 - fontHeight;

        // 绘制直线
        g2d.draw(new Double(x1, y1, x2, y2));
        // 计算箭头角度
        double angle = Math.atan2(y1 - y2, x1 - x2);
        double x3 = x1 - arrowSize * Math.cos(angle - Math.PI / 12);
        double y3 = y1 - arrowSize * Math.sin(angle - Math.PI / 12);
        double x4 = x1 - arrowSize * Math.cos(angle + Math.PI / 12);
        double y4 = y1 - arrowSize * Math.sin(angle + Math.PI / 12);

        // 绘制箭头
        g2d.draw(new Double(x1, y1, x3, y3));
        g2d.draw(new Double(x1, y1, x4, y4));

        // 创建箭头多边形并填充
        Polygon arrowHead = new Polygon();
        arrowHead.addPoint((int) x1, (int) y1);
        arrowHead.addPoint((int) x3, (int) y3);
        arrowHead.addPoint((int) x4, (int) y4);
        g2d.fill(arrowHead);
        g2d.draw(new Double(x2, y2, x2 + contentWidth, y2));

        // 标注的内容增加底色
        g2d.setColor(Color.GRAY);
        g2d.fillRect((int)dx, (int)dy, Math.abs(contentWidth), fontHeight);

        // 绘制标注
        g2d.setColor(Color.WHITE);
        g2d.setFont(font);
        g2d.drawString(content, ltx, lty);
    }

    /***
     * 绘制平铺内容
     *
     **/
    public void drawTile(double x1, double y1, String content) {
        x1 = initBoundaryX(x1);
        y1 = initBoundaryY(y1);

        // 绘制箭头的圆点
        g2d.setColor(Color.ORANGE);
        // 以损伤点为中心画一个radius为半径的圆
        g2d.fillOval((int) x1 - radius, (int) y1 - radius, radius * 2, radius * 2);

        // 底色坐标
        int[] x, y;
        int contentWidth = getWordWidth(content);
        if (x1 + contentWidth <= imageWidth) {
            // 未超出边界,箭头方向朝左←
            // 左上
            int lux = (int)x1 + fontHeight;
            int luy = (int)y1 - fontHeight / 2;
            // 左下
            int ldx = (int)x1 + fontHeight;
            int ldy = (int)y1 + fontHeight / 2;
            // 右上
            int rux = (int)x1 + fontHeight + contentWidth;
            int ruy = (int)y1 - fontHeight / 2;
            // 右下
            int rdx = (int)x1 + fontHeight + contentWidth;
            int rdy = (int)y1 + fontHeight / 2;

            x = new int[]{(int) x1, lux, rux, rdx, ldx};
            y = new int[]{(int) y1, luy, ruy, rdy, ldy};
        }else {
            // 超出边界箭头朝右→
            // 左上
            int lux = (int)x1 - fontHeight- contentWidth;
            int luy = (int)y1 - fontHeight / 2;

            // 左上
            int ldx = (int)x1 - fontHeight - contentWidth;
            int ldy = (int)y1 + fontHeight / 2;

            // 左上
            int rux = (int)x1 - fontHeight;
            int ruy = (int)y1 - fontHeight / 2;

            // 左上
            int rdx = (int)x1 - fontHeight;
            int rdy = (int)y1 + fontHeight / 2;
            x = new int[]{lux, rux, (int) x1, rdx, ldx};
            y = new int[]{luy, ruy, (int) y1, rdy, ldy};
        }
        g2d.setColor(Color.GRAY);
        g2d.fillPolygon(x, y, 5);

        // 绘制内容
        g2d.setColor(Color.WHITE);
        g2d.setFont(font);
        g2d.drawString(content, x[4], y[4] - fontHeight/4);
    }

    /**
     * @Description: 边界检查X
     *
     * @Author: ws
     * @Date: 2025/11/19 11:20
     **/
    private double initBoundaryX(double x) {
        if (x <= 0) {
            x = radius;
        }else if (x >= imageWidth) {
            x = imageWidth -  radius;
        }
        return x;
    }

    /**
     * @Description: 边界检查Y
     *
     * @Author: ws
     * @Date: 2025/11/19 11:20
     **/
    private double initBoundaryY(double y) {
        if (y <= 0) {
            y = radius;
        }else if (y >= imageHeight) {
            y = imageHeight - radius;
        }
        return y;
    }


    /**
     * @Description: 获取文字高度
     *
     * @Author: ws
     * @Date: 2025/11/19 11:20
     **/
    public int getWordWidth(String content) {
        int width = 0;
        for (int i = 0; i < content.length(); i++) {
            width += fontMetrics.charWidth(content.charAt(i));
        }
        return width;
    }
}

大家有兴趣可以完善一下碰撞检查。

相关推荐
靠沿5 小时前
Java数据结构初阶——LinkedList
java·开发语言·数据结构
qq_12498707535 小时前
基于springboot的建筑业数据管理系统的设计与实现(源码+论文+部署+安装)
java·spring boot·后端·毕业设计
一 乐5 小时前
宠物管理|宠物共享|基于Java+vue的宠物共享管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·springboot·宠物
a crazy day5 小时前
Spring相关知识点【详细版】
java·spring·rpc
IT_陈寒5 小时前
Vite 5.0实战:10个你可能不知道的性能优化技巧与插件生态深度解析
前端·人工智能·后端
z***3355 小时前
SQL Server2022版+SSMS安装教程(保姆级)
后端·python·flask
白露与泡影5 小时前
MySQL中的12个良好SQL编写习惯
java·数据库·面试
foundbug9995 小时前
配置Spring框架以连接SQL Server数据库
java·数据库·spring
凯酱5 小时前
@JsonSerialize
java
悦悦子a啊6 小时前
项目案例作业(选做):使用文件改造已有信息系统
java·开发语言·算法