评论

[内容安全]校验一张图片是否含有违法违规内容实战 - Java版

最近在对接内容安全图片校验这一块,遇到了一些坑,代码写完了,分享出来骗点赞

本文只讲实现代码,不讲所使用的框架

用到的部分包

  1. jsoup
  2. thumbnailator
  3. jackson-databind

图片处理

imgSecCheck 接口限制图片的尺寸不能超过 750px * 1334px 且图片大小不能超过 1M ,上传的图片不可能全部都在这个范围内,因此需要对图片的尺寸和大小做一些调整。

上传文件的步骤就不说了,Java中一般都会用 MultipartFile 类型来接收图片,使用 getInputStream 方法即可获取到输入流,直接处理这个流即可,处理完之后返回的也是 InputStream,后面上传的时候可以直接使用。

/**
 *
 * 图片工具类
 *
 * @author: Stephen
 */
public final class ImageUtil {

    /**
     *
     * 改变图片尺寸
     *
     * @param imageStream
     * @return
     * @throws Exception
     */
    public static InputStream changeSize(InputStream imageStream) {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

        BufferedImage target = null;

        int maxWidth = 1334;
        int maxHeight = 750;

        try {
            BufferedImage bufferedImage = ImageIO.read(imageStream);
            int width = bufferedImage.getWidth();
            int height = bufferedImage.getHeight();

            // 如果图片的尺寸没有超过规定的尺寸,直接进入图片大小的处理
            if (width <= maxWidth && height <= maxHeight) {
                ImageIO.write(bufferedImage, "png", byteArrayOutputStream);
                return compressSize(byteArrayOutputStream.toByteArray());
            }

            double widthScale = maxWidth * 1.0 / width;
            double heightScale = maxHeight * 1.0 / height;

            if (widthScale > heightScale) {
                widthScale = heightScale;
                maxWidth = (int) (widthScale * width);
            } else {
                heightScale = widthScale;
                maxHeight = (int) (heightScale * height);
            }
            int type = bufferedImage.getType();

            if (type == BufferedImage.TYPE_CUSTOM) {
                ColorModel colorModel = bufferedImage.getColorModel();
                WritableRaster writableRaster = colorModel.createCompatibleWritableRaster(maxWidth, maxHeight);
                boolean alphaPremultiplied = colorModel.isAlphaPremultiplied();
                target = new BufferedImage(colorModel, writableRaster, alphaPremultiplied, null);
            } else {
                target = new BufferedImage(maxWidth, maxHeight, type);
            }
            Graphics2D graphics2D = target.createGraphics();
            graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
            graphics2D.drawRenderedImage(bufferedImage, AffineTransform.getScaleInstance(widthScale, heightScale));
            graphics2D.dispose();

            ImageIO.write(target, "png", byteArrayOutputStream);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return compressSize(byteArrayOutputStream.toByteArray());
    }

    /**
     *
     * 改变图片大小
     *
     * @param imageBytes
     * @return
     */
    public static InputStream compressSize(byte[] imageBytes) {
        long maxSize = 1024 * 1024;

        // 如果图片的大小没有超过规定的大小,则不做处理
        if (imageBytes.length <= 0 || imageBytes.length <= maxSize) {
            return new ByteArrayInputStream(imageBytes);
        }

        double accuracy = getAccuracy(imageBytes.length / 1024);

        ByteArrayOutputStream byteArrayOutputStream = null;

        try {
            byteArrayOutputStream = new ByteArrayOutputStream(imageBytes.length);
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(imageBytes);

            Thumbnails.of(byteArrayInputStream)
                    .scale(accuracy)
                    .outputQuality(accuracy)
                    .toOutputStream(byteArrayOutputStream);

        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
    }

    /**
     *
     * 自动调节精度(经验数值)
     *
     * @param size
     * @return
     */
    private static double getAccuracy(long size) {
        double accuracy;
        if (size < 900) {
            accuracy = 0.85;
        } else if (size < 2047) {
            accuracy = 0.6;
        } else if (size < 3275) {
            accuracy = 0.44;
        } else {
            accuracy = 0.4;
        }
        return accuracy;
    }
}

调用接口

调用 https 接口使用的是 jsoup,通过 jackson-databind 包直接映射成 Java 实体,方便调用。

/**
 *
 * JSoup 工具类
 *
 * @author: Stephen
 */
public final class JSoupUtil {

    /**
     *
     * 获取Connection
     *
     * @param url
     * @return
     */
    private static Connection connection(String url) {
        return Jsoup.connect(url).ignoreContentType(true).timeout(86400000);
    }

    /**
     *
     * POST请求
     *
     * @param url
     * @param params
     * @param className
     * @param <T>
     * @return
     */
    public static<T> T upload(String url, Map<String, Object> params, Class<T> className) {
        try {
            Connection connection = JSoupUtil.connection(url);
            for (String key : params.keySet()) {
                Object data = params.get(key);
                if (data instanceof String) {
                    connection.data(key, String.valueOf(data));
                } else {
                    connection.data(key, "image.png", (InputStream) data);
                }
            }
            String response = connection.post().text();
            return new ObjectMapper().readValue(response, className);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

使用处理好的图片调用该方法即可完成接口请求,使用方法如下:

String url = "https://api.weixin.qq.com/wxa/img_sec_check?access_token=" + accessToken;

InputStream imageData = ImageUtil.changeSize(imageStream);

Map<String, Object> params = new HashMap<String, Object>(){{
    put("media", imageData);
}};

SecurityCheckResult securityCheckResult = JSoupUtil.upload(url, params, SecurityCheckResult.class);

SecurityCheckResult

/**
 *
 * 安全检查结果
 *
 * @author: Stephen
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public class SecurityCheckResult implements Serializable {

    // 错误码
    @JsonProperty(value = "errcode")
    private Integer errCode;

    // 错误信息
    @JsonProperty(value = "errmsg")
    private String errMsg;

    public Integer getErrCode() {
        return errCode;
    }

    public void setErrCode(Integer errCode) {
        this.errCode = errCode;
    }

    public String getErrMsg() {
        return errMsg;
    }

    public void setErrMsg(String errMsg) {
        this.errMsg = errMsg;
    }
}

瞎歪歪一下

这个接口的调用频率限制为单个 appId 调用上限为 2000 次/分钟,200,000 次/天,注意这里的单个 appId,也就是说一个 appId 一天可以调用 200,000 次,那么有10个小程序,只有一个小程序需要内容安全检查,其他九个不需要内容安全检查,不使用感觉很浪费啊。

在做 UGC 时不能完全依赖这个接口的检测结果,我这边粗略的测了下准确率大概在 80% 以上吧(仅供参考),建议大家还是要配合 mediaCheckAsync 异步接口进行二次检查,就算这两步全做完了,还是需要人工审核的。

最后一次编辑于  11-20  
点赞 5
收藏
评论

4 个评论

  • 袁述(小程序全栈开发工程师)
    袁述(小程序全栈开发工程师)
    11-20

    能直接用云开发解决的,链接:https://developers.weixin.qq.com/community/develop/article/doc/00062c5c7a8ec834dc692913156013

    11-20
    赞同
    回复 4
    • Stephen
      Stephen
      11-20
      看了下你写到,只是一个简易版,无法适用所有场景,只将图片压缩成180px * 180px的小图,完全不考虑宽高比,让图片缩小变形,这样的图片拿去检测能是真实结果?虽然180px * 180px的小图理论上不会超过1M,但也不能排除例外?再说能用Java我为什么要用云开发?
      11-20
      回复
    • 袁述(小程序全栈开发工程师)
      袁述(小程序全栈开发工程师)
      11-20回复Stephen
      嗯嗯,用自己会的就行了。这里的180*180是因为我的业务需要正方形图片。实际可按照需求动态计算比例。
      11-20
      回复
    • 子不语
      子不语
      11-20回复袁述(小程序全栈开发工程师)
      人在标题写了是JAVA版,你为什么还要执着于【云开发】的“广告”?


      而且看了你的文章以及下面的评论,你既然没有做过完整的测试而且也有人反馈有问题,凭啥说是可行的解决方案?
      11-20
      回复
    • 袁述(小程序全栈开发工程师)
      袁述(小程序全栈开发工程师)
      11-20回复子不语
      emmmmm,不论是【云开发】还是【java】。用自己熟悉的就好了。。测试确实不太完整,只测了不违法的图片成功率情况,没有去找违法违规的图片测试。 。受教了,回头去找一下测试。
      11-20
      回复
  • 鲤子
    鲤子
    11-20
    插眼
    11-20
    赞同
    回复
  • 子不语
    子不语
    11-20

    板凳

    11-20
    赞同
    回复
  • 拾忆
    拾忆
    11-20

    沙发

    11-20
    赞同
    回复