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

新星杯·14天创作挑战营·第17期 10w+人浏览 565人参与

01 引言

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

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

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

02 契机

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

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

03 步骤拆解

我们需要用到的API类:

  • BufferedImage
  • Graphics2D
  • Font
  • FontMetrics

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

3.1 瑕疵点花圆

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

伪代码:

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轴朝下为正。

绘制辅助线

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

计算箭头的坐标

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°,表示箭头与辅助线的夹角。

绘制箭头

// 绘制箭头
g2d.draw(new java.awt.geom.Line2D.Double(x1, y1, x3, y3));
g2d.draw(new java.awt.geom.Line2D.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);

3.3 底色

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

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

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

3.4 标注内容

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

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 完整代码

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

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;
    }
}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智_永无止境

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值