基于SpringBoot框架和Flask的图片差异检测与展示系统

目录

[1. 项目目标](#1. 项目目标)

[2. 功能需求](#2. 功能需求)

(1)图片上传功能

(2)差异检测算法

(3)后端服务

(4)前端展示

(5)阿里云服务器存储

(6)数据库记录

(7)检测提示

(8)检测时间优化

[3. 项目展示](#3. 项目展示)

[4. 数据库设计](#4. 数据库设计)

[5. 前端设计](#5. 前端设计)

[6. Flask后端设计](#6. Flask后端设计)

[7. SpringBoot后端设计](#7. SpringBoot后端设计)

(1)阿里云工具类

(2)HTTP客户端工具类

(3)Controller

(4)Service

(5)Mapper


1. 项目目标

  • 设计并实现一个基于Web的图片差异检测与展示系统。
  • 用户可通过系统上传两张仅有几处差别的图片(template和sample),系统自动识别差异并在sample图片上用圆圈标注。
  • 利用阿里云服务器存储用户上传的图片和检测结果,实现数据的安全可靠传输与存储。

2. 功能需求

(1)图片上传功能

用户可以同时上传template和sample两张图片。

(2)差异检测算法

在Python文件中实现差异检测算法,能够准确识别图片间的不同之处。

(3)后端服务

使用Flask搭建Python后端,与SpringBoot框架相结合,处理前端请求并调用差异检测算法。

(4)前端展示

采用Vue框架搭建前端页面,实现用户友好的交互界面。

(5)阿里云服务器存储

将用户上传的图片和Python生成的检测结果保存到阿里云服务器,并返回URL给前端展示。

(6)数据库记录

数据库需记录以下信息:id、用户id、sample和template图片的URL、result图片的URL以及图片上传时间。

(7)检测提示

用户上传图片并按下检测按钮后,系统显示正在检测提示,提高用户体验。

(8)检测时间优化

确保差异检测算法具有较高的执行效率,检测时间不宜过久,以满足用户需求。

3. 项目展示

sample:

template:

前端页面:

检测动画:

结果:

如上图所示,左侧展示的是检测结果(result),而右侧展示的是模板图片(template)。在检测结果中,sample图片与template图片之间的不同之处已经被红色圆圈精确标注出来,从而清晰地指出了两者之间的差异。这意味着系统已经成功识别并圈出了sample图片相对于template图片的不同区域。

4. 数据库设计

5. 前端设计

javascript 复制代码
    // 点击上传图片事件
    submit() {
      if (this.$refs.upload1.uploadFiles.length === 1 && this.$refs.upload2.uploadFiles.length === 1) {
        this.uploadBatchImage(this.fileList1, this.fileList2);
        this.uploaded = true;
      } else {
        Message({
          message: '上传失败!请保证模板和样例同时上传',
          type: 'error',
        });
      }
    },
    
    // 上传文件
    uploadBatchImage(fileList1, fileList2) {
      const loading = this.$loading({
        lock: true,
        text: '图片上传中...',
        spinner: 'el-icon-loading',
        background: 'rgba(0, 0, 0, 0.7)'
      });
      const formData = new FormData();
      // 遍历文件列表,将每个文件添加到formData中
      fileList1.forEach((file) => {
        formData.append(`files`, file.raw, file.name); // `files`是后端期望的字段名
      });
      fileList2.forEach((file) => {
        formData.append(`files`, file.raw, file.name); // `files`是后端期望的字段名
      });

      formData.append('userId', this.userId);

      request
          .post('/checker/upload', formData,
              {
                headers: {
                  'Content-Type': 'multipart/form-data',
                }
              })
          .then(response => {
            loading.close();

            this.checkerVo.id = response.data.data.id;
            this.checkerVo.sampleUrl = response.data.data.sampleUrl;
            this.checkerVo.templateUrl = response.data.data.templateUrl;
            this.checkerVo.userId = response.data.data.userId;
            console.log(this.checkerVo);
            Message({
              message: '上传成功!',
              type: 'success',
            });
          }).catch(error => {
        Message({
          message: '上传失败!',
          type: 'error',
        });
        throw error;
      });
    },
    
    // 点击差异检测事件
    quickCheck() {
      if (this.$refs.upload1.uploadFiles.length === 1 && this.$refs.upload2.uploadFiles.length === 1 && this.uploaded) {
        this.check(2);
      } else {
        Message({
          message: '检测失败!请保证模板和样例同时上传',
          type: 'error',
        });
      }
    },

    // 差异检测
    check(status) {
      const loading = this.$loading({
        lock: true,
        text: '检测中,请稍等几分钟',
        spinner: 'el-icon-loading',
        background: 'rgba(0, 0, 0, 0.7)'
      });
      request
          //向/checker/check/{status}发送消息
          .post("/checker/check/" + status, this.checkerVo)  
          .then((res) => {
            console.log(res.data.data);
            this.resultUrl = res.data.data.resultUrl;
            this.templateUrl = res.data.data.templateUrl;
            this.resultVisible = true;
            this.hasResult = true;
            loading.close();
          })
    },

6. Flask后端设计

由于算法可能涉及商业应用,出于保密考虑,不会公开算法的具体内部实现细节。在此情况下,将算法视为一个黑盒,仅对外展示如何通过Flask框架的接口来调用这个算法。这意味着只提供接口的使用方法,而不涉及算法本身的工作原理和代码实现。

如下代码,DiffQuickCheckUtil为算法实现类,已经封装成工具类,不演示内部算法。

python 复制代码
from datetime import datetime

import cv2
from flask import Flask, request, jsonify

from utils.AliOssUtil import OSSClient
from utils.DiffQuickUtil import DiffQuickCheckUtil
from utils.DiffUtil import DiffCheckUtil
from utils.DownloadUtil import ImageDownloader

app = Flask(__name__)

@app.route('/diffQuickCheck', methods=['POST'])
def diffQuickCheck():
    # 从请求中获取参数
    data = request.get_json()
    id = data.get('id')
    user_id = data.get('user_id')
    sample_url = data.get('sample_url')
    template_url = data.get('template_url')

    downloader = ImageDownloader()
    template_image, sample_image = downloader.get_images(template_url), downloader.get_images(sample_url)

    diffQuickCheckUtil.calculate(template=template_image, sample=sample_image)

    oss_client = OSSClient(
        accessKeyId=''      # 填写你的阿里云OssId
        accessKeySecret=''  # 填写你的阿里云Oss密钥
        endpoint=''         # 填写你的地区
        bucketName=''       # 填写你的bucket名字
    )

    # 由于并发性低,使用当前时间戳作为文件名,可确保图片文件名唯一
    objectName = f'output/user_{user_id}/{datetime.now().strftime("%Y%m%d%H%M%S")}.jpg'
    localFile = './static/output/quickresult.jpg'

    try:
        # 尝试上传文件到oss
        oss_client.upload_file(objectName, localFile)
        fileLink = oss_client.generate_file_link(objectName)
        print(fileLink)

        # 如果上传成功,返回成功信息
        return jsonify({
            "code": 200,
            "msg": "success",
            "data": {
                "id": id,
                "result_url": fileLink
            }
        })

    except Exception as e:
        # 如果发生异常,打印异常信息并返回错误信息
        print(f"An error occurred: {e}")
        return jsonify({
            "code": 500,
            "msg": "Failed to upload the file to OSS.",
            "data": {
                "userId": id,
                "error": str(e)
            }
        })


if __name__ == '__main__':
    diffQuickCheckUtil = DiffQuickCheckUtil(saveName="./static/output/quickresult.jpg")
    app.run(host='0.0.0.0', port=12345)

7. SpringBoot后端设计

(1)阿里云工具类

java 复制代码
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

    /**
     * 文件上传
     *
     * @param bytes
     * @param objectName
     * @return
     */
    public String upload(byte[] bytes, String objectName) {

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            // 创建PutObject请求。
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }

        //文件访问路径规则 https://BucketName.Endpoint/ObjectName
        StringBuilder stringBuilder = new StringBuilder("https://");
        stringBuilder
                .append(bucketName)
                .append(".")
                .append(endpoint)
                .append("/")
                .append(objectName);

        log.info("文件上传到:{}", stringBuilder.toString());

        return stringBuilder.toString();
    }
}

(2)HTTP客户端工具类

HTTP客户端工具类,用于向Flask发送消息

java 复制代码
public class HttpClientUtil {

    static final int TIMEOUT_MSEC = 5 * 100000000;

    //省略其他方式发送请求

    /**
     * 发送POST方式请求 
     */
    public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException {
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";

        try {
            // 创建Http Post请求
            HttpPost httpPost = new HttpPost(url);

            if (paramMap != null) {
                //构造json格式数据
                JSONObject jsonObject = new JSONObject();
                for (Map.Entry<String, String> param : paramMap.entrySet()) {
                    jsonObject.put(param.getKey(), param.getValue());
                }
                StringEntity entity = new StringEntity(jsonObject.toString(), "utf-8");
                //设置请求编码
                entity.setContentEncoding("utf-8");
                //设置数据类型
                entity.setContentType("application/json");
                httpPost.setEntity(entity);
            }

            httpPost.setConfig(builderRequestConfig());

            // 执行http请求
            response = httpClient.execute(httpPost);

            resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                response.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return resultString;
    }

    /**
     * @return {@link RequestConfig }
     */
    private static RequestConfig builderRequestConfig() {
        return RequestConfig.custom()
                .setConnectTimeout(TIMEOUT_MSEC)
                .setConnectionRequestTimeout(TIMEOUT_MSEC)
                .setSocketTimeout(TIMEOUT_MSEC).build();
    }

}

(3)Controller

java 复制代码
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/checker")
public class CheckerController {

    private final ICheckerService checkService;

    /**
     * 上传图片到数据库
     *
     * @param uploadDTO
     * @return {@link Result }
     */
    @PostMapping("/upload")
    public Result upload(@ModelAttribute UploadDTO uploadDTO) throws IOException {
        CheckerVO checkerVO = checkService.upload(uploadDTO);
        if (checkerVO != null) {
            return Result.success(checkerVO);
        } else {
            return Result.error("上传失败");
        }
    }

    /**
     * 图片差异检测
     *
     * @param checkerVo
     * @return {@link Result }<{@link String }>
     */
    @PostMapping("/check/{status}")
    public Result<Map<String,String>> check(@RequestBody CheckerVO checkerVo, @PathVariable Integer status) throws IOException {
        String resultUrl = checkService.check(checkerVo, status);
        Map<String,String> map = new HashMap<>();
        map.put("resultUrl",resultUrl);
        map.put("templateUrl",checkerVo.getTemplateUrl());

        if (resultUrl != null) {
            return Result.success(map);
        } else {
            return Result.error("检测失败");
        }

    }

}

(4)Service

java 复制代码
@Service
@RequiredArgsConstructor
public class CheckerServiceImpl extends ServiceImpl<CheckerMapper, Checker> implements ICheckerService {

    private final CheckerMapper checkerMapper;
    private final AliOssUtil aliOssUtil;
    private final DiffAlgorithmProperties diffAlgorithmProperties;

    /**
     * 上传图片到数据库
     */
    @Override
    public CheckerVO upload(UploadDTO uploadDTO) {
        try {
            //原始文件名
            String originalFilename0 = uploadDTO.getFiles().get(0).getOriginalFilename();
            String originalFilename1 = uploadDTO.getFiles().get(1).getOriginalFilename();
            //截取原始文件名的后缀   dfdfdf.png
            String extension0 = originalFilename0.substring(originalFilename0.lastIndexOf("."));
            String extension1 = originalFilename1.substring(originalFilename1.lastIndexOf("."));
            //构造新文件名称
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
            // 获取当前日期时间并格式化
            LocalDateTime localDateTime = LocalDateTime.now();
            String now = localDateTime.format(formatter);
            Integer userId = uploadDTO.getUserId();
            String objectName0 = "input/user_" + userId + "/template_" + now + extension0;
            String objectName1 = "input/user_" + userId + "/sample_" + now + extension1;
            //文件的请求路径
            String filePath0 = aliOssUtil.upload(uploadDTO.getFiles().get(0).getBytes(), objectName0);
            String filePath1 = aliOssUtil.upload(uploadDTO.getFiles().get(1).getBytes(), objectName1);
            //构建实体类,写入数据库
            Checker checker = new Checker();
            checker.setUserId(uploadDTO.getUserId());
            checker.setSampleUrl(filePath1);
            checker.setTemplateUrl(filePath0);
            checker.setInsertTime(localDateTime);
            checkerMapper.insert(checker);
            return BeanUtil.copyProperties(checker, CheckerVO.class);
        } catch (IOException e) {
            log.error("上传失败:{}", e);
        }
        return null;
    }

    /**
     * 差异检测
     */
    @Override
    public String check(CheckerVO checkerVo, Integer status) {
        Map map = new HashMap();
        map.put("id", checkerVo.getId());
        map.put("user_id", checkerVo.getUserId());
        map.put("sample_url", checkerVo.getSampleUrl());
        map.put("template_url", checkerVo.getTemplateUrl());
        String addr;
        if(status==1){
            addr = "http://" + diffAlgorithmProperties.getIp() + ":" + diffAlgorithmProperties.getPort() + "/diffCheck";
        }else if(status==2){
            addr = "http://" + diffAlgorithmProperties.getIp() + ":" + diffAlgorithmProperties.getPort() + "/diffQuickCheck";
        }else{
            return null;
        }

        try {
            String userCoordinate = HttpClientUtil.doPost4Json(addr, map);
            JSONObject jsonObject = new JSONObject(userCoordinate);
            if (jsonObject.getInt("code") == 200) {
                //解析出resultUrl和id
                JSONObject data = jsonObject.getJSONObject("data");
                String resultUrl = data.getStr("result_url");
                Long id = data.getLong("id");
                //更新数据库
                Checker checker = new Checker();
                checker.setId(id);
                if(status==1) {
                    checker.setResultUrl(resultUrl);
                } else if (status==2) {
                    checker.setQuickResultUrl(resultUrl);
                }
                checkerMapper.updateById(checker);
                return resultUrl;
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        return null;
    }


}

(5)Mapper

采用了MyBatisPlus简化代码。

java 复制代码
@Mapper
public interface CheckerMapper extends BaseMapper<Checker> {
}
相关推荐
代码之光_198019 分钟前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi24 分钟前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
颜淡慕潇1 小时前
【K8S问题系列 |1 】Kubernetes 中 NodePort 类型的 Service 无法访问【已解决】
后端·云原生·容器·kubernetes·问题解决
戴眼镜的猴1 小时前
Spring Boot的过滤器与拦截器的区别
spring boot
尘浮生2 小时前
Java项目实战II基于Spring Boot的光影视频平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·后端·maven·intellij-idea
尚学教辅学习资料2 小时前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理
morris1313 小时前
【SpringBoot】Xss的常见攻击方式与防御手段
java·spring boot·xss·csp
monkey_meng3 小时前
【Rust中的迭代器】
开发语言·后端·rust
余衫马3 小时前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng3 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust