基于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> {
}
相关推荐
莫忘初心丶2 小时前
在 Ubuntu 22 上使用 Gunicorn 启动 Flask 应用程序
python·ubuntu·flask·gunicorn
闲猫2 小时前
go orm GORM
开发语言·后端·golang
丁卯4042 小时前
Go语言中使用viper绑定结构体和yaml文件信息时,标签的使用
服务器·后端·golang
Tirzano3 小时前
springsecurity自定义认证
spring boot·spring
bing_1586 小时前
简单工厂模式 (Simple Factory Pattern) 在Spring Boot 中的应用
spring boot·后端·简单工厂模式
天上掉下来个程小白6 小时前
案例-14.文件上传-简介
数据库·spring boot·后端·mybatis·状态模式
Asthenia04127 小时前
基于Jackson注解的JSON工具封装与Redis集成实战
后端
编程星空7 小时前
css主题色修改后会多出一个css吗?css怎么定义变量?
开发语言·后端·rust
程序员侠客行8 小时前
Spring事务原理 二
java·后端·spring
dmy8 小时前
docker 快速构建开发环境
后端·docker·容器