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