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

发布于:2024-09-18 ⋅ 阅读:(106) ⋅ 点赞:(0)

目录

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


网站公告

今日签到

点亮在社区的每一天
去签到