Commit 939d67f6 by 陈皓

init

parent 96d1c295
imgproc-server @ af911d55
Subproject commit af911d55e1a98d07dc4eee1bd3de61b560aba536
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.zq</groupId>
<artifactId>imgproc</artifactId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>imgproc-server</artifactId>
<version>1.0.0</version>
<name>imgproc-server</name>
<description>imgproc-server</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.4.2</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!--Spring boot Web容器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Spring boot 测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
<!-- OpenCv(Linux) -->
<!-- mvn install:install-file -Dfile=./src/main/resources/lib/opencv-460.jar -DgroupId=org.opencv -DartifactId=opencv -Dversion=4.6.0 -Dpackaging=jar -->
<dependency>
<groupId>org.opencv</groupId>
<artifactId>opencv</artifactId>
<version>4.6.0</version>
</dependency>
<!-- Webp图片格式支持 -->
<dependency>
<groupId>org.sejda.imageio</groupId>
<artifactId>webp-imageio</artifactId>
<version>0.1.6</version>
</dependency>
<!-- JPEG图片格式支持 -->
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-jpeg</artifactId>
<version>3.9.4</version>
</dependency>
<!-- TIFF图片格式支持 -->
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-tiff</artifactId>
<version>3.9.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-imaging</artifactId>
<version>1.0-alpha3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.22</version>
</dependency>
<dependency>
<groupId>org.tukaani</groupId>
<artifactId>xz</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.19</version>
</dependency>
<dependency>
<groupId>com.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId>
<version>2.18.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<!-- 使用@profiles.active@需要添加以下内容 -->
<resources>
<resource>
<directory>src/main/resources</directory>
<!--开启过滤,用指定的参数替换directory下的文件中的参数-->
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>
package com.zq.imgproc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* @author wilmiam
* @since 2022/10/13 11:11
*/
@EnableDiscoveryClient
@SpringBootApplication
public class ImgprocApplication {
public static void main(String[] args) {
SpringApplication.run(ImgprocApplication.class, args);
}
}
package com.zq.imgproc.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
/**
* @author wilmiam
* @since 2023/8/11 10:30
*/
@Slf4j
@Configuration
public class InitConfig {
@Value("${imgconfig.opencv}")
String opencvUrl;
@PostConstruct
public void asposeRegister() {
System.load(opencvUrl);
log.info(">> opencv 加载成功...");
}
}
package com.zq.imgproc.controller;
import cn.hutool.core.codec.Base64Encoder;
import cn.hutool.core.io.FileUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.zq.imgproc.server.ImgProcService;
import com.zq.imgproc.utils.*;
import com.zq.imgproc.vo.DetectionResVo3;
import com.zq.imgproc.vo.OptimizationReq;
import com.zq.imgproc.vo.OptimizationVO;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
/**
* <p>
* 图片处理API
* </p>
*
* @author chenhao
* @since 2023/3/18 9:32
*/
@io.swagger.annotations.Api(tags = "图片处理API")
@RequestMapping("/imgproc/v1")
@RestController
public class ApiController {
@Value("${imgconfig.opencv}")
String opencvUrl;
@Value("${imgconfig.deskew}")
String deskewUrl;
private final ImgProcService service;
@Autowired
public ApiController(ImgProcService service) {
this.service = service;
}
@ApiOperation("测试")
@PostMapping("/ping")
public ResultVo<?> ping() {
return ResultVo.success("测试成功");
}
@ApiOperation("图片检测")
@PostMapping ("/detection")
public ResultVo<DetectionResVo3> detection(@RequestBody OptimizationReq req) throws Exception {
AssertUtils.hasText(req.getFileContent(), "缺少文件内容");
AssertUtils.hasText(req.getFilename(), "缺少文件名");
return ResultVo.success(service.detection2(req));
}
@ApiOperation("图片校正(deskew工具)")
@PostMapping("/imageCorrection")
public ResultVo<?> imageCorrection(@RequestPart MultipartFile file) {
if (file.isEmpty()) {
return ResultVo.fail("文件不能为空");
}
String imgPath = UploadUtils.saveTempFile(file, "api");
String ext = FileUtil.extName(file.getOriginalFilename());
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
// 图片矫正
ImageCorrectionUtil.deskew(imgPath, savePath, deskewUrl, false);
return ResultVo.success(Base64Encoder.encode(FileUtil.readBytes(savePath)));
}
@ApiOperation("去黑边")
@PostMapping("/removeBlack")
public ResultVo<?> removeBlack(@RequestPart MultipartFile file) {
if (file.isEmpty()) {
return ResultVo.fail("文件不能为空");
}
System.load(opencvUrl);
String imgPath = UploadUtils.saveTempFile(file, "api");
String ext = FileUtil.extName(file.getOriginalFilename());
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
// 去黑边
RemoveBlackUtil2.remove(imgPath, savePath);
// 图片弯曲矫正
HashMap<String, Object> map = new HashMap<>();
map.put("file", FileUtil.file(savePath));
String response = HttpUtil.post("http://ddns.gxmailu.com:18888/api/correct", map);
JSONObject res = JSONUtil.parseObj(response);
if (res.get("success", Boolean.class)) {
String base64 = res.get("data", String.class);
return ResultVo.success(base64);
} else {
return ResultVo.fail(res.get("msg", String.class));
}
}
@ApiOperation("图片灰度化")
@PostMapping("/gray")
public ResultVo<?> gray(@RequestPart MultipartFile file) {
if (file.isEmpty()) {
return ResultVo.fail("文件不能为空");
}
System.load(opencvUrl);
String imgPath = UploadUtils.saveTempFile(file, "api");
String ext = FileUtil.extName(file.getOriginalFilename());
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
// 灰度化
ImageUtil.gray(imgPath, savePath);
return ResultVo.success(Base64Encoder.encode(FileUtil.readBytes(savePath)));
}
@ApiOperation("图片边缘检测")
@PostMapping("/canny")
public ResultVo<?> canny(@RequestPart MultipartFile file) {
if (file.isEmpty()) {
return ResultVo.fail("文件不能为空");
}
System.load(opencvUrl);
String imgPath = UploadUtils.saveTempFile(file, "api");
String ext = FileUtil.extName(file.getOriginalFilename());
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
// 边缘检测
ImageUtil.canny(imgPath, savePath);
return ResultVo.success(Base64Encoder.encode(FileUtil.readBytes(savePath)));
}
@ApiOperation("图片弯曲矫正")
@PostMapping("/correct")
public ResultVo<?> correct(@RequestPart MultipartFile file) {
if (file.isEmpty()) {
return ResultVo.fail("文件不能为空");
}
String imgPath = UploadUtils.saveTempFile(file, "api");
// 图片弯曲矫正
HashMap<String, Object> map = new HashMap<>();
map.put("file", FileUtil.file(imgPath));
String response = HttpUtil.post("http://ddns.gxmailu.com:18888/api/correct", map);
JSONObject res = JSONUtil.parseObj(response);
if (res.get("success", Boolean.class)) {
String base64 = res.get("data", String.class);
return ResultVo.success(base64);
} else {
return ResultVo.fail(res.get("msg", String.class));
}
}
@ApiOperation("图片优化")
@PostMapping("/imageOptimization")
public ResultVo<OptimizationVO> imageOptimization(@RequestBody OptimizationReq req) throws IOException {
AssertUtils.hasText(req.getFileContent(), "缺少文件内容");
AssertUtils.hasText(req.getFilename(), "缺少文件名");
return ResultVo.success(service.optimization(req));
}
}
package com.zq.imgproc.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.extra.servlet.ServletUtil;
import com.zq.imgproc.server.ImgProcService;
import com.zq.imgproc.utils.AssertUtils;
import com.zq.imgproc.utils.DecompressUtil;
import com.zq.imgproc.utils.ImageUtil;
import com.zq.imgproc.utils.ResultVo;
import com.zq.imgproc.vo.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* <p>
* 图片检测接口
* </p>
*
* @author chenhao
* @since 2022/10/24 14:58
*/
@Api(tags = "图片检测接口")
@RequestMapping("/imgproc")
@RestController
public class ImgProcController {
// @Resource
// RedisUtils redisUtils;
// private final double CAST = 1;
// private final double DA = 1;
// private final double CLARITY = 10;
private final ImgProcService service;
@Autowired
public ImgProcController(ImgProcService service) {
this.service = service;
}
@ApiOperation("图片检测")
@PostMapping ("/detection")
public ResultVo<DetectionResVo> detection(@RequestPart MultipartFile file) throws Exception {
AssertUtils.notNull(file, "图片不能为空");
return ResultVo.success(service.detection(file));
}
@ApiOperation("文件解压")
@PostMapping ("/decompress")
public ResultVo<?> decompress(@RequestPart MultipartFile[] file) throws Exception {
List<ImgVO> imgs = new ArrayList<>();
for (MultipartFile one : file) {
if (DecompressUtil.isCompress(one)) {
String path = DecompressUtil.decompress(one);
AssertUtils.hasText(path, "解压失败!");
imgs.addAll(DecompressUtil.loopFiles(path));
} else {
String url = ImageUtil.saveTempFile(one);
imgs.add(ImgVO.builder().fileName(one.getOriginalFilename()).url(url).build());
}
}
return ResultVo.success(imgs);
}
@ApiOperation("文件压缩(zip)")
@PostMapping("/compressZip")
public void compressZip(@RequestBody CompressReq req, HttpServletResponse response) {
String path = DecompressUtil.compress(req.getPathList());
InputStream inputStream = FileUtil.getInputStream(path);
String name = new String(req.getName().getBytes(), StandardCharsets.ISO_8859_1);
ServletUtil.write(response, inputStream,"application/octet-stream;charset=utf-8",name);
}
@ApiOperation("图片检测")
@PostMapping("/detectionByPath")
public ResultVo<?> detection(@RequestBody List<ImgVO> pathList) throws Exception {
if (pathList.isEmpty()) {
return ResultVo.success();
}
return ResultVo.success(service.detection(pathList));
}
@ApiOperation("图片校正(deskew工具)")
@PostMapping("/imageCorrection")
public ResultVo<?> imageCorrection(@RequestBody List<ImgVO> pathList) throws IOException {
if (pathList.isEmpty()) {
return ResultVo.success();
}
return ResultVo.success(service.deskew(pathList));
}
@ApiOperation("去黑边")
@PostMapping("/removeBlack")
public ResultVo<?> removeBlack(@RequestBody List<ImgVO> pathList) {
if (pathList.isEmpty()) {
return ResultVo.success();
}
return ResultVo.success(service.removeBlack(pathList));
}
// @ApiOperation("获取配置")
// @PostMapping("/getSetting")
// public ResultVo<?> getSetting() {
// double cast = Convert.toDouble(redisUtils.get("img-cast")) == null ? CAST : Convert.toDouble(redisUtils.get("img-cast"));
// double da = Convert.toDouble(redisUtils.get("img-da")) == null ? DA : Convert.toDouble(redisUtils.get("img-da"));
// double clarity = Convert.toDouble(redisUtils.get("img-clarity")) == null ? CLARITY : Convert.toDouble(redisUtils.get("img-clarity"));
// return ResultVo.success(SettingVO.builder().cast(cast).da(da).clarity(clarity).build());
// }
//
// @ApiOperation("设置配置")
// @PostMapping("/setSetting")
// public ResultVo<?> setSetting(@RequestBody SettingVO setting) {
// if (setting == null) {
// return ResultVo.fail("配置不能为空!");
// }
// redisUtils.setStr("img-cast", String.valueOf(setting.getCast()));
// redisUtils.setStr("img-da", String.valueOf(setting.getDa()));
// redisUtils.setStr("img-clarity", String.valueOf(setting.getClarity()));
// return ResultVo.success();
// }
@ApiOperation("图片旋转")
@PostMapping("/rotate")
public ResultVo<?> rotate(@RequestBody RotateReq req) {
if (req == null) {
return ResultVo.success();
}
return ResultVo.success(service.rotate(req));
}
@ApiOperation("图片灰度化")
@PostMapping("/gray")
public ResultVo<?> rotate(@RequestBody List<ImgVO> pathList) {
if (pathList.isEmpty()) {
return ResultVo.success();
}
return ResultVo.success(service.gray(pathList));
}
@ApiOperation("图片边缘检测")
@PostMapping("/canny")
public ResultVo<?> canny(@RequestBody List<ImgVO> pathList) {
if (pathList.isEmpty()) {
return ResultVo.success();
}
return ResultVo.success(service.canny(pathList));
}
@ApiOperation("图片上传")
@PostMapping("/upload")
public ResultVo<?> upload(@RequestPart MultipartFile file) {
if (file.isEmpty()) {
return ResultVo.fail("文件不能为空");
}
return ResultVo.success(ImageUtil.saveTempFile(file));
}
@ApiOperation("识别红色")
@PostMapping("/recognizeRed")
public ResultVo<Boolean> recognizeRed(@RequestBody OptimizationReq req) {
ResultVo<Boolean> resultVo;
try {
resultVo = ResultVo.success(service.recognizeRed(req));
} catch (Exception e) {
resultVo = ResultVo.success(false);
}
return resultVo;
}
@ApiOperation("去除红色")
@PostMapping("/removeRed")
public ResultVo<?> removeRed(@RequestBody OptimizationReq req) {
ResultVo<?> resultVo;
try {
resultVo = ResultVo.success(service.removeRed(req));
} catch (Exception e) {
resultVo = ResultVo.fail("处理失败");
}
return resultVo;
}
}
package com.zq.imgproc.exception;
/**
* 业务错误
*
* @author wilmiam
* @since 2021-07-09 17:58
*/
public class BusinessException extends RuntimeException {
private int code = 400;
public BusinessException(String message) {
super(message);
}
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public BusinessException(int code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
public int getCode() {
return code;
}
}
\ No newline at end of file
package com.zq.imgproc.utils;
import cn.hutool.core.util.StrUtil;
import com.zq.imgproc.exception.BusinessException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
/**
* 断言验证帮助类
*
* @author wilmiam
* @since 2018-04-03
*/
@SuppressWarnings("all")
public final class AssertUtils {
private static final String[] IMG_EXTS = {"png", "jpg", "jpeg"};
/**
* Don't let anyone instantiate this class
*/
private AssertUtils() {
}
/**
* 判断给定的文件名后缀是否为图片
*
* @param ext 文件名后缀, 不带点
* @param errMsg 错误信息
*/
public static void isImgExt(String ext, String errMsg) {
isImgExt(ext, 400, errMsg);
}
/**
* 判断给定的文件名后缀是否为图片
*
* @param ext 文件名后缀, 不带点
* @param errCode 断言失败的错误代码
* @param errMsg 错误信息
*/
public static void isImgExt(String ext, int errCode, String errMsg) {
if (StrUtil.isBlank(ext) || Arrays.stream(IMG_EXTS).noneMatch(img -> img.equalsIgnoreCase(ext))) {
throw new BusinessException(errCode, errMsg);
}
}
/**
* UnifiedExceptionHandler
* 判断一个布尔表达式, 若表达式为{@code true}则抛出指定错误信息的{@code BusinessException}.
*
* @param expression 布尔表达式
* @param message 断言失败时的错误信息
* @throws BusinessException
*/
public static void notTrue(boolean expression, String message) throws BusinessException {
notTrue(expression, 400, message);
}
/**
* 判断一个布尔表达式, 若表达式为{@code true}则抛出指定错误信息的{@code BusinessException}.
*
* @param expression 布尔表达式
* @param errCode 断言失败时的错误代码
* @param message 断言失败时的错误信息
* @throws BusinessException
*/
public static void notTrue(boolean expression, int errCode, String message) throws BusinessException {
if (expression) {
throw new BusinessException(errCode, message);
}
}
/**
* 判断一个布尔表达式, 若表达式为{@code false}则抛出指定错误信息的{@code BusinessException}.
*
* @param expression 布尔表达式
* @param message 断言失败时的错误信息
* @throws BusinessException
*/
public static void isTrue(boolean expression, String message) throws BusinessException {
isTrue(expression, 400, message);
}
/**
* 判断一个布尔表达式, 若表达式为{@code false}则抛出指定错误信息的{@code BusinessException}.
*
* @param expression 布尔表达式
* @param errCode 断言失败时的错误代码
* @param message 断言失败时的错误信息
* @throws BusinessException
*/
public static void isTrue(boolean expression, int errCode, String message) throws BusinessException {
if (!expression) {
throw new BusinessException(errCode, message);
}
}
/**
* 如果对象为{@code null}, 则抛出异常
*
* @param object 要判断的对象
* @throws BusinessException
*/
public static void notNull(Object object) throws BusinessException {
notNull(object, "不能处理空对象");
}
/**
* 如果对象为{@code null}, 则抛出异常
*
* @param object 要判断的对象
* @param message 断言失败时的错误信息
* @throws BusinessException
*/
public static void notNull(Object object, String message) throws BusinessException {
notNull(object, 400, message);
}
/**
* 如果对象为{@code null}, 则抛出异常
*
* @param object 要判断的对象
* @param errCode 断言失败时的错误代码
* @param errMsg 断言失败时的错误信息
* @throws BusinessException
*/
public static void notNull(Object object, int errCode, String errMsg) throws BusinessException {
if (object == null) {
throw new BusinessException(errCode, errMsg);
}
}
/**
* 如果字符串为{@code null}、空字符串或仅包含空白字符, 则抛出异常
*
* @param text 要进行检查的字符串
* @throws BusinessException
*/
public static void hasText(String text) throws BusinessException {
hasText(text, 400, "参数不能为空字符串");
}
/**
* 如果字符串为{@code null}、空字符串或仅包含空白字符, 则抛出异常
*
* @param text 要进行检查的字符串
* @param message 断言失败时的错误信息
* @throws BusinessException
*/
public static void hasText(String text, String message) throws BusinessException {
hasText(text, 400, message);
}
/**
* 如果字符串为{@code null}、空字符串或仅包含空白字符, 则抛出异常
*
* @param text 要进行检查的字符串
* @param errCode 断言失败时的错误代码
* @param errMsg 错误信息
* @throws BusinessException
*/
public static void hasText(String text, int errCode, String errMsg) throws BusinessException {
if (StrUtil.isBlank(text)) {
throw new BusinessException(errCode, errMsg);
}
}
/**
* 如果数组为{@code null}或长度为0, 则抛出异常
*
* @param array 要进行检查的数组
* @param message 断言失败时的错误信息
* @param <T> 数组的数据类型
* @throws BusinessException
*/
public static <T> void notEmpty(T[] array, String message) throws BusinessException {
notEmpty(array, 400, message);
}
/**
* 如果数组为{@code null}或长度为0, 则抛出异常
*
* @param array 要进行检查的数组
* @param errCode 断言失败时的错误代码
* @param errMsg 错误信息
* @param <T> 数组的数据类型
* @throws BusinessException
*/
public static <T> void notEmpty(T[] array, int errCode, String errMsg) throws BusinessException {
if (array == null || array.length == 0) {
throw new BusinessException(errCode, errMsg);
}
}
/**
* 如果数组里包含有{@code null}的元素, 则抛出异常. 注意: 若数组本身为{@code null}则不会进行处理, 直接返回
*
* @param array 要进行检查的数组
* @param message 断言失败时的错误信息
* @param <T> 数组的数据类型
* @throws BusinessException
*/
public static <T> void noNullElements(T[] array, String message) throws BusinessException {
noNullElements(array, 400, message);
}
/**
* 如果数组里包含有{@code null}的元素, 则抛出异常. 注意: 若数组本身为{@code null}则不会进行处理, 直接返回
*
* @param array 要进行检查的数组
* @param errCode 断言失败时的错误代码
* @param errMsg 错误信息
* @param <T> 数组的数据类型
* @throws BusinessException
*/
public static <T> void noNullElements(T[] array, int errCode, String errMsg) throws BusinessException {
if (array != null) {
for (T element : array) {
if (element == null) {
throw new BusinessException(errCode, errMsg);
}
}
}
}
/**
* 如果集合为{@code null},或者不包含任何元素,则抛出异常
*
* @param collection 要进行检查的集合
* @param message 断言失败时的错误信息
* @throws BusinessException
*/
public static void notEmpty(Collection<?> collection, String message) throws BusinessException {
notEmpty(collection, 400, message);
}
/**
* 如果集合为{@code null},或者不包含任何元素,则抛出异常
*
* @param collection 要进行检查的集合
* @param errCode 断言失败时的错误代码
* @param errMsg 错误信息
* @throws BusinessException
*/
public static void notEmpty(Collection<?> collection, int errCode, String errMsg) throws BusinessException {
if (collection == null || collection.isEmpty()) {
throw new BusinessException(errCode, errMsg);
}
}
/**
* 如果键值对为{@code null},或者不包含任何键值,则抛出异常
*
* @param map 要进行检查的键值对
* @param message 断言失败时的错误信息
* @throws BusinessException
*/
public static void notEmpty(Map<?, ?> map, String message) throws BusinessException {
notEmpty(map, 400, message);
}
/**
* 如果键值对为{@code null},或者不包含任何键值,则抛出异常
*
* @param map 要进行检查的键值对
* @param errCode 断言失败时的错误代码
* @param errMsg 错误信息
* @throws BusinessException
*/
public static void notEmpty(Map<?, ?> map, int errCode, String errMsg) throws BusinessException {
if (map == null || map.isEmpty()) {
throw new BusinessException(errCode, errMsg);
}
}
}
package com.zq.imgproc.utils;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ZipUtil;
import cn.hutool.extra.compress.CompressUtil;
import cn.hutool.extra.compress.extractor.Extractor;
import cn.hutool.http.HttpUtil;
import com.zq.imgproc.vo.ImgVO;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.io.FileUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
* <p>
*
* </p>
*
* @author chenhao
* @since 2023/3/7 14:29
*/
public class DecompressUtil {
private static final String[] IMG_EXTS = {"png", "jpg", "jpeg", "tif", "bmp", "heic"};
private static final String[] COMPRESS_EXTS = {"zip", "7z"};
/**
* 压缩包解压
*
* @param file 压缩包文件
* @return 保存的文件夹路径
* @throws IOException IO异常
*/
public static String decompress(MultipartFile file) throws IOException {
String ext = FileUtil.extName(file.getOriginalFilename());
if ("7z".equals(ext)) {
return decompress7z(file);
} else if ("zip".equals(ext)) {
return decompressZip(file);
} else {
return "";
}
}
/**
* 7z压缩包解压
* @param file 压缩包文件
* @return 保存的文件夹路径
* @throws IOException IO异常
*/
public static String decompress7z(MultipartFile file) throws IOException {
// 文件路径要素
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String filePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "/";
// 解压文件
InputStream inputStream = file.getInputStream();
Extractor extractor = CompressUtil.createExtractor(CharsetUtil.defaultCharset(),
ArchiveStreamFactory.SEVEN_Z,
inputStream);
extractor.extract(FileUtil.file(filePath));
extractor.close();
filePath = filePath + "/";
return filePath;
}
/**
* zip压缩包解压
* @param file 压缩包文件
* @return 保存的文件夹路径
* @throws IOException IO异常
*/
public static String decompressZip(MultipartFile file) throws IOException {
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String filePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "/";
ZipUtil.unzip(file.getInputStream(), FileUtil.file(filePath), Charset.forName("GBK"));
filePath = filePath + "/";
return filePath;
}
/**
* 文件压缩成zip
*
* @param list 图片文件
* @return zip压缩文件
*/
public static String compress(List<ImgVO> list) {
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "/";
for (ImgVO img : list) {
// 网上的文件需要下载
if (img.getUrl().startsWith("http://ddns.gxmailu.com:18888")) {
HttpUtil.downloadFile(img.getUrl(), savePath + img.getFileName());
} else {
FileUtil.copyFile(img.getUrl(), savePath + img.getFileName(), StandardCopyOption.COPY_ATTRIBUTES);
}
}
String filePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + ".zip";
ZipUtil.zip(savePath, filePath, false);
FileUtil.del(savePath);
return filePath;
}
/**
* 遍历文件夹,返回图片文件
*
* @param path 文件及路径
* @return 图片文件列表
* @throws IOException IO异常
*/
public static List<ImgVO> loopFiles(String path) throws IOException {
List<ImgVO> resList = new ArrayList<>();
List<File> fileList = FileUtil.loopFiles(path);
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "/";
int index = 1;
for (File file : fileList) {
String ext = FileUtil.extName(file);
if (StrUtil.isBlank(ext) || Arrays.stream(IMG_EXTS).noneMatch(img -> img.equalsIgnoreCase(ext))) {
continue;
}
// 反转义
String url = savePath + index + "." + ext;
File dest = new File(url);
FileUtils.writeByteArrayToFile(dest, FileUtil.readBytes(file));
index++;
resList.add(ImgVO.builder().fileName(file.getName()).url(url).build());
}
return resList;
}
/**
* 文件类型判断
*
* @param file 压缩包文件
* @return 类型正确返回true
*/
public static boolean isCompress(MultipartFile file) {
String ext = FileUtil.extName(file.getOriginalFilename());
return Arrays.stream(COMPRESS_EXTS).anyMatch(suffix -> suffix.equalsIgnoreCase(ext));
}
}
package com.zq.imgproc.utils;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.zq.imgproc.vo.DetectionResVo3;
import com.zq.imgproc.vo.OptimizationReq;
import com.zq.imgproc.vo.OptimizationVO;
public class DemoUtil {
final static String URL = "localhost:8900/imgproc/v1/";
public static void main(String[] args) {
detection();
// String testImg = "C:/Users/11419/Desktop/Deskew/TestImages/4.png";
// String resImg = "C:/Users/11419/Desktop/Deskew/TestImages/res4.jpg";
// System.out.println(Convert.toInt(-6.25));
// ImgUtil.rotate(FileUtil.file(testImg), Convert.toInt(-6.25), FileUtil.file(resImg));
}
public static void detection() {
String testImg = "C:/Users/11419/Desktop/Deskew/TestImages/3.png";
String resImg = "C:/Users/11419/Desktop/Deskew/TestImages/res.jpg";
String url = URL + "detection";
OptimizationReq req = new OptimizationReq();
req.setFilename("3.png");
req.setFileContent(Base64.encode(FileUtil.file(testImg)));
long start = System.currentTimeMillis();
HttpRequest request = HttpUtil.createPost(url)
.body(JSONUtil.toJsonStr(req));
try (HttpResponse response = request.execute()) {
System.out.println(System.currentTimeMillis() - start);
String res = response.body();
JSONObject object = JSONUtil.parseObj(res);
DetectionResVo3 detectionRes = object.get("data", DetectionResVo3.class);
System.out.println(JSONUtil.toJsonStr(detectionRes));
}
}
public static void imageOptimization() {
String testImg = "C:/Users/11419/Desktop/Deskew/TestImages/F1550.jpg";
String resImg = "C:/Users/11419/Desktop/Deskew/TestImages/res.jpg";
String url = URL + "imageOptimization";
OptimizationReq req = new OptimizationReq();
req.setFilename("4.png");
req.setFileContent(Base64.encode(FileUtil.file(testImg)));
long start = System.currentTimeMillis();
HttpRequest request = HttpUtil.createPost(url)
.body(JSONUtil.toJsonStr(req));
try (HttpResponse response = request.execute()) {
System.out.println(System.currentTimeMillis()- start);
String res = response.body();
JSONObject object = JSONUtil.parseObj(res);
OptimizationVO optimizationVO = object.get("data", OptimizationVO.class);
FileUtil.writeBytes(Base64.decode(optimizationVO.getFileContent()), FileUtil.file(resImg));
optimizationVO.setFileContent(null);
System.out.println(JSONUtil.toJsonStr(optimizationVO));
}
}
}
package com.zq.imgproc.utils;
import cn.hutool.core.convert.Convert;
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Deskew {
public static void main(String[] args) {
System.load("C:/Users/11419/Desktop/lib/opencv_java460.dll");
Mat image = Imgcodecs.imread("C:/Users/11419/Desktop/Deskew/TestImages/4.png");
int angle = getDeskewAngle(image);
System.out.println(angle);
Mat roateMat = rotate(image, angle);
Imgcodecs.imwrite("C:/Users/11419/Desktop/Deskew/TestImages/4res.png", roateMat);
}
public static Integer getDeskewAngle(Mat src) {
// 图片灰度化
Mat gray = src.clone();
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
// // 高斯模糊(?)
// @Cleanup
// UMat blur = gray.clone();
// GaussianBlur(gray, blur, new Size(9, 9), 0);
// // 二值化(?)
// @Cleanup
// UMat thresh = blur.clone();
// threshold(blur, thresh, 0, 255, THRESH_BINARY_INV + THRESH_OTSU);
Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(5, 5));
// 图片膨胀
Mat erode = gray.clone();
Imgproc.erode(gray, erode, kernel);
// 图片腐蚀
Mat dilate = erode.clone();
Imgproc.dilate(erode, dilate, kernel);
// 边缘检测
Mat canny = dilate.clone();
Imgproc.Canny(dilate, canny, 50, 150);
// 霍夫变换得到线条
Mat lines = new Mat();
//累加器阈值参数,小于设置值不返回
int threshold = 90;
//最低线段长度,低于设置值则不返回
double minLineLength = 100;
//间距小于该值的线当成同一条线
double maxLineGap = 10;
// 霍夫变换,通过步长为1,角度为PI/180来搜索可能的直线
Imgproc.HoughLinesP(canny, lines, 1, Math.PI / 180, threshold, minLineLength, maxLineGap);
// 计算倾斜角度
List<Integer> angelList = new ArrayList<>();
for (int i = 0; i < lines.rows(); i++) {
double[] line = lines.get(i, 0);
int k = calculateAngle(line[0], line[1], line[2], line[3]);
angelList.add(k);
}
if (angelList.isEmpty()) {
return 0;
}
gray.release();
kernel.release();
erode.release();
dilate.release();
canny.release();
lines.release();
// 可能还得需要考虑方差来决定选择平均数还是众数
return most(angelList);
}
/**
* 求数组众数
*
* @param angelList 数组
* @return 数组众数
*/
private static int most(List<Integer> angelList) {
if (angelList.isEmpty()) {
return 0;
}
int res = 0;
int max = Integer.MIN_VALUE;
Map<Integer, Integer> map = new HashMap<>();
for (int i : angelList) {
map.put(i, map.getOrDefault(i, 0) + 1);
}
for (Integer i : map.keySet()) {
int count = map.get(i);
if (count > max) {
max = count;
res = i;
}
}
return res;
}
/**
* 计算直线的倾斜角
*/
private static int calculateAngle(double x1, double y1, double x2, double y2) {
if (Math.abs(x2 - x1) < 1e-4) {
return 90;
} else if (Math.abs(y2 - y1) < 1e-4) {
return 0;
} else {
double k = -(y2 - y1) / (x2 - x1);
double res = Math.atan(k) * 57.29577;
return Convert.toInt(Math.round(res));
}
}
/**
* 图片旋转
*
* @param image 输入图片
* @param angle 旋转角度
* @return 输出图片
*/
public static Mat rotate(Mat image, double angle) {
int w = image.cols();
int h = image.rows();
// 定义参数
double scale = 1.0;
Point center = new Point((double) w / 2, (double) h / 2);
// 定义旋转矩阵
Mat rotationMatrix = Imgproc.getRotationMatrix2D(center, -angle, scale);
// 旋转矩阵
Mat rotatedImage = new Mat();
Scalar backgroundColor = new Scalar(255, 255, 255);
Imgproc.warpAffine(image, rotatedImage, rotationMatrix, new Size(w, h), Imgproc.INTER_LINEAR, Core.BORDER_CONSTANT, backgroundColor);
return rotatedImage;
}
}
package com.zq.imgproc.utils;
import cn.hutool.core.util.RuntimeUtil;
import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
/**
* <p>
* 纠偏
* </p>
*
* @author chenhao
* @since 2023/3/8 10:23
*/
public class ImageCorrectionUtil {
/**
* 纠正图片旋转
*
* @param srcImgPath 图片地址
*/
public static void correctImg(String srcImgPath) {
FileOutputStream fos = null;
try {
// 原始图片
File srcFile = new File(srcImgPath);
// 获取偏转角度
int angle = getAngle(srcFile);
if (angle != 90 && angle != 270) {
return;
}
// 原始图片缓存
BufferedImage srcImg = ImageIO.read(srcFile);
// 宽高互换
// 原始宽度
int imgWidth = srcImg.getHeight();
// 原始高度
int imgHeight = srcImg.getWidth();
// 中心点位置
double centerWidth = ((double) imgWidth) / 2;
double centerHeight = ((double) imgHeight) / 2;
// 图片缓存
BufferedImage targetImg = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_RGB);
// 旋转对应角度
Graphics2D g = targetImg.createGraphics();
g.rotate(Math.toRadians(angle), centerWidth, centerHeight);
g.drawImage(srcImg, (imgWidth - srcImg.getWidth()) / 2, (imgHeight - srcImg.getHeight()) / 2, null);
g.rotate(Math.toRadians(-angle), centerWidth, centerHeight);
g.dispose();
// 输出图片
fos = new FileOutputStream(srcFile);
ImageIO.write(targetImg, "jpg", fos);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
/**
* 获取图片旋转角度
*
* @param file 上传图片
* @return 图片旋转角度
*/
private static int getAngle(File file) throws Exception {
Metadata metadata = ImageMetadataReader.readMetadata(file);
for (Directory directory : metadata.getDirectories()) {
for (Tag tag : directory.getTags()) {
if ("Orientation".equals(tag.getTagName())) {
String orientation = tag.getDescription();
if (orientation.contains("90")) {
return 90;
} else if (orientation.contains("180")) {
return 180;
} else if (orientation.contains("270")) {
return 270;
}
}
}
}
return 0;
}
/**
* 调用deskew工具
* Usage:
* deskew [-o output] [-a angle] [-b color] [..] input
* input: Input image file
* Options:
* -o output: 输出图像文件路径(默认:out.png)
* -a angle: 最大期望倾斜角度(两个方向)(默认:10度)
* -b color: 背景颜色十六进制格式RRGGBB|LL|AARRGGBB(默认为黑色)
* Ext. options:
* -q filter: 用于旋转的重采样过滤器 (default: linear,
* values: nearest|linear|cubic|lanczos)
* -t a|treshold: 自动阈值或0..255的值 (default: a)
* -r rect: 仅在内容矩形中进行倾斜检测 (pixels):
* left,top,right,bottom (default: whole page)
* -f format: 输出像素格式 (values: b1|g8|rgb24|rgba32)
* -l angle: 如果倾斜角度较小,则跳过倾斜步骤 (default: 0.01)
* -g flags: Operational flags (any combination of):
* c - auto crop, d - detect only (no output to file)
* -s info: 信息转储 (any combination of):
* s - skew detection stats, p - program parameters, t - timings
* -c specs: 某些文件格式的输出压缩规格。可以定义几个规格-用逗号分隔。支持规格:
* jXX - JPEG compression quality, XX is in range [1,100(best)]
* tSCHEME - TIFF compression scheme: none|lzw|rle|deflate|jpeg|g4
* Supported file formats
* Input: BMP, JPG, PNG, JNG, GIF, DDS, TGA, PBM, PGM, PPM, PAM, PFM, TIF, PSD
* Output: BMP, JPG, PNG, JNG, GIF, DDS, TGA, PGM, PPM, PAM, PFM, TIF, PSD
*
* @return 调用结果
*/
public static String deskew(String src, String dst, String deskewUrl, boolean deskewAngle) {
StringBuilder str = new StringBuilder();
// 命令行位置
str.append(deskewUrl).append(" ");
// 背景颜色
str.append("-b").append(" ").append("FFFFFF").append(" ");
if (deskewAngle) {
// 图片旋转耗时很多,跳过图片旋转
str.append("-l").append(" ").append("80").append(" ");
}
// 输出路径
str.append("-o").append(" ").append(dst).append(" ");
// 输入路径
str.append(src);
return RuntimeUtil.execForStr(str.toString());
}
public static String deskew2(String src, String dst, String deskewpyUrl) {
StringBuilder str = new StringBuilder();
// python启动
str.append("python").append(" ");
// python文件位置
str.append(deskewpyUrl).append(" ");
// 输入文件
str.append("-i").append(" ").append(src).append(" ");
// 输出文件
str.append("-o").append(" ").append(dst).append(" ");
return RuntimeUtil.execForStr(str.toString());
}
public static void main(String[] args) {
String path1 = "C:\\Users\\11419\\Desktop\\Deskew\\TestImages\\3.png";
String path2 = "C:\\Users\\11419\\Desktop\\Deskew\\TestImages\\res3.png";
String du = "C:\\Users\\11419\\Desktop\\lib\\correct.py";
String res = deskew2(path1, path2, du);
System.out.println(res);
}
}
package com.zq.imgproc.utils;
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.utils.Converters;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* <p>
* 去黑边(图像变换时会变更暗,需要进行亮度校正)
* </p>
*
* @author chenhao
* @since 2023/3/8 9:31
*/
@SuppressWarnings("all")
public class RemoveBlackUtil {
public static void main(String[] args) {
long start = System.currentTimeMillis();
String testImg = "C:/Users/11419/Desktop/Deskew/TestImages/4res.png";
String resImg = "C:/Users/11419/Desktop/Deskew/TestImages/res.png";
System.load("C:/Users/11419/Desktop/lib/opencv_java460.dll");
remove(testImg, resImg);
}
public static void remove(String src,String dst) {
//这个必须配置,否则会报错
Mat img = Imgcodecs.imread(src);
if(img.empty()){
return;
}
Mat greyImg = img.clone();
//1.彩色转灰色
Imgproc.cvtColor(img, greyImg, Imgproc.COLOR_BGR2GRAY);
Mat gaussianBlurImg = greyImg.clone();
// 2.高斯滤波,降噪
Imgproc.GaussianBlur(greyImg, gaussianBlurImg, new Size(3,3),2,2);
Mat cannyImg = gaussianBlurImg.clone();
// 3.Canny边缘检测
Imgproc.Canny(gaussianBlurImg, cannyImg, 20, 60, 3, false);
// 4.膨胀,连接边缘
Mat dilateImg = cannyImg.clone();
Imgproc.dilate(cannyImg, dilateImg, new Mat(), new Point(-1, -1), 2, 1, new Scalar(1));
//5.对边缘检测的结果图再进行轮廓提取
List<MatOfPoint> contours = new ArrayList<>();
List<MatOfPoint> drawContours = new ArrayList<>();
Mat hierarchy = new Mat();
Imgproc.findContours(dilateImg, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
Mat linePic = Mat.zeros(dilateImg.rows(), dilateImg.cols(), CvType.CV_8UC3);
//6.找出轮廓对应凸包的四边形拟合
List<MatOfPoint> squares = new ArrayList<>();
List<MatOfPoint> hulls = new ArrayList<>();
MatOfInt hull = new MatOfInt();
MatOfPoint2f approx = new MatOfPoint2f();
approx.convertTo(approx, CvType.CV_32F);
for (int i = 0; i < contours.size(); i++) {
MatOfPoint contour = contours.get(i);
// 边框的凸包
Imgproc.convexHull(contour, hull);
// 用凸包计算出新的轮廓点
Point[] contourPoints = contour.toArray();
int[] indices = hull.toArray();
List<Point> newPoints = new ArrayList<>();
for (int index : indices) {
newPoints.add(contourPoints[index]);
}
MatOfPoint2f contourHull = new MatOfPoint2f();
contourHull.fromList(newPoints);
// 多边形拟合凸包边框(此时的拟合的精度较低)
Imgproc.approxPolyDP(contourHull, approx, Imgproc.arcLength(contourHull, true)*0.02, true);
MatOfPoint mat = new MatOfPoint();
mat.fromArray(approx.toArray());
drawContours.add(mat);
// 筛选出面积大于某一阈值的,且四边形的各个角度都接近直角的凸四边形
MatOfPoint approxf1 = new MatOfPoint();
approx.convertTo(approxf1, CvType.CV_32S);
if (approx.rows() == 4 && Math.abs(Imgproc.contourArea(approx)) > 40000 &&
Imgproc.isContourConvex(approxf1)) {
double maxCosine = 0;
for (int j = 2; j < 5; j++) {
double cosine = Math.abs(getAngle(approxf1.toArray()[j%4], approxf1.toArray()[j-2], approxf1.toArray()[j-1]));
maxCosine = Math.max(maxCosine, cosine);
}
// 角度大概72度
if (maxCosine < 0.3) {
MatOfPoint tmp = new MatOfPoint();
contourHull.convertTo(tmp, CvType.CV_32S);
squares.add(approxf1);
hulls.add(tmp);
}
}
}
//这里是把提取出来的轮廓通过不同颜色的线描述出来,具体效果可以自己去看
Random r = new Random();
//7.找出最大的矩形
int index = findLargestSquare(squares);
MatOfPoint largest_square = null;
if(squares != null && squares.size() > 0){
largest_square = squares.get(index);
}else{
System.out.println("图片无法识别");
return;
}
Mat polyPic = Mat.zeros(img.size(), CvType.CV_8UC3);
//存储矩形的四个凸点
hull = new MatOfInt();
Imgproc.convexHull(largest_square, hull, false);
List<Integer> hullList = hull.toList();
List<Point> polyContoursList = largest_square.toList();
List<Point> hullPointList = new ArrayList<>();
List<Point> lastHullPointList = new ArrayList<>();
for(int i = 0; i < hullList.size();i++){
Imgproc.circle(polyPic, polyContoursList.get(hullList.get(i)), 10, new Scalar(r.nextInt(255),r.nextInt(255), r.nextInt(255), 3));
hullPointList.add(polyContoursList.get(hullList.get(i)));
}
Core.addWeighted(polyPic, 1, img, 1, 0, img);
for(int i = 0; i < hullPointList.size(); i++){
lastHullPointList.add(hullPointList.get(i));
}
//dstPoints储存的是变换后各点的坐标,依次为左上,右上,右下, 左下
//srcPoints储存的是上面得到的四个角的坐标
Point[] dstPoints = {new Point(0,0), new Point(img.cols(),0), new Point(img.cols(),img.rows()), new Point(0,img.rows())};
Point[] srcPoints = new Point[4];
boolean sorted = false;
int n = 4;
//对四个点进行排序 分出左上 右上 右下 左下
while (!sorted && n > 0){
for (int i = 1; i < n; i++){
sorted = true;
if (lastHullPointList.get(i-1).x > lastHullPointList.get(i).x){
Point tempp1 = lastHullPointList.get(i);
Point tempp2 = lastHullPointList.get(i-1);
lastHullPointList.set(i, tempp2);
lastHullPointList.set(i-1, tempp1);
sorted = false;
}
}
n--;
}
//即先对四个点的x坐标进行冒泡排序分出左右,再根据两对坐标的y值比较分出上下
if (lastHullPointList.get(0).y < lastHullPointList.get(1).y){
srcPoints[0] = lastHullPointList.get(0);
srcPoints[3] = lastHullPointList.get(1);
}else{
srcPoints[0] = lastHullPointList.get(1);
srcPoints[3] = lastHullPointList.get(0);
}
if (lastHullPointList.get(2).y < lastHullPointList.get(3).y){
srcPoints[1] = lastHullPointList.get(2);
srcPoints[2] = lastHullPointList.get(3);
}else{
srcPoints[1] = lastHullPointList.get(3);
srcPoints[2] = lastHullPointList.get(2);
}
List<Point> listSrcs = java.util.Arrays.asList(srcPoints[0], srcPoints[1], srcPoints[2], srcPoints[3]);
Mat srcPointsMat = Converters.vector_Point_to_Mat(listSrcs, CvType.CV_32F);
List<Point> dstSrcs = java.util.Arrays.asList(dstPoints[0], dstPoints[1], dstPoints[2], dstPoints[3]);
Mat dstPointsMat = Converters.vector_Point_to_Mat(dstSrcs, CvType.CV_32F);
//参数分别为输入输出图像、变换矩阵、大小。
//坐标变换后就得到了我们要的最终图像。
Mat transMat = Imgproc.getPerspectiveTransform(srcPointsMat, dstPointsMat); //得到变换矩阵
Mat outPic = new Mat();
Imgproc.warpPerspective(img, outPic, transMat, img.size());
ImageUtil.saveImage(outPic, dst);
}
// 根据三个点计算中间那个点的夹角 pt1 pt0 pt2
private static double getAngle(Point pt1, Point pt2, Point pt0) {
double dx1 = pt1.x - pt0.x;
double dy1 = pt1.y - pt0.y;
double dx2 = pt2.x - pt0.x;
double dy2 = pt2.y - pt0.y;
return (dx1*dx2 + dy1*dy2)/Math.sqrt((dx1*dx1 + dy1*dy1)*(dx2*dx2 + dy2*dy2) + 1e-10);
}
// 找到最大的正方形轮廓
private static int findLargestSquare(List<MatOfPoint> squares) {
if (squares.size() == 0) {
return -1;
}
int max_width = 0;
int max_height = 0;
int max_square_idx = 0;
int currentIndex = 0;
for (MatOfPoint square : squares) {
Rect rectangle = Imgproc.boundingRect(square);
if (rectangle.width >= max_width && rectangle.height >= max_height) {
max_width = rectangle.width;
max_height = rectangle.height;
max_square_idx = currentIndex;
}
currentIndex++;
}
return max_square_idx;
}
}
package com.zq.imgproc.utils;
import org.opencv.core.Mat;
import org.opencv.core.Rect;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
/**
* <p>
* 去黑边
* </p>
*
* @author chenhao
* @since 2023/3/10 15:01
*/
public class RemoveBlackUtil2 {
/**
* 去黑边"全黑"阈值
*/
private static final Integer BLACK_VALUE = 40;
public static void remove(String src, String dst) {
Mat mat = Imgcodecs.imread(src);
Mat res = removeBlackEdge(mat);
ImageUtil.saveImage(res, dst);
mat.release();
res.release();
}
/**
* 去除图片黑边,若无黑边,则原图返回。默认“全黑”阈值为 {@code BLACK_VALUE}
*
* @param srcMat 预去除黑边的Mat
* @return 去除黑边之后的Mat
*/
private static Mat removeBlackEdge(Mat srcMat) {
return removeBlackEdge(srcMat, BLACK_VALUE);
}
/**
* 去除图片黑边,若无黑边,则原图返回。
*
* @param blackValue 一般低于5的已经是很黑的颜色了
* @param srcMat 源Mat对象
* @return Mat对象
*/
private static Mat removeBlackEdge(Mat srcMat, int blackValue) {
// 灰度化
Mat grayMat = gray(srcMat);
// 定义边界
int topRow = 0;
int leftCol = 0;
int rightCol = grayMat.width() - 1;
int bottomRow = grayMat.height() - 1;
// 上方黑边判断
for (int row = 0; row < grayMat.height(); row++) {
if (ImageUtil.sum(grayMat.row(row)) / grayMat.width() < blackValue) {
topRow = row;
} else {
break;
}
}
// 左边黑边判断
for (int col = 0; col < grayMat.width(); col++) {
if (ImageUtil.sum(grayMat.col(col)) / grayMat.height() < blackValue) {
leftCol = col;
} else {
break;
}
}
// 右边黑边判断
for (int col = grayMat.width() - 1; col > 0; col--) {
if (ImageUtil.sum(grayMat.col(col)) / grayMat.height() < blackValue) {
rightCol = col;
} else {
break;
}
}
// 下方黑边判断
for (int row = grayMat.height() - 1; row > 0; row--) {
if (ImageUtil.sum(grayMat.row(row)) / grayMat.width() < blackValue) {
bottomRow = row;
} else {
break;
}
}
int x = leftCol;
int y = topRow;
int width = rightCol - leftCol;
int height = bottomRow - topRow;
grayMat.release();
if (leftCol == 0 && rightCol == grayMat.width() - 1 && topRow == 0 && bottomRow == grayMat.height() - 1) {
return srcMat;
}
return cut(srcMat, x, y, width, height);
}
/**
* 灰度处理 BGR灰度处理
*
* @param src 原图Mat
* @return Mat 灰度后的Mat
*/
private static Mat gray(Mat src) {
Mat gray = new Mat();
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
return gray;
}
/**
* 按照指定的尺寸截取Mat,坐标原点为左上角
*
* @param src 源Mat
* @param x x
* @param y y
* @param width width
* @param height height
* @return 截取后的Mat
*/
private static Mat cut(Mat src, int x, int y, int width, int height) {
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
if (width > src.width()) {
width = src.width();
}
if (height > src.height()) {
height = src.height();
}
// 截取尺寸
Rect rect = new Rect(x, y, width, height);
return new Mat(src, rect);
}
}
package com.zq.imgproc.utils;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
/**
* 统一的接口响应信息
*
* @author wilmiam
* @since 2021-07-09 17:45
*/
@ApiModel("API响应消息")
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class ResultVo<T> implements Serializable {
@ApiModelProperty(value = "成功标记", example = "true")
private boolean success;
@ApiModelProperty("错误码")
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
private int errCode;
@ApiModelProperty("错误信息")
private String errMsg;
@ApiModelProperty("响应的数据")
private T data;
public static ResultVo success() {
return success(null);
}
public static <E> ResultVo<E> success(E data) {
ResultVo<E> result = new ResultVo<>();
result.setSuccess(true);
result.setData(data);
return result;
}
public static ResultVo fail(String errMsg) {
return fail(500, errMsg);
}
public static ResultVo fail(int errCode, String errMsg) {
ResultVo result = new ResultVo<>();
result.setSuccess(false);
result.setErrCode(errCode);
result.setErrMsg(errMsg);
return result;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public int getErrCode() {
return errCode;
}
public void setErrCode(int errCode) {
this.errCode = errCode;
}
public String getErrMsg() {
return errMsg;
}
public void setErrMsg(String errMsg) {
this.errMsg = errMsg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
package com.zq.imgproc.utils;
import cn.hutool.core.convert.Convert;
import lombok.extern.slf4j.Slf4j;
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.opencv.highgui.HighGui.imshow;
import static org.opencv.highgui.HighGui.waitKey;
@Slf4j
public class TestUtil {
public static void main(String[] args) {
System.load("C:/Users/11419/Desktop/lib/opencv_java460.dll");
Mat image = Imgcodecs.imread("C:/Users/11419/Desktop/Deskew/TestImages/res.png");
// 转换为灰度图像
Mat grayImage = new Mat();
Imgproc.cvtColor(image, grayImage, Imgproc.COLOR_BGR2GRAY);
// 创建一个掩码,初始化为全黑
Mat mask = Mat.zeros(new Size(grayImage.cols() + 2, grayImage.rows() + 2), CvType.CV_8U);
// 执行漫水填充算法
Imgproc.floodFill(grayImage, mask, new org.opencv.core.Point(0, 0), new Scalar(255), null,
new Scalar(5, 5, 5, 5), new Scalar(5, 5, 5, 5));
// 进行二值化处理
Mat binaryImage = new Mat();
Imgproc.threshold(grayImage, binaryImage, 1, 255, Imgproc.THRESH_BINARY);
// 创建一个与二值化图像大小相同的全白图像
Mat filledImage = Mat.zeros(binaryImage.size(), CvType.CV_8U);
filledImage.setTo(new Scalar(255));
// 将非黑色区域从二值化图像复制到全白图像中
binaryImage.copyTo(filledImage, binaryImage);
imshow("test", binaryImage);
waitKey();
}
private static void test() {
String testImg = "C:/Users/11419/Desktop/Deskew/TestImages/4.png";
String resImg = "C:/Users/11419/Desktop/Deskew/TestImages/res.png";
long start = System.currentTimeMillis();
Mat src = Imgcodecs.imread(testImg);
// 图片灰度化
Mat gray = src.clone();
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
// // 高斯模糊(?)
// @Cleanup
// UMat blur = gray.clone();
// GaussianBlur(gray, blur, new Size(9, 9), 0);
// // 二值化(?)
// @Cleanup
// UMat thresh = blur.clone();
// threshold(blur, thresh, 0, 255, THRESH_BINARY_INV + THRESH_OTSU);
Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(5, 5));
// 图片膨胀
Mat erode = gray.clone();
Imgproc.erode(gray, erode, kernel);
// 图片腐蚀
Mat dilate = erode.clone();
Imgproc.dilate(erode, dilate, kernel);
// 边缘检测
Mat canny = dilate.clone();
Imgproc.Canny(dilate, canny, 50, 150);
// 霍夫变换得到线条
Mat lines = new Mat();
//累加器阈值参数,小于设置值不返回
int threshold = 90;
//最低线段长度,低于设置值则不返回
double minLineLength = 100;
//间距小于该值的线当成同一条线
double maxLineGap = 10;
// 霍夫变换,通过步长为1,角度为PI/180来搜索可能的直线
Imgproc.HoughLinesP(canny, lines, 1, Math.PI / 180, threshold, minLineLength, maxLineGap);
List<Integer> angelList = new ArrayList<>();
for (int i = 0; i < lines.rows(); i++) {
double[] line = lines.get(i, 0);
int k = calculateAngle(line[0], line[1], line[2], line[3]);
angelList.add(k);
}
// if (angelList.isEmpty()) {
// return 0.0;
// }
// 可能还得需要考虑方差来决定选择平均数还是众数
int angle = most(angelList);
Mat rotated = rotate(src, angle, 1.0);
gray.release();
kernel.release();
erode.release();
dilate.release();
lines.release();
canny.release();
}
/**
* 求数组众数
*
* @param angelList 数组
* @return 数组众数
*/
private static int most(List<Integer> angelList) {
if (angelList.isEmpty()) {
return 0;
}
int res = 0;
int max = Integer.MIN_VALUE;
Map<Integer, Integer> map = new HashMap<>();
for (int i : angelList) {
map.put(i, map.getOrDefault(i, 0) + 1);
}
for (Integer i : map.keySet()) {
int count = map.get(i);
if (count > max) {
max = count;
res = i;
}
}
return res;
}
/**
* 计算直线的倾斜角
*/
private static int calculateAngle(double x1, double y1, double x2, double y2) {
double dx = x2 - x1;
double dy = y2 - y1;
if (Math.abs(dx) < 1e-4) {
return 90;
} else if (Math.abs(dy) < 1e-4) {
return 0;
} else {
double radians = Math.atan2(dy, dx);
double degrees = Math.toDegrees(radians);
return Convert.toInt(Math.round(degrees));
}
}
public static Mat rotate(Mat image, double angle, double scale) {
int w = image.cols();
int h = image.rows();
Point center = new Point((double) w / 2, (double) h / 2);
Mat rotationMatrix = Imgproc.getRotationMatrix2D(center, angle, scale);
// 设置填充颜色为白色
Scalar backgroundColor = new Scalar(255, 255, 255);
Mat rotatedImage = new Mat();
Imgproc.warpAffine(image, rotatedImage, rotationMatrix, new Size(w, h), Imgproc.INTER_LINEAR, Core.BORDER_CONSTANT, backgroundColor);
imshow("test", rotatedImage);
waitKey();
return rotatedImage;
}
}
package com.zq.imgproc.utils;
import cn.hutool.core.io.FileUtil;
import com.zq.imgproc.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
/**
* @author wilmiam
* @since 2021-07-09 18:05
*/
@Slf4j
public class UploadUtils {
private final static String DATE_FORMAT = "/yyyyMM/dd/";
/**
* 保存临时文件
*
* @return 保存地址
*/
public static String saveTempFile(MultipartFile file, String systemName) {
try {
String originalFilename = file.getOriginalFilename();
//取文件扩展名
String ext = Objects.requireNonNull(originalFilename).substring(originalFilename.lastIndexOf(".") + 1);
//生成新文件名
String name = UuidUtils.uuidNoDash() + "." + ext;
String yyyyMMdd = new SimpleDateFormat(DATE_FORMAT).format(new Date());
File dest = new File("/data/temp/" + systemName + yyyyMMdd + name);
FileUtil.writeBytes(file.getBytes(), dest);
return "/data/temp/" + systemName + yyyyMMdd + name;
} catch (IOException e) {
log.error("文件保存失败:{}", e.getMessage());
throw new BusinessException("文件保存失败");
}
}
}
package com.zq.imgproc.utils;
import java.util.UUID;
/**
* Utility class that handles the uuid stuff.
*
* @author wilmiam
* @since 2021-07-09 18:05
*/
public class UuidUtils {
private static final String URN_UUID_REGEX = "^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$";
private static final String NODASH_UUID_REGEX = "^[a-f0-9]{32}$";
/**
* Don't let anyone instantiate this class
*/
private UuidUtils() {
}
/**
* 返回一个随机的带有分隔符"-"的36位UUID字符串
*
* @return
*/
public static String uuid() {
return UUID.randomUUID().toString();
}
/**
* 返回一个随机的没有分隔符"-"的32位UUID字符串
*
* @return
*/
public static String uuidNoDash() {
return uuid().replaceAll("-", "");
}
/**
* 判断一个字符串是否是一个UUID字符串
*
* @param str 要进行判断的字符串
* @return
*/
public static boolean isUuid(String str) {
return ValidateUtil.isNotBlank(str) && str.matches(URN_UUID_REGEX);
}
/**
* 判断一个字符串是否是一个没有分隔符的uuid字符串
*
* @param str 要进行判断的字符串
* @return
*/
public static boolean isNoDashUuid(String str) {
return ValidateUtil.isNotBlank(str) && str.matches(NODASH_UUID_REGEX);
}
}
package com.zq.imgproc.vo;
import lombok.Data;
import java.util.List;
/**
* <p>
* 压缩请求
* </p>
*
* @author chenhao
* @since 2023/3/9 17:35
*/
@Data
public class CompressReq {
String name;
List<ImgVO> pathList;
}
package com.zq.imgproc.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <p>
* 图片返回结果封装类
* </p>
*
* @author chenhao
* @since 2022/10/27 11:25
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class DetectionResVo {
@ApiModelProperty("图片的水平分辨率")
private Integer widthResolution;
@ApiModelProperty("图片的垂直分辨率")
private Integer heightResolution;
@ApiModelProperty("图片的DPI")
private Integer dpi;
@ApiModelProperty("图片的亮度情况,1表示亮度正常,2表示过亮,3表示过暗")
private Integer brightness;
@ApiModelProperty("图片的清晰度,true表示清晰,false表示模糊")
private Boolean clarity;
@ApiModelProperty("图片的倾斜角度")
private double angle;
@ApiModelProperty("图片的黑边检测,true表示可能存在黑边,false表示不存在黑边")
private Boolean black;
}
package com.zq.imgproc.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <p>
* 图片返回结果封装类(带指标)
* </p>
*
* @author chenhao
* @since 2023/3/10 16:48
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class DetectionResVo2 {
@ApiModelProperty("图片的水平分辨率")
private Integer widthResolution;
@ApiModelProperty("图片的垂直分辨率")
private Integer heightResolution;
@ApiModelProperty("图片的DPI")
private Integer dpi;
@ApiModelProperty("亮度值")
private double cast;
@ApiModelProperty("亮度异常值")
private double da;
@ApiModelProperty("图片的清晰度指标,值越大越清晰")
private double clarity;
@ApiModelProperty("图片的倾斜角度")
private double angle;
@ApiModelProperty("图片的黑边检测,true表示可能存在黑边,false表示不存在黑边")
private Boolean black;
}
package com.zq.imgproc.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <p>
*
* </p>
*
* @author yww
* @since 2023/4/21
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class DetectionResVo3 {
@ApiModelProperty("图片的水平分辨率")
private Integer widthResolution;
@ApiModelProperty("图片的垂直分辨率")
private Integer heightResolution;
@ApiModelProperty("图片的DPI")
private Integer dpi;
@ApiModelProperty("图片平均亮度值")
private double brightness;
@ApiModelProperty("图片的清晰度指标,值越大越清晰")
private double clarity;
@ApiModelProperty("图片的倾斜角度")
private double angle;
@ApiModelProperty("图片的黑边检测,true表示可能存在黑边,false表示不存在黑边")
private Boolean black;
}
package com.zq.imgproc.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <p>
*
* </p>
*
* @author chenhao
* @since 2023/3/7 16:01
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class ImgVO {
String id;
String fileName;
String url;
DetectionResVo2 detectionRes;
}
package com.zq.imgproc.vo;
import lombok.Data;
/**
* <p>
*
* </p>
*
* @author yww
* @since 2023/4/20
*/
@Data
public class OptimizationReq {
private String fileContent;
private String filename;
}
package com.zq.imgproc.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <p>
* 图片优化结果类
* </p>
*
* @author chenhao
* @since 2023/4/20
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class OptimizationVO {
@ApiModelProperty("base64图片")
private String fileContent;
@ApiModelProperty("文件名称")
private String filename;
@ApiModelProperty("是否经过亮度调整")
private boolean isBrightness;
@ApiModelProperty("图片原始平均亮度值")
private double originalBrightnessVal;
@ApiModelProperty("图片修正后的平均亮度值")
private double brightnessVal;
@ApiModelProperty("是否经过清晰度调整")
private boolean isClarity;
@ApiModelProperty("图片原始清晰度值")
private double originalClarityVal;
@ApiModelProperty("图片清晰度调整过后的清晰度值")
private double clarityVal;
@ApiModelProperty("是否经过黑边处理")
private boolean isRemoveBlack;
@ApiModelProperty("图片纠偏的角度,纠偏出错返回-1")
private double deskewAngel;
@ApiModelProperty("图片是否经过弯曲矫正")
private boolean isCorrect;
}
package com.zq.imgproc.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* <p>
* 图片旋转
* </p>
*
* @author chenhao
* @since 2023/3/10 17:32
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class RotateReq {
/**
* 旋转角度
*/
Integer degree;
List<ImgVO> pathList;
}
package com.zq.imgproc.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <p>
* 亮度和清晰度指标设置
* </p>
*
* @author chenhao
* @since 2023/3/10 17:03
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class SettingVO {
@ApiModelProperty("亮度值")
private double cast;
@ApiModelProperty("亮度异常值")
private double da;
@ApiModelProperty("图片的清晰度指标,值越大越清晰")
private double clarity;
}
server:
port: 8900
spring:
application:
name: IMGPROC-SERVER
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
mvc:
format:
date-time: yyyy-MM-dd HH:mm:ss
servlet:
multipart:
max-file-size: 100MB
max-request-size: 150MB
# IP 本地解析
ip:
local-parsing: true
imgconfig:
opencv: /opt/tianjin/lib/opencv_java460.so
deskew: /opt/tianjin/lib/Deskew/Bin/deskew
deskewpy: /opt/tianjin/lib/correct.py
#imgconfig:
# opencv: C:/Users/11419/Desktop/lib/opencv_java460.dll
# deskew: C:/Users/11419/Desktop/Deskew/Bin/deskew.exe
# deskewpy: C:/Users/11419/Desktop/lib/correct.py
spring:
profiles:
active: @profiles.active@
cloud:
config:
name: config
profile: ${spring.profiles.active}
discovery:
enabled: true
service-id: CONFIG-SERVER
eureka:
instance:
prefer-ip-address: true
lease-renewal-interval-in-seconds: 2 #每间隔1s,向服务端发送一次心跳,证明自己依然"存活"
lease-expiration-duration-in-seconds: 6 #告诉服务端,如果我2s之内没有给你发心跳,就代表我"死"了,将我踢出掉。
instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}
client:
service-url:
defaultZone: @eureka.server.url@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--<include resource="org/springframework/boot/logging/logback/base.xml"/>-->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<property name="default_log_path" value="logs"/>
<property name="default_log_file" value="imgproc"/><!--主要日志文件名-->
<property name="LOG_PATH" value="${LOG_PATH:-${default_log_path}}"/>
<property name="LOG_FILE" value="${LOG_FILE:-${default_log_file}}"/>
<!-- the name of the application's logging context -->
<!-- by default each JMXConfigurator instance will be registered under the same name in the same JVM -->
<!-- we need to set the contextName for different apps, so that the jmxconfigurator won't collide -->
<contextName>user</contextName>
<jmxConfigurator/>
<!--主要日志配置 开始-->
<appender name="SIZED_ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d %-5p [%t] %logger : %m%n</pattern>
<charset>UTF-8</charset>
</encoder>
<file>${LOG_FILE}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_FILE}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 日志文档保留天数 -->
<maxHistory>15</maxHistory>
</rollingPolicy>
</appender>
<!-- 异步输出 -->
<appender name="main-logger-appender" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="SIZED_ROLLING_FILE"/>
</appender>
<!--主要日志配置 结束-->
<!--DEBUG日志配置 开始-->
<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d %-5p [%t] %logger : %m%n</pattern>
<charset>UTF-8</charset>
</encoder>
<file>debug.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/debug.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 日志文档保留天数 -->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 异步输出 -->
<appender name="debug-appender" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="DEBUG_FILE"/>
</appender>
<!--DEBUG日志配置 结束-->
<!--INFO日志配置 开始-->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d %-5p [%t] %logger : %m%n</pattern>
<charset>UTF-8</charset>
</encoder>
<file>info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/info.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 日志文档保留天数 -->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 异步输出 -->
<appender name="info-appender" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="INFO_FILE"/>
</appender>
<!--INFO日志配置 结束-->
<!--WARN日志配置 开始-->
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d %-5p [%t] %logger : %m%n</pattern>
<charset>UTF-8</charset>
</encoder>
<file>warn.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/warn.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 日志文档保留天数 -->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 异步输出 -->
<appender name="warn-appender" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="WARN_FILE"/>
</appender>
<!--WARN日志配置 结束-->
<!--ERROR错误日志配置 开始-->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>error.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d %-5p [%t] %logger : %m%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 异步输出 -->
<appender name="error-appender" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="ERROR_FILE"/>
</appender>
<!--ERROR错误日志配置 结束-->
<logger name="com.zq.imgproc" level="DEBUG"/>
<logger name="com.zq.imgproc.dao" level="INFO"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="main-logger-appender"/>
<appender-ref ref="debug-appender"/>
<appender-ref ref="info-appender"/>
<appender-ref ref="warn-appender"/>
<appender-ref ref="error-appender"/>
</root>
</configuration>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zq</groupId>
<artifactId>imgproc</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>imgprocessing-server</artifactId>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.4.2</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!--Spring boot Web容器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.19</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.9</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>opencv-platform</artifactId>
<version>4.7.0-1.5.9</version>
</dependency>
<!-- Additional dependencies required to use CUDA and cuDNN -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>opencv-platform-gpu</artifactId>
<version>4.7.0-1.5.9</version>
</dependency>
<dependency>
<groupId>com.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId>
<version>2.18.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-imaging</artifactId>
<version>1.0-alpha3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<!-- 使用@profiles.active@需要添加以下内容 -->
<resources>
<resource>
<directory>src/main/resources</directory>
<!--开启过滤,用指定的参数替换directory下的文件中的参数-->
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>
\ No newline at end of file
package com.zq.processing;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class ImgprocessingApplication {
public static void main(String[] args) {
SpringApplication.run(ImgprocessingApplication.class, args);
}
}
package com.zq.processing.controller;
import cn.hutool.core.codec.Base64Encoder;
import cn.hutool.core.io.FileUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.zq.processing.service.ImgProcService;
import com.zq.processing.utils.*;
import com.zq.processing.vo.DetectionResVo;
import com.zq.processing.vo.OptimizationReq;
import com.zq.processing.vo.OptimizationVO;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
/**
* <p>
* 图片处理API
* </p>
*
* @author chenhao
* @since 2023/3/18 9:32
*/
@io.swagger.annotations.Api(tags = "图片处理API")
@RequestMapping("/imgproc/v1")
@RestController
public class ApiController {
@Value("${imgconfig.opencv}")
String opencvUrl;
@Value("${imgconfig.deskew}")
String deskewUrl;
private final ImgProcService service;
@Autowired
public ApiController(ImgProcService service) {
this.service = service;
}
@ApiOperation("测试")
@PostMapping("/ping")
public ResultVo<?> ping() {
return ResultVo.success("测试成功");
}
@ApiOperation("图片检测")
@PostMapping ("/detection")
public ResultVo<DetectionResVo> detection(@RequestBody OptimizationReq req) throws Exception {
AssertUtils.hasText(req.getFileContent(), "缺少文件内容");
AssertUtils.hasText(req.getFilename(), "缺少文件名");
return ResultVo.success(service.detection(req));
}
@ApiOperation("图片校正(deskew工具)")
@PostMapping("/imageCorrection")
public ResultVo<?> imageCorrection(@RequestPart MultipartFile file) {
if (file.isEmpty()) {
return ResultVo.fail("文件不能为空");
}
String imgPath = UploadUtils.saveTempFile(file, "api");
String ext = FileUtil.extName(file.getOriginalFilename());
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
// 图片矫正
ImageCorrectionUtil.deskew(imgPath, savePath, deskewUrl, false);
return ResultVo.success(Base64Encoder.encode(FileUtil.readBytes(savePath)));
}
@ApiOperation("去黑边")
@PostMapping("/removeBlack")
public ResultVo<?> removeBlack(@RequestPart MultipartFile file) {
if (file.isEmpty()) {
return ResultVo.fail("文件不能为空");
}
System.load(opencvUrl);
String imgPath = UploadUtils.saveTempFile(file, "api");
String ext = FileUtil.extName(file.getOriginalFilename());
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
// 去黑边
RemoveBlackUtil.remove(imgPath, savePath);
// 图片弯曲矫正
HashMap<String, Object> map = new HashMap<>();
map.put("file", FileUtil.file(savePath));
String response = HttpUtil.post("http://ddns.gxmailu.com:18888/api/correct", map);
JSONObject res = JSONUtil.parseObj(response);
if (res.get("success", Boolean.class)) {
String base64 = res.get("data", String.class);
return ResultVo.success(base64);
} else {
return ResultVo.fail(res.get("msg", String.class));
}
}
@ApiOperation("图片灰度化")
@PostMapping("/gray")
public ResultVo<?> gray(@RequestPart MultipartFile file) {
if (file.isEmpty()) {
return ResultVo.fail("文件不能为空");
}
System.load(opencvUrl);
String imgPath = UploadUtils.saveTempFile(file, "api");
String ext = FileUtil.extName(file.getOriginalFilename());
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
// 灰度化
ImageUtil.gray(imgPath, savePath);
return ResultVo.success(Base64Encoder.encode(FileUtil.readBytes(savePath)));
}
@ApiOperation("图片边缘检测")
@PostMapping("/canny")
public ResultVo<?> canny(@RequestPart MultipartFile file) {
if (file.isEmpty()) {
return ResultVo.fail("文件不能为空");
}
System.load(opencvUrl);
String imgPath = UploadUtils.saveTempFile(file, "api");
String ext = FileUtil.extName(file.getOriginalFilename());
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
// 边缘检测
ImageUtil.canny(imgPath, savePath);
return ResultVo.success(Base64Encoder.encode(FileUtil.readBytes(savePath)));
}
@ApiOperation("图片弯曲矫正")
@PostMapping("/correct")
public ResultVo<?> correct(@RequestPart MultipartFile file) {
if (file.isEmpty()) {
return ResultVo.fail("文件不能为空");
}
String imgPath = UploadUtils.saveTempFile(file, "api");
// 图片弯曲矫正
HashMap<String, Object> map = new HashMap<>();
map.put("file", FileUtil.file(imgPath));
String response = HttpUtil.post("http://ddns.gxmailu.com:18888/api/correct", map);
JSONObject res = JSONUtil.parseObj(response);
if (res.get("success", Boolean.class)) {
String base64 = res.get("data", String.class);
return ResultVo.success(base64);
} else {
return ResultVo.fail(res.get("msg", String.class));
}
}
@ApiOperation("图片优化")
@PostMapping("/imageOptimization")
public ResultVo<OptimizationVO> imageOptimization(@RequestBody OptimizationReq req) throws IOException {
AssertUtils.hasText(req.getFileContent(), "缺少文件内容");
AssertUtils.hasText(req.getFilename(), "缺少文件名");
return ResultVo.success(service.optimization(req));
}
}
package com.zq.processing.exception;
/**
* 业务错误
*
* @author wilmiam
* @since 2021-07-09 17:58
*/
public class BusinessException extends RuntimeException {
private int code = 400;
public BusinessException(String message) {
super(message);
}
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public BusinessException(int code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
public int getCode() {
return code;
}
}
\ No newline at end of file
package com.zq.processing.service;
import cn.hutool.core.codec.Base64Decoder;
import cn.hutool.core.codec.Base64Encoder;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.FileUtil;
import com.zq.processing.utils.ImageCorrectionUtil;
import com.zq.processing.utils.ImageUtil;
import com.zq.processing.utils.RemoveBlackUtil;
import com.zq.processing.utils.UuidUtils;
import com.zq.processing.vo.DetectionResVo;
import com.zq.processing.vo.OptimizationReq;
import com.zq.processing.vo.OptimizationVO;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.opencv.opencv_core.Mat;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imread;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imwrite;
/**
* <p>
* 图片检测服务
* </p>
*
* @author chenhao
* @since 2022/10/24 15:02
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class ImgProcService {
@Value("${imgconfig.deskew}")
String deskewUrl;
@Value("${imgconfig.deskewpy}")
String deskewpyUrl;
public DetectionResVo detection(OptimizationReq req) throws Exception {
// 1. 临时保存图片
String ext = FileUtil.extName(req.getFilename());
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String filePath = "/data/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
FileUtil.writeBytes(Base64Decoder.decode(req.getFileContent()), filePath);
if ("heic".equals(ext)) {
BufferedImage bufferedImage = ImageIO.read(new File(filePath));
ext = "jpg";
filePath = "/data/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
ImageIO.write(bufferedImage, "jpg", new File(filePath));
}
DetectionResVo res = new DetectionResVo();
@Cleanup
Mat image = imread(filePath);
// 检测图片的分辨率
res.setWidthResolution(image.cols());
res.setHeightResolution(image.rows());
// 检测图片的DPI
res.setDpi(ImageUtil.getDpi(FileUtil.file(filePath)));
// 检测图片清晰度
res.setClarity(ImageUtil.imageSharpnessDetector(filePath));
// 检测图片的亮度
res.setBrightness(ImageUtil.brightness(filePath));
// 检测图片倾斜角度
res.setAngle(getAngle(filePath));
// 检测图片的黑边
res.setBlack(RemoveBlackUtil.blackDetection(image));
image.release();
return res;
}
/**
* 通过矫正工具获取的倾斜角度
*
* @param imageUrl 图片路径
* @return 图片倾斜角度
*/
private Double getAngle(String imageUrl) {
// 纠正文件的保存路径
String ext = FileUtil.extName(imageUrl);
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/data/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
return 0.0;
}
public OptimizationVO optimization(OptimizationReq req) throws IOException {
OptimizationVO res = new OptimizationVO();
// 1. 临时保存图片
String ext = FileUtil.extName(req.getFilename());
String filename = req.getFilename();
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/data/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "/";
String filePath = savePath + filename;
FileUtil.writeBytes(Base64Decoder.decode(req.getFileContent()), filePath);
log.info("{}图片进行优化", filePath);
// 处理heic图片
if ("heic".equals(ext)) {
BufferedImage bufferedImage = ImageIO.read(new File(filePath));
ext = "jpg";
filename = FileUtil.mainName(filename) + ".jpg";
filePath = savePath + filename;
ImageIO.write(bufferedImage, "jpg", new File(filePath));
log.info("{}图片从heic转换为jpg", filePath);
}
String newPath = filePath;
// 2. 执行纠偏
long start = System.currentTimeMillis();
String deskerFile = savePath + "(1)." + ext;
String resMsg = ImageCorrectionUtil.deskew2(filePath, deskerFile, deskewpyUrl);
Double skewAngle = Convert.toDouble(resMsg);
if (skewAngle == null) {
log.error(resMsg);
String deskewRes = ImageCorrectionUtil.deskew(filePath, deskerFile, deskewUrl, false);
Pattern pattern = Pattern.compile("Skew angle found \\[deg\\]: (-?\\d+(?:\\.\\d+)?)");
Matcher matcher = pattern.matcher(deskewRes);
if (matcher.find()) {
skewAngle = Double.parseDouble(matcher.group(1));
log.info("矫正工具矫正角度【{}】", skewAngle);
} else {
res.setDeskewAngel(0);
}
}
if (Double.compare(skewAngle, 1) < 0 && Double.compare(skewAngle, -1) > 0) {
res.setDeskewAngel(0);
} else {
newPath = deskerFile;
res.setDeskewAngel(skewAngle);
}
log.info("纠偏所需时间【{}】", System.currentTimeMillis() - start);
// 3. 黑边处理
start = System.currentTimeMillis();
String blackUrl = savePath + "(2)." + ext;
@Cleanup
Mat bMat = imread(newPath);
if (RemoveBlackUtil.blackDetection(bMat)) {
res.setRemoveBlack(true);
RemoveBlackUtil.remove(newPath, blackUrl);
newPath = blackUrl;
} else {
res.setRemoveBlack(false);
}
res.setCorrect(false);
log.info("黑边处理消耗的时间【{}】", System.currentTimeMillis() - start);
// 4。 图片亮度检测
// 计算图像的平均亮度
start = System.currentTimeMillis();
double brightness = ImageUtil.brightness(newPath);
res.setOriginalBrightnessVal(brightness);
// 亮度异常执行亮度矫正
if (brightness < 100 || brightness > 500) {
res.setBrightness(true);
// 计算亮度调整值
double alpha = 300 / brightness;
// 执行亮度调整
@Cleanup
Mat grayImage = imread(newPath);
@Cleanup
Mat adjustedImage = new Mat();
grayImage.convertTo(adjustedImage, -1, alpha, 0);
// 保存调整后的图像
imwrite(newPath, adjustedImage);
res.setBrightnessVal(ImageUtil.brightness(newPath));
} else {
res.setBrightness(false);
res.setBrightnessVal(0);
}
log.info("亮度检测所需时间【{}】", System.currentTimeMillis() - start);
// 5. 图片清晰度检测
start = System.currentTimeMillis();
double laplacianScore = ImageUtil.imageSharpnessDetector(newPath);
res.setOriginalClarityVal(laplacianScore);
if (laplacianScore < 500) {
res.setClarity(true);
Mat cMat = ImageUtil.unsharpMasking(newPath);
// 保存调整后的图像
imwrite(newPath, cMat);
res.setClarityVal(ImageUtil.imageSharpnessDetector(newPath));
cMat.release();
} else {
res.setClarity(false);
res.setClarityVal(0);
}
log.info("图片清晰度所需时间【{}】", System.currentTimeMillis() - start);
res.setFilename(filename);
res.setFileContent(Base64Encoder.encode(FileUtil.readBytes(newPath)));
return res;
// 6. 图片弯曲矫正
// String correctUrl = savePath + "(3)." + ext;
// HashMap<String, Object> map = new HashMap<>();
// map.put("file", FileUtil.file(newPath));
// String response = HttpUtil.post("http://ddns.gxmailu.com:18888/api/correct", map);
// JSONObject correctRes = JSONUtil.parseObj(response);
// if (ObjectUtil.isNotNull(correctRes) && correctRes.get("success", Boolean.class)) {
// String base64 = correctRes.get("data", String.class);
// res.setCorrect(true);
// res.setFileContent(base64);
// res.setFilename(filename);
// } else {
// res.setCorrect(false);
// res.setFilename(filename);
// res.setFileContent(Base64Encoder.encode(FileUtil.readBytes(newPath)));
// }
// return res;
}
}
package com.zq.processing.utils;
import cn.hutool.core.util.StrUtil;
import com.zq.processing.exception.BusinessException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
/**
* 断言验证帮助类
*
* @author wilmiam
* @since 2018-04-03
*/
@SuppressWarnings("all")
public final class AssertUtils {
private static final String[] IMG_EXTS = {"png", "jpg", "jpeg"};
/**
* Don't let anyone instantiate this class
*/
private AssertUtils() {
}
/**
* 判断给定的文件名后缀是否为图片
*
* @param ext 文件名后缀, 不带点
* @param errMsg 错误信息
*/
public static void isImgExt(String ext, String errMsg) {
isImgExt(ext, 400, errMsg);
}
/**
* 判断给定的文件名后缀是否为图片
*
* @param ext 文件名后缀, 不带点
* @param errCode 断言失败的错误代码
* @param errMsg 错误信息
*/
public static void isImgExt(String ext, int errCode, String errMsg) {
if (StrUtil.isBlank(ext) || Arrays.stream(IMG_EXTS).noneMatch(img -> img.equalsIgnoreCase(ext))) {
throw new BusinessException(errCode, errMsg);
}
}
/**
* UnifiedExceptionHandler
* 判断一个布尔表达式, 若表达式为{@code true}则抛出指定错误信息的{@code BusinessException}.
*
* @param expression 布尔表达式
* @param message 断言失败时的错误信息
* @throws BusinessException
*/
public static void notTrue(boolean expression, String message) throws BusinessException {
notTrue(expression, 400, message);
}
/**
* 判断一个布尔表达式, 若表达式为{@code true}则抛出指定错误信息的{@code BusinessException}.
*
* @param expression 布尔表达式
* @param errCode 断言失败时的错误代码
* @param message 断言失败时的错误信息
* @throws BusinessException
*/
public static void notTrue(boolean expression, int errCode, String message) throws BusinessException {
if (expression) {
throw new BusinessException(errCode, message);
}
}
/**
* 判断一个布尔表达式, 若表达式为{@code false}则抛出指定错误信息的{@code BusinessException}.
*
* @param expression 布尔表达式
* @param message 断言失败时的错误信息
* @throws BusinessException
*/
public static void isTrue(boolean expression, String message) throws BusinessException {
isTrue(expression, 400, message);
}
/**
* 判断一个布尔表达式, 若表达式为{@code false}则抛出指定错误信息的{@code BusinessException}.
*
* @param expression 布尔表达式
* @param errCode 断言失败时的错误代码
* @param message 断言失败时的错误信息
* @throws BusinessException
*/
public static void isTrue(boolean expression, int errCode, String message) throws BusinessException {
if (!expression) {
throw new BusinessException(errCode, message);
}
}
/**
* 如果对象为{@code null}, 则抛出异常
*
* @param object 要判断的对象
* @throws BusinessException
*/
public static void notNull(Object object) throws BusinessException {
notNull(object, "不能处理空对象");
}
/**
* 如果对象为{@code null}, 则抛出异常
*
* @param object 要判断的对象
* @param message 断言失败时的错误信息
* @throws BusinessException
*/
public static void notNull(Object object, String message) throws BusinessException {
notNull(object, 400, message);
}
/**
* 如果对象为{@code null}, 则抛出异常
*
* @param object 要判断的对象
* @param errCode 断言失败时的错误代码
* @param errMsg 断言失败时的错误信息
* @throws BusinessException
*/
public static void notNull(Object object, int errCode, String errMsg) throws BusinessException {
if (object == null) {
throw new BusinessException(errCode, errMsg);
}
}
/**
* 如果字符串为{@code null}、空字符串或仅包含空白字符, 则抛出异常
*
* @param text 要进行检查的字符串
* @throws BusinessException
*/
public static void hasText(String text) throws BusinessException {
hasText(text, 400, "参数不能为空字符串");
}
/**
* 如果字符串为{@code null}、空字符串或仅包含空白字符, 则抛出异常
*
* @param text 要进行检查的字符串
* @param message 断言失败时的错误信息
* @throws BusinessException
*/
public static void hasText(String text, String message) throws BusinessException {
hasText(text, 400, message);
}
/**
* 如果字符串为{@code null}、空字符串或仅包含空白字符, 则抛出异常
*
* @param text 要进行检查的字符串
* @param errCode 断言失败时的错误代码
* @param errMsg 错误信息
* @throws BusinessException
*/
public static void hasText(String text, int errCode, String errMsg) throws BusinessException {
if (StrUtil.isBlank(text)) {
throw new BusinessException(errCode, errMsg);
}
}
/**
* 如果数组为{@code null}或长度为0, 则抛出异常
*
* @param array 要进行检查的数组
* @param message 断言失败时的错误信息
* @param <T> 数组的数据类型
* @throws BusinessException
*/
public static <T> void notEmpty(T[] array, String message) throws BusinessException {
notEmpty(array, 400, message);
}
/**
* 如果数组为{@code null}或长度为0, 则抛出异常
*
* @param array 要进行检查的数组
* @param errCode 断言失败时的错误代码
* @param errMsg 错误信息
* @param <T> 数组的数据类型
* @throws BusinessException
*/
public static <T> void notEmpty(T[] array, int errCode, String errMsg) throws BusinessException {
if (array == null || array.length == 0) {
throw new BusinessException(errCode, errMsg);
}
}
/**
* 如果数组里包含有{@code null}的元素, 则抛出异常. 注意: 若数组本身为{@code null}则不会进行处理, 直接返回
*
* @param array 要进行检查的数组
* @param message 断言失败时的错误信息
* @param <T> 数组的数据类型
* @throws BusinessException
*/
public static <T> void noNullElements(T[] array, String message) throws BusinessException {
noNullElements(array, 400, message);
}
/**
* 如果数组里包含有{@code null}的元素, 则抛出异常. 注意: 若数组本身为{@code null}则不会进行处理, 直接返回
*
* @param array 要进行检查的数组
* @param errCode 断言失败时的错误代码
* @param errMsg 错误信息
* @param <T> 数组的数据类型
* @throws BusinessException
*/
public static <T> void noNullElements(T[] array, int errCode, String errMsg) throws BusinessException {
if (array != null) {
for (T element : array) {
if (element == null) {
throw new BusinessException(errCode, errMsg);
}
}
}
}
/**
* 如果集合为{@code null},或者不包含任何元素,则抛出异常
*
* @param collection 要进行检查的集合
* @param message 断言失败时的错误信息
* @throws BusinessException
*/
public static void notEmpty(Collection<?> collection, String message) throws BusinessException {
notEmpty(collection, 400, message);
}
/**
* 如果集合为{@code null},或者不包含任何元素,则抛出异常
*
* @param collection 要进行检查的集合
* @param errCode 断言失败时的错误代码
* @param errMsg 错误信息
* @throws BusinessException
*/
public static void notEmpty(Collection<?> collection, int errCode, String errMsg) throws BusinessException {
if (collection == null || collection.isEmpty()) {
throw new BusinessException(errCode, errMsg);
}
}
/**
* 如果键值对为{@code null},或者不包含任何键值,则抛出异常
*
* @param map 要进行检查的键值对
* @param message 断言失败时的错误信息
* @throws BusinessException
*/
public static void notEmpty(Map<?, ?> map, String message) throws BusinessException {
notEmpty(map, 400, message);
}
/**
* 如果键值对为{@code null},或者不包含任何键值,则抛出异常
*
* @param map 要进行检查的键值对
* @param errCode 断言失败时的错误代码
* @param errMsg 错误信息
* @throws BusinessException
*/
public static void notEmpty(Map<?, ?> map, int errCode, String errMsg) throws BusinessException {
if (map == null || map.isEmpty()) {
throw new BusinessException(errCode, errMsg);
}
}
}
package com.zq.processing.utils;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.zq.processing.vo.DetectionResVo;
import com.zq.processing.vo.OptimizationReq;
import com.zq.processing.vo.OptimizationVO;
public class DemoUtil {
final static String URL = "localhost:9100/imgproc/v1/";
public static void main(String[] args) {
detection();
// String testImg = "C:/Users/11419/Desktop/Deskew/TestImages/4.png";
// String resImg = "C:/Users/11419/Desktop/Deskew/TestImages/res4.jpg";
// System.out.println(Convert.toInt(-6.25));
// ImgUtil.rotate(FileUtil.file(testImg), Convert.toInt(-6.25), FileUtil.file(resImg));
}
public static void detection() {
String testImg = "C:/Users/11419/Desktop/Deskew/TestImages/3.png";
String resImg = "C:/Users/11419/Desktop/Deskew/TestImages/res.jpg";
String url = URL + "detection";
OptimizationReq req = new OptimizationReq();
req.setFilename("3.png");
req.setFileContent(Base64.encode(FileUtil.file(testImg)));
long start = System.currentTimeMillis();
HttpRequest request = HttpUtil.createPost(url)
.body(JSONUtil.toJsonStr(req));
try (HttpResponse response = request.execute()) {
System.out.println(System.currentTimeMillis() - start);
String res = response.body();
JSONObject object = JSONUtil.parseObj(res);
DetectionResVo detectionRes = object.get("data", DetectionResVo.class);
System.out.println(JSONUtil.toJsonStr(detectionRes));
}
}
public static void imageOptimization() {
String testImg = "C:/Users/11419/Desktop/Deskew/TestImages/2.png";
String resImg = "C:/Users/11419/Desktop/Deskew/TestImages/res.jpg";
String url = URL + "imageOptimization";
OptimizationReq req = new OptimizationReq();
req.setFilename("3.png");
req.setFileContent(Base64.encode(FileUtil.file(testImg)));
long start = System.currentTimeMillis();
HttpRequest request = HttpUtil.createPost(url)
.body(JSONUtil.toJsonStr(req));
try (HttpResponse response = request.execute()) {
System.out.println(System.currentTimeMillis()- start);
String res = response.body();
JSONObject object = JSONUtil.parseObj(res);
OptimizationVO optimizationVO = object.get("data", OptimizationVO.class);
FileUtil.writeBytes(Base64.decode(optimizationVO.getFileContent()), FileUtil.file(resImg));
optimizationVO.setFileContent(null);
System.out.println(JSONUtil.toJsonStr(optimizationVO));
}
}
}
package com.zq.processing.utils;
import lombok.Cleanup;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Size;
import org.bytedeco.opencv.opencv_core.UMat;
import org.bytedeco.opencv.opencv_imgproc.Vec4iVector;
import static org.bytedeco.opencv.global.opencv_core.CV_8UC3;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imread;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imwrite;
import static org.bytedeco.opencv.global.opencv_imgproc.*;
public class Deskew {
public static void main(String[] args) {
String testImg = "C:/Users/11419/Desktop/Deskew/TestImages/6.png";
String resImg = "C:/Users/11419/Desktop/Deskew/TestImages/res.png";
@Cleanup
Mat src = imread(testImg);
// 图片灰度化
@Cleanup
Mat gray = ImageUtil.gray(src);
// // 高斯模糊(?)
// @Cleanup
// UMat blur = gray.clone();
// GaussianBlur(gray, blur, new Size(9, 9), 0);
// // 二值化(?)
// @Cleanup
// UMat thresh = blur.clone();
// threshold(blur, thresh, 0, 255, THRESH_BINARY_INV + THRESH_OTSU);
@Cleanup
Mat kernel = getStructuringElement(MORPH_RECT, new Size(5, 5));
// 图片膨胀
Mat erode = gray.clone();
erode(gray, erode, kernel);
// 图片腐蚀
@Cleanup
Mat dilate = erode.clone();
dilate(erode, dilate, kernel);
// 边缘检测
@Cleanup
Mat canny = dilate.clone();
Canny(dilate, canny, 50, 150);
imwrite(resImg, canny);
// 霍夫变换得到线条
@Cleanup
Vec4iVector lines = new Vec4iVector();
//累加器阈值参数,小于设置值不返回
int threshold = 30;
//最低线段长度,低于设置值则不返回
double minLineLength = 0;
//间距小于该值的线当成同一条线
double maxLineGap = 200;
// 霍夫变换,通过步长为1,角度为PI/180来搜索可能的直线
HoughLinesP(canny, lines, 1, Math.PI / 180, threshold, minLineLength, maxLineGap);
}
public static void getDeskewAngle(String filePath) {
@Cleanup
Mat src = imread(filePath);
UMat usrc = src.getUMat(CV_8UC3);
// 图片灰度化
@Cleanup
UMat gray = ImageUtil.gray(usrc);
// // 高斯模糊(?)
// @Cleanup
// UMat blur = gray.clone();
// GaussianBlur(gray, blur, new Size(9, 9), 0);
// // 二值化(?)
// @Cleanup
// UMat thresh = blur.clone();
// threshold(blur, thresh, 0, 255, THRESH_BINARY_INV + THRESH_OTSU);
@Cleanup
Mat kernel = getStructuringElement(MORPH_RECT, new Size(5, 5));
// 图片膨胀
UMat erode = gray.clone();
erode(gray, erode, kernel.getUMat(CV_8UC3));
// 图片腐蚀
@Cleanup
UMat dilate = erode.clone();
dilate(erode, dilate, kernel.getUMat(CV_8UC3));
// 边缘检测
@Cleanup
UMat canny = dilate.clone();
Canny(dilate, canny, 50, 150);
// 霍夫变换得到线条
@Cleanup
Vec4iVector lines = new Vec4iVector();
//累加器阈值参数,小于设置值不返回
int threshold = 30;
//最低线段长度,低于设置值则不返回
double minLineLength = 0;
//间距小于该值的线当成同一条线
double maxLineGap = 200;
// 霍夫变换,通过步长为1,角度为PI/180来搜索可能的直线
HoughLinesP(canny.getMat(CV_8UC3), lines, 1, Math.PI / 180, threshold, minLineLength, maxLineGap);
}
}
package com.zq.processing.utils;
import cn.hutool.core.util.RuntimeUtil;
import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
/**
* <p>
* 纠偏工具Deskew
* </p>
*
* @author chenhao
* @since 2023/3/8 10:23
*/
public class ImageCorrectionUtil {
/**
* 纠正图片旋转
*
* @param srcImgPath 图片地址
*/
public static void correctImg(String srcImgPath) {
FileOutputStream fos = null;
try {
// 原始图片
File srcFile = new File(srcImgPath);
// 获取偏转角度
int angle = getAngle(srcFile);
if (angle != 90 && angle != 270) {
return;
}
// 原始图片缓存
BufferedImage srcImg = ImageIO.read(srcFile);
// 宽高互换
// 原始宽度
int imgWidth = srcImg.getHeight();
// 原始高度
int imgHeight = srcImg.getWidth();
// 中心点位置
double centerWidth = ((double) imgWidth) / 2;
double centerHeight = ((double) imgHeight) / 2;
// 图片缓存
BufferedImage targetImg = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_RGB);
// 旋转对应角度
Graphics2D g = targetImg.createGraphics();
g.rotate(Math.toRadians(angle), centerWidth, centerHeight);
g.drawImage(srcImg, (imgWidth - srcImg.getWidth()) / 2, (imgHeight - srcImg.getHeight()) / 2, null);
g.rotate(Math.toRadians(-angle), centerWidth, centerHeight);
g.dispose();
// 输出图片
fos = new FileOutputStream(srcFile);
ImageIO.write(targetImg, "jpg", fos);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
/**
* 获取图片旋转角度
*
* @param file 上传图片
* @return 图片旋转角度
*/
private static int getAngle(File file) throws Exception {
Metadata metadata = ImageMetadataReader.readMetadata(file);
for (Directory directory : metadata.getDirectories()) {
for (Tag tag : directory.getTags()) {
if ("Orientation".equals(tag.getTagName())) {
String orientation = tag.getDescription();
if (orientation.contains("90")) {
return 90;
} else if (orientation.contains("180")) {
return 180;
} else if (orientation.contains("270")) {
return 270;
}
}
}
}
return 0;
}
/**
* 调用deskew工具
* Usage:
* deskew [-o output] [-a angle] [-b color] [..] input
* input: Input image file
* Options:
* -o output: 输出图像文件路径(默认:out.png)
* -a angle: 最大期望倾斜角度(两个方向)(默认:10度)
* -b color: 背景颜色十六进制格式RRGGBB|LL|AARRGGBB(默认为黑色)
* Ext. options:
* -q filter: 用于旋转的重采样过滤器 (default: linear,
* values: nearest|linear|cubic|lanczos)
* -t a|treshold: 自动阈值或0..255的值 (default: a)
* -r rect: 仅在内容矩形中进行倾斜检测 (pixels):
* left,top,right,bottom (default: whole page)
* -f format: 输出像素格式 (values: b1|g8|rgb24|rgba32)
* -l angle: 如果倾斜角度较小,则跳过倾斜步骤 (default: 0.01)
* -g flags: Operational flags (any combination of):
* c - auto crop, d - detect only (no output to file)
* -s info: 信息转储 (any combination of):
* s - skew detection stats, p - program parameters, t - timings
* -c specs: 某些文件格式的输出压缩规格。可以定义几个规格-用逗号分隔。支持规格:
* jXX - JPEG compression quality, XX is in range [1,100(best)]
* tSCHEME - TIFF compression scheme: none|lzw|rle|deflate|jpeg|g4
* Supported file formats
* Input: BMP, JPG, PNG, JNG, GIF, DDS, TGA, PBM, PGM, PPM, PAM, PFM, TIF, PSD
* Output: BMP, JPG, PNG, JNG, GIF, DDS, TGA, PGM, PPM, PAM, PFM, TIF, PSD
*
* @return 调用结果
*/
public static String deskew(String src, String dst, String deskewUrl, boolean deskewAngle) {
StringBuilder str = new StringBuilder();
// 命令行位置
str.append(deskewUrl).append(" ");
// 背景颜色
str.append("-b").append(" ").append("FFFFFF").append(" ");
if (deskewAngle) {
// 图片旋转耗时很多,跳过图片旋转
str.append("-l").append(" ").append("80").append(" ");
}
// 输出路径
str.append("-o").append(" ").append(dst).append(" ");
// 输入路径
str.append(src);
return RuntimeUtil.execForStr(str.toString());
}
public static String deskew2(String src, String dst, String deskewpyUrl) {
StringBuilder str = new StringBuilder();
// python启动
str.append("python").append(" ");
// python文件位置
str.append(deskewpyUrl).append(" ");
// 输入文件
str.append("-i").append(" ").append(src).append(" ");
// 输出文件
str.append("-o").append(" ").append(dst).append(" ");
return RuntimeUtil.execForStr(str.toString());
}
public static void main(String[] args) {
String path1 = "C:\\Users\\11419\\Desktop\\Deskew\\TestImages\\3.png";
String path2 = "C:\\Users\\11419\\Desktop\\Deskew\\TestImages\\res3.png";
String du = "C:\\Users\\11419\\Desktop\\lib\\correct.py";
String res = deskew2(path1, path2, du);
System.out.println(res);
}
}
package com.zq.processing.utils;
import cn.hutool.core.convert.Convert;
import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
import lombok.Cleanup;
import org.apache.commons.imaging.ImageInfo;
import org.apache.commons.imaging.Imaging;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Scalar;
import org.bytedeco.opencv.opencv_core.Size;
import org.bytedeco.opencv.opencv_core.UMat;
import java.io.File;
import static org.bytedeco.opencv.global.opencv_core.*;
import static org.bytedeco.opencv.global.opencv_imgcodecs.*;
import static org.bytedeco.opencv.global.opencv_imgproc.*;
public class ImageUtil {
/**
* 图片灰度化
*
* @param mat 图片地址
*/
public static Mat gray(Mat mat) {
// 灰度化
Mat gray = mat.clone();
if (mat.channels() == 4 || mat.channels() == 3) {
cvtColor(mat, gray, COLOR_BGR2GRAY);
} else if (mat.channels() == 2) {
cvtColor(mat, gray, COLOR_BGR5652GRAY);
} else {
gray = mat;
}
return gray;
}
/**
* 图片灰度化
*
* @param mat 图片地址
*/
public static UMat gray(UMat mat) {
// 灰度化
UMat gray = mat.clone();
if (mat.channels() == 4 || mat.channels() == 3) {
cvtColor(mat, gray, COLOR_BGR2GRAY);
} else if (mat.channels() == 2) {
cvtColor(mat, gray, COLOR_BGR5652GRAY);
} else {
gray = mat;
}
return gray;
}
/**
* 图片灰度化
*
* @param src 图片地址
* @param dst 灰度化图片保存地址
*/
public static void gray(String src, String dst) {
@Cleanup
Mat mat = imread(src);
// 灰度化
@Cleanup
Mat gray = gray(mat);
imwrite(dst, gray);
}
/**
* 边缘检测
*
* @param src 图片地址
* @param dst 边缘检测图片保存地址
*/
public static void canny(String src, String dst) {
@Cleanup
Mat mat = imread(src);
@Cleanup
Mat cannyImg = canny(mat);
imwrite(dst, cannyImg);
}
/**
* 灰度化图片后进行canny边缘检测
*
* @param src 图片矩阵
* @return 边缘检测之后的图片矩阵
*/
private static Mat canny(Mat src) {
// 灰度化
@Cleanup
Mat gray = gray(src);
@Cleanup
Mat mat = gray.clone();
Canny(gray, mat, 60, 200);
return mat;
}
/**
* 获取图片DPI
*
* @param file 图片文件
* @return [水平DPI,垂直DPI]
*/
public static Integer getDpi(File file) throws Exception {
int res;
ImageInfo imageInfo = Imaging.getImageInfo(file);
res = imageInfo.getPhysicalWidthDpi();
if (res == -1) {
Metadata metadata = ImageMetadataReader.readMetadata(file);
for (Directory directory : metadata.getDirectories()) {
for (Tag tag : directory.getTags()) {
if ("X Resolution".equals(tag.getTagName())) {
res = Convert.toInt(tag.getDescription());
}
}
}
}
return res;
}
/**
* 计算图片清晰度
*
* @param path 图片路径
* @return 图片清晰度
*/
public static double imageSharpnessDetector(String path) {
@Cleanup
Mat image = imread(path);
@Cleanup
Mat grayImage = gray(image);
// 计算拉普拉斯
@Cleanup
Mat laplacian = new Mat();
Laplacian(grayImage, laplacian, CV_64F);
// 计算清晰度
@Cleanup
Mat mean = new Mat();
@Cleanup
Mat stdDev = new Mat();
meanStdDev(laplacian, mean, stdDev);
return Math.pow(stdDev.createIndexer().getDouble(0, 0, 0), 2.0);
}
/**
* 计算图片平均亮度
*
* @param path 图片路径
* @return 图片平均亮度
*/
public static double brightness(String path) {
@Cleanup
Mat grayImage = imread(path, IMREAD_GRAYSCALE);
// 计算图像的平均亮度
Scalar mean = mean(grayImage);
return mean.get(0);
}
/**
* 执行高斯模糊, UnsharpMasking
*
* @param path 图片路径
* @return 执行后的结果
*/
public static Mat unsharpMasking(String path) {
@Cleanup
Mat image = imread(path);
// 高斯模糊
Mat blurred = new Mat();
GaussianBlur(image, blurred, new Size(3, 3), 0);
// UnsharpMasking
@Cleanup
Mat unsharpMasked = new Mat();
addWeighted(image, 1.5, blurred, -0.5, 0, unsharpMasked);
return unsharpMasked;
}
}
package com.zq.processing.utils;
import lombok.Cleanup;
import org.bytedeco.javacpp.indexer.UByteIndexer;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Rect;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imread;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imwrite;
import static org.bytedeco.opencv.global.opencv_imgproc.COLOR_BGR2GRAY;
import static org.bytedeco.opencv.global.opencv_imgproc.cvtColor;
/**
* 去除黑边
*/
public class RemoveBlackUtil {
/**
* 去黑边"全黑"阈值
*/
private static final Integer BLACK_VALUE = 40;
/**
* 去除黑边
*
* @param src 原图片路径
* @param dst 处理后图片路径
*/
public static void remove(String src, String dst) {
@Cleanup
Mat mat = imread(src);
Mat res = removeBlackEdge(mat);
imwrite(dst, res);
}
/**
* 去除图片黑边,若无黑边,则原图返回。默认“全黑”阈值为 {@code BLACK_VALUE}
*
* @param srcMat 预去除黑边的Mat
* @return 去除黑边之后的Mat
*/
private static Mat removeBlackEdge(Mat srcMat) {
return removeBlackEdge(srcMat, BLACK_VALUE);
}
private static Mat removeBlackEdge(Mat srcMat, int blackValue) {
// 图片进行灰度化
@Cleanup
Mat grayMat = srcMat.clone();
cvtColor(srcMat, grayMat, COLOR_BGR2GRAY);
// 定义边界
int topRow = 0;
int leftCol = 0;
int rightCol = grayMat.cols() - 1;
int bottomRow = grayMat.rows() - 1;
UByteIndexer indexer = grayMat.createIndexer();
// 上方黑边判断
for (int row = 0; row < grayMat.rows(); row++) {
if (sum(grayMat.row(row), indexer) / grayMat.cols() < blackValue) {
topRow = row;
} else {
break;
}
}
// 左边黑边判断
for (int col = 0; col < grayMat.cols(); col++) {
if (sum(grayMat.col(col), indexer) / grayMat.rows() < blackValue) {
leftCol = col;
} else {
break;
}
}
// 右边黑边判断
for (int col = grayMat.cols() - 1; col > 0; col--) {
if (sum(grayMat.col(col), indexer) / grayMat.rows() < blackValue) {
rightCol = col;
} else {
break;
}
}
// 下方黑边判断
for (int row = grayMat.rows() - 1; row > 0; row--) {
if (sum(grayMat.row(row), indexer) / grayMat.cols() < blackValue) {
bottomRow = row;
} else {
break;
}
}
int x = leftCol;
int y = topRow;
int width = rightCol - leftCol;
int height = bottomRow - topRow;
if (leftCol == 0 && rightCol == grayMat.cols() - 1 && topRow == 0 && bottomRow == grayMat.rows() - 1) {
return srcMat;
}
return cut(srcMat, x, y, width, height);
}
/**
* 按照指定的尺寸截取Mat,坐标原点为左上角
*
* @param src 源Mat
* @param x x
* @param y y
* @param width width
* @param height height
* @return 截取后的Mat
*/
private static Mat cut(Mat src, int x, int y, int width, int height) {
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
if (width > src.cols()) {
width = src.cols();
}
if (height > src.rows()) {
height = src.rows();
}
// 截取尺寸
Rect rect = new Rect(x, y, width, height);
return new Mat(src, rect);
}
/**
* 像素求和
*
* @param mat mat
* @return sum
*/
private static int sum(Mat mat, UByteIndexer indexer) {
int sum = 0;
for (int row = 0; row < mat.rows(); row++) {
for (int col = 0; col < mat.cols(); col++) {
sum += indexer.get(row, col, 0);
}
}
return sum;
}
/**
* 黑边检测
*
* @param src 图片矩阵
* @return true表示可能存在黑边
*/
public static boolean blackDetection(Mat src) {
// 图片进行灰度化
@Cleanup
Mat grayMat = src.clone();
cvtColor(src, grayMat, COLOR_BGR2GRAY);
int width = src.cols();
int height = src.rows();
// 定义初始边界
int top = 0;
int left = 0;
int right = width - 1;
int bottom = height - 1;
UByteIndexer indexer = grayMat.createIndexer();
// 上方黑边判断
for (int row = 0; row < height; row++) {
if (sum(grayMat.row(row), indexer) / width < BLACK_VALUE) {
top = row;
} else {
break;
}
}
// 左边黑边判断
for (int col = 0; col < width; col++) {
if (sum(grayMat.col(col), indexer) / height < BLACK_VALUE) {
left = col;
} else {
break;
}
}
// 右边黑边判断
for (int col = width - 1; col > 0; col--) {
if (sum(grayMat.col(col), indexer) / height < BLACK_VALUE) {
right = col;
} else {
break;
}
}
// 下方黑边判断
for (int row = height - 1; row > 0; row--) {
if (sum(grayMat.row(row), indexer) / width < BLACK_VALUE) {
bottom = row;
} else {
break;
}
}
// 若是边界没有被更新,认为不存在黑边
return top != 0 || left != 0 || right != width - 1 || bottom != height - 1;
}
}
package com.zq.processing.utils;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
/**
* 统一的接口响应信息
*
* @author wilmiam
* @since 2021-07-09 17:45
*/
@ApiModel("API响应消息")
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class ResultVo<T> implements Serializable {
@ApiModelProperty(value = "成功标记", example = "true")
private boolean success;
@ApiModelProperty("错误码")
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
private int errCode;
@ApiModelProperty("错误信息")
private String errMsg;
@ApiModelProperty("响应的数据")
private T data;
public static ResultVo success() {
return success(null);
}
public static <E> ResultVo<E> success(E data) {
ResultVo<E> result = new ResultVo<>();
result.setSuccess(true);
result.setData(data);
return result;
}
public static ResultVo fail(String errMsg) {
return fail(500, errMsg);
}
public static ResultVo fail(int errCode, String errMsg) {
ResultVo result = new ResultVo<>();
result.setSuccess(false);
result.setErrCode(errCode);
result.setErrMsg(errMsg);
return result;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public int getErrCode() {
return errCode;
}
public void setErrCode(int errCode) {
this.errCode = errCode;
}
public String getErrMsg() {
return errMsg;
}
public void setErrMsg(String errMsg) {
this.errMsg = errMsg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
package com.zq.processing.utils;
public class TestUtil {
public static void main(String[] args) {
String testImg = "C:/Users/11419/Desktop/Deskew/TestImages/6.png";
String resImg = "C:/Users/11419/Desktop/Deskew/TestImages/res.png";
}
}
package com.zq.processing.utils;
import cn.hutool.core.io.FileUtil;
import com.zq.processing.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
/**
* @author wilmiam
* @since 2021-07-09 18:05
*/
@Slf4j
public class UploadUtils {
private final static String DATE_FORMAT = "/yyyyMM/dd/";
/**
* 保存临时文件
*
* @return 保存地址
*/
public static String saveTempFile(MultipartFile file, String systemName) {
try {
String originalFilename = file.getOriginalFilename();
//取文件扩展名
String ext = Objects.requireNonNull(originalFilename).substring(originalFilename.lastIndexOf(".") + 1);
//生成新文件名
String name = UuidUtils.uuidNoDash() + "." + ext;
String yyyyMMdd = new SimpleDateFormat(DATE_FORMAT).format(new Date());
File dest = new File("/data/temp/" + systemName + yyyyMMdd + name);
FileUtil.writeBytes(file.getBytes(), dest);
return "/data/temp/" + systemName + yyyyMMdd + name;
} catch (IOException e) {
log.error("文件保存失败:{}", e.getMessage());
throw new BusinessException("文件保存失败");
}
}
}
package com.zq.processing.utils;
import java.util.UUID;
/**
* Utility class that handles the uuid stuff.
*
* @author wilmiam
* @since 2021-07-09 18:05
*/
public class UuidUtils {
private static final String URN_UUID_REGEX = "^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$";
private static final String NODASH_UUID_REGEX = "^[a-f0-9]{32}$";
/**
* Don't let anyone instantiate this class
*/
private UuidUtils() {
}
/**
* 返回一个随机的带有分隔符"-"的36位UUID字符串
*
* @return
*/
public static String uuid() {
return UUID.randomUUID().toString();
}
/**
* 返回一个随机的没有分隔符"-"的32位UUID字符串
*
* @return
*/
public static String uuidNoDash() {
return uuid().replaceAll("-", "");
}
/**
* 判断一个字符串是否是一个UUID字符串
*
* @param str 要进行判断的字符串
* @return
*/
public static boolean isUuid(String str) {
return ValidateUtil.isNotBlank(str) && str.matches(URN_UUID_REGEX);
}
/**
* 判断一个字符串是否是一个没有分隔符的uuid字符串
*
* @param str 要进行判断的字符串
* @return
*/
public static boolean isNoDashUuid(String str) {
return ValidateUtil.isNotBlank(str) && str.matches(NODASH_UUID_REGEX);
}
}
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment