基于SpringBoot框架和Flask的图片差异检测与展示系统
目录
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 datetimeimport cv2
from flask import Flask, request, jsonifyfrom utils.AliOssUtil import OSSClient
from utils.DiffQuickUtil import DiffQuickCheckUtil
from utils.DiffUtil import DiffCheckUtil
from utils.DownloadUtil import ImageDownloaderapp = 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='' # 填写你的阿里云OssIdaccessKeySecret='' # 填写你的阿里云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:# 尝试上传文件到ossoss_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/ObjectNameStringBuilder 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;/*** 上传图片到数据库*/@Overridepublic CheckerVO upload(UploadDTO uploadDTO) {try {//原始文件名String originalFilename0 = uploadDTO.getFiles().get(0).getOriginalFilename();String originalFilename1 = uploadDTO.getFiles().get(1).getOriginalFilename();//截取原始文件名的后缀 dfdfdf.pngString 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;}/*** 差异检测*/@Overridepublic 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和idJSONObject 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> {
}