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.server;
import cn.hutool.core.codec.Base64Decoder;
import cn.hutool.core.codec.Base64Encoder;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.io.FileUtil;
import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
import com.zq.imgproc.utils.*;
import com.zq.imgproc.vo.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.imaging.ImageInfo;
import org.apache.commons.imaging.Imaging;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfDouble;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <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;
/**
* 黑边像素阈值
*/
private static final Integer THRESHOLD = 30;
public List<ImgVO> detection(List<ImgVO> pathList) throws Exception {
List<ImgVO> res = new ArrayList<>(pathList.size());
for (ImgVO one : pathList) {
DetectionResVo2 vo = new DetectionResVo2();
String path = one.getUrl();
Mat image = Imgcodecs.imread(path);
// 检测图片的分辨率
vo.setWidthResolution(image.width());
vo.setHeightResolution(image.height());
// 检测图片的DPI
vo.setDpi(getDpi(FileUtil.file(path)));
// 检测图片清晰度
vo.setClarity(clarityDetection(image));
// 检测图片的亮度
double[] arr = brightnessDetection(image);
if (arr != null) {
vo.setCast(arr[0]);
vo.setDa(arr[1]);
}
// 检测图片倾斜角度
vo.setAngle(getAngle(image));
// 检测图片的黑边
vo.setBlack(blackDetection(image));
one.setDetectionRes(vo);
res.add(one);
}
return res;
}
public DetectionResVo detection(MultipartFile file) throws Exception {
DetectionResVo res = new DetectionResVo();
Mat image = ImageUtil.getMat(file);
// 检测图片的分辨率
res.setWidthResolution(image.width());
res.setHeightResolution(image.height());
// 检测图片的DPI
res.setDpi(getDpi(file));
// 检测图片清晰度
res.setClarity(clarityDetection(image) > 10);
// 检测图片的亮度
res.setBrightness(brightness(brightnessDetection(image)));
// 检测图片倾斜角度
res.setAngle(getAngle(image));
// 检测图片的黑边
res.setBlack(blackDetection(image));
return res;
}
public DetectionResVo3 detection2(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));
}
DetectionResVo3 res = new DetectionResVo3();
Mat image = Imgcodecs.imread(filePath);
// 检测图片的分辨率
res.setWidthResolution(image.width());
res.setHeightResolution(image.height());
// 检测图片的DPI
res.setDpi(getDpi(FileUtil.file(filePath)));
// 检测图片清晰度
res.setClarity(ImageUtil.imageSharpnessDetector(image));
// 检测图片的亮度
res.setBrightness(ImageUtil.brightness(image));
// 检测图片倾斜角度
res.setAngle(Deskew.getDeskewAngle(image));
// 检测图片的黑边
res.setBlack(blackDetection(image));
image.release();
return res;
}
/**
* 图片亮度检测
* cast为计算出的偏差值,小于1表示比较正常,大于1表示存在亮度异常;当cast异常时,da大于0表示过亮,da小于0表示过暗
*
* @param src 图片矩阵
* @return [cast, da]
*/
private double[] brightnessDetection(Mat src) {
// 灰度化,转为灰度图
Mat gray = src.clone();
// 获取原图的列数,RGB图像为3列
if (3 == src.channels()) {
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
} else {
log.error("该图片不是RGB图像,灰度化失败!");
return null;
}
double a = 0;
int[] hist = new int[256];
for (int i = 0; i < gray.rows(); i++) {
for (int j = 0; j < gray.cols(); j++) {
a += gray.get(i, j)[0] - 128;
int index = (int) gray.get(i, j)[0];
hist[index]++;
}
}
double da = a / (gray.rows() * gray.cols());
double ma = 0;
for (int i = 0; i < hist.length; i++) {
ma += Math.abs(i - 128 - da) * hist[i];
}
ma = ma / (gray.rows() * gray.cols());
double cast = Math.abs(da) / Math.abs(ma);
return new double[]{cast, da};
}
/**
* 用默认标准判断图片亮度情况
*
* @return 图片的亮度情况,1表示亮度正常,2表示过亮,3表示过暗
*/
private Integer brightness(double[] arr) {
if (arr == null) {
return -1;
}
double cast = arr[0];
double da = arr[1];
if (cast < 1) {
return 1;
} else {
if (da > 0) {
return 2;
} else {
return 3;
}
}
}
/**
* 获取图片DPI
*
* @param file 图片文件
* @return [水平DPI,垂直DPI]
*/
private Integer getDpi(MultipartFile file) throws Exception {
int res;
ImageInfo imageInfo = Imaging.getImageInfo(file.getInputStream(), file.getOriginalFilename());
res = imageInfo.getPhysicalWidthDpi();
if (res == -1) {
Metadata metadata = ImageMetadataReader.readMetadata(file.getInputStream());
for (Directory directory : metadata.getDirectories()) {
for (Tag tag : directory.getTags()) {
if ("X Resolution".equals(tag.getTagName())) {
res = Convert.toInt(tag.getDescription());
}
}
}
}
return res;
}
/**
* 获取图片DPI
*
* @param file 图片文件
* @return [水平DPI,垂直DPI]
*/
private 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 src 图片矩阵
* @return true表示可能存在黑边
*/
private boolean blackDetection(Mat src) {
int width = src.width();
int height = src.height();
// 定义初始边界
int top = 0;
int left = 0;
int right = width - 1;
int bottom = height - 1;
// 上方黑边判断
for (int row = 0; row < height; row++) {
if (ImageUtil.sum(src.row(row)) / width < THRESHOLD) {
top = row;
} else {
break;
}
}
// 左边黑边判断
for (int col = 0; col < width; col++) {
if (ImageUtil.sum(src.col(col)) / height < THRESHOLD) {
left = col;
} else {
break;
}
}
// 右边黑边判断
for (int col = width - 1; col > 0; col--) {
if (ImageUtil.sum(src.col(col)) / height < THRESHOLD) {
right = col;
} else {
break;
}
}
// 下方黑边判断
for (int row = height - 1; row > 0; row--) {
if (ImageUtil.sum(src.row(row)) / width < THRESHOLD) {
bottom = row;
} else {
break;
}
}
// 若是边界没有被更新,认为不存在黑边
return top != 0 || left != 0 || right != width - 1 || bottom != height - 1;
}
/**
* 图像清晰度,求出灰度图的平均灰度和标准差
* 平均值和方差越大,代表图片越清晰
*
* @param src 图片矩阵
* @return [mean, std] 灰度图的平均灰度和标准差
*/
private double clarityDetection(Mat src) {
Mat gray = src.clone();
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
// 拉普拉斯平滑处理
// 阈值太低会导致正常图片被误断为模糊图片,阈值太高会导致模糊图片被误判为正常图片
Mat laplacImg = new Mat();
Imgproc.Laplacian(gray, laplacImg, CvType.CV_64F);
// 标准差,stddev.get(0,0)[0]
MatOfDouble stddev = new MatOfDouble();
// 平均灰度值,mean.get(0,0)[0]
MatOfDouble mean = new MatOfDouble();
Core.meanStdDev(laplacImg, mean, stddev);
double[] stds = stddev.get(0, 0);
return stds[0];
}
/**
* 灰度化图片后进行canny边缘检测
*
* @param src 图片矩阵
* @return 边缘检测之后的图片矩阵
*/
private Mat canny(Mat src) {
// 灰度化
Mat gray = src.clone();
if (src.channels() == 4 || src.channels() == 3) {
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
} else if (src.channels() == 2) {
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR5652GRAY);
} else {
gray = src;
}
Mat mat = gray.clone();
Imgproc.Canny(gray, mat, 60, 200);
return mat;
}
/**
* 通过霍夫变换后,获取直线求出平均倾斜角度
* threshold
* 累加平面的阈值参数,int类型,超过设定阈值才被检测出线段,值越大,基本上意味着检出的线段越长,检出的线段个数越少。
* minLineLength
* 线段以像素为单位的最小长度,根据应用场景设置
* maxLineGap
* 同一方向上两条线段判定为一条线段的最大允许间隔(断裂),超过了设定值,则把两条线段当成一条线段,值越大,允许线段上的断裂越大,越有可能检出潜在的直线段
*
* @param mat 图片
* @return 倾斜角度
*/
private double getAngle(Mat mat) {
Mat canny = ImageUtil.canny(mat, 60, 200, 3);
Mat lines = new Mat();
//累加器阈值参数,小于设置值不返回
int threshold = 30;
//最低线段长度,低于设置值则不返回
double minLineLength = 0;
//间距小于该值的线当成同一条线
double maxLineGap = 200;
// 霍夫变换,通过步长为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;
}
lines.release();
canny.release();
// 可能还得需要考虑方差来决定选择平均数还是众数
return most(angelList);
}
/**
* 通过矫正工具获取的倾斜角度
*
* @param imageUrl 图片路径
* @return 图片倾斜角度
*/
private double getAngle2(String imageUrl) {
// 纠正文件的保存路径
String ext = FileUtil.extName(imageUrl);
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/data/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
String deskewRes = ImageCorrectionUtil.deskew(imageUrl, savePath, deskewUrl, true);
if (deskewRes.contains("Error")) {
return 0.0d;
} else {
Pattern pattern = Pattern.compile("Skew angle found \\[deg\\]: (-?\\d+(?:\\.\\d+)?)");
Matcher matcher = pattern.matcher(deskewRes);
if (matcher.find()) {
return Double.parseDouble(matcher.group(1));
} else {
return 0.0d;
}
}
}
/**
* 通过矫正工具获取的倾斜角度
*
* @param imageUrl 图片路径
* @return 图片倾斜角度
*/
private Double getAngle3(String imageUrl) {
// 纠正文件的保存路径
String ext = FileUtil.extName(imageUrl);
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/data/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
String deskewRes = ImageCorrectionUtil.deskew2(imageUrl, savePath, deskewpyUrl);
Double angle = Convert.toDouble(deskewRes);
return angle == null ? getAngle2(imageUrl) : angle;
}
/**
* 求数组平均数,四舍五入保留一位
*
* @param angelList 数组
* @return 数组平均数
*/
private static double avg(List<Integer> angelList) {
int sum = 0;
for (int i : angelList) {
sum += i;
}
if (sum == 0) {
return 0.0;
}
BigDecimal bigDecimal = new BigDecimal(sum).divide(new BigDecimal(angelList.size()), RoundingMode.HALF_UP);
return bigDecimal.setScale(1, RoundingMode.HALF_UP).doubleValue();
}
/**
* 求数组众数
*
* @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 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));
}
}
public List<ImgVO> imageCorrection(List<ImgVO> pathList) {
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/data/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "/";
int index = 1;
for (ImgVO one : pathList) {
String path = one.getUrl();
String ext = FileUtil.extName(one.getFileName());
ImageCorrectionUtil.correctImg(path);
Mat image = Imgcodecs.imread(path);
double angle = getAngle(canny(image));
// 通过倾斜角度旋转图片
ImgUtil.rotate(FileUtil.file(path), Convert.toInt(angle) , FileUtil.file(savePath + index + "." + ext));
one.setUrl(savePath + index + "." + ext);
index++;
}
return pathList;
}
public List removeBlack(List<ImgVO> pathList) {
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/data/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "/";
int index = 1;
// 除黑边
for (ImgVO one : pathList) {
String ext = FileUtil.extName(one.getFileName());
RemoveBlackUtil2.remove(one.getUrl(), savePath + index + "." + ext);
one.setUrl(savePath + index + "." + ext);
index++;
}
// 修正
return ImageUtil.correct(pathList);
}
public List<ImgVO> deskew(List<ImgVO> pathList) {
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/data/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "/";
int index = 1;
for (ImgVO one : pathList) {
String ext = FileUtil.extName(one.getFileName());
if (ext.equals("heic")) {
Mat mat = Imgcodecs.imread(one.getUrl());
double angle = getAngle(mat);
ImageUtil.rotateImage(one.getUrl(), savePath + index + "." + ext, angle);
} else {
ImageCorrectionUtil.deskew(one.getUrl(), savePath + index + "." + ext, deskewUrl, false);
}
one.setUrl(savePath + index + "." + ext);
index++;
}
return pathList;
}
public List<ImgVO> rotate(RotateReq req) {
List<ImgVO> pathList = req.getPathList();
Integer degree = req.getDegree();
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/data/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "/";
int index = 1;
for (ImgVO one : pathList) {
String ext = FileUtil.extName(one.getFileName());
ImageUtil.rotateImage(one.getUrl(), savePath + index + "." + ext, degree);
one.setUrl(savePath + index + "." + ext);
index++;
}
return pathList;
}
public List<ImgVO> gray(List<ImgVO> pathList) {
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/data/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "/";
int index = 1;
for (ImgVO one : pathList) {
String ext = FileUtil.extName(one.getFileName());
ImageUtil.gray(one.getUrl(), savePath + index + "." + ext);
one.setUrl(savePath + index + "." + ext);
index++;
}
return pathList;
}
public List<ImgVO> canny(List<ImgVO> pathList) {
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/data/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "/";
int index = 1;
for (ImgVO one : pathList) {
String ext = FileUtil.extName(one.getFileName());
Mat src = Imgcodecs.imread(one.getUrl());
Mat cannyImg = canny(src);
ImageUtil.saveImage(cannyImg, savePath + index + "." + ext);
one.setUrl(savePath + index + "." + ext);
index++;
}
return pathList;
}
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;
// 3. 黑边处理
long start = System.currentTimeMillis();
String blackUrl = savePath + "(1)." + ext;
Mat aMat = Imgcodecs.imread(newPath);
if (blackDetection(aMat)) {
res.setRemoveBlack(true);
RemoveBlackUtil2.remove(newPath, blackUrl);
newPath = blackUrl;
} else {
res.setRemoveBlack(false);
}
aMat.release();
log.info("黑边处理消耗的时间【{}】", System.currentTimeMillis() - start);
// 2. 执行纠偏
start = System.currentTimeMillis();
String deskerFile = savePath + "(2)." + ext;
Mat bMat = Imgcodecs.imread(newPath);
double skewAngle = Deskew.getDeskewAngle(bMat);
if (Double.compare(skewAngle, 1) < 0 && Double.compare(skewAngle, -1) > 0) {
res.setDeskewAngel(0);
} else {
Mat rotateMat = Deskew.rotate(bMat, skewAngle);
Imgcodecs.imwrite(deskerFile, rotateMat);
newPath = deskerFile;
res.setDeskewAngel(skewAngle);
rotateMat.release();
}
bMat.release();
log.info("纠偏所需时间【{}】", System.currentTimeMillis() - start);
// 4。 图片亮度检测
// 计算图像的平均亮度
start = System.currentTimeMillis();
Mat cMat = Imgcodecs.imread(newPath);
double brightness = ImageUtil.brightness(cMat);
res.setOriginalBrightnessVal(brightness);
// 亮度异常执行亮度矫正
if (brightness < 100 || brightness > 500) {
res.setBrightness(true);
// 计算亮度调整值
double alpha = 300 / brightness;
// 执行亮度调整
Mat grayImage = Imgcodecs.imread(newPath);
Mat adjustedImage = new Mat();
grayImage.convertTo(adjustedImage, -1, alpha, 0);
// 保存调整后的图像
Imgcodecs. imwrite(newPath, adjustedImage);
res.setBrightnessVal(ImageUtil.brightness(newPath));
adjustedImage.release();
grayImage.release();
} else {
res.setBrightness(false);
res.setBrightnessVal(0);
}
cMat.release();
log.info("亮度检测所需时间【{}】", System.currentTimeMillis() - start);
// 5. 图片清晰度检测
start = System.currentTimeMillis();
Mat dMat = Imgcodecs.imread(newPath);
double laplacianScore = ImageUtil.imageSharpnessDetector(dMat);
res.setOriginalClarityVal(laplacianScore);
if (laplacianScore < 500) {
res.setClarity(true);
Mat unsharpMaskingMat = ImageUtil.unsharpMasking(dMat);
// 保存调整后的图像
Imgcodecs.imwrite(newPath, unsharpMaskingMat);
res.setClarityVal(ImageUtil.imageSharpnessDetector(unsharpMaskingMat));
unsharpMaskingMat.release();
} else {
res.setClarity(false);
res.setClarityVal(0);
}
dMat.release();
log.info("图片清晰度所需时间【{}】", System.currentTimeMillis() - start);
res.setFilename(filename);
res.setFileContent(Base64Encoder.encode(FileUtil.readBytes(newPath)));
res.setCorrect(false);
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;
}
public Boolean recognizeRed(OptimizationReq req) {
// 暂时保存
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);
Mat mat = Imgcodecs.imread(filePath);
// 转为HSV空间
Mat hsv = new Mat();
Imgproc.cvtColor(mat, hsv, Imgproc.COLOR_BGR2HSV);
int nums = 0;
for (int i = 0; i < hsv.rows(); i++) {
for (int j = 0; j < hsv.cols(); j++) {
double[] clone = hsv.get(i, j).clone();
double h = clone[0];
double s = clone[1];
double v = clone[2];
// 红色的hsv范围判断
if ((h > 0 && h < 10) || (h > 156 && h < 180)) {
if (s > 43 && s < 255) {
if (v < 255 && v > 46) {
nums++;
}
}
}
}
}
FileUtil.del(FileUtil.file(filePath));
return nums > 8000;
}
public String removeRed(OptimizationReq req) {
// 暂时保存
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);
Mat mat = Imgcodecs.imread(filePath);
List<Mat> mats = new ArrayList<>();
// 分离图片通道
Core.split(mat, mats);
// 获取红色通道的矩阵
Mat red = mats.get(2);
// 二值化
Mat threshRed = new Mat();
Imgproc.threshold(red, threshRed, 120, 255, Imgproc.THRESH_BINARY);
String dst = savePath + "test." + ext;
Imgcodecs.imwrite(dst, threshRed);
return Base64Encoder.encode(FileUtil.readBytes(dst));
}
}
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 cn.hutool.core.img.ImgUtil;
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.exception.BusinessException;
import com.zq.imgproc.vo.ImgVO;
import org.apache.commons.imaging.ImageInfo;
import org.apache.commons.imaging.ImageReadException;
import org.apache.commons.imaging.Imaging;
import org.apache.commons.io.FileUtils;
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.springframework.web.multipart.MultipartFile;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
/**
* <p>
* 图像处理工具类
* </p>
*
* @author chenhao
* @since 2023/3/10 15:06
*/
public class ImageUtil {
/**
* 使用Graphics2D进行旋转图片
*
* @param image 需要旋转的图片
* @param degree 旋转的角度
* @return 旋转后的图片
*/
public static BufferedImage rotateImage(BufferedImage image, double degree) {
int width = image.getWidth();
int height = image.getHeight();
int type = image.getType();
BufferedImage res = new BufferedImage(width, height, type);
Graphics2D graphics = res.createGraphics();
graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
// 设置图片底色
graphics.setBackground(Color.BLACK);
// 填充底图
graphics.fillRect(0, 0, width, height);
// 按中心点旋转图片
graphics.rotate(Math.toRadians(degree), width >> 1, height >> 1);
graphics.drawImage(image, 0, 0, null);
// 关闭Graphics2D
graphics.dispose();
return res;
}
/**
* 使用Graphics2D进行旋转图片
*
* @param src 输入路径
* @param dst 输出路径
* @param degree 旋转角度
*/
public static void rotateImage(String src, String dst, double degree) {
BufferedImage image = ImgUtil.read(src);
ImgUtil.write(rotateImage(image, degree), FileUtil.file(dst));
}
/**
* 将图片转换为BufferedImage.TYPE_3BYTE_BGR
*
* @param image 图片流
* @return 图片流
*/
public static BufferedImage convert(BufferedImage image) {
BufferedImage convertedImage = new BufferedImage(image.getWidth(), image.getHeight(),
BufferedImage.TYPE_3BYTE_BGR);
convertedImage.getGraphics().drawImage(image, 0, 0, null);
return convertedImage;
}
/**
* 根据文件转换为mat对象
*
* @param file 文件
* @return Mat
*/
public static Mat getMat(MultipartFile file) throws IOException, ImageReadException {
ImageInfo imageInfo = Imaging.getImageInfo(file.getInputStream(), file.getOriginalFilename());
InputStream inputStream = file.getInputStream();
BufferedImage image = ImgUtil.read(inputStream);
image = ImageUtil.convert(image);
DataBuffer data = image.getRaster().getDataBuffer();
byte[] bytes = ((DataBufferByte) data).getData();
Mat mat = new Mat(imageInfo.getHeight(), imageInfo.getWidth(), CvType.CV_8UC3);
mat.put(0, 0, bytes);
return mat;
}
/**
* 保存图片到指定位置
*
* @param mat 图片文件
* @param filePath 文件路径
*/
public static void saveImage(Mat mat, String filePath) {
MatOfByte matOfByte = new MatOfByte();
Imgcodecs.imencode(".png", mat, matOfByte);
byte[] byteArray = matOfByte.toArray();
FileUtil.writeBytes(byteArray, filePath);
}
/**
* 像素求和
*
* @param mat mat
* @return sum
*/
public static int sum(Mat mat) {
int sum = 0;
for (int row = 0; row < mat.height(); row++) {
for (int col = 0; col < mat.width(); col++) {
sum += mat.get(row, col)[0];
}
}
return sum;
}
/**
* 图片灰度化
*
* @param src 图片地址
* @param dst 灰度化图片保存地址
*/
public static void gray(String src, String dst) {
// 灰度化
Mat mat = Imgcodecs.imread(src);
Mat gray = mat.clone();
if (mat.channels() == 4 || mat.channels() == 3) {
Imgproc.cvtColor(mat, gray, Imgproc.COLOR_BGR2GRAY);
} else if (mat.channels() == 2) {
Imgproc.cvtColor(mat, gray, Imgproc.COLOR_BGR5652GRAY);
} else {
gray = mat;
}
saveImage(gray, dst);
}
/**
* 边缘检测
*
* @param src 图片地址
* @param dst 边缘检测图片保存地址
*/
public static void canny(String src, String dst) {
Mat mat = Imgcodecs.imread(src);
Mat cannyImg = canny(mat);
ImageUtil.saveImage(cannyImg, dst);
}
/**
* 灰度化图片后进行canny边缘检测
*
* @param src 图片矩阵
* @return 边缘检测之后的图片矩阵
*/
private static Mat canny(Mat src) {
// 灰度化
Mat gray = src.clone();
if (src.channels() == 4 || src.channels() == 3) {
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
} else if (src.channels() == 2) {
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR5652GRAY);
} else {
gray = src;
}
Mat mat = gray.clone();
Imgproc.Canny(gray, mat, 60, 200);
return mat;
}
/**
* 灰度化图片后进行canny边缘检测
*/
public static Mat canny(Mat src, int threshold1, int threshold2, int apertureSize) {
// 灰度化
Mat gray = new Mat();
if (src.channels() == 4 || src.channels() == 3) {
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
} else if (src.channels() == 2) {
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR5652GRAY);
} else {
gray = src;
}
Mat mat = new Mat();
Imgproc.Canny(gray, mat, threshold1, threshold2, apertureSize);
gray.release();
return mat;
}
/**
* 保存临时图片
*
* @param file 图片文件
* @return 图片保存地址
*/
public static String saveTempFile(MultipartFile file) {
try {
String originalFilename = file.getOriginalFilename();
//取文件扩展名
String ext = Objects.requireNonNull(originalFilename).substring(originalFilename.lastIndexOf(".") + 1);
//生成新文件名
String name = UuidUtils.uuidNoDash() + "." + ext;
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
File dest = new File("/file/temp/" + yyyyMMdd + name);
FileUtils.writeByteArrayToFile(dest, file.getBytes());
return "/file/temp" + yyyyMMdd + name;
} catch (IOException e) {
throw new BusinessException("文件保存失败");
}
}
/**
* 图片弯曲矫正
*
* @param pathList 图片列表
* @return 弯曲矫正之后的图片列表
*/
public static List correct(List<ImgVO> pathList) {
String url = "http://ddns.gxmailu.com:18888/imgproc/correct";
String body = JSONUtil.toJsonStr(pathList);
String res = HttpUtil.post(url, body);
JSONObject jsonObject = JSONUtil.parseObj(res);
if (jsonObject.getByPath("success", Boolean.class)) {
return jsonObject.getByPath("data", ArrayList.class);
} else {
return pathList;
}
}
/**
* 计算图片清晰度
*
* @param path 图片路径
* @return 图片清晰度
*/
public static double imageSharpnessDetector(String path) {
Mat image = Imgcodecs.imread(path);
Mat grayImage = new Mat();
Imgproc.cvtColor(image, grayImage, Imgproc.COLOR_BGR2GRAY);
// 计算拉普拉斯
Mat laplacian = new Mat();
Imgproc.Laplacian(grayImage, laplacian, CvType.CV_64F);
// 计算清晰度
MatOfDouble mean = new MatOfDouble();
MatOfDouble stdDev = new MatOfDouble();
Core.meanStdDev(laplacian, mean, stdDev);
laplacian.release();
// 计算拉普拉斯分数
return Math.pow(stdDev.get(0,0)[0], 2.0);
}
/**
* 计算图片清晰度
*
* @param image 图片矩阵
* @return 图片清晰度
*/
public static double imageSharpnessDetector(Mat image) {
// 图片灰度化
Mat grayImage = image.clone();
Imgproc.cvtColor(image, grayImage, Imgproc.COLOR_BGR2GRAY);
// 拉普拉斯计算
Mat laplacian = new Mat();
Imgproc.Laplacian(grayImage, laplacian, CvType.CV_64F);
// 计算清晰度
MatOfDouble mean = new MatOfDouble();
MatOfDouble stdDev = new MatOfDouble();
Core.meanStdDev(laplacian, mean, stdDev);
// 释放内存
laplacian.release();
grayImage.release();
// 计算拉普拉斯分数
return Math.pow(stdDev.get(0,0)[0], 2.0);
}
/**
* 计算图片平均亮度
*
* @param path 图片路径
* @return 图片平均亮度
*/
public static double brightness(String path) {
Mat grayImage = Imgcodecs.imread(path, Imgcodecs.IMREAD_GRAYSCALE);
// 计算图像的平均亮度
Scalar mean = Core.mean(grayImage);
grayImage.release();
return mean.val[0];
}
/**
* 计算图片平均亮度
*
* @param image 图片路径
* @return 图片平均亮度
*/
public static double brightness(Mat image) {
// 图片灰度化
Mat grayImage = image.clone();
Imgproc.cvtColor(image, grayImage, Imgproc.COLOR_BGR2GRAY);
// 计算图像的平均亮度
Scalar mean = Core.mean(grayImage);
// 释放内存
grayImage.release();
return mean.val[0];
}
/**
* 执行高斯模糊, UnsharpMasking
*
* @param path 图片路径
* @return 执行后的结果
*/
public static Mat unsharpMasking(String path) {
Mat image = Imgcodecs.imread(path);
// 高斯模糊
Mat blurred = new Mat();
Imgproc.GaussianBlur(image, blurred, new Size(3, 3), 0);
// UnsharpMasking
Mat unsharpMasked = new Mat();
Core.addWeighted(image, 1.5, blurred, -0.5, 0, unsharpMasked);
image.release();
blurred.release();
return unsharpMasked;
}
/**
* 执行高斯模糊, UnsharpMasking
*
* @param image 图片路径
* @return 执行后的结果
*/
public static Mat unsharpMasking(Mat image) {
// 高斯模糊
Mat blurred = new Mat();
Imgproc.GaussianBlur(image, blurred, new Size(3, 3), 0);
// UnsharpMasking
Mat unsharpMasked = new Mat();
Core.addWeighted(image, 1.5, blurred, -0.5, 0, unsharpMasked);
blurred.release();
return unsharpMasked;
}
}
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.utils;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 验证工具类
*
* @author wilmiam
* @since 2021-07-09 18:05
*/
public class ValidateUtil {
/**
* URL验证正则表达式
*/
private static final String URL_REGEX = "^(http|https)\\://([a-zA-Z0-9\\.\\-]+(\\:[a-zA-Z0-9\\.&%\\$\\-]+)*@)*((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])|localhost|([a-zA-Z0-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{1,10}))(\\:[0-9]+)*(/($|[a-zA-Z0-9\\.\\,\\?\\'\\\\\\+&%\\$#\\=~_\\-]+))*$";
/**
* 安全SQL验证正则表达式
*/
private static final String SAFE_SQL_REGEX = "[-|;|,|\\/|\\(|\\)|\\[|\\]|\\}|\\{|%|@|\\*|!|\\']";
/**
* 身份证验证加权因子数组,将前17位加权因子保存在数组里
*/
private static final int[] IDCARD_WI = new int[]{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};
/**
* 身份证验证校验码数组,这是除以11后,可能产生的11位余数、验证码,也保存
*/
private static final int[] IDCARD_Y = new int[]{1, 0, 10, 9, 8, 7, 6, 5, 4, 3, 2};
/**
* 身份证验证,最后一位为X时,校验码所在数组的位置
*/
private static final int IDCARD_MOD_X = 2;
/**
* 身份证验证,当校验码为10时,最后一位必须是X
*/
private static final String IDCARD_LAST_X = "X";
/**
* 身份证校验正则表达式
*/
private static final String IDCARD_REGEX = "^[1-9]\\d{7}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}$|^[1-9]\\d{5}[1-9]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}([0-9]|X)$";
/**
* 身份证号码长度
*/
private static final int IDCARD_LENG = 18;
/**
* 单个IP的正则表达式
*/
private static final String SINGLE_IP_REGEX = "([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}";
/**
* 以逗号分隔的多个IP的正则表达式
*/
private static final String IP_REGEX = SINGLE_IP_REGEX + "(," + SINGLE_IP_REGEX + ")*";
private static final Class<?>[] NUMBER_TYPES = {
Byte.class, Short.class, Integer.class, Long.class, BigInteger.class, Float.class, Double.class, BigDecimal.class};
/**
* 判断对象是否为空
*
* @param obj 要判断的对象
* @return 判断结果
*/
public static boolean isBlank(final Object obj) {
if (obj == null) {
return true;
}
if (obj instanceof String) {
return "".equals(((String) obj).trim());
}
if (obj instanceof Collection) {
final Collection collection = (Collection) obj;
return collection.isEmpty();
}
return false;
}
/**
* 判断对象是否不为空
*
* @param obj
* @return
*/
public static boolean isNotBlank(final Object obj) {
return !isBlank(obj);
}
/**
* 检查参数是否有空
*
* @param objects 待验证对象,可以是多个
* @return 验证结果
*/
public static boolean hasBlank(final Object... objects) {
for (final Object obj : objects) {
if (isBlank(obj)) {
return true;
}
}
return false;
}
/**
* 判断是否是Base64字符串
*
* @param str Base64字符串
* @return 判断结果
*/
public static boolean isBase64(final String str) {
return isMatch(str, "[A-Za-z0-9\\+\\/\\=]");
}
/**
* 判断是否是Email地址字符串
*
* @param strEmail URL地址字符串
* @return 判断结果
*/
public static boolean isEmail(final String strEmail) {
return isMatch(strEmail, "^[\\w-]+(\\.[\\w-]+)*@[\\w-]+(\\.[\\w-]+)+$");
}
/**
* 验证手机号码正确性
*
* @param strMobilePhone 手机号码
* @return
*/
public static boolean isMobilePhoneNo(final String strMobilePhone) {
return isMatch(strMobilePhone, "^0?(13[0-9]|14[5-9]|15[012356789]|166|17[0-8]|18[0-9]|19[8-9])[0-9]{8}$");
}
/**
* 是否为手机号码
*
* @param phone
* @return
*/
public static boolean isInternationalPhoneNo(final String phone) {
return isMatch(phone, "^[0-9]{4,14}$");
}
/**
* 判断是否为正整数字符串
*
* @param intStr 数字字符串
* @return 判断结果
*/
public static boolean isInt(final String intStr) {
return isMatch(intStr, "^[0-9]*$");
}
/**
* 判断是否为IP字符串
*
* @param ipStr ip字符串
* @return 判断结果
*/
public static boolean isIp(final String ipStr) {
return isMatch(ipStr, "^((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)$");
}
/**
* 判断字符串是否和正则表达式相匹配,大小写敏感
*
* @param str 字符串
* @param regEx 正则表达式
* @return 判断结果
*/
public static boolean isMatch(final String str, final String regEx) {
return isMatch(str, regEx, false);
}
/**
* 判断字符串是否和正则表达式相匹配
*
* @param str 字符串
* @param regEx 正则表达式
* @param caseInsensetive 是否不区分大小写, true为不区分, false为区分
* @return 判断结果
*/
public static boolean isMatch(final String str, final String regEx, final boolean caseInsensetive) {
if (ValidateUtil.isNotBlank(str) && ValidateUtil.isNotBlank(regEx)) {
Pattern pattern;
if (caseInsensetive) {
pattern = Pattern.compile(regEx, Pattern.CASE_INSENSITIVE);
} else {
pattern = Pattern.compile(regEx);
}
final Matcher matcher = pattern.matcher(str);
return matcher.find();
}
return false;
}
/**
* 判断是否是数字
*
* @param strNumber 数字字符串
* @return 判断结果
*/
public static boolean isNumber(final String strNumber) {
return isMatch(strNumber, "^\\d+$");
}
/**
* 判断是否是Sql危险字符
*
* @param sqlStr sql字符串
* @return 判断结果
*/
public static boolean isSafeSqlString(final String sqlStr) {
return isMatch(sqlStr, SAFE_SQL_REGEX);
}
/**
* 判断是否是URL地址字符串
*
* @param strUrl URL地址字符串
* @return 判断结果
*/
public static boolean isURL(final String strUrl) {
return isMatch(strUrl, URL_REGEX);
}
/**
* 检查对象类型是不是Number类型
*
* @param type
* @return
*/
public static boolean isNumber(Class<?> type) {
// Class为final类,其equals不能重写,而Class的equals方法是直接继承自Object类
// return (this == obj)
// 所以在比较Class是否相等时,使用equals和使用==是一样的
return Arrays.stream(NUMBER_TYPES).anyMatch(numberType -> numberType.equals(type));
}
/**
* 身份证15位编码规则:dddddd yymmdd xx p
* dddddd:6位地区编码
* yymmdd: 出生年(两位年)月日,如:910215
* xx: 顺序编码,系统产生,无法确定
* p: 性别,奇数为男,偶数为女
* <p>
* 身份证18位编码规则:dddddd yyyymmdd xxx y
* dddddd:6位地区编码
* yyyymmdd: 出生年(四位年)月日,如:19910215
* xxx:顺序编码,系统产生,无法确定,奇数为男,偶数为女
* y: 校验码,该位数值可通过前17位计算获得
* <p>
* 前17位号码加权因子为 Wi = [ 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 ]
* 验证位 Y = [ 1, 0, 10, 9, 8, 7, 6, 5, 4, 3, 2 ]
* 如果验证码恰好是10,为了保证身份证是十八位,那么第十八位将用X来代替
* 校验位计算公式:Y_P = mod( ∑(Ai×Wi),11 )
* i为身份证号码1...17 位; Y_P为校验码Y所在校验码数组位置
*/
public static boolean isIdCardNo(final String idCardNo) {
if (!isMatch(idCardNo, IDCARD_REGEX)) {
return false;
}
if (idCardNo.length() == IDCARD_LENG) {
// 用来保存前17位各自乖以加权因子后的总和
int idCardWiSum = 0;
for (int i = 0; i < IDCARD_WI.length; i++) {
idCardWiSum += Integer.parseInt(idCardNo.substring(i, i + 1)) * IDCARD_WI[i];
}
// 计算出校验码所在数组的位置
int idCardMod = idCardWiSum % 11;
// 得到最后一位身份证号码
String idCardLast = idCardNo.substring(17);
if (idCardMod == IDCARD_MOD_X) {
return IDCARD_LAST_X.equalsIgnoreCase(idCardLast);
} else {
//用计算出的验证码与最后一位身份证号码匹配,如果一致,说明通过,否则是无效的身份证号码
return String.valueOf(IDCARD_Y[idCardMod]).equals(idCardLast);
}
}
return true;
}
/**
* 判断字符串是否是以逗号分隔的IP字符串。
* <p>
* 逗号必须是英文半角, 单个ip也认为符合要求, 空字符串返回false
* </p>
*
* @param ipStr
* @return
*/
public static boolean isComaSplitIp(String ipStr) {
boolean result = false;
if (ValidateUtil.isNotBlank(ipStr)) {
result = ipStr.matches(IP_REGEX);
}
return result;
}
}
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);
}
}
package com.zq.processing.utils;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 验证工具类
*
* @author wilmiam
* @since 2021-07-09 18:05
*/
public class ValidateUtil {
/**
* URL验证正则表达式
*/
private static final String URL_REGEX = "^(http|https)\\://([a-zA-Z0-9\\.\\-]+(\\:[a-zA-Z0-9\\.&%\\$\\-]+)*@)*((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])|localhost|([a-zA-Z0-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{1,10}))(\\:[0-9]+)*(/($|[a-zA-Z0-9\\.\\,\\?\\'\\\\\\+&%\\$#\\=~_\\-]+))*$";
/**
* 安全SQL验证正则表达式
*/
private static final String SAFE_SQL_REGEX = "[-|;|,|\\/|\\(|\\)|\\[|\\]|\\}|\\{|%|@|\\*|!|\\']";
/**
* 身份证验证加权因子数组,将前17位加权因子保存在数组里
*/
private static final int[] IDCARD_WI = new int[]{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};
/**
* 身份证验证校验码数组,这是除以11后,可能产生的11位余数、验证码,也保存
*/
private static final int[] IDCARD_Y = new int[]{1, 0, 10, 9, 8, 7, 6, 5, 4, 3, 2};
/**
* 身份证验证,最后一位为X时,校验码所在数组的位置
*/
private static final int IDCARD_MOD_X = 2;
/**
* 身份证验证,当校验码为10时,最后一位必须是X
*/
private static final String IDCARD_LAST_X = "X";
/**
* 身份证校验正则表达式
*/
private static final String IDCARD_REGEX = "^[1-9]\\d{7}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}$|^[1-9]\\d{5}[1-9]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}([0-9]|X)$";
/**
* 身份证号码长度
*/
private static final int IDCARD_LENG = 18;
/**
* 单个IP的正则表达式
*/
private static final String SINGLE_IP_REGEX = "([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}";
/**
* 以逗号分隔的多个IP的正则表达式
*/
private static final String IP_REGEX = SINGLE_IP_REGEX + "(," + SINGLE_IP_REGEX + ")*";
private static final Class<?>[] NUMBER_TYPES = {
Byte.class, Short.class, Integer.class, Long.class, BigInteger.class, Float.class, Double.class, BigDecimal.class};
/**
* 判断对象是否为空
*
* @param obj 要判断的对象
* @return 判断结果
*/
public static boolean isBlank(final Object obj) {
if (obj == null) {
return true;
}
if (obj instanceof String) {
return "".equals(((String) obj).trim());
}
if (obj instanceof Collection) {
final Collection collection = (Collection) obj;
return collection.isEmpty();
}
return false;
}
/**
* 判断对象是否不为空
*
* @param obj
* @return
*/
public static boolean isNotBlank(final Object obj) {
return !isBlank(obj);
}
/**
* 检查参数是否有空
*
* @param objects 待验证对象,可以是多个
* @return 验证结果
*/
public static boolean hasBlank(final Object... objects) {
for (final Object obj : objects) {
if (isBlank(obj)) {
return true;
}
}
return false;
}
/**
* 判断是否是Base64字符串
*
* @param str Base64字符串
* @return 判断结果
*/
public static boolean isBase64(final String str) {
return isMatch(str, "[A-Za-z0-9\\+\\/\\=]");
}
/**
* 判断是否是Email地址字符串
*
* @param strEmail URL地址字符串
* @return 判断结果
*/
public static boolean isEmail(final String strEmail) {
return isMatch(strEmail, "^[\\w-]+(\\.[\\w-]+)*@[\\w-]+(\\.[\\w-]+)+$");
}
/**
* 验证手机号码正确性
*
* @param strMobilePhone 手机号码
* @return
*/
public static boolean isMobilePhoneNo(final String strMobilePhone) {
return isMatch(strMobilePhone, "^0?(13[0-9]|14[5-9]|15[012356789]|166|17[0-8]|18[0-9]|19[8-9])[0-9]{8}$");
}
/**
* 是否为手机号码
*
* @param phone
* @return
*/
public static boolean isInternationalPhoneNo(final String phone) {
return isMatch(phone, "^[0-9]{4,14}$");
}
/**
* 判断是否为正整数字符串
*
* @param intStr 数字字符串
* @return 判断结果
*/
public static boolean isInt(final String intStr) {
return isMatch(intStr, "^[0-9]*$");
}
/**
* 判断是否为IP字符串
*
* @param ipStr ip字符串
* @return 判断结果
*/
public static boolean isIp(final String ipStr) {
return isMatch(ipStr, "^((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)$");
}
/**
* 判断字符串是否和正则表达式相匹配,大小写敏感
*
* @param str 字符串
* @param regEx 正则表达式
* @return 判断结果
*/
public static boolean isMatch(final String str, final String regEx) {
return isMatch(str, regEx, false);
}
/**
* 判断字符串是否和正则表达式相匹配
*
* @param str 字符串
* @param regEx 正则表达式
* @param caseInsensetive 是否不区分大小写, true为不区分, false为区分
* @return 判断结果
*/
public static boolean isMatch(final String str, final String regEx, final boolean caseInsensetive) {
if (ValidateUtil.isNotBlank(str) && ValidateUtil.isNotBlank(regEx)) {
Pattern pattern;
if (caseInsensetive) {
pattern = Pattern.compile(regEx, Pattern.CASE_INSENSITIVE);
} else {
pattern = Pattern.compile(regEx);
}
final Matcher matcher = pattern.matcher(str);
return matcher.find();
}
return false;
}
/**
* 判断是否是数字
*
* @param strNumber 数字字符串
* @return 判断结果
*/
public static boolean isNumber(final String strNumber) {
return isMatch(strNumber, "^\\d+$");
}
/**
* 判断是否是Sql危险字符
*
* @param sqlStr sql字符串
* @return 判断结果
*/
public static boolean isSafeSqlString(final String sqlStr) {
return isMatch(sqlStr, SAFE_SQL_REGEX);
}
/**
* 判断是否是URL地址字符串
*
* @param strUrl URL地址字符串
* @return 判断结果
*/
public static boolean isURL(final String strUrl) {
return isMatch(strUrl, URL_REGEX);
}
/**
* 检查对象类型是不是Number类型
*
* @param type
* @return
*/
public static boolean isNumber(Class<?> type) {
// Class为final类,其equals不能重写,而Class的equals方法是直接继承自Object类
// return (this == obj)
// 所以在比较Class是否相等时,使用equals和使用==是一样的
return Arrays.stream(NUMBER_TYPES).anyMatch(numberType -> numberType.equals(type));
}
/**
* 身份证15位编码规则:dddddd yymmdd xx p
* dddddd:6位地区编码
* yymmdd: 出生年(两位年)月日,如:910215
* xx: 顺序编码,系统产生,无法确定
* p: 性别,奇数为男,偶数为女
* <p>
* 身份证18位编码规则:dddddd yyyymmdd xxx y
* dddddd:6位地区编码
* yyyymmdd: 出生年(四位年)月日,如:19910215
* xxx:顺序编码,系统产生,无法确定,奇数为男,偶数为女
* y: 校验码,该位数值可通过前17位计算获得
* <p>
* 前17位号码加权因子为 Wi = [ 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 ]
* 验证位 Y = [ 1, 0, 10, 9, 8, 7, 6, 5, 4, 3, 2 ]
* 如果验证码恰好是10,为了保证身份证是十八位,那么第十八位将用X来代替
* 校验位计算公式:Y_P = mod( ∑(Ai×Wi),11 )
* i为身份证号码1...17 位; Y_P为校验码Y所在校验码数组位置
*/
public static boolean isIdCardNo(final String idCardNo) {
if (!isMatch(idCardNo, IDCARD_REGEX)) {
return false;
}
if (idCardNo.length() == IDCARD_LENG) {
// 用来保存前17位各自乖以加权因子后的总和
int idCardWiSum = 0;
for (int i = 0; i < IDCARD_WI.length; i++) {
idCardWiSum += Integer.parseInt(idCardNo.substring(i, i + 1)) * IDCARD_WI[i];
}
// 计算出校验码所在数组的位置
int idCardMod = idCardWiSum % 11;
// 得到最后一位身份证号码
String idCardLast = idCardNo.substring(17);
if (idCardMod == IDCARD_MOD_X) {
return IDCARD_LAST_X.equalsIgnoreCase(idCardLast);
} else {
//用计算出的验证码与最后一位身份证号码匹配,如果一致,说明通过,否则是无效的身份证号码
return String.valueOf(IDCARD_Y[idCardMod]).equals(idCardLast);
}
}
return true;
}
/**
* 判断字符串是否是以逗号分隔的IP字符串。
* <p>
* 逗号必须是英文半角, 单个ip也认为符合要求, 空字符串返回false
* </p>
*
* @param ipStr
* @return
*/
public static boolean isComaSplitIp(String ipStr) {
boolean result = false;
if (ValidateUtil.isNotBlank(ipStr)) {
result = ipStr.matches(IP_REGEX);
}
return result;
}
}
package com.zq.processing.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/21
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class DetectionResVo {
@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.processing.vo;
import lombok.Data;
/**
* <p>
*
* </p>
*
* @author chenhao
* @since 2023/4/20
*/
@Data
public class OptimizationReq {
private String fileContent;
private String filename;
}
package com.zq.processing.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;
}
server:
port: 9100
spring:
application:
name: IMGPROCEING-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="imgproceing"/><!--主要日志文件名-->
<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.proceing" level="DEBUG"/>
<logger name="com.zq.proceing.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>
File added
import math
from typing import Tuple, Union
import cv2
import numpy as np
import argparse
from deskew import determine_skew
def rotate(
image: np.ndarray, angle: float, background: Union[int, Tuple[int, int, int]]
) -> np.ndarray:
old_width, old_height = image.shape[:2]
angle_radian = math.radians(angle)
width = abs(np.sin(angle_radian) * old_height) + abs(np.cos(angle_radian) * old_width)
height = abs(np.sin(angle_radian) * old_width) + abs(np.cos(angle_radian) * old_height)
image_center = tuple(np.array(image.shape[1::-1]) / 2)
rot_mat = cv2.getRotationMatrix2D(image_center, angle, 1.0)
rot_mat[1, 2] += (width - old_width) / 2
rot_mat[0, 2] += (height - old_height) / 2
return cv2.warpAffine(image, rot_mat, (int(round(height)), int(round(width))), borderValue=background)
if __name__ == '__main__':
# 参数配置
parser = argparse.ArgumentParser()
parser.description = '请输入纠偏图片文件路径!'
# 添加参数
parser.add_argument('-i', '--input', type=str, help='Input file path')
parser.add_argument('-o', '--output', type=str, help='Output file path')
# 解析参数
args = parser.parse_args()
src = args.input
dest = args.output
image = cv2.imread(src)
grayscale = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
angle = determine_skew(grayscale)
print(angle)
rotated = rotate(image, angle, (0, 0, 0))
cv2.imwrite(dest, rotated)
#!/bin/bash
JAR_PATH=$(pwd)
JAR_NAME=imgproc-server-1.0.0.jar
PORT=8510
ACTIVE=wanpro
PROJECT_NAME=${JAR_PATH##*/} #获取JAR_PATH最后一个/后面的内容
LOG_PATH=/data/logs/${PROJECT_NAME}
LOG_NAME=
# JVM="-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -Xms512m -Xmn768m -Xmx2048m -Xss1024k -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC"
#JVM="-Xms512m -Xmx4096m -Xmn1024m -Xss256k -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=1024m -XX:+DisableExplicitGC -XX:NewRatio=8 -XX:SurvivorRatio=6 -XX:+UseParNewGC -XX:ParallelGCThreads=8 -XX:ConcGCThreads=8 -XX:+UseConcMarkSweepGC -XX:+UseBiasedLocking -XX:CMSInitiatingOccupancyFraction=70 -Xloggc:/opt/interface/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/interface/dumpLogs"
JVM="-Xms512m -Xmx4096m -Xmn1024m -Xss256k -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=1024m -XX:+DisableExplicitGC -XX:NewRatio=8 -XX:SurvivorRatio=6 -XX:+UseG1GC -XX:ParallelGCThreads=8 -XX:ConcGCThreads=8 -XX:+UseBiasedLocking -XX:G1HeapRegionSize=8m -XX:InitiatingHeapOccupancyPercent=70 -Xloggc:/opt/interface/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/interface/dumpLogs"
#使用说明,用来提示输入参数
usage() {
echo "Usage: sh boot.sh [start|stop|restart|status|log [name]]"
exit 1
}
#检查程序是否在运行
is_exist(){
pid=`ps -ef|grep $JAR_NAME|grep -v grep|awk '{print $2}'`
#如果不存在返回0,存在返回1
if [ -z "${pid}" ]; then
return 0
else
return 1
fi
}
#启动方法
start(){
is_exist
if [ $? -eq 1 ]; then
echo "${JAR_NAME} is already running. pid = ${pid}"
else
nohup java -jar ${JVM} ${JAR_PATH}/${JAR_NAME} --spring.profiles.active=${ACTIVE} --server.port=${PORT} > ${JAR_PATH}/${PROJECT_NAME}.log 2>&1 &
sleep 0.5s
is_exist
if [ $? -eq 1 ]; then
echo "${JAR_NAME} is running success. pid = ${pid}"
log
else
echo "${JAR_NAME} startup failed."
cat ${JAR_PATH}/${PROJECT_NAME}.log
fi
fi
}
#停止方法
stop(){
is_exist
if [ $? -eq 1 ]; then
kill -9 $pid
is_exist
if [ $? -eq 1 ]; then
echo "${JAR_NAME} still in the running. pid = ${pid}"
else
echo "${JAR_NAME} has stopped running."
fi
else
echo "${JAR_NAME} is not running"
fi
}
#输出运行状态
status(){
is_exist
if [ $? -eq 1 ]; then
echo "${JAR_NAME} is running. pid is ${pid}"
else
echo "${JAR_NAME} is not running."
fi
}
#重启
restart(){
is_exist
if [ $? -eq 1 ]; then
kill -15 $pid
is_exist
isExist=$?
while [ $isExist -eq 1 ]
do
sleep 1s
is_exist
isExist=$?
echo "$JAR_NAME pid=$pid 正在停止中,长时间停不掉使用stop方法."
done
echo "${JAR_NAME} has stopped running."
else
echo "${JAR_NAME} is not running"
fi
if [ -f "${JAR_PATH}/${JAR_NAME}.u" ];then
echo "备份当前文件为${JAR_NAME}.`date +%Y%m%d%H%M%S`"
mv ${JAR_PATH}/${JAR_NAME} ${JAR_PATH}/${JAR_NAME}.`date +%Y%m%d%H%M%S`
echo "重命名更新文件${JAR_NAME}.u为$JAR_NAME"
mv ${JAR_PATH}/${JAR_NAME}.u ${JAR_PATH}/${JAR_NAME}
fi
start
}
#输出动态日志
log(){
if [ -n "$LOG_NAME" ]; then
tail -f -n 200 ${LOG_PATH}/${LOG_NAME}.log
else
#默认输出总日志
tail -f -n 200 ${JAR_PATH}/${PROJECT_NAME}.log
fi
}
#根据输入参数,选择执行对应方法,不输入则执行使用说明
case "$1" in
"start")
start
;;
"stop")
stop
;;
"status")
status
;;
"restart")
restart
;;
"log")
LOG_NAME=$2
log
;;
"help")
usage
;;
*)
if [ -n "$1" ]; then
usage
else
start
fi
;;
esac
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
<module>gateway-server</module> <module>gateway-server</module>
<module>eureka-server</module> <module>eureka-server</module>
<module>config-server</module> <module>config-server</module>
<module>imgprocessing-server</module>
<module>imgproc-server</module> <module>imgproc-server</module>
</modules> </modules>
......
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