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

news/2024/9/17 5:25:05 标签: flask, 后端, spring boot, 阿里云服务器

目录

1. 项目目标

2. 功能需求

(1)图片上传功能

(2)差异检测算法

(3)后端服务

(4)前端展示

(5)阿里云服务器存储

(6)数据库记录

(7)检测提示

(8)检测时间优化

3. 项目展示

4. 数据库设计

5. 前端设计

6. Flask后端设计

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. 前端设计

    // 点击上传图片事件
    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为算法实现类,已经封装成工具类,不演示内部算法。

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)阿里云工具类

@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发送消息

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

@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

@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简化代码。

@Mapper
public interface CheckerMapper extends BaseMapper<Checker> {
}


http://www.niftyadmin.cn/n/5645810.html

相关文章

oracle数据的完整性

一.数据的完整性 向某张表插入数据、更新、删除数据之前会对数据做校验&#xff0c;目 的就是为了确保数据的正确性、一致性、最大限度 减少重复 的数据、避免脏数据&#xff0c;这就是数据完整性。以下的数据就是脏 数据&#xff1a; 如何减少数据冗余、避免脏数据&#xff1…

mysql可重复读不能解决幻读吗?

1、可重复读和幻读的概念 1.1、可重复读 可重复读是数据库的四个隔离级别之一,可重复读可以保证在一个事物之内读取到的数据永远是相同的(通过mvcc表快照实现的),哪怕这期间有其它事务对数据做了修改,也不会影响当前事务的查询。 1.2、幻读 网上有不少博客说:幻读是一个事物内…

【论文编写】利用在线工具生成表格图片的Latex语句

表格生成 在线工具网址 使用示例 首先&#xff0c;获取一个表格图片&#xff0c;如下所示。 在线工具上传。 编辑器自动生成代码。 直接放在latex编辑器中查看效果&#xff0c;基本可用&#xff0c;如下&#xff1a; 调整细节&#xff0c;添加需要用的usepackage&#xff…

LLM系列 | 38:解读阿里开源语音多模态模型Qwen2-Audio

引言 模型概述 模型架构 训练方法 性能评估 实战演示 总结 引言 金山挂月窥禅径&#xff0c;沙鸟听经恋法门。 小伙伴们好&#xff0c;我是微信公众号《小窗幽记机器学习》的小编&#xff1a;卖铁观音的小男孩&#xff0c;今天这篇小作文主要是介绍阿里巴巴的语音多模…

分享一个基于uniapp科技馆服务微信小程序 博物馆管理小程序(源码、调试、LW、开题、PPT)

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人 八年开发经验&#xff0c;擅长Java、Python、PHP、.NET、Node.js、Android、微信小程序、爬虫、大数据、机器学习等&#xff0c;大家有这一块的问题可以一起交流&…

vue原理分析(六)--研究new Vue()

今天我们来分析使用new Vue() 之前研究时&#xff0c;只是说是在创建一个实例。并没有深入进行研究 在vue的源码中找下Vue的构造函数 function Vue(options) {if (!(this instanceof Vue)) {warn$2(Vue is a constructor and should be called with the new keyword);}this.…

基于Pinia和Compute的持久化localStorage登录态管理Vuejs 源码教学

pinia Pinia是一个专为Vue3设计的状态管理库,它借鉴了Vuex的一些概念,但更加轻量灵活,使得状态管理变得更加简单直观。Pinia通过提供一种基于Vue3响应式API的状态管理机制,让我们可以更加优雅地管理应用程序的状态。 computed Vue的computed属性是一种特殊的数据属性,它…

量化投资策略与技术学习PART9:量化选股之筹码选股

筹码选股的基本思想是通过判断某只股票的筹码分布情况来判断股票未来的涨跌。根据主力持仓理论&#xff0c;如果主力资金开始收集筹码&#xff0c;则意味着在未来一段时间该股票价格出现上涨的概率比较大&#xff1b;如果主力资金开始派发筹码&#xff0c;则意味着在未来一段时…