commit 07759933e03be25e545758abfa95808ce4495452 Author: the1dog Date: Mon Aug 17 23:10:05 2020 +0800 a diff --git a/yuxue/Application.java b/yuxue/Application.java new file mode 100644 index 0000000..20b46ee --- /dev/null +++ b/yuxue/Application.java @@ -0,0 +1,36 @@ +package com.yuxue; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +import lombok.extern.slf4j.Slf4j; + +/** + * spring boot 启动类 + * @author yuxue + * @date 2019-12-06 + */ +@SpringBootApplication +@MapperScan("mapper") +@EnableScheduling //开启对定时任务的支持 +@Slf4j +public class Application { + + public static void main(String[] args) { + + String version = System.getProperty("java.version"); + if (Integer.parseInt( + version.substring(0,1)) == 1 + && Integer.parseInt(version.substring(2, 3)) >= 8 + && Integer.parseInt(version.substring(6)) >= 60 + || Integer.parseInt(version.substring(0,1))>=9) { + SpringApplication.run(Application.class, args); + } else { + log.error("java version need greater than 1.8.60, and do not use open jdk !!!"); + } + } + + +} diff --git a/yuxue/annotation/RetExclude.java b/yuxue/annotation/RetExclude.java new file mode 100644 index 0000000..0611c54 --- /dev/null +++ b/yuxue/annotation/RetExclude.java @@ -0,0 +1,22 @@ +package com.yuxue.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * 自定义方法注解 + * controller层api,如果添加了该注解,则不进行返回值封装 + * 即:返回值封装排除注解 + * @author yuxue + * @date 2019-08-19 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RetExclude { + + String value() default ""; + +} diff --git a/yuxue/aop/AroundMethod.java b/yuxue/aop/AroundMethod.java new file mode 100644 index 0000000..5afeda8 --- /dev/null +++ b/yuxue/aop/AroundMethod.java @@ -0,0 +1,42 @@ +package com.yuxue.aop; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import com.yuxue.annotation.RetExclude; +import com.yuxue.entity.Result; + +import lombok.extern.slf4j.Slf4j; + + +/** + * method环绕处理 + * @author yuxue + * @date 2019-08-20 + */ +@Slf4j +public class AroundMethod implements MethodInterceptor{ + + @Override + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + + Object ret = null; + try { + ret = methodInvocation.proceed(); + if(ret instanceof Result) { // 防止出现二次封装 + return ret; + } + } catch (Throwable e) { + // 运行时出现异常,抛出,由ResultReturnExceptionHandler统一处理 + throw e; + } + + RetExclude re = methodInvocation.getMethod().getAnnotation(RetExclude.class); + if(null != re && null != re.value()) { + log.info("api添加了封装排除注解"); + return ret; + } + // log.info("封装返回值"); + return Result.ok(ret); + } +} \ No newline at end of file diff --git a/yuxue/aop/DefaultAopConfig.java b/yuxue/aop/DefaultAopConfig.java new file mode 100644 index 0000000..eda564d --- /dev/null +++ b/yuxue/aop/DefaultAopConfig.java @@ -0,0 +1,34 @@ +package com.yuxue.aop; + +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.aop.support.JdkRegexpMethodPointcut; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +/** + * 返回值封装aop + * @author yuxue + * @date 2019-08-20 + */ +@Configuration +@ConditionalOnMissingBean(DefaultPointcutAdvisor.class) +public class DefaultAopConfig { + + // @Value("${test.aop.pointcut:com.yuxue..*.controller..*.*(..)}") + @Value("${test.aop.pointcut:com.yuxue.controller..*.*(..)}") + private String pattern; + + @Bean("resultAop") + public DefaultPointcutAdvisor resultAop() { + DefaultPointcutAdvisor pfb = new DefaultPointcutAdvisor(); + JdkRegexpMethodPointcut j = new JdkRegexpMethodPointcut(); + j.setPattern(pattern); + AroundMethod method = new AroundMethod(); + pfb.setAdvice(method); + pfb.setPointcut(j); + return pfb; + } +} diff --git a/yuxue/aop/WebAop.java b/yuxue/aop/WebAop.java new file mode 100644 index 0000000..b5eda02 --- /dev/null +++ b/yuxue/aop/WebAop.java @@ -0,0 +1,56 @@ +package com.yuxue.aop; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; + +/** + * controller 层日志aop + * @author yuxue + * @date 2018-09-07 + */ +@Aspect +@Slf4j +@Component +public class WebAop { + + @Pointcut("execution(* com.yuxue.controller..*.*(..))") + public void webLog() {} + + @Before("webLog()") + public void doBefore(JoinPoint joinPoint) throws Throwable { + // 接收到请求,记录请求内容 + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletRequest request = attributes.getRequest(); + + log.info("===================="); + log.info("Cookie: " + request.getHeader("Cookie")); + log.info(request.getMethod() + "=>" + request.getRequestURL().toString()); + log.info("IP: " + request.getRemoteAddr()); + log.info("CLASS_METHOD: " + + joinPoint.getSignature().getDeclaringTypeName() + + "." + + joinPoint.getSignature().getName()); + log.info("ARGS: " + Arrays.toString(joinPoint.getArgs())); + log.info("====================\n"); + } + + + @AfterReturning(returning = "ret", pointcut = "webLog()") + public void doAfterReturning(Object ret) throws Throwable { + // 关闭: 返回前进行内容结果日志输出 + log.info("RESPONSE: " + ret); + log.info("====================\n"); + } + +} diff --git a/yuxue/config/CommandRunner.java b/yuxue/config/CommandRunner.java new file mode 100644 index 0000000..1ef4759 --- /dev/null +++ b/yuxue/config/CommandRunner.java @@ -0,0 +1,33 @@ +package com.yuxue.config; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +import lombok.extern.slf4j.Slf4j; + + +/** + * 配置自动启动浏览器 + * + */ +@Slf4j +@Component +public class CommandRunner implements CommandLineRunner { + + @Value("${server.port}") + private String port; + + @Override + public void run(String... args) { + try { + String os = System.getProperty("os.name").toLowerCase(); + if(os.contains("windows")) { + // 默认浏览器打开 + // Runtime.getRuntime().exec("cmd /c start http://localhost:" + port + "/index"); + } + } catch (Exception ex) { + ex.printStackTrace(); + log.error("打开默认浏览器异常", ex); + } + } +} diff --git a/yuxue/config/CorsConfig.java b/yuxue/config/CorsConfig.java new file mode 100644 index 0000000..c1babee --- /dev/null +++ b/yuxue/config/CorsConfig.java @@ -0,0 +1,40 @@ +package com.yuxue.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * 跨域通配 + * 支持 @CrossOrigin 注解局部声明 + * @author yuxue + * @date 2018-09-07 + */ +@Configuration +public class CorsConfig { + + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + /** * 至少需要addMapping *** */ + registry + .addMapping("/**") + .allowedOrigins("*") + .allowedMethods("PUT", "DELETE", "GET", "POST", "OPTIONS", "HEAD") + .allowedHeaders("Content-Type", "X-Requested-With", "accept", "Authorization", "Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers") + .allowCredentials(true)//是否带上cookie + .maxAge(3600) + .exposedHeaders( + "access-control-allow-headers", + "access-control-allow-methods", + "access-control-allow-origin", + "access-Control-allow-credentials", + "access-control-max-age", + "X-Frame-Options"); + } + }; + } +} diff --git a/yuxue/config/DefaultMvcConfig.java b/yuxue/config/DefaultMvcConfig.java new file mode 100644 index 0000000..600056a --- /dev/null +++ b/yuxue/config/DefaultMvcConfig.java @@ -0,0 +1,26 @@ +package com.yuxue.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.LocaleResolver; + + + +/** + * 自定义配置 + * @author yuxue + * @date 2019-06-13 + */ +@Configuration +public class DefaultMvcConfig { + + /** + * 国际化配置 + * 设置区域信息 + * @return + */ + @Bean + public LocaleResolver localeResolver(){ + return new MyLocaleResolver(); + } +} diff --git a/yuxue/config/DruidConfig.java b/yuxue/config/DruidConfig.java new file mode 100644 index 0000000..c374bd2 --- /dev/null +++ b/yuxue/config/DruidConfig.java @@ -0,0 +1,48 @@ +package com.yuxue.config; + +import com.alibaba.druid.filter.Filter; +import com.alibaba.druid.filter.stat.StatFilter; +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.support.http.StatViewServlet; +import com.google.common.collect.Lists; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * druid连接池的配置,配置如何处理慢sql, + */ +@Configuration +public class DruidConfig { + + // 这个注解读取配置文件前缀为prefix的配置,将外部的配置文件与这里绑定 + // 容器的开启与关闭 + @ConfigurationProperties(prefix = "spring.druid") + @Bean(initMethod = "init", destroyMethod = "close") + public DruidDataSource dataSource() { + DruidDataSource dataSource = new DruidDataSource(); + dataSource.setProxyFilters(Lists.newArrayList(statFilter())); + return dataSource; + } + + // bean注解,成为spring的bean,利用filter将慢sql的日志打印出来 + //@Bean + public Filter statFilter() { + StatFilter statFilter = new StatFilter(); + // 多长时间定义为慢sql,这里定义为5s + statFilter.setSlowSqlMillis(5000); + // 是否打印出慢日志 + statFilter.setLogSlowSql(true); + // 是否将日志合并起来 + statFilter.setMergeSql(true); + return statFilter; + } + + // 这是配置druid的监控 + @Bean + public ServletRegistrationBean servletRegistrationBean() { + return new ServletRegistrationBean(new StatViewServlet(), "/druid/*"); + } + +} diff --git a/yuxue/config/LocalDateTimeSerializerConfig.java b/yuxue/config/LocalDateTimeSerializerConfig.java new file mode 100644 index 0000000..82dd1b5 --- /dev/null +++ b/yuxue/config/LocalDateTimeSerializerConfig.java @@ -0,0 +1,29 @@ +package com.yuxue.config; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; + +@Configuration +public class LocalDateTimeSerializerConfig { + + @Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}") + private String pattern; + + @Bean + public LocalDateTimeSerializer localDateTimeDeserializer() { + return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern)); + } + + @Bean + public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { + return builder -> builder.serializerByType(LocalDateTime.class, localDateTimeDeserializer()); + } + +} \ No newline at end of file diff --git a/yuxue/config/MyLocaleResolver.java b/yuxue/config/MyLocaleResolver.java new file mode 100644 index 0000000..8527a26 --- /dev/null +++ b/yuxue/config/MyLocaleResolver.java @@ -0,0 +1,32 @@ +package com.yuxue.config; + +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.LocaleResolver; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Locale; + +/** + * 配置多语言 + * @author yuxue + * @date 2019-06-13 + */ +public class MyLocaleResolver implements LocaleResolver { + + @Override + public Locale resolveLocale(HttpServletRequest request) { + String l = request.getParameter("i18n"); + Locale locale = Locale.getDefault(); + if(!StringUtils.isEmpty(l)){ + String[] split = l.split("_"); + locale = new Locale(split[0],split[1]); + } + return locale; + } + + @Override + public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) { + + } +} diff --git a/yuxue/config/PageHelperConfig.java b/yuxue/config/PageHelperConfig.java new file mode 100644 index 0000000..c7e24f7 --- /dev/null +++ b/yuxue/config/PageHelperConfig.java @@ -0,0 +1,30 @@ +package com.yuxue.config; + +import com.github.pagehelper.PageInterceptor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Properties; + + +/** + * 分页查询控制 + * @author yuxue + * @date 2018-09-07 + */ +@Configuration +public class PageHelperConfig { + + @Value("${pagehelper.helperDialect}") + private String helperDialect; + + @Bean + public PageInterceptor pageInterceptor() { + PageInterceptor pageInterceptor = new PageInterceptor(); + Properties properties = new Properties(); + properties.setProperty("helperDialect", helperDialect); + pageInterceptor.setProperties(properties); + return pageInterceptor; + } +} diff --git a/yuxue/config/Swagger2.java b/yuxue/config/Swagger2.java new file mode 100644 index 0000000..5ed4db1 --- /dev/null +++ b/yuxue/config/Swagger2.java @@ -0,0 +1,51 @@ +package com.yuxue.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.bind.annotation.RestController; + +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + + +/** + * 在swagger-annotations jar包中 1.5.X版本以上, 注解 io.swagger.annotations.API 中的description被废弃了。 + * 新的swagger组件中使用了新的方法来对Web api 进行分组。原来使用 description ,默认一个Controller类中包含的方法构成一 个api分组。 + * 现在使用tag,可以更加方便的分组。 + * 比如把两个Controller类里的方法划分成同一个分组。tag的key用来区分不同的分组。tag的value用做分组的描述。 + * @ApiOperation 中value是api的简要说明,在界面api 链接的右侧,少于120个字符。 + * @ApiOperation 中notes是api的详细说明,需要点开api 链接才能看到。 + * @ApiOperation 中 produces 用来标记api返回值的具体类型。这里是json格式,utf8编码。 + */ +/** + * 集成swagger2 接口管理文档 + * @author yuxue + * @date 2018-09-07 + */ +@Configuration +@EnableSwagger2 +public class Swagger2 { + + @Bean + public Docket createRestApi() { + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class)) + .paths(PathSelectors.any()) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("Image Recognition API") + .description("图像识别技术") + .version("1.0.0") + .build(); + } +} diff --git a/yuxue/config/ThreadPoolConfig.java b/yuxue/config/ThreadPoolConfig.java new file mode 100644 index 0000000..608f585 --- /dev/null +++ b/yuxue/config/ThreadPoolConfig.java @@ -0,0 +1,51 @@ +package com.yuxue.config; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +@ComponentScan("com.yuxue.auth.service.impl") +@EnableAsync +public class ThreadPoolConfig implements AsyncConfigurer { + + @Bean(name = "taskExecutor") + public TaskExecutor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + + // 设置核心线程数 + executor.setCorePoolSize(4); + // 设置最大线程数 + executor.setMaxPoolSize(8); + // 设置队列容量 + executor.setQueueCapacity(100); + // 设置线程活跃时间(秒) + executor.setKeepAliveSeconds(60); + // 设置默认线程名称 + executor.setThreadNamePrefix("localThread:"); + // 设置拒绝策略 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + // 等待所有任务结束后再关闭线程池 + executor.setWaitForTasksToCompleteOnShutdown(true); + return executor; + } + + @Override + public Executor getAsyncExecutor() { + return taskExecutor(); + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return null; + } + +} diff --git a/yuxue/constant/Constant.java b/yuxue/constant/Constant.java new file mode 100644 index 0000000..4e05e48 --- /dev/null +++ b/yuxue/constant/Constant.java @@ -0,0 +1,126 @@ +package com.yuxue.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 系统常量 + * @author yuxue + * @date 2018-09-07 + */ +public class Constant { + + public static final String UTF8 = "UTF-8"; + + // 车牌识别, 默认车牌图片保存路径 + // public static String DEFAULT_DIR = "./PlateDetect/"; // 使用项目的相对路径 + public static String DEFAULT_DIR = "D:/PlateDetect/"; // 使用盘符的绝对路径 + + // 车牌识别, 默认车牌图片处理过程temp路径 + // public static String DEFAULT_TEMP_DIR = "./PlateDetect/temp/"; // 使用项目的相对路径 + public static String DEFAULT_TEMP_DIR = "D:/PlateDetect/temp/"; // 使用盘符的绝对路径 + + // 车牌识别,默认处理图片类型 + public static String DEFAULT_TYPE = "png,jpg,jpeg"; + + public static String DEFAULT_ANN_PATH = "res/model/ann.xml"; + //public static String DEFAULT_ANN_PATH = "D:/PlateDetect/train/chars_recognise_ann/ann.xml"; + + public static String DEFAULT_SVM_PATH = "res/model/svm.xml"; + + public static final int DEFAULT_WIDTH = 136; // cols + public static final int DEFAULT_HEIGHT = 36; // rows + + // 车牌识别,判断是否车牌的正则表达式 + public static String plateReg = "([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}(([0-9]{5}[DF])|([DF]([A-HJ-NP-Z0-9])[0-9]{4})))|([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1})"; + + public static int predictSize = 10; + + + public static int neurons = 40; + + // 中国车牌; 34个字符; 没有 字母I、字母O + public final static char strCharacters[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', /* 没有I */ 'J', 'K', 'L', 'M', 'N', /* 没有O */'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' }; + + // 没有I和0, 10个数字与24个英文字符之和 + public final static Integer numCharacter = strCharacters.length; + + // 并不全面,有些省份没有训练数据所以没有字符 + // 有些后面加数字2的表示在训练时常看到字符的一种变形,也作为训练数据存储 + public final static String strChinese[] = { + "zh_cuan", /*川*/ + "zh_e", /*鄂*/ + "zh_gan", /*赣*/ + "zh_gan1", /*甘*/ + "zh_gui", /*贵*/ + "zh_gui1", /*桂*/ + "zh_hei", /*黑*/ + "zh_hu", /*沪*/ + "zh_ji", /*冀*/ + "zh_jin", /*津*/ + "zh_jing", /*京*/ + "zh_jl", /*吉*/ + "zh_liao", /*辽*/ + "zh_lu", /*鲁*/ + "zh_meng", /*蒙*/ + "zh_min", /*闽*/ + "zh_ning", /*宁*/ + "zh_qing", /*青*/ + "zh_qiong", /*琼*/ + "zh_shan", /*陕*/ + "zh_su", /*苏*/ + "zh_sx", /*晋*/ + "zh_wan", /*皖*/ + "zh_xiang", /*湘*/ + "zh_xin", /*新*/ + "zh_yu", /*豫*/ + "zh_yu1", /*渝*/ + "zh_yue", /*粤*/ + "zh_yun", /*云*/ + "zh_zang", /*藏*/ + "zh_zhe" /*浙*/ + }; + + /* 34+31=65 34个字符跟31个汉字 */ + public final static Integer numAll = strCharacters.length + strChinese.length; + + public static Map KEY_CHINESE_MAP = new HashMap(); + static { + if (KEY_CHINESE_MAP.isEmpty()) { + KEY_CHINESE_MAP.put("zh_cuan", "川"); + KEY_CHINESE_MAP.put("zh_e", "鄂"); + KEY_CHINESE_MAP.put("zh_gan", "赣"); + KEY_CHINESE_MAP.put("zh_gan1", "甘"); + KEY_CHINESE_MAP.put("zh_gui", "贵"); + KEY_CHINESE_MAP.put("zh_gui1", "桂"); + KEY_CHINESE_MAP.put("zh_hei", "黑"); + KEY_CHINESE_MAP.put("zh_hu", "沪"); + KEY_CHINESE_MAP.put("zh_ji", "冀"); + KEY_CHINESE_MAP.put("zh_jin", "津"); + KEY_CHINESE_MAP.put("zh_jing", "京"); + KEY_CHINESE_MAP.put("zh_jl", "吉"); + KEY_CHINESE_MAP.put("zh_liao", "辽"); + KEY_CHINESE_MAP.put("zh_lu", "鲁"); + KEY_CHINESE_MAP.put("zh_meng", "蒙"); + KEY_CHINESE_MAP.put("zh_min", "闽"); + KEY_CHINESE_MAP.put("zh_ning", "宁"); + KEY_CHINESE_MAP.put("zh_qing", "青"); + KEY_CHINESE_MAP.put("zh_qiong", "琼"); + KEY_CHINESE_MAP.put("zh_shan", "陕"); + KEY_CHINESE_MAP.put("zh_su", "苏"); + KEY_CHINESE_MAP.put("zh_sx", "晋"); + KEY_CHINESE_MAP.put("zh_wan", "皖"); + KEY_CHINESE_MAP.put("zh_xiang", "湘"); + KEY_CHINESE_MAP.put("zh_xin", "新"); + KEY_CHINESE_MAP.put("zh_yu", "豫"); + KEY_CHINESE_MAP.put("zh_yu1", "渝"); + KEY_CHINESE_MAP.put("zh_yue", "粤"); + KEY_CHINESE_MAP.put("zh_yun", "云"); + KEY_CHINESE_MAP.put("zh_zang", "藏"); + KEY_CHINESE_MAP.put("zh_zhe", "浙"); + } + } + + +} diff --git a/yuxue/controller/CommonController.java b/yuxue/controller/CommonController.java new file mode 100644 index 0000000..ec0bd82 --- /dev/null +++ b/yuxue/controller/CommonController.java @@ -0,0 +1,41 @@ +package com.yuxue.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import com.yuxue.annotation.RetExclude; + +import springfox.documentation.annotations.ApiIgnore; + + + +@ApiIgnore +@Controller +public class CommonController { + + @RetExclude + @RequestMapping(value = "", method = { RequestMethod.GET }) + public String doc() { + return "redirect:swagger-ui.html"; + } + + @RetExclude + @RequestMapping(value = "login", method = { RequestMethod.GET }) + public String loginPage() { + return "home/login"; + } + + @RetExclude + @RequestMapping(value = "index", method = { RequestMethod.GET }) + public String indexPage() { + return "home/index"; + } + + @RetExclude + @RequestMapping(value = "unauthorized", method = { RequestMethod.GET }) + public String unauthorizedPage() { + return "unauthorized"; + } + +} diff --git a/yuxue/controller/FaceController.java b/yuxue/controller/FaceController.java new file mode 100644 index 0000000..3b5f4cd --- /dev/null +++ b/yuxue/controller/FaceController.java @@ -0,0 +1,49 @@ +package com.yuxue.controller; + +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.core.MatOfRect; +import org.opencv.core.Rect; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.imgproc.Imgproc; +import org.opencv.objdetect.CascadeClassifier; + +/** + * 识别人脸 + * Detects faces in an image, draws boxes around them, + * and writes the results to "faceDetection.png". + */ +public class FaceController { + + static { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + } + + public static void main(String[] args) { + + // Create a face detector from the cascade file in the resources directory. + // 创建识别器 + CascadeClassifier faceDetector = new CascadeClassifier("/src/main/resources/haarcascades/lbpcascade_frontalface.xml"); + + String imgPath = "/src/main/resources/DetectFace/AverageMaleFace.jpg"; + Mat image = Imgcodecs.imread(imgPath); + + Mat dst = new Mat(); + Imgproc.Canny(image, dst, 130, 250); + + // Detect faces in the image. MatOfRect is a special container class for Rect. + MatOfRect faceDetections = new MatOfRect(); + faceDetector.detectMultiScale(dst, faceDetections); + + System.out.println(String.format("识别出 %s 张人脸", faceDetections.toArray().length)); + + // Draw a bounding box around each face. + for (Rect rect : faceDetections.toArray()) { + // Core.rectangle(image, new Point(rect.x, rect.y), new Point(rect.x + rect.width, rect.y + rect.height), new Scalar(0, 255, 0)); + } + + // Save the visualized detection. + // System.out.println(String.format("Writing %s", filename)); + //Highgui.imwrite(filename, image); + } +} diff --git a/yuxue/controller/FileController.java b/yuxue/controller/FileController.java new file mode 100644 index 0000000..fe2be56 --- /dev/null +++ b/yuxue/controller/FileController.java @@ -0,0 +1,90 @@ +package com.yuxue.controller; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import com.yuxue.annotation.RetExclude; +import com.yuxue.exception.ResultReturnException; +import com.yuxue.service.FileService; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; + + + + +@Api(description = "文件管理") +@RestController +@RequestMapping("/file") +public class FileController { + + @Autowired + private FileService service; + + + /** + * 加载文件树结构 + * 输入:文件夹路径,缺省值:D:\\PlateDetect\\ 文件类型,缺省值:png,jpg,jpeg + * 输出:当前目录下第一层级文件的list + * @param dir + * @return + */ + @ApiOperation(value = "获取文件结构", notes = "") + @ApiImplicitParam(name = "dir", value = "文件夹路径", required = true, paramType = "query", dataType = "String") + @RequestMapping(value = "/getFileTreeByDir", method = RequestMethod.GET) + public Object getFileTreeByDir(String dir, String typeFilter) { + try { + if(null != dir) { + dir = URLDecoder.decode(dir, "utf-8"); + } + } catch (UnsupportedEncodingException e) { + throw new ResultReturnException("dir参数异常"); + } + return service.getFileTreeByDir(dir, typeFilter); + } + + + /** + * 预览图片文件 + * @param filePath + * @param response + * @return + * @throws IOException + */ + @RetExclude + @ApiOperation(value = "预览文件", notes = "根据路径,直接读取盘符文件; 返回输出流") + @GetMapping(value = "/readFile", produces= {"image/jpeg"}) + public ResponseEntity readFile(String filePath, HttpServletResponse response) throws IOException { + try { + filePath = URLDecoder.decode(filePath, "utf-8"); + } catch (UnsupportedEncodingException e) { + throw new ResultReturnException("filePath参数异常"); + } + //文件输出流,输出到客户端 + File file = service.readFile(filePath); + InputStreamResource isr = new InputStreamResource(new FileInputStream(file)); + HttpHeaders headers = new HttpHeaders(); + return new ResponseEntity(isr, headers, HttpStatus.OK); + } + + + + + +} diff --git a/yuxue/controller/OpencvDemo.java b/yuxue/controller/OpencvDemo.java new file mode 100644 index 0000000..1c2d775 --- /dev/null +++ b/yuxue/controller/OpencvDemo.java @@ -0,0 +1,49 @@ +package com.yuxue.controller; + +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.Scalar; + + +/** + * opencv 官方demo + * 用于测试opencv环境是否正常 + * windows下环境配置: + * 1、官网下载对应版本的openvp:https://opencv.org/releases/page/2/ 当前使用4.0.1版本 + * 2、双击exe文件安装,将 安装目录下\build\java\x64\opencv_java401.dll 拷贝到\build\x64\vc14\bin\目录下 + * 3、eclipse添加User Libraries + * 4、项目右键build path,添加步骤三新增的lib + * + * 官方demo,需要本地安装opencv,除该demo之前,均不需要安装,使用maven依赖即可, + * 二者之间具体有什么差别,暂时还没有时间去深入研究 + * @author yuxue + * @date 2020-04-22 14:04 + */ +public class OpencvDemo { + + static { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + } + + public static void main(String[] args) { + System.out.println("Welcome to OpenCV " + Core.VERSION); + + + Mat m1 = Mat.eye(3, 3, CvType.CV_8UC1); + System.out.println("m = " + m1.dump()); + System.err.println("=================="); + + + Mat m = new Mat(5, 10, CvType.CV_8UC1, new Scalar(0)); + System.out.println("OpenCV Mat: " + m); + Mat mr1 = m.row(1); + mr1.setTo(new Scalar(1)); + Mat mc5 = m.col(5); + mc5.setTo(new Scalar(5)); + System.out.println("OpenCV Mat data:\n" + m.dump()); + + + } + +} diff --git a/yuxue/controller/PlateController.java b/yuxue/controller/PlateController.java new file mode 100644 index 0000000..67927b7 --- /dev/null +++ b/yuxue/controller/PlateController.java @@ -0,0 +1,92 @@ +package com.yuxue.controller; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import com.yuxue.exception.ResultReturnException; +import com.yuxue.service.PlateService; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; + + + +@Api(description = "车牌识别") +@RestController +@RequestMapping("/plate") +public class PlateController { + + @Autowired + private PlateService service; + + + /** + * 扫描d:/PlateDetect目录图片的基础信息 + * 将扫描到的信息,更新到数据库 + * 排除temp目录 + */ + @ApiOperation(value = "更新IMG文件基础信息", notes = "") + @RequestMapping(value = "/refreshFileInfo", method = RequestMethod.GET) + public void refreshFileInfo() { + service.refreshFileInfo(); + } + + + /** + * 根据数据库的图片基础信息,进行车牌识别 + * 更新图片识别信息到数据库 + * 生成识别结果; 多线程执行 + */ + @ApiOperation(value = "图片车牌识别", notes = "路径不能包含中文,opencv路径转码过程乱码会报异常") + @RequestMapping(value = "/recogniseAll", method = RequestMethod.GET) + public Object recogniseAll() { + return service.recogniseAll(); + } + + + + /** + * 车牌识别接口 + * 输入:图片path + * 处理:识别过程切图,识别结果切图;切图保存到temp/timestamp文件夹,图片文件名按timestamp排序 + * 操作过程结果保存数据库,操作前检查数据库及temp文件夹下是否有对应的切图文件 + * 输出:返回过程切图、识别结果切图文件路径集合 + */ + @ApiOperation(value = "图片车牌识别", notes = "路径不能包含中文,opencv路径转码过程乱码会报异常") + @ApiImplicitParams({ + @ApiImplicitParam(name = "filePath", value = "文件路径", required = true, paramType = "query", dataType = "String"), + @ApiImplicitParam(name = "reRecognise", value = "重新识别", paramType = "query", dataType = "Boolean", defaultValue="false") + }) + @RequestMapping(value = "/recognise", method = RequestMethod.GET) + public Object recognise(String filePath, Boolean reRecognise) { + try { + if(null != filePath) { + filePath = URLDecoder.decode(filePath, "utf-8"); + } + if(null == reRecognise) { + reRecognise = false; + } + } catch (UnsupportedEncodingException e) { + throw new ResultReturnException("filePath参数异常"); + } + return service.recognise(filePath, reRecognise); + } + + + @ApiOperation(value = "获取处理步骤", notes = "") + @RequestMapping(value = "/getProcessStep", method = RequestMethod.GET) + public Object getProcessStep() { + return service.getProcessStep(); + } + + + + +} diff --git a/yuxue/controller/SystemMenuController.java b/yuxue/controller/SystemMenuController.java new file mode 100644 index 0000000..078bc84 --- /dev/null +++ b/yuxue/controller/SystemMenuController.java @@ -0,0 +1,84 @@ +package com.yuxue.controller; + +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.yuxue.entity.SystemMenuEntity; +import com.yuxue.service.SystemMenuService; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; + + + +@Api(description = "菜单管理") +@RestController +@RequestMapping("/systemMenu") +public class SystemMenuController { + + @Autowired + private SystemMenuService service; + + /** + * 分页查询 + * @param pageNo + * @param pageSize + * @param entity + */ + @ApiOperation(value = "分页获取记录", notes = "分页获取记录") + @ApiImplicitParams({ + @ApiImplicitParam(name = "pageNo", value = "当前页码", required = true, paramType = "query", dataType = "Integer", defaultValue = "1"), + @ApiImplicitParam(name = "pageSize", value = "每页数量", required = true, paramType = "query", dataType = "Integer", defaultValue = "10"), + @ApiImplicitParam(name = "map", value = "举例:{} or {\"name\":\"张三\"}", dataType = "entity") + }) + @RequestMapping(value = "/queryByPage", method = RequestMethod.POST) + public Object queryByPage(@RequestParam Integer pageNo, @RequestParam Integer pageSize, @RequestBody Map map) { + return service.queryByPage(pageNo, pageSize, map); + } + + @ApiOperation(value = "按条件查询", notes = "不分页", response = SystemMenuEntity.class) + @ApiImplicitParams({ + @ApiImplicitParam(name = "map", value = "举例:{} or {\"name\":\"张三\"}", dataType = "entity") + }) + @RequestMapping(value = "/queryByCondition", method = RequestMethod.POST) + public Object queryByCondition(@RequestBody Map map) { + return service.queryByCondition(map); + } + + + /** + * Post请求,新增数据,成功返回ID + * @param entity + */ + @ApiOperation(value = "新增数据,成功返回ID", notes = "新增数据,成功返回ID") + @ApiImplicitParam(name = "entity", value = "举例:{} or {\"name\":\"张三\"}", required = true, dataType = "entity") + @RequestMapping(value = "", method = RequestMethod.POST) + public Object save(@RequestBody SystemMenuEntity entity) { + return service.save(entity); + } + + + /** + * 获取登录用户的权限下菜单 + * @return + */ + @ApiOperation(value = "获取登录用户菜单", notes = "") + @GetMapping("/getUserMenu") + public Object getUserMenu() { + return service.getUserMenu(); + } + + + + +} + diff --git a/yuxue/easypr/core/CharsIdentify.java b/yuxue/easypr/core/CharsIdentify.java new file mode 100644 index 0000000..6daf5b8 --- /dev/null +++ b/yuxue/easypr/core/CharsIdentify.java @@ -0,0 +1,83 @@ +package com.yuxue.easypr.core; + +import org.bytedeco.javacpp.opencv_core; +import org.bytedeco.javacpp.opencv_core.Mat; +import org.bytedeco.javacpp.opencv_ml.ANN_MLP; + +import com.yuxue.constant.Constant; +import com.yuxue.util.Convert; + + +/** + * 字符检测 + * @author yuxue + * @date 2020-04-24 15:31 + */ +public class CharsIdentify { + + private ANN_MLP ann=ANN_MLP.create(); + + public CharsIdentify() { + loadModel(Constant.DEFAULT_ANN_PATH); + } + + public void loadModel(String path) { + this.ann.clear(); + // 加载ann配置文件 图像转文字的训练库文件 + //ann=ANN_MLP.loadANN_MLP(path, "ann"); + ann = ANN_MLP.load(path); + } + + + /** + * @param input + * @param isChinese + * @return + */ + public String charsIdentify(final Mat input, final Boolean isChinese, final Boolean isSpeci) { + String result = ""; + + /*String name = "D:/PlateDetect/train/chars_recognise_ann/" + System.currentTimeMillis() + ".jpg"; + opencv_imgcodecs.imwrite(name, input); + Mat img = opencv_imgcodecs.imread(name); + Mat f = CoreFunc.features(img, Constant.predictSize);*/ + + Mat f = CoreFunc.features(input, Constant.predictSize); + + int index = this.classify(f, isChinese, isSpeci); + + System.err.print(index); + if (index < Constant.numCharacter) { + result = String.valueOf(Constant.strCharacters[index]); + } else { + String s = Constant.strChinese[index - Constant.numCharacter]; + result = Constant.KEY_CHINESE_MAP.get(s); // 编码转中文 + } + System.err.println(result); + return result; + } + + private int classify(final Mat f, final Boolean isChinses, final Boolean isSpeci) { + int result = -1; + + Mat output = new Mat(1, 140, opencv_core.CV_32F); + + ann.predict(f, output, 0); // 预测结果 + + int ann_min = (!isChinses) ? ((isSpeci) ? 10 : 0) : Constant.numCharacter; + int ann_max = (!isChinses) ? Constant.numCharacter : Constant.numAll; + + float maxVal = -2; + + for (int j = ann_min; j < ann_max; j++) { + float val = Convert.toFloat(output.ptr(0, j)); + if (val > maxVal) { + maxVal = val; + result = j; + } + } + return result; + } + + +} diff --git a/yuxue/easypr/core/CharsRecognise.java b/yuxue/easypr/core/CharsRecognise.java new file mode 100644 index 0000000..ebed2e0 --- /dev/null +++ b/yuxue/easypr/core/CharsRecognise.java @@ -0,0 +1,136 @@ +package com.yuxue.easypr.core; + +import java.util.Vector; + +import org.bytedeco.javacpp.opencv_core.Mat; + +import com.yuxue.enumtype.PlateColor; + +/** + * 字符识别 + * + * @author yuxue + * @date 2020-04-24 15:31 + */ +public class CharsRecognise { + + private CharsSegment charsSegment = new CharsSegment(); + + private CharsIdentify charsIdentify = new CharsIdentify(); + + + public void loadANN(final String s) { + charsIdentify.loadModel(s); + } + + /** + * Chars segment and identify 字符分割与识别 + * + * @param plate: the input plate + * @return the result of plate recognition + */ + public String charsRecognise(final Mat plate, String tempPath) { + + // 车牌字符方块集合 + Vector matVec = new Vector(); + // 车牌识别结果 + String plateIdentify = ""; + + int result = charsSegment.charsSegment(plate, matVec, tempPath); + if (0 == result) { + for (int j = 0; j < matVec.size(); j++) { + Mat charMat = matVec.get(j); + // 默认首个字符块是中文字符 第二个字符块是字母 + String charcater = charsIdentify.charsIdentify(charMat, (0 == j), (1 == j)); + plateIdentify = plateIdentify + charcater; + } + } + + return plateIdentify; + } + + /** + * 是否开启调试模式 + * + * @param isDebug + */ + public void setCRDebug(final boolean isDebug) { + charsSegment.setDebug(isDebug); + } + + /** + * 获取调试模式状态 + * + * @return + */ + public boolean getCRDebug() { + return charsSegment.getDebug(); + } + + /** + * 获得车牌颜色 + * + * @param input + * @return + */ + public final String getPlateType(final Mat input) { + PlateColor result = CoreFunc.getPlateType(input, true); + return result.desc; + } + + /** + * 设置柳丁大小变量 + * + * @param param + */ + public void setLiuDingSize(final int param) { + charsSegment.setLiuDingSize(param); + } + + /** + * 设置颜色阈值 + * + * @param param + */ + public void setColorThreshold(final int param) { + charsSegment.setColorThreshold(param); + } + + /** + * 设置蓝色百分比 + * + * @param param + */ + public void setBluePercent(final float param) { + charsSegment.setBluePercent(param); + } + + /** + * 得到蓝色百分比 + * + * @param param + */ + public final float getBluePercent() { + return charsSegment.getBluePercent(); + } + + /** + * 设置白色百分比 + * + * @param param + */ + public void setWhitePercent(final float param) { + charsSegment.setWhitePercent(param); + } + + /** + * 得到白色百分比 + * + * @param param + */ + public final float getWhitePercent() { + return charsSegment.getWhitePercent(); + } + + +} diff --git a/yuxue/easypr/core/CharsSegment.java b/yuxue/easypr/core/CharsSegment.java new file mode 100644 index 0000000..340e8f0 --- /dev/null +++ b/yuxue/easypr/core/CharsSegment.java @@ -0,0 +1,453 @@ +package com.yuxue.easypr.core; + +import static com.yuxue.easypr.core.CoreFunc.getPlateType; +import static org.bytedeco.javacpp.opencv_core.CV_32F; +import static org.bytedeco.javacpp.opencv_core.countNonZero; +import static org.bytedeco.javacpp.opencv_imgproc.CV_CHAIN_APPROX_NONE; +import static org.bytedeco.javacpp.opencv_imgproc.CV_RETR_EXTERNAL; +import static org.bytedeco.javacpp.opencv_imgproc.CV_RGB2GRAY; +import static org.bytedeco.javacpp.opencv_imgproc.CV_THRESH_BINARY; +import static org.bytedeco.javacpp.opencv_imgproc.CV_THRESH_BINARY_INV; +import static org.bytedeco.javacpp.opencv_imgproc.CV_THRESH_OTSU; +import static org.bytedeco.javacpp.opencv_imgproc.INTER_LINEAR; +import static org.bytedeco.javacpp.opencv_imgproc.boundingRect; +import static org.bytedeco.javacpp.opencv_imgproc.cvtColor; +import static org.bytedeco.javacpp.opencv_imgproc.findContours; +import static org.bytedeco.javacpp.opencv_imgproc.resize; +import static org.bytedeco.javacpp.opencv_imgproc.threshold; +import static org.bytedeco.javacpp.opencv_imgproc.warpAffine; + +import java.util.Vector; + +import org.bytedeco.javacpp.opencv_core; +import org.bytedeco.javacpp.opencv_core.Mat; +import org.bytedeco.javacpp.opencv_core.MatVector; +import org.bytedeco.javacpp.opencv_core.Rect; +import org.bytedeco.javacpp.opencv_core.Scalar; +import org.bytedeco.javacpp.opencv_core.Size; +import org.bytedeco.javacpp.opencv_imgcodecs; + +import com.yuxue.enumtype.PlateColor; +import com.yuxue.util.Convert; + +/** + * 字符分割 + * @author yuxue + * @date 2020-04-28 09:45 + */ +public class CharsSegment { + + // preprocessChar所用常量 + final static int CHAR_SIZE = 20; + final static int HORIZONTAL = 1; + final static int VERTICAL = 0; + + final static int DEFAULT_LIUDING_SIZE = 7; + final static int DEFAULT_MAT_WIDTH = 136; + + final static int DEFAULT_COLORTHRESHOLD = 150; + final static float DEFAULT_BLUEPERCEMT = 0.3f; + final static float DEFAULT_WHITEPERCEMT = 0.1f; + + private int liuDingSize = DEFAULT_LIUDING_SIZE; + private int theMatWidth = DEFAULT_MAT_WIDTH; + + private int colorThreshold = DEFAULT_COLORTHRESHOLD; + + private float bluePercent = DEFAULT_BLUEPERCEMT; + private float whitePercent = DEFAULT_WHITEPERCEMT; + + private boolean isDebug = true; + + + /** + * 字符分割 + * + * @param input + * @param resultVec + * @return
    + *
  • more than zero: the number of chars; + *
  • -3: null; + *
+ */ + public int charsSegment(final Mat input, Vector resultVec, String tempPath) { + if (input.data().isNull()) { + return -3; + } + + // 判断车牌颜色以此确认threshold方法 + Mat img_threshold = new Mat(); + + Mat input_grey = new Mat(); + cvtColor(input, input_grey, CV_RGB2GRAY); + + int w = input.cols(); + int h = input.rows(); + Mat tmpMat = new Mat(input, new Rect((int) (w * 0.1), (int) (h * 0.1), (int) (w * 0.8), (int) (h * 0.8))); + + PlateColor color= getPlateType(tmpMat, true); + switch (color) { + case BLUE: + threshold(input_grey, img_threshold, 10, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); + break; + + case YELLOW: + threshold(input_grey, img_threshold, 10, 255, CV_THRESH_OTSU + CV_THRESH_BINARY_INV); + break; + + case GREEN: + threshold(input_grey, img_threshold, 10, 255, CV_THRESH_OTSU + CV_THRESH_BINARY_INV); + break; + + default: + return -3; + } + + if (this.isDebug) { + opencv_imgcodecs.imwrite(tempPath + "debug_char_threshold.jpg", img_threshold); + } + + // 去除车牌上方的柳钉以及下方的横线等干扰 //会导致虚拟机崩溃 + // clearLiuDing(img_threshold); + /*if (this.isDebug) { + String str = tempPath + "debug_char_clearLiuDing.jpg"; + opencv_imgcodecs.imwrite(str, img_threshold); + }*/ + + // 找轮廓 + Mat img_contours = new Mat(); + img_threshold.copyTo(img_contours); + + MatVector contours = new MatVector(); + + findContours(img_contours, contours, // a vector of contours + CV_RETR_EXTERNAL, // retrieve the external contours + CV_CHAIN_APPROX_NONE); // all pixels of each contours + + // Remove patch that are no inside limits of aspect ratio and area. + // 将不符合特定尺寸的图块排除出去 + Vector vecRect = new Vector(); + for (int i = 0; i < contours.size(); ++i) { + Rect mr = boundingRect(contours.get(i)); + Mat contour = new Mat(img_threshold, mr); + + if (this.isDebug) { + String str = tempPath + "debug_char_contour"+i+".jpg"; + opencv_imgcodecs.imwrite(str, contour); + } + + if (verifySizes(contour)) { // 将不符合特定尺寸的图块排除出去 + vecRect.add(mr); + } + } + if (vecRect.size() == 0) { + return -3; + } + + Vector sortedRect = new Vector(); + // 对符合尺寸的图块按照从左到右进行排序 + SortRect(vecRect, sortedRect); + + // 获得指示城市的特定Rect,如苏A的"A" + int specIndex = GetSpecificRect(sortedRect, color); + + if (this.isDebug) { + if (specIndex < sortedRect.size()) { + Mat specMat = new Mat(img_threshold, sortedRect.get(specIndex)); + String str = tempPath + "debug_specMat.jpg"; + opencv_imgcodecs.imwrite(str, specMat); + } + } + + // 根据特定Rect向左反推出中文字符 + // 这样做的主要原因是根据findContours方法很难捕捉到中文字符的准确Rect,因此仅能 + // 通过特定算法来指定 + Rect chineseRect = new Rect(); + if (specIndex < sortedRect.size()) { + chineseRect = GetChineseRect(sortedRect.get(specIndex)); + } else { + return -3; + } + + if (this.isDebug) { + Mat chineseMat = new Mat(img_threshold, chineseRect); + String str = tempPath + "debug_chineseMat.jpg"; + opencv_imgcodecs.imwrite(str, chineseMat); + } + + // 新建一个全新的排序Rect + // 将中文字符Rect第一个加进来,因为它肯定是最左边的 + // 其余的Rect只按照顺序去6个,车牌只可能是7个字符!这样可以避免阴影导致的“1”字符 + Vector newSortedRect = new Vector(); + newSortedRect.add(chineseRect); + RebuildRect(sortedRect, newSortedRect, specIndex, color); + + if (newSortedRect.size() == 0) { + return -3; + } + + for (int i = 0; i < newSortedRect.size(); i++) { + Rect mr = newSortedRect.get(i); + Mat auxRoi = new Mat(img_threshold, mr); + + auxRoi = preprocessChar(auxRoi); + if (this.isDebug) { + String str = tempPath + "debug_char_auxRoi_" + Integer.valueOf(i).toString() + ".jpg"; + opencv_imgcodecs.imwrite(str, auxRoi); + } + resultVec.add(auxRoi); + } + return 0; + } + + /** + * 字符尺寸验证;去掉尺寸不符合的图块 + * @param r + * @return + */ + public static Boolean verifySizes(Mat r) { + float aspect = 45.0f / 90.0f; + float charAspect = (float) r.cols() / (float) r.rows(); + float error = 0.7f; + float minHeight = 10f; + float maxHeight = 35f; + // We have a different aspect ratio for number 1, and it can be ~0.2 + float minAspect = 0.05f; + float maxAspect = aspect + aspect * error; + // area of pixels + float area = countNonZero(r); + // bb area + float bbArea = r.cols() * r.rows(); + // % of pixel in area + float percPixels = area / bbArea; + + return percPixels <= 1 && charAspect > minAspect && charAspect < maxAspect && r.rows() >= minHeight && r.rows() < maxHeight; + } + + /** + * 字符预处理: 统一每个字符的大小 + * + * @param in + * @return + */ + private Mat preprocessChar(Mat in) { + int h = in.rows(); + int w = in.cols(); + int charSize = CHAR_SIZE; + Mat transformMat = Mat.eye(2, 3, CV_32F).asMat(); + int m = Math.max(w, h); + transformMat.ptr(0, 2).put(Convert.getBytes(((m - w) / 2f))); + transformMat.ptr(1, 2).put(Convert.getBytes((m - h) / 2f)); + + Mat warpImage = new Mat(m, m, in.type()); + warpAffine(in, warpImage, transformMat, warpImage.size(), INTER_LINEAR, opencv_core.BORDER_CONSTANT, new Scalar(0)); + + Mat out = new Mat(); + resize(warpImage, out, new Size(charSize, charSize)); + + return out; + } + + /** + * 去除车牌上方的钮钉 + *

+ * 计算每行元素的阶跃数,如果小于X认为是柳丁,将此行全部填0(涂黑), X可根据实际调整 + * + * @param img + * @return + */ + private Mat clearLiuDing(Mat img) { + final int x = this.liuDingSize; + + Mat jump = Mat.zeros(1, img.rows(), CV_32F).asMat(); + for (int i = 0; i < img.rows(); i++) { + int jumpCount = 0; + for (int j = 0; j < img.cols() - 1; j++) { + if (img.ptr(i, j).get() != img.ptr(i, j + 1).get()) + jumpCount++; + } + jump.ptr(i).put(Convert.getBytes((float) jumpCount)); + } + for (int i = 0; i < img.rows(); i++) { + if (Convert.toFloat(jump.ptr(i)) <= x) { + for (int j = 0; j < img.cols(); j++) { + img.ptr(i, j).put((byte) 0); + } + } + } + return img; + } + + /** + * 根据特殊车牌来构造猜测中文字符的位置和大小 + * + * @param rectSpe + * @return + */ + private Rect GetChineseRect(final Rect rectSpe) { + int height = rectSpe.height(); + float newwidth = rectSpe.width() * 1.15f; + int x = rectSpe.x(); + int y = rectSpe.y(); + + int newx = x - (int) (newwidth * 1.15); + newx = Math.max(newx, 0); + Rect a = new Rect(newx, y, (int) newwidth, height); + return a; + } + + /** + * 找出指示城市的字符的Rect,例如苏A7003X,就是A的位置 + * 之所以选择城市的字符位置,是因为该位置不管什么字母,占用的宽度跟高度的差不多,能大大提高位置的准确性 + * @param vecRect + * @return + */ + private int GetSpecificRect(final Vector vecRect, PlateColor color) { + Vector xpositions = new Vector(); + int maxHeight = 0; + int maxWidth = 0; + for (int i = 0; i < vecRect.size(); i++) { + xpositions.add(vecRect.get(i).x()); + + if (vecRect.get(i).height() > maxHeight) { + maxHeight = vecRect.get(i).height(); + } + if (vecRect.get(i).width() > maxWidth) { + maxWidth = vecRect.get(i).width(); + } + } + + int specIndex = 0; + for (int i = 0; i < vecRect.size(); i++) { + Rect mr = vecRect.get(i); + int midx = mr.x() + mr.width() / 2; + + if(PlateColor.GREEN.equals(color)) { + if ((mr.width() > maxWidth * 0.8 || mr.height() > maxHeight * 0.8) + && (midx < this.theMatWidth * 2 / 8 && midx > this.theMatWidth / 8)) { + specIndex = i; + } + } else { + // 如果一个字符有一定的大小,并且在整个车牌的1/7到2/7之间,则是我们要找的特殊车牌 + if ((mr.width() > maxWidth * 0.8 || mr.height() > maxHeight * 0.8) + && (midx < this.theMatWidth * 2 / 7 && midx > this.theMatWidth / 7)) { + specIndex = i; + } + } + } + + return specIndex; + } + + /** + * 这个函数做两个事情 + *

    + *
  • 把特殊字符Rect左边的全部Rect去掉,后面再重建中文字符的位置; + *
  • 从特殊字符Rect开始,依次选择6个Rect,多余的舍去。 + *
      + * + * @param vecRect + * @param outRect + * @param specIndex + * @return + */ + private int RebuildRect(final Vector vecRect, Vector outRect, int specIndex, PlateColor color) { + // 最大只能有7个Rect,减去中文的就只有6个Rect + int count = 6; + if(PlateColor.GREEN.equals(color)) { + count = 7; // 绿牌要多一个 + } + for (int i = 0; i < vecRect.size(); i++) { + // 将特殊字符左边的Rect去掉,这个可能会去掉中文Rect,不过没关系,我们后面会重建。 + if (i < specIndex) + continue; + + outRect.add(vecRect.get(i)); + if (--count == 0) + break; + } + + return 0; + } + + /** + * 将Rect按位置从左到右进行排序 + * + * @param vecRect + * @param out + * @return + */ + public static void SortRect(final Vector vecRect, Vector out) { + Vector orderIndex = new Vector(); + Vector xpositions = new Vector(); + for (int i = 0; i < vecRect.size(); ++i) { + orderIndex.add(i); + xpositions.add(vecRect.get(i).x()); + } + + float min = xpositions.get(0); + int minIdx; + for (int i = 0; i < xpositions.size(); ++i) { + min = xpositions.get(i); + minIdx = i; + for (int j = i; j < xpositions.size(); ++j) { + if (xpositions.get(j) < min) { + min = xpositions.get(j); + minIdx = j; + } + } + int aux_i = orderIndex.get(i); + int aux_min = orderIndex.get(minIdx); + orderIndex.remove(i); + orderIndex.insertElementAt(aux_min, i); + orderIndex.remove(minIdx); + orderIndex.insertElementAt(aux_i, minIdx); + + float aux_xi = xpositions.get(i); + float aux_xmin = xpositions.get(minIdx); + xpositions.remove(i); + xpositions.insertElementAt((int) aux_xmin, i); + xpositions.remove(minIdx); + xpositions.insertElementAt((int) aux_xi, minIdx); + } + + for (int i = 0; i < orderIndex.size(); i++) + out.add(vecRect.get(orderIndex.get(i))); + + return; + } + + public void setLiuDingSize(int param) { + this.liuDingSize = param; + } + + public void setColorThreshold(int param) { + this.colorThreshold = param; + } + + public void setBluePercent(float param) { + this.bluePercent = param; + } + + public final float getBluePercent() { + return this.bluePercent; + } + + public void setWhitePercent(float param) { + this.whitePercent = param; + } + + public final float getWhitePercent() { + return this.whitePercent; + } + + public boolean getDebug() { + return this.isDebug; + } + + public void setDebug(boolean isDebug) { + this.isDebug = isDebug; + } + + +} diff --git a/yuxue/easypr/core/CoreFunc.java b/yuxue/easypr/core/CoreFunc.java new file mode 100644 index 0000000..e0f8c22 --- /dev/null +++ b/yuxue/easypr/core/CoreFunc.java @@ -0,0 +1,269 @@ +package com.yuxue.easypr.core; + +import org.bytedeco.javacpp.BytePointer; +import org.bytedeco.javacpp.opencv_core; +import org.bytedeco.javacpp.opencv_core.Mat; +import org.bytedeco.javacpp.opencv_core.MatVector; +import org.bytedeco.javacpp.opencv_core.Size; +import org.bytedeco.javacpp.opencv_highgui; +import org.bytedeco.javacpp.opencv_imgproc; +import org.bytedeco.javacpp.indexer.FloatIndexer; + +import com.yuxue.enumtype.Direction; +import com.yuxue.enumtype.PlateColor; + + +/** + * + * @author yuxue + * @date 2020-05-16 21:09 + */ +public class CoreFunc { + + /** + * 根据一幅图像与颜色模板获取对应的二值图 + * + * @param src + * 输入RGB图像 + * @param r + * 颜色模板(蓝色、黄色) + * @param adaptive_minsv + * S和V的最小值由adaptive_minsv这个bool值判断 + *
        + *
      • 如果为true,则最小值取决于H值,按比例衰减 + *
      • 如果为false,则不再自适应,使用固定的最小值minabs_sv + *
      + * @return 输出灰度图(只有0和255两个值,255代表匹配,0代表不匹配) + */ + public static Mat colorMatch(final Mat src, final PlateColor r, final boolean adaptive_minsv) { + final float max_sv = 255; + final float minref_sv = 64; + final float minabs_sv = 95; + + // 转到HSV空间进行处理,颜色搜索主要使用的是H分量进行蓝色与黄色的匹配工作 + Mat src_hsv = new Mat(); + opencv_imgproc.cvtColor(src, src_hsv, opencv_imgproc.CV_BGR2HSV); + MatVector hsvSplit = new MatVector(); + opencv_core.split(src_hsv, hsvSplit); + opencv_imgproc.equalizeHist(hsvSplit.get(2), hsvSplit.get(2)); + opencv_core.merge(hsvSplit, src_hsv); + + // 匹配模板基色,切换以查找想要的基色 + int min_h = r.minH; + int max_h = r.maxH; + + float diff_h = (float) ((max_h - min_h) / 2); + int avg_h = (int) (min_h + diff_h); + + int channels = src_hsv.channels(); + int nRows = src_hsv.rows(); + // 图像数据列需要考虑通道数的影响; + int nCols = src_hsv.cols() * channels; + + // 连续存储的数据,按一行处理 + if (src_hsv.isContinuous()) { + nCols *= nRows; + nRows = 1; + } + + for (int i = 0; i < nRows; ++i) { + BytePointer p = src_hsv.ptr(i); + for (int j = 0; j < nCols; j += 3) { + int H = p.get(j) & 0xFF; + int S = p.get(j + 1) & 0xFF; + int V = p.get(j + 2) & 0xFF; + + boolean colorMatched = false; + + if (H > min_h && H < max_h) { + int Hdiff = 0; + if (H > avg_h) + Hdiff = H - avg_h; + else + Hdiff = avg_h - H; + + float Hdiff_p = Hdiff / diff_h; + + float min_sv = 0; + if (true == adaptive_minsv) + min_sv = minref_sv - minref_sv / 2 * (1 - Hdiff_p); + else + min_sv = minabs_sv; + + if ((S > min_sv && S <= max_sv) && (V > min_sv && V <= max_sv)) + colorMatched = true; + } + + if (colorMatched == true) { + p.put(j, (byte) 0); + p.put(j + 1, (byte) 0); + p.put(j + 2, (byte) 255); + } else { + p.put(j, (byte) 0); + p.put(j + 1, (byte) 0); + p.put(j + 2, (byte) 0); + } + } + } + + // 获取颜色匹配后的二值灰度图 + MatVector hsvSplit_done = new MatVector(); + opencv_core.split(src_hsv, hsvSplit_done); + Mat src_grey = hsvSplit_done.get(2); + + return src_grey; + } + + /** + * 判断一个车牌的颜色 + * + * @param src + * 车牌mat + * @param r + * 颜色模板 + * @param adaptive_minsv + * S和V的最小值由adaptive_minsv这个bool值判断 + *
        + *
      • 如果为true,则最小值取决于H值,按比例衰减 + *
      • 如果为false,则不再自适应,使用固定的最小值minabs_sv + *
      + * @return + */ + public static boolean plateColorJudge(final Mat src, final PlateColor color, final boolean adaptive_minsv) { + // 判断阈值 + final float thresh = 0.49f; + + Mat gray = colorMatch(src, color, adaptive_minsv); + + float percent = (float) opencv_core.countNonZero(gray) / (gray.rows() * gray.cols()); + + return (percent > thresh) ? true : false; + } + + /** + * getPlateType 判断车牌的类型 + * + * @param src + * @param adaptive_minsv + * S和V的最小值由adaptive_minsv这个bool值判断 + *
        + *
      • 如果为true,则最小值取决于H值,按比例衰减 + *
      • 如果为false,则不再自适应,使用固定的最小值minabs_sv + *
      + * @return + */ + public static PlateColor getPlateType(final Mat src, final boolean adaptive_minsv) { + if (plateColorJudge(src, PlateColor.BLUE, adaptive_minsv) == true) { + return PlateColor.BLUE; + } else if (plateColorJudge(src, PlateColor.YELLOW, adaptive_minsv) == true) { + return PlateColor.YELLOW; + } else if (plateColorJudge(src, PlateColor.GREEN, adaptive_minsv) == true) { + return PlateColor.GREEN; + } else { + return PlateColor.UNKNOWN; + } + } + + /** + * 获取垂直或水平方向直方图 + * + * @param img + * @param direction + * @return + */ + public static float[] projectedHistogram(final Mat img, Direction direction) { + int sz = 0; + switch (direction) { + case HORIZONTAL: + sz = img.rows(); + break; + + case VERTICAL: + sz = img.cols(); + break; + + default: + break; + } + + // 统计这一行或一列中,非零元素的个数,并保存到nonZeroMat中 + float[] nonZeroMat = new float[sz]; + opencv_core.extractChannel(img, img, 0); + for (int j = 0; j < sz; j++) { + Mat data = (direction == Direction.HORIZONTAL) ? img.row(j) : img.col(j); + int count = opencv_core.countNonZero(data); + nonZeroMat[j] = count; + } + + // Normalize histogram + float max = 0; + for (int j = 0; j < nonZeroMat.length; ++j) { + max = Math.max(max, nonZeroMat[j]); + } + + if (max > 0) { + for (int j = 0; j < nonZeroMat.length; ++j) { + nonZeroMat[j] /= max; + } + } + + return nonZeroMat; + } + + /** + * Assign values to feature + *

      + * 样本特征为水平、垂直直方图和低分辨率图像所组成的矢量 + * + * @param in + * @param sizeData + * 低分辨率图像size = sizeData*sizeData, 可以为0 + * @return + */ + public static Mat features(final Mat in, final int sizeData) { + + float[] vhist = projectedHistogram(in, Direction.VERTICAL); + float[] hhist = projectedHistogram(in, Direction.HORIZONTAL); + + Mat lowData = new Mat(); + if (sizeData > 0) { + // resize.cpp:3784: error: (-215:Assertion failed) !ssize.empty() in function 'cv::resize' + opencv_imgproc.resize(in, lowData, new Size(sizeData, sizeData)); + } + + int numCols = vhist.length + hhist.length + lowData.cols() * lowData.rows(); + Mat out = Mat.zeros(1, numCols, opencv_core.CV_32F).asMat(); + FloatIndexer idx = out.createIndexer(); + + int j = 0; + for (int i = 0; i < vhist.length; ++i, ++j) { + idx.put(0, j, vhist[i]); + } + for (int i = 0; i < hhist.length; ++i, ++j) { + idx.put(0, j, hhist[i]); + } + for (int x = 0; x < lowData.cols(); x++) { + for (int y = 0; y < lowData.rows(); y++, ++j) { + float val = lowData.ptr(x, y).get(0) & 0xFF; + idx.put(0, j, val); + } + } + + return out; + } + + + + /** + * 显示图像 + * @param title + * @param src + */ + public static void showImage(final String title, final Mat src) { + if (src != null) { + opencv_highgui.imshow(title, src); + opencv_highgui.cvWaitKey(0); + } + } + +} diff --git a/yuxue/easypr/core/Features.java b/yuxue/easypr/core/Features.java new file mode 100644 index 0000000..6384828 --- /dev/null +++ b/yuxue/easypr/core/Features.java @@ -0,0 +1,90 @@ +package com.yuxue.easypr.core; + +import static com.yuxue.easypr.core.CoreFunc.features; +import static org.bytedeco.javacpp.opencv_core.merge; +import static org.bytedeco.javacpp.opencv_core.split; + +import org.bytedeco.javacpp.opencv_core.Mat; +import org.bytedeco.javacpp.opencv_core.MatVector; +import org.bytedeco.javacpp.opencv_imgproc; + +/** + * + * @author yuxue + * @date 2020-05-05 08:26 + */ +public class Features implements SVMCallback { + + /*** + * EasyPR的getFeatures回调函数 + * 本函数是生成直方图均衡特征的回调函数 + * @param image + * @return + */ + @Override + public Mat getHisteqFeatures(final Mat image) { + return histeq(image); + } + + private Mat histeq(Mat in) { + Mat out = new Mat(in.size(), in.type()); + if (in.channels() == 3) { + Mat hsv = new Mat(); + MatVector hsvSplit = new MatVector(); + opencv_imgproc.cvtColor(in, hsv, opencv_imgproc.CV_BGR2HSV); + split(hsv, hsvSplit); + opencv_imgproc.equalizeHist(hsvSplit.get(2), hsvSplit.get(2)); + merge(hsvSplit, hsv); + opencv_imgproc.cvtColor(hsv, out, opencv_imgproc.CV_HSV2BGR); + hsv = null; + hsvSplit = null; + System.gc(); + } else if (in.channels() == 1) { + opencv_imgproc.equalizeHist(in, out); + } + return out; + } + + /** + * EasyPR的getFeatures回调函数 + * 本函数是获取垂直和水平的直方图图值 + * @param image + * @return + */ + @Override + public Mat getHistogramFeatures(Mat image) { + Mat grayImage = new Mat(); + opencv_imgproc.cvtColor(image, grayImage, opencv_imgproc.CV_RGB2GRAY); + + Mat img_threshold = new Mat(); + opencv_imgproc.threshold(grayImage, img_threshold, 0, 255, opencv_imgproc.CV_THRESH_OTSU + opencv_imgproc.CV_THRESH_BINARY); + + return features(img_threshold, 0); + } + + /** + * 本函数是获取SITF特征子的回调函数 + * + * @param image + * @return + */ + @Override + public Mat getSIFTFeatures(final Mat image) { + // TODO: 待完善 + return null; + } + + /** + * 本函数是获取HOG特征子的回调函数 + * + * @param image + * @return + */ + @Override + public Mat getHOGFeatures(final Mat image) { + // TODO: 待完善 + return null; + } + + +} diff --git a/yuxue/easypr/core/PlateDetect.java b/yuxue/easypr/core/PlateDetect.java new file mode 100644 index 0000000..d304177 --- /dev/null +++ b/yuxue/easypr/core/PlateDetect.java @@ -0,0 +1,111 @@ +package com.yuxue.easypr.core; + +import java.util.Vector; + +import org.bytedeco.javacpp.opencv_core.Mat; + + +/** + * 车牌检测识别 + * 分两个步骤: 1、车牌定位 2、车牌判断 + * @author yuxue + * @date 2020-04-24 15:33 + */ +public class PlateDetect { + + // 车牌定位, 图片处理对象 + private PlateLocate plateLocate = new PlateLocate(); + + // 切图判断对象 + private PlateJudge plateJudge = new PlateJudge(); + + /** + * @param src 图片路径,不能包含中文及特殊字符 + * @param resultVec 车牌的图块集合 + * @return the error number + *

        + *
      • 0: plate detected successfully; + *
      • -1: source Mat is empty; + *
      • -2: plate not detected. + *
      + */ + public int plateDetect(final Mat src, Vector resultVec) { + Vector matVec = plateLocate.plateLocate(src); // 定位 + + if (0 == matVec.size()) { + return -1; + } + + if (0 != plateJudge.plateJudge(matVec, resultVec)) { //对多幅图像进行SVM判断 + return -2; + } + return 0; + } + + + /** + * 生活模式与工业模式切换 + * @param pdLifemode + */ + public void setPDLifemode(boolean pdLifemode) { + plateLocate.setLifemode(pdLifemode); + } + + public void setGaussianBlurSize(int gaussianBlurSize) { + plateLocate.setGaussianBlurSize(gaussianBlurSize); + } + + public final int getGaussianBlurSize() { + return plateLocate.getGaussianBlurSize(); + } + + public void setMorphSizeWidth(int morphSizeWidth) { + plateLocate.setMorphSizeWidth(morphSizeWidth); + } + + public final int getMorphSizeWidth() { + return plateLocate.getMorphSizeWidth(); + } + + public void setMorphSizeHeight(int morphSizeHeight) { + plateLocate.setMorphSizeHeight(morphSizeHeight); + } + + public final int getMorphSizeHeight() { + return plateLocate.getMorphSizeHeight(); + } + + public void setVerifyError(float verifyError) { + plateLocate.setVerifyError(verifyError); + } + + public final float getVerifyError() { + return plateLocate.getVerifyError(); + } + + public void setVerifyAspect(float verifyAspect) { + plateLocate.setVerifyAspect(verifyAspect); + } + + public final float getVerifyAspect() { + return plateLocate.getVerifyAspect(); + } + + public void setVerifyMin(int verifyMin) { + plateLocate.setVerifyMin(verifyMin); + } + + public void setVerifyMax(int verifyMax) { + plateLocate.setVerifyMax(verifyMax); + } + + public void setJudgeAngle(int judgeAngle) { + plateLocate.setJudgeAngle(judgeAngle); + } + + public void setDebug(boolean debug, String tempPath) { + plateLocate.setDebug(debug); + plateLocate.setTempPath(tempPath); + } + +} diff --git a/yuxue/easypr/core/PlateJudge.java b/yuxue/easypr/core/PlateJudge.java new file mode 100644 index 0000000..a743ea4 --- /dev/null +++ b/yuxue/easypr/core/PlateJudge.java @@ -0,0 +1,107 @@ +package com.yuxue.easypr.core; + +import org.bytedeco.javacpp.opencv_core; +import org.bytedeco.javacpp.opencv_imgproc; + +import java.util.Vector; + +import org.bytedeco.javacpp.opencv_core.Mat; +import org.bytedeco.javacpp.opencv_core.Rect; +import org.bytedeco.javacpp.opencv_core.Size; +import org.bytedeco.javacpp.opencv_ml.SVM; + +import com.yuxue.constant.Constant; + + +/** + * 车牌判断 + * @author yuxue + * @date 2020-04-26 15:21 + */ +public class PlateJudge { + + private SVM svm = SVM.create(); + + public PlateJudge() { + loadSVM(Constant.DEFAULT_SVM_PATH); + } + + public void loadSVM(String path) { + svm.clear(); + // svm=SVM.loadSVM(path, "svm"); + svm=SVM.load(path); + } + + /** + * EasyPR的getFeatures回调函数, 用于从车牌的image生成svm的训练特征features + */ + private SVMCallback features = new Features(); + + + /** + * 对单幅图像进行SVM判断 + * @param inMat + * @return + */ + public int plateJudge(final Mat inMat) { + int ret = 1; + // 使用com.yuxue.train.SVMTrain 生成的训练库文件 + Mat features = this.features.getHistogramFeatures(inMat); + /*Mat samples = features.reshape(1, 1); + samples.convertTo(samples, opencv_core.CV_32F);*/ + + Mat p = features.reshape(1, 1); + p.convertTo(p, opencv_core.CV_32FC1); + ret = (int) svm.predict(features); + return ret; + + // 使用com.yuxue.train.PlateRecoTrain 生成的训练库文件 + // 在使用的过程中,传入的样本切图要跟训练的时候处理切图的方法一致 + /*Mat grayImage = new Mat(); + opencv_imgproc.cvtColor(inMat, grayImage, opencv_imgproc.CV_RGB2GRAY); + Mat dst = new Mat(); + opencv_imgproc.Canny(grayImage, dst, 130, 250); + Mat samples = dst.reshape(1, 1); + samples.convertTo(samples, opencv_core.CV_32F);*/ + + // 正样本为0 负样本为1 + /*if(svm.predict(samples) <= 0) { + ret = 1; + }*/ + /*ret = (int)svm.predict(samples); + System.err.println(ret); + return ret ;*/ + + } + + /** + * 对多幅图像进行SVM判断 + * @param inVec + * @param resultVec + * @return + */ + public int plateJudge(Vector inVec, Vector resultVec) { + + for (int j = 0; j < inVec.size(); j++) { + Mat inMat = inVec.get(j); + + if (1 == plateJudge(inMat)) { + resultVec.add(inMat); + } else { // 再取中间部分判断一次 + int w = inMat.cols(); + int h = inMat.rows(); + + Mat tmpDes = inMat.clone(); + Mat tmpMat = new Mat(inMat, new Rect((int) (w * 0.05), (int) (h * 0.1), (int) (w * 0.9), (int) (h * 0.8))); + opencv_imgproc.resize(tmpMat, tmpDes, new Size(inMat.size())); + + if (plateJudge(tmpDes) == 1) { + resultVec.add(inMat); + } + } + } + return 0; + } + + +} diff --git a/yuxue/easypr/core/PlateLocate.java b/yuxue/easypr/core/PlateLocate.java new file mode 100644 index 0000000..c889d80 --- /dev/null +++ b/yuxue/easypr/core/PlateLocate.java @@ -0,0 +1,354 @@ +package com.yuxue.easypr.core; + +import java.util.Vector; + +import static org.bytedeco.javacpp.opencv_core.*; +import static org.bytedeco.javacpp.opencv_imgproc.*; + +import com.yuxue.constant.Constant; +import org.bytedeco.javacpp.opencv_imgcodecs; + +import org.bytedeco.javacpp.opencv_core.CvPoint2D32f; +import org.bytedeco.javacpp.opencv_core.Mat; +import org.bytedeco.javacpp.opencv_core.MatVector; +import org.bytedeco.javacpp.opencv_core.Point; +import org.bytedeco.javacpp.opencv_core.Point2f; +import org.bytedeco.javacpp.opencv_core.RotatedRect; +import org.bytedeco.javacpp.opencv_core.Scalar; +import org.bytedeco.javacpp.opencv_core.Size; + + +/** + * 车牌定位 + * @author yuxue + * @date 2020-04-24 15:33 + */ +public class PlateLocate { + + // PlateLocate所用常量 + public static final int DEFAULT_GAUSSIANBLUR_SIZE = 5; + public static final int SOBEL_SCALE = 1; + public static final int SOBEL_DELTA = 0; + public static final int SOBEL_DDEPTH = CV_16S; + public static final int SOBEL_X_WEIGHT = 1; + public static final int SOBEL_Y_WEIGHT = 0; + public static final int DEFAULT_MORPH_SIZE_WIDTH = 17; + public static final int DEFAULT_MORPH_SIZE_HEIGHT = 3; + + // showResultMat所用常量 + public static final int WIDTH = 136; + public static final int HEIGHT = 36; + public static final int TYPE = CV_8UC3; + + // verifySize所用常量 + public static final int DEFAULT_VERIFY_MIN = 3; + public static final int DEFAULT_VERIFY_MAX = 20; + + final float DEFAULT_ERROR = 0.6f; + final float DEFAULT_ASPECT = 3.75f; + + // 角度判断所用常量 + public static final int DEFAULT_ANGLE = 30; + + // 高斯模糊所用变量 + protected int gaussianBlurSize = DEFAULT_GAUSSIANBLUR_SIZE; + + // 连接操作所用变量 + protected int morphSizeWidth = DEFAULT_MORPH_SIZE_WIDTH; + protected int morphSizeHeight = DEFAULT_MORPH_SIZE_HEIGHT; + + // verifySize所用变量 + protected float error = DEFAULT_ERROR; + protected float aspect = DEFAULT_ASPECT; + protected int verifyMin = DEFAULT_VERIFY_MIN; + protected int verifyMax = DEFAULT_VERIFY_MAX; + + // 角度判断所用变量 + protected int angle = DEFAULT_ANGLE; + + // 是否开启调试模式,0关闭,非0开启 + protected boolean debug = true; + + // 开启调试模式之后,切图文件保存路径 + protected String tempPath = Constant.DEFAULT_TEMP_DIR + System.currentTimeMillis() + "/"; + + /** + * 生活模式与工业模式切换 + * @param islifemode + * 如果为真,则设置各项参数为定位生活场景照片(如百度图片)的参数,否则恢复默认值。 + * + */ + public void setLifemode(boolean islifemode) { + if (islifemode) { + setGaussianBlurSize(5); + setMorphSizeWidth(9); + setMorphSizeHeight(3); + setVerifyError(0.9f); + setVerifyAspect(4); + setVerifyMin(1); + setVerifyMax(30); + } else { + setGaussianBlurSize(DEFAULT_GAUSSIANBLUR_SIZE); + setMorphSizeWidth(DEFAULT_MORPH_SIZE_WIDTH); + setMorphSizeHeight(DEFAULT_MORPH_SIZE_HEIGHT); + setVerifyError(DEFAULT_ERROR); + setVerifyAspect(DEFAULT_ASPECT); + setVerifyMin(DEFAULT_VERIFY_MIN); + setVerifyMax(DEFAULT_VERIFY_MAX); + } + } + + /** + * 定位车牌图像 + * @param src 原始图像 + * @return 一个Mat的向量,存储所有抓取到的图像 + */ + public Vector plateLocate(Mat src) { + Vector resultVec = new Vector(); + + Mat src_blur = new Mat(); + Mat src_gray = new Mat(); + Mat grad = new Mat(); + + int scale = SOBEL_SCALE; + int delta = SOBEL_DELTA; + int ddepth = SOBEL_DDEPTH; + + // 高斯模糊。Size中的数字影响车牌定位的效果。 + GaussianBlur(src, src_blur, new Size(gaussianBlurSize, gaussianBlurSize), 0, 0, BORDER_DEFAULT); + if (debug) { + opencv_imgcodecs.imwrite(tempPath + "debug_GaussianBlur.jpg", src_blur); + } + + // Convert it to gray 将图像进行灰度化 + cvtColor(src_blur, src_gray, CV_RGB2GRAY); + if (debug) { + opencv_imgcodecs.imwrite(tempPath + "debug_gray.jpg", src_gray); + } + + // 对图像进行Sobel 运算,得到的是图像的一阶水平方向导数。 + + // Generate grad_x and grad_y + Mat grad_x = new Mat(); + Mat grad_y = new Mat(); + Mat abs_grad_x = new Mat(); + Mat abs_grad_y = new Mat(); + + Sobel(src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT); + convertScaleAbs(grad_x, abs_grad_x); + + Sobel(src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT); + convertScaleAbs(grad_y, abs_grad_y); + + // Total Gradient (approximate) + addWeighted(abs_grad_x, SOBEL_X_WEIGHT, abs_grad_y, SOBEL_Y_WEIGHT, 0, grad); + + if (debug) { + opencv_imgcodecs.imwrite(tempPath + "debug_Sobel.jpg", grad); + } + + // 对图像进行二值化。将灰度图像(每个像素点有256 个取值可能)转化为二值图像(每个像素点仅有1 和0 两个取值可能)。 + + Mat img_threshold = new Mat(); + threshold(grad, img_threshold, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); + + if (debug) { + opencv_imgcodecs.imwrite(tempPath + "debug_threshold.jpg", img_threshold); + } + + // 使用闭操作。对图像进行闭操作以后,可以看到车牌区域被连接成一个矩形装的区域。 + + Mat element = getStructuringElement(MORPH_RECT, new Size(morphSizeWidth, morphSizeHeight)); + morphologyEx(img_threshold, img_threshold, MORPH_CLOSE, element); + + if (debug) { + opencv_imgcodecs.imwrite(tempPath + "debug_morphology.jpg", img_threshold); + } + + // Find 轮廓 of possibles plates 求轮廓。求出图中所有的轮廓。这个算法会把全图的轮廓都计算出来,因此要进行筛选。 + + MatVector contours = new MatVector(); + findContours(img_threshold, contours, // a vector of contours + CV_RETR_EXTERNAL, // 提取外部轮廓 + CV_CHAIN_APPROX_NONE); // all pixels of each contours + + Mat result = new Mat(); + if (debug) { + src.copyTo(result); + // 将轮廓描绘到图上输出 + drawContours(result, contours, -1, new Scalar(0, 0, 255, 255)); + opencv_imgcodecs.imwrite(tempPath + "debug_Contours.jpg", result); + } + + // Start to iterate to each contour founded + // 筛选。对轮廓求最小外接矩形,然后验证,不满足条件的淘汰。 + Vector rects = new Vector(); + + for (int i = 0; i < contours.size(); ++i) { + RotatedRect mr = minAreaRect(contours.get(i)); + if (verifySizes(mr)) + rects.add(mr); + } + + int k = 1; + for (int i = 0; i < rects.size(); i++) { + RotatedRect minRect = rects.get(i); + /*if (debug) { + Point2f rect_points = new Point2f(4); + minRect.points(rect_points); + + for (int j = 0; j < 4; j++) { + Point pt1 = new Point(new CvPoint2D32f(rect_points.position(j))); + Point pt2 = new Point(new CvPoint2D32f(rect_points.position((j + 1) % 4))); + + line(result, pt1, pt2, new Scalar(0, 255, 255, 255), 1, 8, 0); + } + }*/ + + // rotated rectangle drawing + // 旋转这部分代码确实可以将某些倾斜的车牌调整正,但是它也会误将更多正的车牌搞成倾斜!所以综合考虑,还是不使用这段代码。 + // 2014-08-14,由于新到的一批图片中发现有很多车牌是倾斜的,因此决定再次尝试这段代码。 + + float r = minRect.size().width() / minRect.size().height(); + float angle = minRect.angle(); + Size rect_size = new Size((int) minRect.size().width(), (int) minRect.size().height()); + if (r < 1) { + angle = 90 + angle; + rect_size = new Size(rect_size.height(), rect_size.width()); + } + // 如果抓取的方块旋转超过m_angle角度,则不是车牌,放弃处理 + if (angle - this.angle < 0 && angle + this.angle > 0) { + Mat img_rotated = new Mat(); + Mat rotmat = getRotationMatrix2D(minRect.center(), angle, 1); + warpAffine(src, img_rotated, rotmat, src.size()); // CV_INTER_CUBIC + + Mat resultMat = showResultMat(img_rotated, rect_size, minRect.center(), k++); + resultVec.add(resultMat); + } + } + + return resultVec; + } + + + /** + * 对minAreaRect获得的最小外接矩形,用纵横比进行判断 + * + * @param mr + * @return + */ + private boolean verifySizes(RotatedRect mr) { + float error = this.error; + + // China car plate size: 440mm*140mm,aspect 3.142857 + float aspect = this.aspect; + int min = 44 * 14 * verifyMin; // minimum area + int max = 44 * 14 * verifyMax; // maximum area + + // Get only patchs that match to a respect ratio. + float rmin = aspect - aspect * error; + float rmax = aspect + aspect * error; + + int area = (int) (mr.size().height() * mr.size().width()); + float r = mr.size().width() / mr.size().height(); + if (r < 1) + r = mr.size().height() / mr.size().width(); + + return area >= min && area <= max && r >= rmin && r <= rmax; + } + + /** + * 显示最终生成的车牌图像,便于判断是否成功进行了旋转。 + * @param src + * @param rect_size + * @param center + * @param index + * @return + */ + private Mat showResultMat(Mat src, Size rect_size, Point2f center, int index) { + Mat img_crop = new Mat(); + getRectSubPix(src, rect_size, center, img_crop); + + if (debug) { + opencv_imgcodecs.imwrite(tempPath + "debug_crop_" + index + ".jpg", img_crop); + } + + Mat resultResized = new Mat(); + resultResized.create(HEIGHT, WIDTH, TYPE); + resize(img_crop, resultResized, resultResized.size(), 0, 0, INTER_CUBIC); + if (debug) { + opencv_imgcodecs.imwrite(tempPath + "debug_resize_" + index + ".jpg", resultResized); + } + return resultResized; + } + + + + public String getTempPath() { + return tempPath; + } + + public void setTempPath(String tempPath) { + this.tempPath = tempPath; + } + + public void setGaussianBlurSize(int gaussianBlurSize) { + this.gaussianBlurSize = gaussianBlurSize; + } + + public final int getGaussianBlurSize() { + return this.gaussianBlurSize; + } + + public void setMorphSizeWidth(int morphSizeWidth) { + this.morphSizeWidth = morphSizeWidth; + } + + public final int getMorphSizeWidth() { + return this.morphSizeWidth; + } + + public void setMorphSizeHeight(int morphSizeHeight) { + this.morphSizeHeight = morphSizeHeight; + } + + public final int getMorphSizeHeight() { + return this.morphSizeHeight; + } + + public void setVerifyError(float error) { + this.error = error; + } + + public final float getVerifyError() { + return this.error; + } + + public void setVerifyAspect(float aspect) { + this.aspect = aspect; + } + + public final float getVerifyAspect() { + return this.aspect; + } + + public void setVerifyMin(int verifyMin) { + this.verifyMin = verifyMin; + } + + public void setVerifyMax(int verifyMax) { + this.verifyMax = verifyMax; + } + + public void setJudgeAngle(int angle) { + this.angle = angle; + } + + public void setDebug(boolean debug) { + this.debug = debug; + } + public boolean getDebug() { + return debug; + } + +} diff --git a/yuxue/easypr/core/SVMCallback.java b/yuxue/easypr/core/SVMCallback.java new file mode 100644 index 0000000..037c198 --- /dev/null +++ b/yuxue/easypr/core/SVMCallback.java @@ -0,0 +1,44 @@ +package com.yuxue.easypr.core; + +import org.bytedeco.javacpp.opencv_core.Mat; + + +/** + * @author Created by fanwenjie + * @author lin.yao + * + */ +public interface SVMCallback { + + /*** + * EasyPR的getFeatures回调函数,本函数是生成直方图均衡特征的回调函数 + * + * @param image + * @return + */ + public abstract Mat getHisteqFeatures(final Mat image); + + /** + * EasyPR的getFeatures回调函数, 本函数是获取垂直和水平的直方图图值 + * + * @param image + * @return + */ + public abstract Mat getHistogramFeatures(final Mat image); + + /** + * 本函数是获取SITF特征子的回调函数 + * + * @param image + * @return + */ + public abstract Mat getSIFTFeatures(final Mat image); + + /** + * 本函数是获取HOG特征子的回调函数 + * + * @param image + * @return + */ + public abstract Mat getHOGFeatures(final Mat image); +} diff --git a/yuxue/entity/PlateFileEntity.java b/yuxue/entity/PlateFileEntity.java new file mode 100644 index 0000000..18528d2 --- /dev/null +++ b/yuxue/entity/PlateFileEntity.java @@ -0,0 +1,81 @@ +package com.yuxue.entity; + +import java.io.Serializable; +import java.util.List; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * t_plate_file + * @author yuxue + * 2020-04-30 11:04:47.169 + */ +@Data +@NoArgsConstructor +public class PlateFileEntity implements Serializable { + /** + * id + */ + private Integer id; + + /** + * fileName + */ + private String fileName; + + /** + * filePath + */ + private String filePath; + + /** + * fileType + */ + private String fileType; + + /** + * fileLength + */ + private Integer fileLength; + + /** + * plate + */ + private String plate; + + /** + * plateColor + */ + private String plateColor; + + /** + * lastRecoTime + */ + private String lastRecoTime; + + /** + * tempPath + */ + private String tempPath; + + /** + * recoPlate + */ + private String recoPlate; + + /** + * recoColor + */ + private String recoColor; + + /** + * recoCorrect + * 0未识别 1正确 2错误 3未检测到车牌 + */ + private Integer recoCorrect; + + private List debug; + + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/yuxue/entity/PlateRecoDebugEntity.java b/yuxue/entity/PlateRecoDebugEntity.java new file mode 100644 index 0000000..0ca1532 --- /dev/null +++ b/yuxue/entity/PlateRecoDebugEntity.java @@ -0,0 +1,66 @@ +package com.yuxue.entity; + +import java.io.Serializable; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * t_plate_reco_debug + * @author yuxue + * 2020-04-30 16:17:58.795 + */ +@Data +@NoArgsConstructor +public class PlateRecoDebugEntity implements Serializable { + /** + * id + */ + private Integer id; + + /** + * parentId + */ + private Integer parentId; + + /** + * fileName + */ + private String fileName; + + /** + * filePath + */ + private String filePath; + + /** + * debugType + */ + private String debugType; + + /** + * fileLength + */ + private Integer fileLength; + + /** + * lastRecoTime + */ + private String lastRecoTime; + + /** + * recoPlate + */ + private String recoPlate; + + /** + * plateColor + */ + private String plateColor; + + /** + * sort + */ + private Integer sort; + + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/yuxue/entity/Result.java b/yuxue/entity/Result.java new file mode 100644 index 0000000..d245ecb --- /dev/null +++ b/yuxue/entity/Result.java @@ -0,0 +1,93 @@ +package com.yuxue.entity; + +import java.util.HashMap; + +import com.yuxue.exception.ErrorEnum; + + +/** + * 返回值封装模型类 + * @author yuxue + * @date 2018-09-07 + */ +public class Result extends HashMap { + + private static final long serialVersionUID = 1L; + + private static final Integer SUCCESS_CODE = 200; + private static final String SUCCESS_INFO = "Success!"; + + public Result() { + put("code", SUCCESS_CODE); + put("msg", SUCCESS_INFO); + put("success", true); + } + + public Result(Object obj) { + put("code", SUCCESS_CODE); + put("msg", SUCCESS_INFO); + put("obj", obj); + put("success", true); + } + + public static Result ok() { + return new Result(); + } + + public static Result ok(Object obj) { + return new Result(obj); + } + + /** + * 待办任务切面需要返回的数据 + * 与前端业务逻辑无关 + * + * @param todo + * @return + */ + public static Result ok(Object obj, Object todo) { + Result result = new Result(obj); + result.put("todo", todo); + return result; + } + + public static Result error() { + return error(ErrorEnum.COMMON_ERROR); + } + + public static Result error(String msg) { + Result result = error(ErrorEnum.COMMON_ERROR); + result.put("msg", msg); + return result; + } + + public static Result error(String msg, int code) { + Result result = error(ErrorEnum.COMMON_ERROR); + result.put("msg", msg); + result.put("code", code); + return result; + } + + public static Result error(ErrorEnum fwWebError) { + Result result = new Result(); + result.put("code", fwWebError.code); + result.put("msg", fwWebError.msg); + result.put("success", false); + return result; + } + + public static Result error(int code, String msg) { + Result result = new Result(); + result.put("code", code); + result.put("msg", msg); + result.put("success", false); + return result; + } + + + @Override + public Result put(String key, Object value) { + super.put(key, value); + return this; + } +} diff --git a/yuxue/entity/SystemMenuEntity.java b/yuxue/entity/SystemMenuEntity.java new file mode 100644 index 0000000..7b334c8 --- /dev/null +++ b/yuxue/entity/SystemMenuEntity.java @@ -0,0 +1,53 @@ +package com.yuxue.entity; + +import java.io.Serializable; +import java.util.Date; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * t_system_menu + * @author + */ +@Data +@NoArgsConstructor +public class SystemMenuEntity implements Serializable { + private Integer id; + + private String menuName; + + private String menuUrl; + + private Integer parentId; + + private Integer sort; + + private Integer menuLevel; + + private String menuIcon; + + private Integer showFlag; + + private Integer platform; + + private Integer menuType; + + private String permission; + + private Date updateTime; + + private Integer editorId; + + private String createTime; + + private Integer creatorId; + + private Integer version; + + private Integer delFlag; + + private static final long serialVersionUID = 1L; + + +} \ No newline at end of file diff --git a/yuxue/entity/TempPlateFileEntity.java b/yuxue/entity/TempPlateFileEntity.java new file mode 100644 index 0000000..cf71987 --- /dev/null +++ b/yuxue/entity/TempPlateFileEntity.java @@ -0,0 +1,51 @@ +package com.yuxue.entity; + +import java.io.Serializable; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * temp_plate_file + * @author yuxue + * 2020-04-30 09:39:59.928 + */ +@Data +@NoArgsConstructor +public class TempPlateFileEntity implements Serializable { + /** + * id + */ + private Integer id; + + /** + * fileName + */ + private String fileName; + + /** + * filePath + */ + private String filePath; + + /** + * fileType + */ + private String fileType; + + /** + * fileLength + */ + private Long fileLength; + + /** + * parentId + */ + private Integer parentId; + + /** + * level + */ + private Integer level; + + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/yuxue/enumtype/Direction.java b/yuxue/enumtype/Direction.java new file mode 100644 index 0000000..bb791ff --- /dev/null +++ b/yuxue/enumtype/Direction.java @@ -0,0 +1,46 @@ +package com.yuxue.enumtype; + +public enum Direction { + + VERTICAL("VERTICAL","垂直"), + HORIZONTAL("HORIZONTAL","水平"), + UNKNOWN("UNKNOWN","未知"); + + public final String code; + public final String desc; + + Direction(String code, String desc) { + this.code = code; + this.desc = desc; + } + + public static String getDesc(String code) { + Direction[] enums = values(); + for (Direction type : enums) { + if (type.code().equals(code)) { + return type.desc(); + } + } + return null; + } + + public static String getCode(String desc) { + Direction[] enums = values(); + for (Direction type : enums) { + if (type.desc().equals(desc)) { + return type.code(); + } + } + return null; + } + + + public String code() { + return this.code; + } + + public String desc() { + return this.desc; + } + +} diff --git a/yuxue/enumtype/PlateColor.java b/yuxue/enumtype/PlateColor.java new file mode 100644 index 0000000..c305c2c --- /dev/null +++ b/yuxue/enumtype/PlateColor.java @@ -0,0 +1,59 @@ +package com.yuxue.enumtype; + + +/** + * 车牌颜色 + * @author yuxue + * @date 2020-05-08 12:38 + */ +public enum PlateColor { + + BLUE("BLUE","蓝牌", 100, 130), + GREEN("GREEN","绿牌", 38, 100), + YELLOW("YELLOW","黄牌", 15, 40), + UNKNOWN("UNKNOWN","未知", 0, 0); + + public final String code; + public final String desc; + + // opencv颜色识别的HSV中各个颜色所对应的H的范围: Orange 0-22 Yellow 22- 38 Green 38-75 Blue 75-130 + public final int minH; + public final int maxH; + + PlateColor(String code, String desc, int minH, int maxH) { + this.code = code; + this.desc = desc; + this.minH = minH; + this.maxH = maxH; + } + + public static String getDesc(String code) { + PlateColor[] enums = values(); + for (PlateColor type : enums) { + if (type.code().equals(code)) { + return type.desc(); + } + } + return null; + } + + public static String getCode(String desc) { + PlateColor[] enums = values(); + for (PlateColor type : enums) { + if (type.desc().equals(desc)) { + return type.code(); + } + } + return null; + } + + + public String code() { + return this.code; + } + + public String desc() { + return this.desc; + } + +} diff --git a/yuxue/exception/ErrorEnum.java b/yuxue/exception/ErrorEnum.java new file mode 100644 index 0000000..581135b --- /dev/null +++ b/yuxue/exception/ErrorEnum.java @@ -0,0 +1,69 @@ +package com.yuxue.exception; + +/** + * 系统错误提示 + * @author yuxue + * @date 2018-09-07 + */ +public enum ErrorEnum { + + // 200-->Success! + // 6000-->Fail! + + // common + COMMON_ERROR("Fail!", 6000), + COMMON_PARAMS_ERR("提交参数不合法", 6001), + COMMON_PARAMS_ID_ERR("提交参数ID不合法", 6002), + COMMON_EMPTY_CONDITION_RESULT("没有找到符合条件的数据", 6003), + COMMON_PARAMS_NOT_EXIST("提交的字段不存在,或者参数格式错误", 6004), + + // sql + SQL_ERROR("mysql通用错误", 6100), + SQL_INSERT_FAIL("增加失败", 6101), + SQL_DELETE_FAIL("删除失败", 6102), + SQL_UPDATE_FAIL("修改失败", 6103), + SQL_RECORD_EXIST("添加重复记录", 6104), + SQL_ID_NOT_EXIST("主键ID不能为空", 6105), + SQL_VERSION_NOT_EXIST("数据版本version不能为空", 6106), + + // io + FILE_IO_ERROR("io通用错误", 6200), + FILE_NOT_EXIST("文件没找到,请联系管理员", 6201), + FILE_DATA_NULL("文档中不不存在有效的数据", 6202), + FILE_DATA_ERR("文档中的数据格式错误", 6203), + + // form + INVALID_PASSWORD("密码格式错误", 6300), + INVALID_EMAIL("邮件格式错误", 6301), + INVALID_NAME("账号格式错误", 6302), + INVALID_PARAMS("填写字段不合法", 6303), + + + // shiro-login + NO_LOGIN("用户未登录", 401), + UNAUTHORIZED("权限不足", 7001), + ADMIN_ONLY("只有管理员账号可以调用这个接口", 6402), + NO_PERSSIOM("没有权限请求", 6403), + WRONG_ACCOUNT_OR_PSW("账号或密码错误", 6404), + WRONG_ACCOUNT_PSW("账号密码错误", 6405), + WRONG_ACCOUNT_WRONG("用户没有权限(令牌、用户名、密码错误)", 401), + + // uploading + UPLOAD_FILE_TYPE_ERROR("上传文件格式错误", 6500), + UPLOAD_FILE_UPLOADING("uploading", 6501), + UPLOAD_FILE_NOT_EXIST("文件不存在", 6502), + UPLOAD_FILE_SIZE_MAX("上传的文件大小超出限制", 6503), + + // es + ES_BIG_PAGE_SEARCH("单页查询数据不能超过10000!", 9000); + + // NoSQL + + public final String msg; + public final int code; + + ErrorEnum(String msg, int code) { + this.msg = msg; + this.code = code; + } +} diff --git a/yuxue/exception/ResultReturnException.java b/yuxue/exception/ResultReturnException.java new file mode 100644 index 0000000..6df44ac --- /dev/null +++ b/yuxue/exception/ResultReturnException.java @@ -0,0 +1,53 @@ +package com.yuxue.exception; + + +/** + * 自定义runtime异常 + * @author yuxue + * @date 2018-09-07 + */ +public class ResultReturnException extends RuntimeException { + private static final long serialVersionUID = 1L; + + private String msg = ErrorEnum.COMMON_ERROR.msg; + private int code = ErrorEnum.COMMON_ERROR.code; + + public ResultReturnException(ErrorEnum error) { + super(error.msg); + this.msg = error.msg; + this.code = error.code; + } + + public ResultReturnException(String msg) { + super(msg); + this.msg = msg; + } + + public ResultReturnException(String msg, Throwable e) { + super(msg, e); + this.msg = msg; + } + + @Deprecated + public ResultReturnException(String msg, int code) { + super(msg); + this.msg = msg; + this.code = code; + } + + @Deprecated + public ResultReturnException(String msg, int code, Throwable e) { + super(msg, e); + this.msg = msg; + this.code = code; + } + + public String getMsg() { + return msg; + } + + public int getCode() { + return code; + } + +} diff --git a/yuxue/exception/ResultReturnExceptionHandler.java b/yuxue/exception/ResultReturnExceptionHandler.java new file mode 100644 index 0000000..7765faf --- /dev/null +++ b/yuxue/exception/ResultReturnExceptionHandler.java @@ -0,0 +1,84 @@ +package com.yuxue.exception; + + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.multipart.MultipartException; + +import com.yuxue.entity.Result; + + +/** + * 捕获RestController抛出的异常 + * @author yuxue + * @date 2018-09-06 + */ +@RestControllerAdvice +public class ResultReturnExceptionHandler { + + protected static Logger log=LoggerFactory.getLogger(ResultReturnExceptionHandler.class); + + /** 捕捉shiro的异常 *//* + @ResponseStatus(HttpStatus.UNAUTHORIZED) + @ExceptionHandler(ShiroException.class) + public Result handle401(ShiroException e) { + log.error(e.getMessage(), e); + return Result.error(ErrorEnum.UNAUTHORIZED); + } + + *//** 捕捉UnauthorizedException *//* + @ResponseStatus(HttpStatus.UNAUTHORIZED) + @ExceptionHandler(UnauthorizedException.class) + public Result handle401() { + return Result.error(ErrorEnum.UNAUTHORIZED); + }*/ + + /** 文件上传大小异常 */ + @ExceptionHandler(MultipartException.class) + public Result handleMultipart(Throwable t) { + log.error(t.getMessage(), t); + return Result.error(ErrorEnum.UPLOAD_FILE_SIZE_MAX); + } + + /** jackson转换Bean * */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public Result handleJsonConv(Throwable t) { + log.error(t.getMessage(), t); + return Result.error(ErrorEnum.COMMON_PARAMS_NOT_EXIST); + } + + /** 异常参数处理器 */ + @ExceptionHandler(IllegalArgumentException.class) + public Result handleRRException(Throwable e) { + //log.error(e.getMessage(), e); + return Result.error(ErrorEnum.COMMON_PARAMS_ERR.code, e.getMessage()); + } + + /** 自定义异常 */ + @ExceptionHandler(ResultReturnException.class) + public Result handleRRException(ResultReturnException e) { + log.error(exTraceBack(e), e); + return Result.error(e.getCode(), e.getMsg()); + } + + @ExceptionHandler(Exception.class) + public Result handleException(Exception e) { + log.error(exTraceBack(e), e); + return Result.error("系统发生错误,请联系管理员"); + } + + public static String exTraceBack(Exception e) { + StringBuilder sb = new StringBuilder(); + StackTraceElement[] stackTrace = e.getStackTrace(); + for (int i = 0; i < stackTrace.length; i++) { + sb.append("<---"); + sb.append(String.format("[%s * %s] ", stackTrace[i].getClassName(), stackTrace[i].getMethodName())); + } + sb.append(e.getMessage()); + return sb.toString(); + } +} diff --git a/yuxue/mapper/PlateFileMapper.java b/yuxue/mapper/PlateFileMapper.java new file mode 100644 index 0000000..60d74da --- /dev/null +++ b/yuxue/mapper/PlateFileMapper.java @@ -0,0 +1,25 @@ +package com.yuxue.mapper; + +import com.yuxue.entity.PlateFileEntity; +import java.util.List; +import java.util.Map; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PlateFileMapper { + int deleteByPrimaryKey(Integer id); + + int insert(PlateFileEntity record); + + int insertSelective(PlateFileEntity record); + + PlateFileEntity selectByPrimaryKey(Integer id); + + List selectByCondition(Map map); + + int updateByPrimaryKeySelective(PlateFileEntity record); + + int updateByPrimaryKey(PlateFileEntity record); + + List getUnRecogniseList(); +} \ No newline at end of file diff --git a/yuxue/mapper/PlateRecoDebugMapper.java b/yuxue/mapper/PlateRecoDebugMapper.java new file mode 100644 index 0000000..a547f55 --- /dev/null +++ b/yuxue/mapper/PlateRecoDebugMapper.java @@ -0,0 +1,30 @@ +package com.yuxue.mapper; + +import java.util.List; +import java.util.Map; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import com.yuxue.entity.PlateRecoDebugEntity; + +@Mapper +public interface PlateRecoDebugMapper { + int deleteByPrimaryKey(Integer id); + + int insert(PlateRecoDebugEntity record); + + int insertSelective(PlateRecoDebugEntity record); + + PlateRecoDebugEntity selectByPrimaryKey(Integer id); + + List selectByCondition(Map map); + + int updateByPrimaryKeySelective(PlateRecoDebugEntity record); + + int updateByPrimaryKey(PlateRecoDebugEntity record); + + int deleteByParentId(@Param("parentId")Integer parentId); + + int batchInsert(@Param("list")List list); +} \ No newline at end of file diff --git a/yuxue/mapper/SystemMenuMapper.java b/yuxue/mapper/SystemMenuMapper.java new file mode 100644 index 0000000..3ff9de0 --- /dev/null +++ b/yuxue/mapper/SystemMenuMapper.java @@ -0,0 +1,25 @@ +package com.yuxue.mapper; + +import java.util.List; +import java.util.Map; + +import org.apache.ibatis.annotations.Mapper; + +import com.yuxue.entity.SystemMenuEntity; + +@Mapper +public interface SystemMenuMapper { + int deleteByPrimaryKey(Integer id); + + int insert(SystemMenuEntity record); + + int insertSelective(SystemMenuEntity record); + + SystemMenuEntity selectByPrimaryKey(Integer id); + + List selectByCondition(Map map); + + int updateByPrimaryKeySelective(SystemMenuEntity record); + + int updateByPrimaryKey(SystemMenuEntity record); +} \ No newline at end of file diff --git a/yuxue/mapper/TempPlateFileMapper.java b/yuxue/mapper/TempPlateFileMapper.java new file mode 100644 index 0000000..dc75d03 --- /dev/null +++ b/yuxue/mapper/TempPlateFileMapper.java @@ -0,0 +1,34 @@ +package com.yuxue.mapper; + +import java.util.List; +import java.util.Map; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import com.yuxue.entity.TempPlateFileEntity; + +@Mapper +public interface TempPlateFileMapper { + int deleteByPrimaryKey(Integer id); + + int insert(TempPlateFileEntity record); + + int insertSelective(TempPlateFileEntity record); + + TempPlateFileEntity selectByPrimaryKey(Integer id); + + List selectByCondition(Map map); + + int updateByPrimaryKeySelective(TempPlateFileEntity record); + + int updateByPrimaryKey(TempPlateFileEntity record); + + int turncateTable(); + + int batchInsert(@Param("list")List list); + + int updateFileInfo(); + + +} \ No newline at end of file diff --git a/yuxue/service/FileService.java b/yuxue/service/FileService.java new file mode 100644 index 0000000..c0c8b64 --- /dev/null +++ b/yuxue/service/FileService.java @@ -0,0 +1,16 @@ +package com.yuxue.service; + +import java.io.File; +import java.util.List; + +import com.alibaba.fastjson.JSONObject; + + +public interface FileService { + + List getFileTreeByDir(String dir, String typeFilter); + + File readFile(String filePath); + + +} \ No newline at end of file diff --git a/yuxue/service/PlateService.java b/yuxue/service/PlateService.java new file mode 100644 index 0000000..ae7f044 --- /dev/null +++ b/yuxue/service/PlateService.java @@ -0,0 +1,16 @@ +package com.yuxue.service; + + +public interface PlateService { + + public Object getProcessStep(); + + Object recognise(String filePath, boolean reRecognise); + + Object refreshFileInfo(); + + Object recogniseAll(); + + + +} \ No newline at end of file diff --git a/yuxue/service/SystemMenuService.java b/yuxue/service/SystemMenuService.java new file mode 100644 index 0000000..2facce0 --- /dev/null +++ b/yuxue/service/SystemMenuService.java @@ -0,0 +1,30 @@ +package com.yuxue.service; + +import java.util.List; +import java.util.Map; + +import com.github.pagehelper.PageInfo; +import com.yuxue.entity.SystemMenuEntity; + + +/** + * 服务实现层接口 + * @author yuxue + * @date 2019-06-20 16:15:23 + */ +public interface SystemMenuService { + + public SystemMenuEntity getByPrimaryKey(Integer id); + + public PageInfo queryByPage(Integer pageNo, Integer pageSize, Map map); + + public List queryByCondition(Map map); + + public Map save(SystemMenuEntity systemMenuEntity); + + public Integer deleteById(Integer id); + + public Integer updateById(SystemMenuEntity systemMenuEntity); + + public Object getUserMenu(); +} diff --git a/yuxue/service/impl/FileServiceImpl.java b/yuxue/service/impl/FileServiceImpl.java new file mode 100644 index 0000000..41fa4fb --- /dev/null +++ b/yuxue/service/impl/FileServiceImpl.java @@ -0,0 +1,62 @@ +package com.yuxue.service.impl; + +import java.io.File; +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.alibaba.druid.util.StringUtils; +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Lists; +import com.yuxue.constant.Constant; +import com.yuxue.exception.ResultReturnException; +import com.yuxue.service.FileService; +import com.yuxue.util.FileUtil; + + +@Service +public class FileServiceImpl implements FileService { + + @Override + public List getFileTreeByDir(String dir, String typeFilter) { + if(StringUtils.isEmpty(dir)){ + dir = Constant.DEFAULT_DIR; + } + if(StringUtils.isEmpty(typeFilter)){ + typeFilter = Constant.DEFAULT_TYPE; + } + + File f = new File(dir); + List list = FileUtil.listFile(f, typeFilter, false); + List result = Lists.newArrayList(); + list.stream().forEach(n->{ + JSONObject jo = new JSONObject(); + jo.put("id", n.getAbsolutePath()); + jo.put("pid", n.getParentFile().getAbsolutePath()); + jo.put("filePath", n.getAbsolutePath()); + jo.put("fileName", n.getName()); + jo.put("isDir", n.isDirectory()); + result.add(jo); + }); + return result; + } + + + @Override + public File readFile(String filePath) { + + File f = new File(filePath); + if(!f.exists() || f.isDirectory()) { + throw new ResultReturnException("filePath参数异常,找不到指定的文件: " + filePath); + } + + if(!f.exists() || f.isDirectory()) { + throw new ResultReturnException("读取图片异常:" + f.getName()); + } + return f; + } + + + + +} diff --git a/yuxue/service/impl/PlateServiceImpl.java b/yuxue/service/impl/PlateServiceImpl.java new file mode 100644 index 0000000..dc8c49a --- /dev/null +++ b/yuxue/service/impl/PlateServiceImpl.java @@ -0,0 +1,271 @@ +package com.yuxue.service.impl; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Vector; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.bytedeco.javacpp.opencv_core.Mat; +import org.bytedeco.javacpp.opencv_imgcodecs; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.yuxue.constant.Constant; +import com.yuxue.easypr.core.CharsRecognise; +import com.yuxue.easypr.core.CoreFunc; +import com.yuxue.easypr.core.PlateDetect; +import com.yuxue.entity.PlateFileEntity; +import com.yuxue.entity.PlateRecoDebugEntity; +import com.yuxue.entity.TempPlateFileEntity; +import com.yuxue.enumtype.PlateColor; +import com.yuxue.mapper.PlateFileMapper; +import com.yuxue.mapper.PlateRecoDebugMapper; +import com.yuxue.mapper.TempPlateFileMapper; +import com.yuxue.service.PlateService; +import com.yuxue.util.FileUtil; + + + +@Service +public class PlateServiceImpl implements PlateService { + + + // 车牌定位处理步骤,该map用于表示步骤图片的顺序 + private static Map debugMap = Maps.newLinkedHashMap(); + static { + debugMap.put("result", 99); + debugMap.put("debug_GaussianBlur", 0); // 高斯模糊 + debugMap.put("debug_gray", 1); // 图像灰度化 + debugMap.put("debug_Sobel", 2); // Sobel 算子 + debugMap.put("debug_threshold", 3); //图像二值化 + debugMap.put("debug_morphology", 4); // 图像闭操作 + debugMap.put("debug_Contours", 5); // 提取外部轮廓 + debugMap.put("debug_result", 6); // 原图处理结果 + debugMap.put("debug_crop", 7); // 切图 + debugMap.put("debug_resize", 8); // 切图resize + debugMap.put("debug_char_threshold", 9); // + // debugMap.put("debug_char_clearLiuDing", 10); // 去除柳钉 + debugMap.put("debug_specMat", 11); // + debugMap.put("debug_chineseMat", 12); // + debugMap.put("debug_char_auxRoi", 13); // + } + + + @Autowired + private PlateFileMapper plateFileMapper; + + @Autowired + private PlateRecoDebugMapper plateRecoDebugMapper; + + @Autowired + private TempPlateFileMapper tempPlateFileMapper; + + + @Override + public Object recognise(String filePath, boolean reRecognise) { + filePath = filePath.replaceAll("\\\\", "/"); + File f = new File(filePath); + PlateFileEntity e = null; + + Map paramMap = Maps.newHashMap(); + paramMap.put("filePath", filePath); + List list= plateFileMapper.selectByCondition(paramMap); + if(null == list || list.size() <= 0) { + if(FileUtil.checkFile(f)) { + e = new PlateFileEntity(); + e.setFileName(f.getName()); + e.setFilePath(f.getAbsolutePath().replaceAll("\\\\", "/")); + e.setFileType(f.getName().substring(f.getName().lastIndexOf(".") + 1)); + plateFileMapper.insertSelective(e); + } + reRecognise = true; + } else { + e = list.get(0); + } + + if(reRecognise) { + doRecognise(f, e, 0); // 重新识别 + e = plateFileMapper.selectByPrimaryKey(e.getId()); // 重新识别之后,重新获取一下数据 + } + + // 查询数据库,返回结果 + paramMap.clear(); + paramMap.put("parentId", e.getId()); + e.setDebug(plateRecoDebugMapper.selectByCondition(paramMap)); + + return e; + } + + + + @Override + @Transactional(propagation = Propagation.REQUIRED) + public Object refreshFileInfo() { + File baseDir = new File(Constant.DEFAULT_DIR); + if(!baseDir.exists() || !baseDir.isDirectory()) { + return null; + } + List resultList = Lists.newArrayList(); + + // 获取baseDir下第一层级的目录, 仅获取文件夹,不递归子目录,遍历 + List folderList = FileUtil.listFile(baseDir, ";", false); + folderList.parallelStream().forEach(folder -> { + if(!folder.getName().equals("temp")) { + // 遍历每一个文件夹, 递归获取文件夹下的图片 + List imgList = FileUtil.listFile(folder, Constant.DEFAULT_TYPE, true); + if(null != imgList && imgList.size() > 0) { + imgList.parallelStream().forEach(n->{ + TempPlateFileEntity entity = new TempPlateFileEntity(); + entity.setFilePath(n.getAbsolutePath().replaceAll("\\\\", "/")); + entity.setFileName(n.getName()); + entity.setFileType(n.getName().substring(n.getName().lastIndexOf(".") + 1)); + resultList.add(entity); + }); + } + } + }); + + tempPlateFileMapper.turncateTable(); + tempPlateFileMapper.batchInsert(resultList); + tempPlateFileMapper.updateFileInfo(); + + return 1; + } + + + @Override + public Object recogniseAll() { + // 查询到还没有进行车牌识别的图片 + List list = plateFileMapper.getUnRecogniseList(); + + // 开启多线程进行识别 + Random r = new Random(99); + list.parallelStream().forEach(n->{ + File f = new File(n.getFilePath()); + if(FileUtil.checkFile(f)) { + doRecognise(f, n, r.nextInt()); + } + }); + + return 1; + } + + + @Override + public Object getProcessStep() { + return debugMap; + } + + + /** + * 单张图片 车牌识别 + * 拷贝文件到临时目录 + * 过程及结果更新数据库 + * @param f 调用方需要验证文件存在 + * @param result + * @return + */ + public Object doRecognise(File f, PlateFileEntity e, Integer seed) { + + // 插入识别过程图片数据信息 通过temp文件夹的文件,更新数据库 + List debug = Lists.newArrayList(); + + Long ct = System.currentTimeMillis(); + String targetPath = Constant.DEFAULT_TEMP_DIR.concat(ct.toString() + seed) + .concat(f.getAbsolutePath().substring(f.getAbsolutePath().lastIndexOf("."))); + + // 先将文件拷贝并且重命名到不包含中文及特殊字符的目录下 + FileUtil.copyAndRename(f.getAbsolutePath(), targetPath); + + // 开始识别,生成过程及结果切图,将识别结果更新到数据库 + Mat src = opencv_imgcodecs.imread(targetPath); + + String tempPath = Constant.DEFAULT_TEMP_DIR + ct + "/"; + FileUtil.createDir(tempPath); // 创建文件夹 + + // 车牌检测对象 + PlateDetect plateDetect = new PlateDetect(); + plateDetect.setPDLifemode(true); + plateDetect.setDebug(true, tempPath); // 将过程的图块保存到盘符 + + Vector matVector = new Vector(); + if (0 == plateDetect.plateDetect(src, matVector)) { // 定位及判断,获取到车牌图块Mat + + CharsRecognise cr = new CharsRecognise(); + cr.setCRDebug(true); + for (int i = 0; i < matVector.size(); ++i) { // 遍历车牌图块Mat,进行识别 + Mat img = matVector.get(i); + + String palte = cr.charsRecognise(img, tempPath); // 字符识别 + PlateColor color = CoreFunc.getPlateType(img, true); + String fileName = "result_" + i + ".png"; + + // 识别的车牌,保存图片文件 + String str = tempPath + fileName; + // 此方法生成的文件,中文名称都是乱码,试了各种编解码均无效,OpenCV自身的编解码问题。 + opencv_imgcodecs.imwrite(str, img); + // 重命名文件,让生成的文件包含中文 + // String newName = palte + "_"+ color + ".png"; + // FileUtil.renameFile(str, newName); + + PlateRecoDebugEntity de = new PlateRecoDebugEntity(); + de.setRecoPlate(palte); + de.setFilePath(str); + de.setFileName(fileName); + de.setPlateColor(color.desc); + de.setParentId(e.getId()); + de.setDebugType("result"); + de.setSort(debugMap.get("result")); + debug.add(de); + } + } else { + e.setRecoCorrect(3); // 未检测到车牌 + } + + new File(targetPath).delete(); // 删除拷贝的文件 + + e.setTempPath(tempPath); + + List debugList = FileUtil.listFile(new File(tempPath), Constant.DEFAULT_TYPE, false); + + debugList.parallelStream().forEach(d -> { + String name = d.getName().substring(0, d.getName().lastIndexOf(".")); + + Pattern pattern = Pattern.compile("\\d+$"); + Matcher matcher = pattern.matcher(name); + if(matcher.find()) { + name = name.substring(0, name.lastIndexOf("_")); + } + + if(!"result".equals(name)) { + PlateRecoDebugEntity de = new PlateRecoDebugEntity(); + de.setRecoPlate(""); + de.setFilePath(d.getAbsolutePath().replaceAll("\\\\", "/")); + de.setFileName(d.getName()); + de.setPlateColor(""); + de.setParentId(e.getId()); + de.setDebugType(name); + de.setSort(debugMap.get(name)); + debug.add(de); + } + }); + + // 更新图片主表信息 + plateFileMapper.updateByPrimaryKeySelective(e); + + plateRecoDebugMapper.deleteByParentId(e.getId()); + + plateRecoDebugMapper.batchInsert(debug); + + return 1; + } + + +} diff --git a/yuxue/service/impl/SystemMenuServiceImpl.java b/yuxue/service/impl/SystemMenuServiceImpl.java new file mode 100644 index 0000000..4bf056c --- /dev/null +++ b/yuxue/service/impl/SystemMenuServiceImpl.java @@ -0,0 +1,101 @@ +package com.yuxue.service.impl; + + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.google.common.collect.Maps; +import com.yuxue.entity.SystemMenuEntity; +import com.yuxue.mapper.SystemMenuMapper; +import com.yuxue.service.SystemMenuService; + +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.annotation.Propagation; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 服务实现层 + * @author yuxue + * @date 2019-06-20 16:15:23 + */ +@Service +public class SystemMenuServiceImpl implements SystemMenuService { + + @Autowired + private SystemMenuMapper systemMenuMapper; + + + @Override + public SystemMenuEntity getByPrimaryKey(Integer id) { + SystemMenuEntity entity = systemMenuMapper.selectByPrimaryKey(id); + return entity; + } + + @Override + public PageInfo queryByPage(Integer pageNo, Integer pageSize, Map map) { + PageHelper.startPage(pageNo, pageSize); + PageInfo page = new PageInfo(systemMenuMapper.selectByCondition(map)); + return page; + } + + @Override + public List queryByCondition(Map map) { + return systemMenuMapper.selectByCondition(map); + } + + @Override + @Transactional(propagation = Propagation.REQUIRED) + public Map save(SystemMenuEntity entity) { + entity.setId(0); + systemMenuMapper.insertSelective(entity); + + Map result = new HashMap<>(); + result.put("id" , entity.getId()); + return result; + } + + @Override + @Transactional(propagation = Propagation.REQUIRED) + public Integer deleteById(Integer id){ + return systemMenuMapper.deleteByPrimaryKey(id); + } + + @Override + @Transactional(propagation = Propagation.REQUIRED) + public Integer updateById(SystemMenuEntity systemMenuEntity) { + if(null == systemMenuEntity || systemMenuEntity.getId() <= 0){ + return 0; + } + return systemMenuMapper.updateByPrimaryKeySelective(systemMenuEntity); + } + + + @Override + public Object getUserMenu() { + Map map = Maps.newHashMap(); + //根据角色查询菜单--未完成 //根据层级 sort排序 + map.put("showFlag", 1); + List menus = systemMenuMapper.selectByCondition(map); + + //按层级封装,最多三级 + Map result = Maps.newHashMap(); + + result.put("first", menus.stream().filter(n -> { + return n.getMenuLevel() == 1; + })); + result.put("second", menus.stream().filter(n -> { + return n.getMenuLevel() == 2; + })); + result.put("third", menus.stream().filter(n -> { + return n.getMenuLevel() == 3; + })); + return result; + } + + + +} diff --git a/yuxue/train/ANNTrain.java b/yuxue/train/ANNTrain.java new file mode 100644 index 0000000..37c1f78 --- /dev/null +++ b/yuxue/train/ANNTrain.java @@ -0,0 +1,215 @@ +package com.yuxue.train; + +import java.util.Random; +import java.util.Vector; + +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.TermCriteria; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.ml.ANN_MLP; +import org.opencv.ml.Ml; +import org.opencv.ml.TrainData; + +import com.yuxue.constant.Constant; +import com.yuxue.util.FileUtil; +import com.yuxue.util.PlateUtil; + + +/** + * 基于org.opencv包实现的训练 + * + * 图片文字识别训练 + * 训练出来的库文件,用于识别图片中的数字及字母 + * + * 测试了一段时间之后,发现把中文独立出来识别,准确率更高一点 + * + * 训练的ann.xml应用: + * 1、替换res/model/ann.xml文件 + * 2、修改com.yuxue.easypr.core.CharsIdentify.charsIdentify(Mat, Boolean, Boolean)方法 + * + * @author yuxue + * @date 2020-05-14 22:16 + */ +public class ANNTrain { + + private ANN_MLP ann = ANN_MLP.create(); + + static { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + } + + // 默认的训练操作的根目录 + private static final String DEFAULT_PATH = "D:/PlateDetect/train/chars_recognise_ann/"; + + // 训练模型文件保存位置 + private static final String MODEL_PATH = DEFAULT_PATH + "ann.xml"; + + public void train(int _predictsize, int _neurons) { + Mat samples = new Mat(); // 使用push_back,行数列数不能赋初始值 + Vector trainingLabels = new Vector(); + Random rand = new Random(); + // 加载数字及字母字符 + for (int i = 0; i < Constant.numCharacter; i++) { + String str = DEFAULT_PATH + "learn/" + Constant.strCharacters[i]; + Vector files = new Vector(); + FileUtil.getFiles(str, files); // 文件名不能包含中文 + + // int count = 200; // 控制从训练样本中,抽取指定数量的样本 + int count = files.size(); // 控制从训练样本中,抽取指定数量的样本 + for (int j = 0; j < count; j++) { + + String filename = ""; + if(j < files.size()) { + filename = files.get(j); + } else { + filename = files.get(rand.nextInt(files.size() - 1)); // 样本不足,随机重复提取已有的样本 + } + + Mat img = Imgcodecs.imread(filename, 0); + + Mat f = PlateUtil.features(img, _predictsize); + samples.push_back(f); + trainingLabels.add(i); // 每一幅字符图片所对应的字符类别索引下标 + + // 增加随机平移样本 + samples.push_back(PlateUtil.features(PlateUtil.randTranslate(img), _predictsize)); + trainingLabels.add(i); + + // 增加随机旋转样本 + samples.push_back(PlateUtil.features(PlateUtil.randRotate(img), _predictsize)); + trainingLabels.add(i); + + // 增加膨胀样本 + samples.push_back(PlateUtil.features(PlateUtil.dilate(img), _predictsize)); + trainingLabels.add(i); + + // 增加腐蚀样本 + /*samples.push_back(PlateUtil.features(PlateUtil.erode(img), _predictsize)); + trainingLabels.add(i); */ + } + } + + samples.convertTo(samples, CvType.CV_32F); + + //440 vhist.length + hhist.length + lowData.cols() * lowData.rows(); + // CV_32FC1 CV_32SC1 CV_32F + Mat classes = Mat.zeros(trainingLabels.size(), Constant.strCharacters.length, CvType.CV_32F); + + float[] labels = new float[trainingLabels.size()]; + for (int i = 0; i < labels.length; ++i) { + classes.put(i, trainingLabels.get(i), 1.f); + } + + // samples.type() == CV_32F || samples.type() == CV_32S + TrainData train_data = TrainData.create(samples, Ml.ROW_SAMPLE, classes); + + ann.clear(); + Mat layers = new Mat(1, 3, CvType.CV_32F); + layers.put(0, 0, samples.cols()); // 样本特征数 140 10*10 + 20+20 + layers.put(0, 1, _neurons); // 神经元个数 + layers.put(0, 2, classes.cols()); // 字符数 + + ann.setLayerSizes(layers); + ann.setActivationFunction(ANN_MLP.SIGMOID_SYM, 1, 1); + ann.setTrainMethod(ANN_MLP.BACKPROP); + TermCriteria criteria = new TermCriteria(TermCriteria.EPS + TermCriteria.MAX_ITER, 30000, 0.0001); + ann.setTermCriteria(criteria); + ann.setBackpropWeightScale(0.1); + ann.setBackpropMomentumScale(0.1); + ann.train(train_data); + + // FileStorage fsto = new FileStorage(MODEL_PATH, FileStorage.WRITE); + // ann.write(fsto, "ann"); + ann.save(MODEL_PATH); + } + + + public void predict() { + ann.clear(); + ann = ANN_MLP.load(MODEL_PATH); + + int total = 0; + int correct = 0; + + // 遍历测试样本下的所有文件,计算预测准确率 + for (int i = 0; i < Constant.strCharacters.length; i++) { + + char c = Constant.strCharacters[i]; + String path = DEFAULT_PATH + "learn/" + c; + + Vector files = new Vector(); + FileUtil.getFiles(path, files); + + for (String filePath : files) { + + Mat img = Imgcodecs.imread(filePath, 0); + Mat f = PlateUtil.features(img, Constant.predictSize); + + int index = 0; + double maxVal = -2; + Mat output = new Mat(1, Constant.strCharacters.length, CvType.CV_32F); + ann.predict(f, output); // 预测结果 + for (int j = 0; j < Constant.strCharacters.length; j++) { + double val = output.get(0, j)[0]; + if (val > maxVal) { + maxVal = val; + index = j; + } + } + + // 膨胀 + f = PlateUtil.features(PlateUtil.dilate(img), Constant.predictSize); + ann.predict(f, output); // 预测结果 + for (int j = 0; j < Constant.strCharacters.length; j++) { + double val = output.get(0, j)[0]; + if (val > maxVal) { + maxVal = val; + index = j; + } + } + + String result = String.valueOf(Constant.strCharacters[index]); + if(result.equals(String.valueOf(c))) { + correct++; + } else { + // 删除异常样本 + /*File f1 = new File(filePath); + f1.delete();*/ + + System.err.print(filePath); + System.err.println("\t预测结果:" + result); + } + total++; + } + + } + + System.out.print("total:" + total); + System.out.print("\tcorrect:" + correct); + System.out.print("\terror:" + (total - correct)); + System.out.println("\t计算准确率为:" + correct / (total * 1.0)); + + //牛逼,我操 total:13178 correct:13139 error:39 计算准确率为:0.9970405220822584 + + return; + } + + public static void main(String[] args) { + + ANNTrain annT = new ANNTrain(); + // 这里演示只训练model文件夹下的ann.xml,此模型是一个predictSize=10,neurons=40的ANN模型 + // 可根据需要训练不同的predictSize或者neurons的ANN模型 + // 根据机器的不同,训练时间不一样,但一般需要10分钟左右,所以慢慢等一会吧 + // 可以考虑中文,数字字母分开训练跟识别,提高准确性 + annT.train(Constant.predictSize, Constant.neurons); + + annT.predict(); + + System.out.println("The end."); + return; + } + + +} \ No newline at end of file diff --git a/yuxue/train/CnANNTrain.java b/yuxue/train/CnANNTrain.java new file mode 100644 index 0000000..6dd4323 --- /dev/null +++ b/yuxue/train/CnANNTrain.java @@ -0,0 +1,228 @@ +package com.yuxue.train; + +import java.io.File; +import java.util.Random; +import java.util.Vector; + +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.TermCriteria; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.ml.ANN_MLP; +import org.opencv.ml.Ml; +import org.opencv.ml.TrainData; + +import com.yuxue.constant.Constant; +import com.yuxue.util.FileUtil; +import com.yuxue.util.PlateUtil; + + +/** + * 基于org.opencv官方包实现的训练 + * + * 图片文字识别训练 + * 训练出来的库文件,用于识别图片中的中文字符 + * 测试了一段时间之后,发现把中文独立出来识别,准确率更高一点 + * + * @author yuxue + * @date 2020-07-02 22:16 + */ +public class CnANNTrain { + + private ANN_MLP ann = ANN_MLP.create(); + + static { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + } + + // 默认的训练操作的根目录 + private static final String DEFAULT_PATH = "D:/PlateDetect/train/chars_recognise_ann/"; + + // 训练模型文件保存位置 + private static final String MODEL_PATH = DEFAULT_PATH + "ann_cn.xml"; + + + public void train(int _predictsize, int _neurons) { + Mat samples = new Mat(); // 使用push_back,行数列数不能赋初始值 + Vector trainingLabels = new Vector(); + Random rand = new Random(); + + // 加载汉字字符 + for (int i = 0; i < Constant.strChinese.length; i++) { + String str = DEFAULT_PATH + "learn/" + Constant.strChinese[i]; + Vector files = new Vector(); + FileUtil.getFiles(str, files); + + // int count = 300; // 控制从训练样本中,抽取指定数量的样本 + int count = files.size(); // 不添加随机样本 + for (int j = 0; j < count; j++) { + + String filename = ""; + if(j < files.size()) { + filename = files.get(j); + } else { + filename = files.get(rand.nextInt(files.size() - 1)); // 样本不足,随机重复提取已有的样本 + } + + Mat img = Imgcodecs.imread(filename, 0); + + // 原图样本 + samples.push_back(PlateUtil.features(img, _predictsize)); + trainingLabels.add(i); + + // 增加随机平移样本 + samples.push_back(PlateUtil.features(PlateUtil.randTranslate(img), _predictsize)); + trainingLabels.add(i); + + // 增加随机旋转样本 + samples.push_back(PlateUtil.features(PlateUtil.randRotate(img), _predictsize)); + trainingLabels.add(i); + + // 增加腐蚀样本 + samples.push_back(PlateUtil.features(PlateUtil.erode(img), _predictsize)); + trainingLabels.add(i); + } + } + + samples.convertTo(samples, CvType.CV_32F); + + //440 vhist.length + hhist.length + lowData.cols() * lowData.rows(); + // CV_32FC1 CV_32SC1 CV_32F + Mat classes = Mat.zeros(trainingLabels.size(), Constant.strChinese.length, CvType.CV_32F); + + float[] labels = new float[trainingLabels.size()]; + for (int i = 0; i < labels.length; ++i) { + classes.put(i, trainingLabels.get(i), 1.f); + } + + // samples.type() == CV_32F || samples.type() == CV_32S + TrainData train_data = TrainData.create(samples, Ml.ROW_SAMPLE, classes); + + ann.clear(); + Mat layers = new Mat(1, 3, CvType.CV_32F); + layers.put(0, 0, samples.cols()); // 样本特征数 140 10*10 + 20+20 + layers.put(0, 1, _neurons); // 神经元个数 + layers.put(0, 2, classes.cols()); // 字符数 + + ann.setLayerSizes(layers); + ann.setActivationFunction(ANN_MLP.SIGMOID_SYM, 1, 1); + ann.setTrainMethod(ANN_MLP.BACKPROP); + TermCriteria criteria = new TermCriteria(TermCriteria.EPS + TermCriteria.MAX_ITER, 30000, 0.0001); + ann.setTermCriteria(criteria); + ann.setBackpropWeightScale(0.1); + ann.setBackpropMomentumScale(0.1); + ann.train(train_data); + + // FileStorage fsto = new FileStorage(MODEL_PATH, FileStorage.WRITE); + // ann.write(fsto, "ann"); + ann.save(MODEL_PATH); + } + + + public void predict() { + ann.clear(); + ann = ANN_MLP.load(MODEL_PATH); + + int total = 0; + int correct = 0; + + // 遍历测试样本下的所有文件,计算预测准确率 + for (int i = 0; i < Constant.strChinese.length; i++) { + + String strChinese = Constant.strChinese[i]; + String path = DEFAULT_PATH + "learn/" + strChinese; + Vector files = new Vector(); + FileUtil.getFiles(path, files); + + for (String filePath : files) { + Mat img = Imgcodecs.imread(filePath, 0); + Mat f = PlateUtil.features(img, Constant.predictSize); + + int index = 0; + double maxVal = -2; + + Mat output = new Mat(1, Constant.strChinese.length, CvType.CV_32F); + ann.predict(f, output); // 预测结果 + for (int j = 0; j < Constant.strChinese.length; j++) { + double val = output.get(0, j)[0]; + if (val > maxVal) { + maxVal = val; + index = j; + } + } + + // 腐蚀 -- 识别中文字符效果会好一点,识别数字及字母效果会更差 + f = PlateUtil.features(PlateUtil.erode(img), Constant.predictSize); + ann.predict(f, output); // 预测结果 + for (int j = 0; j < Constant.strChinese.length; j++) { + double val = output.get(0, j)[0]; + if (val > maxVal) { + maxVal = val; + index = j; + } + } + + String result = Constant.strChinese[index]; + + if(result.equals(strChinese)) { + correct++; + } else { + // 删除异常样本 + /*File f1 = new File(filePath); + f1.delete();*/ + + System.err.print(filePath); + System.err.println("\t预测结果:" + Constant.KEY_CHINESE_MAP.get(result)); + } + total++; + } + } + System.out.print("total:" + total); + System.out.print("\tcorrect:" + correct); + System.out.print("\terror:" + (total - correct)); + System.out.println("\t计算准确率为:" + correct / (total * 1.0)); + + //预测结果: + //单字符100样本数 total:3230 correct:2725 error:505 计算准确率为:0.8436532507739938 + //单字符200样本数 total:3230 correct:2889 error:341 计算准确率为:0.8944272445820434 + //单字符300样本数 total:3230 correct:2943 error:287 计算准确率为:0.9111455108359133 + //单字符400样本数 total:3230 correct:2937 error:293 计算准确率为:0.9092879256965944 + //无随机样本 total:3230 correct:3050 error:180 计算准确率为:0.9442724458204335 + //无随机,删除异常样本 total:3050 correct:2987 error:63 计算准确率为:0.979344262295082 + //无随机,删除异常样本 total:2987 correct:2973 error:14 计算准确率为:0.9953130231001004 + //无随机,删除异常样本 total:2987 correct:2932 error:55 计算准确率为:0.9815868764646802 + //无随机,删除异常样本 total:2987 correct:2971 error:16 计算准确率为:0.9946434549715434 + + // 个人测试多次之后,得出结论: + // 1、每个字符下样本数量不一致,最多的299个样本,最少的不到10个样本;从测试结果来看,样本太少会影响预测结果 + // 2、这里的训练跟测试的样本都是基于相同的样本文件,所以测试结果存在一定的局限性,仅供参考; + // 3、测试过程中,使用了随机样本,实际上发现重复样本对预测结果影响不大 + // 4、中文字符分离出来之后,预测准确性要高很多 + // 5、随机平移、随机旋转、膨胀、腐蚀,会增加样本数量,同时增加预测准确性 + // 6、每次重新训练后,结果是不一致的,,没有重新训练,多次使用样本预测,结果是一致的 + // 7、经过多次测试,这里的训练方案跟预测结果,准确率在90%左右 + // 8、用于训练的样本,尽量要多一点,样本特征丰富一点,这样子可以提高准确性;但是用于预测的样本,要尽量规范、正常 + + return; + } + + + public static void main(String[] args) { + + CnANNTrain annT = new CnANNTrain(); + + // 这里演示只训练model文件夹下的ann.xml,此模型是一个predictSize=10,neurons=40的ANN模型 + // 可根据需要训练不同的predictSize或者neurons的ANN模型 + // 根据机器的不同,训练时间不一样,但一般需要10分钟左右,所以慢慢等一会吧 + // 可以考虑中文,数字字母分开训练跟识别,提高准确性 + annT.train(Constant.predictSize, Constant.neurons); + + annT.predict(); + + System.out.println("The end."); + return; + } + + +} \ No newline at end of file diff --git a/yuxue/train/SVMTrain.java b/yuxue/train/SVMTrain.java new file mode 100644 index 0000000..98effbd --- /dev/null +++ b/yuxue/train/SVMTrain.java @@ -0,0 +1,279 @@ +package com.yuxue.train; + +import java.io.File; +import java.util.List; + +import org.opencv.core.Core; +import org.opencv.core.Core.MinMaxLocResult; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.TermCriteria; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.imgproc.Imgproc; +import org.opencv.ml.Ml; +import org.opencv.ml.SVM; +import org.opencv.ml.TrainData; + +import com.google.common.collect.Lists; +import com.yuxue.constant.Constant; +import com.yuxue.enumtype.Direction; +import com.yuxue.util.FileUtil; + +/** + * 基于org.opencv官方包实现的训练 + * + * windows下环境配置: + * 1、官网下载对应版本的openvp:https://opencv.org/releases/page/2/ 当前使用4.0.1版本 + * 2、双击exe文件安装,将 安装目录下\build\java\x64\opencv_java401.dll 拷贝到\build\x64\vc14\bin\目录下 + * 3、eclipse添加User Libraries + * 4、项目右键build path,添加步骤三新增的lib + * + * 图片识别车牌训练 + * 训练出来的库文件,用于判断切图是否包含车牌 + * + * 训练的svm.xml应用: + * 1、替换res/model/svm.xml文件 + * 2、修改com.yuxue.easypr.core.PlateJudge.plateJudge(Mat) 方法 + * 将样本处理方法切换一下,即将对应被注释掉的模块代码取消注释 + * @author yuxue + * @date 2020-05-13 10:10 + */ +public class SVMTrain { + + // 默认的训练操作的根目录 + private static final String DEFAULT_PATH = "D:/PlateDetect/train/plate_detect_svm/"; + + // 训练模型文件保存位置 + private static final String MODEL_PATH = DEFAULT_PATH + "svm2.xml"; + + static { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + } + + public static void main(String[] arg) { + // 训练, 生成svm.xml库文件 + train(); + + // 识别,判断样本文件是否是车牌 + pridect(); + } + + public static void train() { + + // 正样本 // 136 × 36 像素 训练的源图像文件要相同大小 + List imgList0 = FileUtil.listFile(new File(DEFAULT_PATH + "/learn/HasPlate"), Constant.DEFAULT_TYPE, false); + + // 负样本 // 136 × 36 像素 训练的源图像文件要相同大小 + List imgList1 = FileUtil.listFile(new File(DEFAULT_PATH + "/learn/NoPlate"), Constant.DEFAULT_TYPE, false); + + // 标记:正样本用 0 表示,负样本用 1 表示。 + int labels[] = createLabelArray(imgList0.size(), imgList1.size()); + int sample_num = labels.length; // 图片数量 + + // 用于存放所有样本的矩阵 + Mat trainingDataMat = null; + + // 存放标记的Mat,每个图片都要给一个标记 + Mat labelsMat = new Mat(sample_num, 1, CvType.CV_32SC1); + labelsMat.put(0, 0, labels); + + for (int i = 0; i < sample_num; i++) { // 遍历所有的正负样本,处理样本用于生成训练的库文件 + String path = ""; + if(i < imgList0.size()) { + path = imgList0.get(i).getAbsolutePath(); + } else { + path = imgList1.get(i - imgList0.size()).getAbsolutePath(); + } + + Mat inMat = Imgcodecs.imread(path); // 读取样本文件 + Mat dst = getFeature(inMat); // 获取样本文件的特征 + + // 创建一个行数为sample_num, 列数为 rows*cols 的矩阵; 用于存放样本 + if (trainingDataMat == null) { + trainingDataMat = new Mat(sample_num, dst.rows() * dst.cols(), CvType.CV_32F); + } + + // 将样本矩阵转换成只有一行的矩阵,保存为float数组 + float[] arr = new float[dst.rows() * dst.cols()]; + int l = 0; + for (int j = 0; j < dst.rows(); j++) { // 遍历行 + for (int k = 0; k < dst.cols(); k++) { // 遍历列 + double[] a = dst.get(j, k); + arr[l] = (float) a[0]; + l++; + } + } + + trainingDataMat.put(i, 0, arr); // 多张图的特征合并到一个矩阵 + } + + // Imgcodecs.imwrite(DEFAULT_PATH + "trainingDataMat.jpg", trainingDataMat); + + // 配置SVM训练器参数 + TermCriteria criteria = new TermCriteria(TermCriteria.EPS + TermCriteria.MAX_ITER, 20000, 0.0001); + SVM svm = SVM.create(); + svm.setTermCriteria(criteria); // 指定 + svm.setKernel(SVM.RBF); // 使用预先定义的内核初始化 + svm.setType(SVM.C_SVC); // SVM的类型,默认是:SVM.C_SVC + svm.setGamma(0.1); // 核函数的参数 + svm.setNu(0.1); // SVM优化问题参数 + svm.setC(1); // SVM优化问题的参数C + svm.setP(0.1); + svm.setDegree(0.1); + svm.setCoef0(0.1); + + TrainData td = TrainData.create(trainingDataMat, Ml.ROW_SAMPLE, labelsMat);// 类封装的训练数据 + boolean success = svm.train(td.getSamples(), Ml.ROW_SAMPLE, td.getResponses());// 训练统计模型 + System.out.println("svm training result: " + success); + svm.save(MODEL_PATH);// 保存模型 + } + + + public static void pridect() { + // 加载训练得到的 xml 模型文件 + SVM svm = SVM.load(MODEL_PATH); + + // 136 × 36 像素 需要跟训练的源图像文件保持相同大小 + doPridect(svm, DEFAULT_PATH + "test/A01_NMV802_0.jpg"); + doPridect(svm, DEFAULT_PATH + "test/debug_resize_1.jpg"); + doPridect(svm, DEFAULT_PATH + "test/debug_resize_2.jpg"); + doPridect(svm, DEFAULT_PATH + "test/debug_resize_3.jpg"); + doPridect(svm, DEFAULT_PATH + "test/S22_KG2187_3.jpg"); + doPridect(svm, DEFAULT_PATH + "test/S22_KG2187_5.jpg"); + } + + public static void doPridect(SVM svm, String imgPath) { + Mat src = Imgcodecs.imread(imgPath); + Mat dst = getFeature(src); + // 如果训练时使用这个标识,那么符合的图像会返回9.0 + float flag = svm.predict(dst); + + if (flag == 0) { + System.err.println(imgPath + ": 目标符合"); + } + if (flag == 1) { + System.out.println(imgPath + ": 目标不符合"); + } + } + + public static int[] createLabelArray(Integer i1, Integer i2) { + int labels[] = new int[i1 + i2]; + + for (int i = 0; i < labels.length; i++) { + if(i < i1) { + labels[i] = 0; + } else { + labels[i] = 1; + } + } + return labels; + } + + + public static Mat getFeature(Mat inMat) { + + Mat histogram = getHistogramFeatures(inMat); + Mat color = getColorFeatures(inMat); + + List list = Lists.newArrayList(); + list.add(histogram); + list.add(color); + + Mat dst = new Mat(); + // hconcat 水平拼接 // vconcat 垂直拼接 + Core.hconcat(list, dst); + return dst; + } + + + public static Mat getHistogramFeatures(Mat src) { + Mat img_grey = new Mat(); + Imgproc.cvtColor(src, img_grey, Imgproc.COLOR_BGR2GRAY); + + Mat img_threshold = new Mat(); + Imgproc.threshold(img_grey, img_threshold, 0, 255, Imgproc.THRESH_OTSU + Imgproc.THRESH_BINARY); + + // Histogram features + float[] vhist = projectedHistogram(img_threshold, Direction.VERTICAL); + float[] hhist = projectedHistogram(img_threshold, Direction.HORIZONTAL); + + // Last 10 is the number of moments components + int numCols = vhist.length + hhist.length; + + Mat features = Mat.zeros(1, numCols, CvType.CV_32F); + int j = 0; + for (int i = 0; i < vhist.length; i++) { + features.put(0, j, vhist[i]); + j++; + } + for (int i = 0; i < hhist.length; i++) { + features.put(0, j, hhist[i]); + j++; + } + return features; + } + + public static float[] projectedHistogram(Mat inMat, Direction direction){ + Mat img = new Mat(); + inMat.copyTo(img); + int sz = img.rows(); + if(Direction.VERTICAL.equals(direction)) { + sz = img.cols(); + } + // 统计这一行或一列中,非零元素的个数,并保存到nonZeroMat中 + float[] nonZeroMat = new float[sz]; + Core.extractChannel(img, img, 0); // 提取0通道 + for (int j = 0; j < sz; j++) { + Mat data = Direction.HORIZONTAL.equals(direction) ? img.row(j) : img.col(j); + int count = Core.countNonZero(data); + nonZeroMat[j] = count; + } + // Normalize histogram + float max = 1F; + for (int j = 0; j < nonZeroMat.length; j++) { + max = Math.max(max, nonZeroMat[j]); + } + for (int j = 0; j < nonZeroMat.length; j++) { + nonZeroMat[j] /= max; + } + return nonZeroMat; + } + + + public static Mat getColorFeatures(Mat src) { + Mat src_hsv = new Mat(); + Imgproc.cvtColor(src, src_hsv, Imgproc.COLOR_BGR2GRAY); + + int sz = 180; + int[] h = new int[180]; + + for (int i = 0; i < src_hsv.rows(); i++) { + for (int j = 0; j < src_hsv.cols(); j++) { + int H = (int) src_hsv.get(i, j)[0];// 0-180 + if (H > sz - 1) { + H = sz - 1; + } + if (H < 0) { + H = 0; + } + h[H]++; + } + } + // 创建黑色的图 + Mat features = Mat.zeros(1, sz, CvType.CV_32F); + + for (int j = 0; j < sz; j++) { + features.put(0, j, (float)h[j]); + } + + MinMaxLocResult m = Core.minMaxLoc(features); + double max = m.maxVal; + + if (max > 0) { + features.convertTo(features, -1, 1.0f / max, 0); + } + return features; + } + + +} diff --git a/yuxue/util/Convert.java b/yuxue/util/Convert.java new file mode 100644 index 0000000..4e44733 --- /dev/null +++ b/yuxue/util/Convert.java @@ -0,0 +1,87 @@ +package com.yuxue.util; + +import org.bytedeco.javacpp.BytePointer; + +/** + * There are 3 kinds of convert functions: + * 1. [float|double|int|long] to[Float|Double|Int|Long](BytePointer pointer) + * 2. byte[] getBytes([float|double|int|long] value) + * 3. [float|double|int|long] to[Float|Double|Int|Long](byte[] value) + * + * @author lin.yao + * + */ +public class Convert { + + public static float toFloat(BytePointer pointer) { + byte[] buffer = new byte[4]; + pointer.get(buffer); + return toFloat(buffer); + } + + public static double toDouble(BytePointer pointer) { + byte[] buffer = new byte[8]; + pointer.get(buffer); + return toDouble(buffer); + } + + public static int toInt(BytePointer pointer) { + byte[] buffer = new byte[4]; + pointer.get(buffer); + return toInt(buffer); + } + + public static long toLong(BytePointer pointer) { + byte[] buffer = new byte[8]; + pointer.get(buffer); + return toLong(buffer); + } + + public static byte[] getBytes(float value) { + return getBytes(Float.floatToIntBits(value)); + } + + public static byte[] getBytes(double value) { + return getBytes(Double.doubleToLongBits(value)); + } + + public static byte[] getBytes(int value) { + final int length = 4; + byte[] buffer = new byte[length]; + for (int i = 0; i < length; ++i) + buffer[i] = (byte) ((value >> (i * 8)) & 0xFF); + return buffer; + } + + public static byte[] getBytes(long value) { + final int length = 8; + byte[] buffer = new byte[length]; + for (int i = 0; i < length; ++i) + buffer[i] = (byte) ((value >> (i * 8)) & 0xFF); + return buffer; + } + + public static int toInt(byte[] value) { + final int length = 4; + int n = 0; + for (int i = 0; i < length; ++i) + n += (value[i] & 0xFF) << (i * 8); + return n; + } + + public static long toLong(byte[] value) { + final int length = 8; + long n = 0; + for (int i = 0; i < length; ++i) + n += ((long) (value[i] & 0xFF)) << (i * 8); + return n; + } + + public static double toDouble(byte[] value) { + return Double.longBitsToDouble(toLong(value)); + } + + public static float toFloat(byte[] value) { + return Float.intBitsToFloat(toInt(value)); + } +} diff --git a/yuxue/util/FileUtil.java b/yuxue/util/FileUtil.java new file mode 100644 index 0000000..404afad --- /dev/null +++ b/yuxue/util/FileUtil.java @@ -0,0 +1,196 @@ +package com.yuxue.util; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Vector; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import com.yuxue.exception.ResultReturnException; + + +/** + * + * @author yuxue + * @date 2020-04-19 15:23 + */ +public class FileUtil { + + static Lock lock = new ReentrantLock(); + + public static boolean copyAndRename(String from, String to) { + Path sourcePath = Paths.get(from); + Path destinationPath = Paths.get(to); + try { + Files.copy(sourcePath, destinationPath); + } catch(FileAlreadyExistsException e) { + } catch (IOException e) { + e.printStackTrace(); + } + return true; + } + + public static boolean checkFile(final File file) { + if(file.exists() && file.isFile()) { + return true; + } + return false; + } + + /** + * 重命名文件 + * @param file + * @param newName 可以是文件名,也可以是路径+文件名 + * @return + */ + public static boolean renameFile(String filePath, String newName) { + File file = new File(filePath); + return renameFile(file, newName); + } + + /** + * 重命名文件 + * @param file + * @param newName 可以是文件名,也可以是路径+文件名 + * @return + */ + public static boolean renameFile(File file, String newName) { + if(file.exists()) { + String targetPath = null; + if(newName.indexOf("/") >= 0 || newName.indexOf("\\\\") >= 0) { + targetPath = newName; + } else { + targetPath = file.getParentFile().getAbsolutePath() + "/" + newName; + } + + File targetFile = new File(targetPath); + file.renameTo(targetFile); + return true; + } + return false; + } + + public static void createDir(String dir) { + File file = new File(dir); + if(file.exists() && file.isDirectory()) { + return ; + } else { + file.mkdirs(); + } + } + + /** + * 删除并重新创建目录 + * @param dir + */ + public static void recreateDir(final String dir) { + new File(dir).delete(); + new File(dir).mkdir(); + } + + + /** + * 递归获取文件信息 + * @param path String类型 + * @param files + */ + public static void getFiles(final String path, Vector files) { + getFiles(new File(path), files); + } + + + /** + * 递归获取文件信息 + * @param dir FIle类型 + * @param files + */ + private static void getFiles(final File dir, Vector files) { + File[] filelist = dir.listFiles(); + for (File file : filelist) { + if (file.isDirectory()) { + getFiles(file, files); + } else { + files.add(file.getAbsolutePath()); + } + } + } + + + /** + * + * @param dir + * @param filename + * @param recursive + * @return + */ + public static List listFile(File dir, final String fileType, boolean recursive) { + if (!dir.exists()) { + throw new ResultReturnException("目录:" + dir + "不存在"); + } + + if (!dir.isDirectory()) { + throw new ResultReturnException(dir + "不是目录"); + } + + FileFilter ff = null; + if (fileType == null || fileType.length() == 0) { + ff = new FileFilter() { + @Override + public boolean accept(File pathname) { + return true; + } + }; + } else { + ff = new FileFilter() { + @Override + public boolean accept(File pathname) { + if (pathname.isDirectory()) { + return true; + } + String name = pathname.getName().toLowerCase(); + String format = name.substring(name.lastIndexOf(".") + 1); + if (fileType.contains(format)) { + return true; + } else { + return false; + } + } + }; + } + return listFile(dir, ff, recursive); + } + + + + /** + * + * @param dir + * @param ff + * @param recursive 是否遍历子目录 + * @return + */ + public static List listFile(File dir, FileFilter ff, boolean recursive) { + List list = new ArrayList(); + File[] files = dir.listFiles(ff); + if (files != null && files.length > 0) { + for (File f : files) { + // 如果是文件,添加文件到list中 + if (f.isFile() || (f.isDirectory() && !f.getName().startsWith("."))) { + list.add(f); + } else if (recursive) { + // 获取子目录中的文件,添加子目录中的经过过滤的所有文件添加到list + list.addAll(listFile(f, ff, true)); + } + } + } + return list; + } + +} diff --git a/yuxue/util/ImageUtil.java b/yuxue/util/ImageUtil.java new file mode 100644 index 0000000..7ec453e --- /dev/null +++ b/yuxue/util/ImageUtil.java @@ -0,0 +1,899 @@ +package com.yuxue.util; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.Vector; + +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfPoint; +import org.opencv.core.MatOfPoint2f; +import org.opencv.core.Point; +import org.opencv.core.Rect; +import org.opencv.core.RotatedRect; +import org.opencv.core.Scalar; +import org.opencv.core.Size; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.imgproc.Imgproc; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.yuxue.constant.Constant; +import com.yuxue.enumtype.PlateColor; + + +/** + * 车牌图片处理工具类 + * 将原图,经过算法处理,得到车牌的图块 + * @author yuxue + * @date 2020-05-18 12:07 + */ +public class ImageUtil { + + private static String DEFAULT_BASE_TEST_PATH = "D:/PlateDetect/temp/"; + + static { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + } + + // 车牌定位处理步骤,该map用于表示步骤图片的顺序 + private static Map debugMap = Maps.newLinkedHashMap(); + static { + debugMap.put("yuantu", 0); // 原图 + debugMap.put("gaussianBlur", 0); // 高斯模糊 + debugMap.put("gray", 0); // 图像灰度化 + debugMap.put("sobel", 0); // Sobel 运算,得到图像的一阶水平方向导数 + debugMap.put("threshold", 0); //图像二值化 + debugMap.put("morphology", 0); // 图像闭操作 + debugMap.put("clearInnerHole", 0); // 降噪 + debugMap.put("clearSmallConnArea", 0); // 降噪 + debugMap.put("clearAngleConn", 0); // 降噪 + debugMap.put("clearHole", 0); // 降噪 + debugMap.put("contours", 0); // 提取外部轮廓 + debugMap.put("screenblock", 0); // 外部轮廓筛选 + debugMap.put("crop", 0); // 切图 + debugMap.put("resize", 0); // 切图resize + + // 设置index, 用于debug生成文件时候按名称排序 + Integer index = 100; + for (Entry entry : debugMap.entrySet()) { + entry.setValue(index); + index ++; + } + } + + public static void main(String[] args) { + Instant start = Instant.now(); + String tempPath = DEFAULT_BASE_TEST_PATH + "test/"; + String filename = tempPath + "/100_yuantu.jpg"; + filename = tempPath + "/100_yuantu1.jpg"; + // filename = tempPath + "/109_crop_0.png"; + + // 读取原图 + Mat src = Imgcodecs.imread(filename); + + Boolean debug = true; + + // 高斯模糊 + Mat gsMat = ImageUtil.gaussianBlur(src, debug, tempPath); + + // 灰度图 + Mat gray = ImageUtil.gray(gsMat, debug, tempPath); + + Mat sobel = ImageUtil.sobel(gray, debug, tempPath); + + Mat threshold = ImageUtil.threshold(sobel, debug, tempPath); + + // Mat scharr = ImageUtil.scharr(gray, debug, tempPath); + // Mat threshold = ImageUtil.threshold(scharr, debug, tempPath); + + Mat morphology = ImageUtil.morphology(threshold, debug, tempPath); + + List contours = ImageUtil.contours(src, morphology, debug, tempPath); + + Vector rects = ImageUtil.screenBlock(src, contours, debug, tempPath); + + PlateUtil.loadSvmModel("D:/PlateDetect/train/plate_detect_svm/svm2.xml"); + PlateUtil.loadAnnModel("D:/PlateDetect/train/chars_recognise_ann/ann.xml"); + + Vector dst = new Vector(); + PlateUtil.hasPlate(rects, dst, debug, tempPath); + + System.err.println("识别到的车牌数量:" + dst.size()); + dst.stream().forEach(inMat -> { + PlateColor color = PlateUtil.getPlateColor(inMat, true, debug, tempPath); + System.err.println(color.desc); + + Vector charMat = new Vector(); + PlateUtil.charsSegment(inMat, color, charMat, debug, tempPath); + + + }); + + /*String filename = tempPath + "/hsvMat_1590994270425.jpg"; + Mat src = Imgcodecs.imread(filename); + Vector charMat = new Vector(); + PlateUtil.charsSegment(src, PlateColor.BLUE, charMat, true, tempPath);*/ + + Instant end = Instant.now(); + System.err.println("总耗时:" + Duration.between(start, end).toMillis()); + + // ImageUtil.rgb2Hsv(src, debug, tempPath); + // ImageUtil.getHSVValue(src, debug, tempPath); + } + + + + /** + * 高斯模糊 + * @param inMat + * @param debug + * @return + */ + public static final int DEFAULT_GAUSSIANBLUR_SIZE = 5; + public static Mat gaussianBlur(Mat inMat, Boolean debug, String tempPath) { + Mat dst = new Mat(); + Imgproc.GaussianBlur(inMat, dst, new Size(DEFAULT_GAUSSIANBLUR_SIZE, DEFAULT_GAUSSIANBLUR_SIZE), 0, 0, Core.BORDER_DEFAULT); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("gaussianBlur") + "_gaussianBlur.jpg", dst); + } + return dst; + } + + + /** + * 将图像进行灰度化 + * @param inMat + * @param debug + * @param tempPath + * @return + */ + public static Mat gray(Mat inMat, Boolean debug, String tempPath) { + Mat dst = new Mat(); + Imgproc.cvtColor(inMat, dst, Imgproc.COLOR_BGR2GRAY); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("gray") + "_gray.jpg", dst); + } + inMat.release(); + return dst; + } + + + /** + * 对图像进行Sobel 运算,得到图像的一阶水平方向导数 + * @param inMat 灰度图 + * @param debug + * @param tempPath + * @return + */ + public static final int SOBEL_SCALE = 1; + public static final int SOBEL_DELTA = 0; + public static final int SOBEL_X_WEIGHT = 1; + public static final int SOBEL_Y_WEIGHT = 0; + public static Mat sobel(Mat inMat, Boolean debug, String tempPath) { + Mat dst = new Mat(); + Mat grad_x = new Mat(); + Mat grad_y = new Mat(); + Mat abs_grad_x = new Mat(); + Mat abs_grad_y = new Mat(); + + // Sobel滤波 计算水平方向灰度梯度的绝对值 + Imgproc.Sobel(inMat, grad_x, CvType.CV_16S, 1, 0, 3, SOBEL_SCALE, SOBEL_DELTA, Core.BORDER_DEFAULT); + Core.convertScaleAbs(grad_x, abs_grad_x); // 增强对比度 + + Imgproc.Sobel(inMat, grad_y, CvType.CV_16S, 0, 1, 3, SOBEL_SCALE, SOBEL_DELTA, Core.BORDER_DEFAULT); + Core.convertScaleAbs(grad_y, abs_grad_y); + grad_x.release(); + grad_y.release(); + + // 计算结果梯度 + Core.addWeighted(abs_grad_x, SOBEL_X_WEIGHT, abs_grad_y, SOBEL_Y_WEIGHT, 0, dst); + abs_grad_x.release(); + abs_grad_y.release(); + + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("sobel") + "_sobel.jpg", dst); + } + return dst; + } + + + /** + * 对图像进行scharr 运算,得到图像的一阶水平方向导数 + * @param inMat + * @param debug + * @param tempPath + * @return + */ + public static Mat scharr(Mat inMat, Boolean debug, String tempPath) { + + Mat dst = new Mat(); + + Mat grad_x = new Mat(); + Mat grad_y = new Mat(); + Mat abs_grad_x = new Mat(); + Mat abs_grad_y = new Mat(); + + //注意求梯度的时候我们使用的是Scharr算法,sofia算法容易收到图像细节的干扰 + //所谓梯度运算就是对图像中的像素点进行就导数运算,从而得到相邻两个像素点的差异值 by:Tantuo + Imgproc.Scharr(inMat, grad_x, CvType.CV_32F, 1, 0); + Imgproc.Scharr(inMat, grad_y, CvType.CV_32F, 0, 1); + //openCV中有32位浮点数的CvType用于保存可能是负值的像素数据值 + Core.convertScaleAbs(grad_x, abs_grad_x); + Core.convertScaleAbs(grad_y, abs_grad_y); + //openCV中使用release()释放Mat类图像,使用recycle()释放BitMap类图像 + grad_x.release(); + grad_y.release(); + + Core.addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst); + abs_grad_x.release(); + abs_grad_y.release(); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("sobel") + "_sobel.jpg", dst); + } + return dst; + } + + + /** + * 对图像进行二值化。将灰度图像(每个像素点有256个取值可能, 0代表黑色,255代表白色) + * 转化为二值图像(每个像素点仅有1和0两个取值可能) + * @param inMat + * @param debug + * @param tempPath + * @return + */ + public static Mat threshold(Mat inMat, Boolean debug, String tempPath) { + Mat dst = new Mat(); + Imgproc.threshold(inMat, dst, 100, 255, Imgproc.THRESH_OTSU + Imgproc.THRESH_BINARY); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("threshold") + "_threshold.jpg", dst); + } + inMat.release(); + return dst; + } + + + /** + * 使用闭操作。对图像进行闭操作以后,可以看到车牌区域被连接成一个矩形装的区域 + * @param inMat + * @param debug + * @param tempPath + * @return + */ + // public static final int DEFAULT_MORPH_SIZE_WIDTH = 15; + // public static final int DEFAULT_MORPH_SIZE_HEIGHT = 3; + public static final int DEFAULT_MORPH_SIZE_WIDTH = 9; + public static final int DEFAULT_MORPH_SIZE_HEIGHT = 3; + public static Mat morphology(Mat inMat, Boolean debug, String tempPath) { + Mat dst = new Mat(inMat.size(), CvType.CV_8UC1); + Size size = new Size(DEFAULT_MORPH_SIZE_WIDTH, DEFAULT_MORPH_SIZE_HEIGHT); + Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, size); + Imgproc.morphologyEx(inMat, dst, Imgproc.MORPH_CLOSE, element); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("morphology") + "_morphology0.jpg", dst); + } + + // 填补内部孔洞,为了去除小连通区域的时候,降低影响 + Mat a = clearInnerHole(dst, 8, 16, debug, tempPath); + + // 去除小连通区域 + Mat b = clearSmallConnArea(a, 1, 10, debug, tempPath); + + // 按斜边去除 + // Mat e = clearAngleConn(b, 5, debug, tempPath); + + // 填补边缘孔洞 + // Mat d = clearHole(a, 4, 2, debug, tempPath); + + return b; + } + + + /** + * Find 轮廓 of possibles plates 求轮廓。求出图中所有的轮廓。 + * 这个算法会把全图的轮廓都计算出来,因此要进行筛选。 + * @param src 原图 + * @param inMat morphology Mat + * @param debug + * @param tempPath + * @return + */ + public static List contours(Mat src, Mat inMat, Boolean debug, String tempPath) { + List contours = Lists.newArrayList(); + Mat hierarchy = new Mat(); + // 提取外部轮廓 + // CV_RETR_EXTERNAL只检测最外围轮廓, + // CV_RETR_LIST 检测所有的轮廓 + // CV_CHAIN_APPROX_NONE 保存物体边界上所有连续的轮廓点到contours向量内 + Imgproc.findContours(inMat, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE); + + if (debug) { + Mat result = new Mat(); + src.copyTo(result); // 复制一张图,不在原图上进行操作,防止后续需要使用原图 + // 将轮廓描绘到原图 + Imgproc.drawContours(result, contours, -1, new Scalar(0, 0, 255, 255)); + // 输出带轮廓的原图 + Imgcodecs.imwrite(tempPath + debugMap.get("contours") + "_contours.jpg", result); + } + return contours; + } + + + /** + * 根据轮廓, 筛选出可能是车牌的图块 + * @param src + * @param matVector + * @param debug + * @param tempPath + * @return + */ + public static final int DEFAULT_ANGLE = 30; // 角度判断所用常量 + public static final int TYPE = CvType.CV_8UC3; + public static Vector screenBlock(Mat src, List contours, Boolean debug, String tempPath){ + Vector dst = new Vector(); + List mv = Lists.newArrayList(); // 用于在原图上描绘筛选后的结果 + for (int i = 0, j = 0; i < contours.size(); i++) { + MatOfPoint m1 = contours.get(i); + MatOfPoint2f m2 = new MatOfPoint2f(); + m1.convertTo(m2, CvType.CV_32F); + // RotatedRect 该类表示平面上的旋转矩形,有三个属性: 矩形中心点(质心); 边长(长和宽); 旋转角度 + // boundingRect()得到包覆此轮廓的最小正矩形, minAreaRect()得到包覆轮廓的最小斜矩形 + RotatedRect mr = Imgproc.minAreaRect(m2); + + double angle = Math.abs(mr.angle); + + if (checkPlateSize(mr) && angle <= DEFAULT_ANGLE) { // 判断尺寸及旋转角度 ±30°,排除不合法的图块 + mv.add(contours.get(i)); + Size rect_size = new Size((int) mr.size.width, (int) mr.size.height); + if (mr.size.width / mr.size.height < 1) { // 宽度小于高度 + angle = 90 + angle; // 旋转90° + rect_size = new Size(rect_size.height, rect_size.width); + } + + // 旋转角度,根据需要是否进行角度旋转 + /*Mat img_rotated = new Mat(); + Mat rotmat = Imgproc.getRotationMatrix2D(mr.center, angle, 1); // 旋转 + Imgproc.warpAffine(src, img_rotated, rotmat, src.size()); // 仿射变换 考虑是否需要进行投影变换? + */ + + // 切图 + Mat img_crop = new Mat(); + Imgproc.getRectSubPix(src, rect_size, mr.center, img_crop); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("crop") + "_crop_" + j + ".png", img_crop); + } + + // 处理切图,调整为指定大小 + Mat resized = new Mat(Constant.DEFAULT_HEIGHT, Constant.DEFAULT_WIDTH, TYPE); + Imgproc.resize(img_crop, resized, resized.size(), 0, 0, Imgproc.INTER_CUBIC); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("resize") + "_resize_" + j + ".png", resized); + j++; + } + dst.add(resized); + } + } + if (debug) { + Mat result = new Mat(); + src.copyTo(result); // 复制一张图,不在原图上进行操作,防止后续需要使用原图 + // 将轮廓描绘到原图 + Imgproc.drawContours(result, mv, -1, new Scalar(0, 0, 255, 255)); + // 输出带轮廓的原图 + Imgcodecs.imwrite(tempPath + debugMap.get("screenblock") + "_screenblock.jpg", result); + } + return dst; + } + + /** + * 对minAreaRect获得的最小外接矩形 + * 判断面积以及宽高比是否在制定的范围内 + * 黄牌、蓝牌、绿牌 + * 国内车牌大小: 440mm*140mm,宽高比 3.142857 + * @param mr + * @return + */ + final static float DEFAULT_ERROR = 0.7f; // 宽高比允许70%误差 + final static float DEFAULT_ASPECT = 3.142857f; + public static final int DEFAULT_VERIFY_MIN = 1; + public static final int DEFAULT_VERIFY_MAX = 30; + private static boolean checkPlateSize(RotatedRect mr) { + + // 切图面积取值范围 + int min = 44 * 14 * DEFAULT_VERIFY_MIN; + int max = 44 * 14 * DEFAULT_VERIFY_MAX; + + // 切图横纵比取值范围;关键在于纵横比例 + float rmin = DEFAULT_ASPECT - DEFAULT_ASPECT * DEFAULT_ERROR; + float rmax = DEFAULT_ASPECT + DEFAULT_ASPECT * DEFAULT_ERROR; + + // 切图计算面积 + int area = (int) (mr.size.height * mr.size.width); + // 切图宽高比 + double r = mr.size.width / mr.size.height; + /*if (r < 1) { // 注释掉,不处理width 小于height的图片 + r = mr.size.height / mr.size.width; + }*/ + return min <= area && area <= max && rmin <= r && r <= rmax; + } + + + /** + * rgb图像转换为hsv图像 + * @param inMat + * @param debug + * @param tempPath + * @return + */ + public static Mat rgb2Hsv(Mat inMat, Boolean debug, String tempPath) { + // 转到HSV空间进行处理 + Mat dst = new Mat(); + Imgproc.cvtColor(inMat, dst, Imgproc.COLOR_BGR2HSV); + List hsvSplit = Lists.newArrayList(); + Core.split(dst, hsvSplit); + // 直方图均衡化是一种常见的增强图像对比度的方法,使用该方法可以增强局部图像的对比度,尤其在数据较为相似的图像中作用更加明显 + Imgproc.equalizeHist(hsvSplit.get(2), hsvSplit.get(2)); + Core.merge(hsvSplit, dst); + + if (debug) { + // Imgcodecs.imwrite(tempPath + "hsvMat_"+System.currentTimeMillis()+".jpg", dst); + } + return dst; + } + + + /** + * 获取HSV中各个颜色所对应的H的范围 + * HSV是一种比较直观的颜色模型,所以在许多图像编辑工具中应用比较广泛,这个模型中颜色的参数分别是:色调(H, Hue),饱和度(S,Saturation),明度(V, Value) + * 1.PS软件时,H取值范围是0-360,S取值范围是(0%-100%),V取值范围是(0%-100%)。 + * 2.利用openCV中cvSplit函数的在选择图像IPL_DEPTH_32F类型时,H取值范围是0-360,S取值范围是0-1(0%-100%),V取值范围是0-1(0%-100%)。 + * 3.利用openCV中cvSplit函数的在选择图像IPL_DEPTH_8UC类型时,H取值范围是0-180,S取值范围是0-255,V取值范围是0-255 + * @param inMat + * @param debug + */ + public static void getHSVValue(Mat inMat, Boolean debug, String tempPath) { + int nRows = inMat.rows(); + int nCols = inMat.cols(); + Map map = Maps.newHashMap(); + for (int i = 0; i < nRows; ++i) { + for (int j = 0; j < nCols; j += 3) { + int H = (int)inMat.get(i, j)[0]; + // int S = (int)inMat.get(i, j)[1]; + // int V = (int)inMat.get(i, j)[2]; + if(map.containsKey(H)) { + int count = map.get(H); + map.put(H, count+1); + } else { + map.put(H, 1); + } + } + } + Set set = map.keySet(); + Object[] arr = set.toArray(); + Arrays.sort(arr); + for (Object key : arr) { + System.out.println(key + ": " + map.get(key)); + } + return; + } + + + + /** + * 计算最大内接矩形 + * https://blog.csdn.net/cfqcfqcfqcfqcfq/article/details/53084090 + * @param inMat + * @return + */ + public static Rect maxAreaRect(Mat threshold, Point point) { + int edge[] = new int[4]; + edge[0] = (int) point.x + 1;//top + edge[1] = (int) point.y + 1;//right + edge[2] = (int) point.y - 1;//bottom + edge[3] = (int) point.x - 1;//left + + boolean[] expand = { true, true, true, true};//扩展标记位 + int n = 0; + while (expand[0] || expand[1] || expand[2] || expand[3]){ + int edgeID = n % 4; + expand[edgeID] = expandEdge(threshold, edge, edgeID); + n++; + } + Point tl = new Point(edge[3], edge[0]); + Point br = new Point(edge[1], edge[2]); + return new Rect(tl, br); + } + + + /** + * @brief expandEdge 扩展边界函数 + * @param img:输入图像,单通道二值图,深度为8 + * @param edge 边界数组,存放4条边界值 + * @param edgeID 当前边界号 + * @return 布尔值 确定当前边界是否可以扩展 + */ + public static boolean expandEdge(Mat img, int edge[], int edgeID) { + int nc = img.cols(); + int nr = img.rows(); + + switch (edgeID) { + case 0: + if (edge[0] > nr) { + return false; + } + for (int i = edge[3]; i <= edge[1]; ++i) { + if (img.get(edge[0], i)[0]== 255) {// 遇见255像素表明碰到边缘线 + return false; + } + } + edge[0]++; + return true; + case 1: + if (edge[1] > nc) { + return false; + } + for (int i = edge[2]; i <= edge[0]; ++i) { + if (img.get(i, edge[1])[0] == 255) + return false; + } + edge[1]++; + return true; + case 2: + if (edge[2] < 0) { + return false; + } + for (int i = edge[3]; i <= edge[1]; ++i) { + if (img.get(edge[2], i)[0] == 255) + return false; + } + edge[2]--; + return true; + case 3: + if (edge[3] < 0) { + return false; + } + for (int i = edge[2]; i <= edge[0]; ++i) { + if (img.get(i, edge[3])[0] == 255) + return false; + } + edge[3]--; + return true; + default: + return false; + } + } + + + /** + * 清除白色区域的内部黑色孔洞 + * rowLimit != colsLimit, 使用长方形比正方形好 + * 该算法比较耗时 + * @param inMat + * @param rowLimit + * @param colsLimit + * @param debug + * @param tempPath + * @return + */ + public static Mat clearInnerHole(Mat inMat, int rowLimit, int colsLimit, Boolean debug, String tempPath) { + Instant start = Instant.now(); + int uncheck = 0, normal = 2, replace = 3, white = 255, black = 0; + + Mat dst = new Mat(inMat.size(), CvType.CV_8UC1); + inMat.copyTo(dst); + + // 初始化的图像全部为0,未检查; 全黑图像 + Mat label = new Mat(inMat.size(), CvType.CV_8UC1); + + // 标记所有的白色区域 + for (int i = 0; i < inMat.rows(); i++) { + for (int j = 0; j < inMat.cols(); j++) { + // 白色点较少,遍历白色点速度快 + if (inMat.get(i, j)[0] == white && label.get(i, j)[0] == uncheck) { // 对于二值图,0代表黑色,255代表白色 + label.put(i, j, normal); // 中心点 + + // 执行两次,交换row 跟col; + int condition = 0; + do { + int x1 = i; + int x2 = i + rowLimit >= inMat.rows() ? inMat.rows() - 1 : i + rowLimit; + int y1 = j; + int y2 = j + colsLimit >= inMat.cols() ? inMat.cols() - 1 : j + colsLimit ; + + int count = 0; + // 遍历四条边 + for (int k = x1; k < x2; k++) { + if(inMat.get(k, y1)[0] == black || inMat.get(k, y2)[0] == black) { + count++; + } + } + for (int k = y1; k < y2; k++) { + if(inMat.get(x1, k)[0] == black || inMat.get(x2, k)[0] == black) { + count++; + } + } + + // 根据中心点+limit,定位四个角生成一个矩形, + // 矩形四条边都是白色,内部的黑点标记为 要被替换的对象 + if(count == 0 ) { + for (int n = x1; n < x2; n++) { + for (int m = y1; m < y2; m++) { + if (inMat.get(n, m)[0] == black && label.get(n, m)[0] == uncheck) { + label.put(n, m, replace); + } + } + } + } + int ex = rowLimit; + rowLimit = colsLimit; + colsLimit = ex; + + condition++; + } while (condition == 1); + } + } + } + + + for (int i = 0; i < inMat.rows(); i++) { + for (int j = 0; j < inMat.cols(); j++) { + if(label.get(i, j)[0] == replace) { + dst.put(i, j, white); + } + } + } + label.release(); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("clearInnerHole") + "_clearInnerHole.jpg", dst); + Instant end = Instant.now(); + System.out.println("clearInnerHole执行耗时:" + Duration.between(start, end).toMillis()); + } + return dst; + } + + + /** + * 清除二值图像的黑洞 + * 按矩形清理 + * @param inMat 二值图像 0代表黑色,255代表白色 + * @param rowLimit 像素值 + * @param colsLimit 像素值 + * @param debug + * @param tempPath + */ + public static Mat clearHole(Mat inMat, int rowLimit, int colsLimit, Boolean debug, String tempPath) { + Instant start = Instant.now(); + int uncheck = 0, normal = 2, replace = 3, white = 255, black = 0; + Mat dst = new Mat(inMat.size(), CvType.CV_8UC1); + inMat.copyTo(dst); + + // 初始化的图像全部为0,未检查; 全黑图像 + Mat label = new Mat(inMat.size(), CvType.CV_8UC1); + + // 标记所有的白色区域 + for (int i = 0; i < inMat.rows(); i++) { + for (int j = 0; j < inMat.cols(); j++) { + if (inMat.get(i, j)[0] == white) { // 对于二值图,0代表黑色,255代表白色 + label.put(i, j, normal); // 中心点 + + // 执行两次,交换row 跟col; + int condition = 0; + do { + int x1 = i; + int x2 = i + rowLimit >= inMat.rows() ? inMat.rows() - 1 : i + rowLimit; + int y1 = j; + int y2 = j + colsLimit >= inMat.cols() ? inMat.cols() - 1 : j + colsLimit ; + + int count = 0; + if(inMat.get(x1, y1)[0] == white) {// 左上角 + count++; + } + if(inMat.get(x1, y2)[0] == white) { // 左下角 + count++; + } + if(inMat.get(x2, y1)[0] == white) { // 右上角 + count++; + } + if(inMat.get(x2, y2)[0] == white) { // 右下角 + count++; + } + + // 根据中心点+limit,定位四个角生成一个矩形, + // 将四个角都是白色的矩形,内部的黑点标记为 要被替换的对象 + if(count >=4 ) { + for (int n = x1; n < x2; n++) { + for (int m = y1; m < y2; m++) { + if (inMat.get(n, m)[0] == black && label.get(n, m)[0] == uncheck) { + label.put(n, m, replace); + } + } + } + } + + int ex = rowLimit; + rowLimit = colsLimit; + colsLimit = ex; + + condition++; + } while (condition == 1); + } + } + } + + for (int i = 0; i < inMat.rows(); i++) { + for (int j = 0; j < inMat.cols(); j++) { + if(label.get(i, j)[0] == replace) { + dst.put(i, j, white); // 黑色替换成白色 + } + } + } + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("clearHole") + "_clearHole.jpg", dst); + Instant end = Instant.now(); + System.out.println("clearHole执行耗时:" + Duration.between(start, end).toMillis()); + } + return dst; + } + + /** + * 清除二值图像的细小连接 + * 按水平或者垂直方向清除 + * @param inMat + * @param rowLimit + * @param colsLimit + * @param debug + * @param tempPath + * @return + */ + public static Mat clearSmallConnArea(Mat inMat, int rowLimit, int colsLimit, Boolean debug, String tempPath) { + Instant start = Instant.now(); + int uncheck = 0, normal = 2, replace = 3, white = 255, black = 0; + + Mat dst = new Mat(inMat.size(), CvType.CV_8UC1); + inMat.copyTo(dst); + + // 初始化的图像全部为0,未检查; 全黑图像 + Mat label = new Mat(inMat.size(), CvType.CV_8UC1); + + // 标记所有的白色区域 + for (int i = 0; i < inMat.rows(); i++) { + for (int j = 0; j < inMat.cols(); j++) { + if (inMat.get(i, j)[0] == black) { // 对于二值图,0代表黑色,255代表白色 + label.put(i, j, normal); // 中心点 + // 执行两次,交换row 跟col; + int condition = 0; + do { + int x1 = i; + int x2 = i + rowLimit >= inMat.rows() ? inMat.rows() - 1 : i + rowLimit; + int y1 = j; + int y2 = j + colsLimit >= inMat.cols() ? inMat.cols() - 1 : j + colsLimit ; + + int count = 0; + if(inMat.get(x1, y1)[0] == black) {// 左上角 + count++; + } + if(inMat.get(x1, y2)[0] == black) { // 左下角 + count++; + } + if(inMat.get(x2, y1)[0] == black) { // 右上角 + count++; + } + if(inMat.get(x2, y2)[0] == black) { // 右下角 + count++; + } + + // 根据 中心点+limit,定位四个角生成一个矩形, + // 将四个角都是黑色的矩形,内部的白点标记为 要被替换的对象 + if(count >= 4) { + for (int n = x1; n < x2; n++) { + for (int m = y1; m < y2; m++) { + if (inMat.get(n, m)[0] == white && label.get(n, m)[0] == uncheck) { + label.put(n, m, replace); + } + } + } + } + int ex = rowLimit; + rowLimit = colsLimit; + colsLimit = ex; + + condition++; + } while (condition == 1); + } + } + } + + for (int i = 0; i < inMat.rows(); i++) { + for (int j = 0; j < inMat.cols(); j++) { + if(label.get(i, j)[0] == replace) { + dst.put(i, j, black); // 白色替换成黑色 + } + } + } + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("clearSmallConnArea") + "_clearSmallConnArea.jpg", dst); + Instant end = Instant.now(); + System.out.println("clearSmallConnArea执行耗时:" + Duration.between(start, end).toMillis()); + } + return dst; + } + + + /** + * 清除二值图像的细小连接 + * 按45度斜边清除 + * @param inMat + * @param limit + * @param angle + * @param debug + * @param tempPath + * @return + */ + public static Mat clearAngleConn(Mat inMat, int limit, Boolean debug, String tempPath) { + Instant start = Instant.now(); + int uncheck = 0, normal = 2, replace = 3, white = 255, black = 0; + + Mat dst = new Mat(inMat.size(), CvType.CV_8UC1); + inMat.copyTo(dst); + + // 初始化的图像全部为0,未检查; 全黑图像 + Mat label = new Mat(inMat.size(), CvType.CV_8UC1); + + // 标记所有的白色区域 + for (int i = 0; i < inMat.rows(); i++) { + for (int j = 0; j < inMat.cols(); j++) { + if (inMat.get(i, j)[0] == black) { // 对于二值图,0代表黑色,255代表白色 + label.put(i, j, normal); // 中心点 + + int x1 = i; + int x2 = i + limit >= inMat.rows() ? inMat.rows() - 1 : i + limit; + int y1 = j; + int y2 = j + limit >= inMat.cols() ? inMat.cols() - 1 : j + limit ; + + // 根据 中心点+limit,定位四个角生成一个矩形, + // 将2个角都是黑色的线,内部的白点标记为 要被替换的对象 + // 【\】 斜对角线 + if(inMat.get(x1, y1)[0] == black && inMat.get(x2, y2)[0] == black) { + for (int n = x1, m = y1; n < x2 && m < y2; n++, m++) { + if (inMat.get(n, m)[0] == white && label.get(n, m)[0] == uncheck) { + label.put(n, m, replace); + } + } + } + if(inMat.get(x1, y2)[0] == black && inMat.get(x2, y1)[0] == black) { + // 【/】 斜对角线 + for (int n = x1, m = y2; n < x2 && m > y1; n++, m--) { + if (inMat.get(n, m)[0] == white && label.get(n, m)[0] == uncheck) { + label.put(n, m, replace); + } + } + } + } + } + } + // 白色替换成黑色 + for (int i = 0; i < inMat.rows(); i++) { + for (int j = 0; j < inMat.cols(); j++) { + if(label.get(i, j)[0] == replace) { + dst.put(i, j, black); + } + } + } + + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("clearAngleConn") + "_clearAngleConn.jpg", dst); + Instant end = Instant.now(); + System.out.println("clearAngleConn执行耗时:" + Duration.between(start, end).toMillis()); + } + return dst; + } + + + +} diff --git a/yuxue/util/PlateUtil.java b/yuxue/util/PlateUtil.java new file mode 100644 index 0000000..b1a3af2 --- /dev/null +++ b/yuxue/util/PlateUtil.java @@ -0,0 +1,546 @@ +package com.yuxue.util; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.Vector; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfPoint; +import org.opencv.core.Point; +import org.opencv.core.Rect; +import org.opencv.core.Scalar; +import org.opencv.core.Size; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.imgproc.Imgproc; +import org.opencv.ml.ANN_MLP; +import org.opencv.ml.SVM; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.yuxue.constant.Constant; +import com.yuxue.enumtype.Direction; +import com.yuxue.enumtype.PlateColor; +import com.yuxue.train.SVMTrain; + + +/** + * 车牌处理工具类 + * 车牌切图按字符分割 + * 字符识别 + * 未完成 + * @author yuxue + * @date 2020-05-28 15:11 + */ +public class PlateUtil { + + // 车牌定位处理步骤,该map用于表示步骤图片的顺序 + private static Map debugMap = Maps.newLinkedHashMap(); + static { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + + debugMap.put("platePredict", 0); + debugMap.put("colorMatch", 0); + debugMap.put("plateThreshold", 0); + debugMap.put("plateContours", 0); + debugMap.put("plateRect", 0); + debugMap.put("plateCrop", 0); + debugMap.put("char_clearLiuDing", 0); // 去除柳钉 + debugMap.put("specMat", 0); + debugMap.put("chineseMat", 0); + debugMap.put("char_auxRoi", 0); + + // 设置index, 用于debug生成文件时候按名称排序 + Integer index = 200; + for (Entry entry : debugMap.entrySet()) { + entry.setValue(index); + index ++; + } + + // 这个位置加载模型文件会报错,暂时没时间定位啥问题报错 + /*loadSvmModel("D:/PlateDetect/train/plate_detect_svm/svm2.xml"); + loadAnnModel("D:/PlateDetect/train/chars_recognise_ann/ann.xml");*/ + } + + private static SVM svm = SVM.create(); + + private static ANN_MLP ann=ANN_MLP.create(); + + public static void loadSvmModel(String path) { + svm.clear(); + svm=SVM.load(path); + } + + // 加载ann配置文件 图像转文字的训练库文件 + public static void loadAnnModel(String path) { + ann.clear(); + ann = ANN_MLP.load(path); + } + + + public static void main(String[] args) { + /*System.err.println(PalteUtil.isPlate("粤AI234K")); + System.err.println(PalteUtil.isPlate("鄂CD3098"));*/ + + } + + + /** + * 根据正则表达式判断字符串是否是车牌 + * @param str + * @return + */ + public static Boolean isPlate(String str) { + Pattern p = Pattern.compile(Constant.plateReg); + Boolean bl = false; + Matcher m = p.matcher(str); + while(m.find()) { + bl = true; + break; + } + return bl; + } + + + /** + * 输入车牌切图集合,判断是否包含车牌 + * @param inMat + * @param dst 包含车牌的图块 + */ + public static void hasPlate(Vector inMat, Vector dst, Boolean debug, String tempPath) { + int i = 0; + for (Mat src : inMat) { + if(src.rows() == Constant.DEFAULT_HEIGHT && src.cols() == Constant.DEFAULT_WIDTH) { + Mat samples = SVMTrain.getFeature(src); + float flag = svm.predict(samples); + if (flag == 0) { + dst.add(src); + if(debug) { + System.err.println("目标符合"); + Imgcodecs.imwrite(tempPath + debugMap.get("platePredict") + "_platePredict" + i + ".png", src); + } + i++; + } else { + System.out.println("目标不符合"); + } + } else { + System.err.println("非法图块"); + } + } + return; + } + + + /** + * 判断切图车牌颜色 + * @param inMat + * @return + */ + public static PlateColor getPlateColor(Mat inMat, Boolean adaptive_minsv, Boolean debug, String tempPath) { + // 判断阈值 + final float thresh = 0.70f; + if(colorMatch(inMat, PlateColor.GREEN, adaptive_minsv, debug, tempPath) > thresh) { + return PlateColor.GREEN; + } + if(colorMatch(inMat, PlateColor.YELLOW, adaptive_minsv, debug, tempPath) > thresh) { + return PlateColor.YELLOW; + } + if(colorMatch(inMat, PlateColor.BLUE, adaptive_minsv, debug, tempPath) > thresh) { + return PlateColor.BLUE; + } + return PlateColor.UNKNOWN; + } + + + /** + * 颜色匹配计算 + * @param inMat + * @param r + * @param adaptive_minsv + * @param debug + * @param tempPath + * @return + */ + public static Float colorMatch(Mat inMat, PlateColor r, Boolean adaptive_minsv, Boolean debug, String tempPath) { + final float max_sv = 255; + final float minref_sv = 64; + final float minabs_sv = 95; + + Mat hsvMat = ImageUtil.rgb2Hsv(inMat, debug, tempPath); + + // 匹配模板基色,切换以查找想要的基色 + int min_h = r.minH; + int max_h = r.maxH; + float diff_h = (float) ((max_h - min_h) / 2); + int avg_h = (int) (min_h + diff_h); + + for (int i = 0; i < hsvMat.rows(); ++i) { + for (int j = 0; j < hsvMat.cols(); j += 3) { + int H = (int)hsvMat.get(i, j)[0]; + int S = (int)hsvMat.get(i, j)[1]; + int V = (int)hsvMat.get(i, j)[2]; + + boolean colorMatched = false; + + if ( min_h < H && H <= max_h) { + int Hdiff = Math.abs(H - avg_h); + float Hdiff_p = Hdiff / diff_h; + float min_sv = 0; + if (adaptive_minsv) { + min_sv = minref_sv - minref_sv / 2 * (1 - Hdiff_p); + } else { + min_sv = minabs_sv; + } + if ((min_sv < S && S <= max_sv) && (min_sv < V && V <= max_sv)) { + colorMatched = true; + } + } + + if (colorMatched == true) { + hsvMat.put(i, j, 0, 0, 255); + } else { + hsvMat.put(i, j, 0, 0, 0); + } + } + } + + // 获取颜色匹配后的二值灰度图 + List hsvSplit = Lists.newArrayList(); + Core.split(hsvMat, hsvSplit); + Mat gray = hsvSplit.get(2); + + float percent = (float) Core.countNonZero(gray) / (gray.rows() * gray.cols()); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("colorMatch") + "_colorMatch.jpg", gray); + } + return percent; + } + + + + /** + * 车牌切图,分割成单个字符切图 + * @param inMat 输入原始图像 + * @param charMat 返回字符切图vector + * @param debug + * @param tempPath + */ + public static final int DEFAULT_ANGLE = 30; // 角度判断所用常量 + public static void charsSegment(Mat inMat, PlateColor color, Vector charMat, Boolean debug, String tempPath) { + Mat gray = new Mat(); + Imgproc.cvtColor(inMat, gray, Imgproc.COLOR_BGR2GRAY); + + Mat threshold = new Mat(); + switch (color) { + case BLUE: + Imgproc.threshold(gray, threshold, 10, 255, Imgproc.THRESH_OTSU + Imgproc.THRESH_BINARY); + break; + + case YELLOW: + Imgproc.threshold(gray, threshold, 10, 255, Imgproc.THRESH_OTSU + Imgproc.THRESH_BINARY_INV); + break; + + case GREEN: + Imgproc.threshold(gray, threshold, 10, 255, Imgproc.THRESH_OTSU + Imgproc.THRESH_BINARY_INV); + break; + + default: + return; + } + + // 图片处理,降噪等 + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("plateThreshold") + "_plateThreshold.jpg", threshold); + } + + // 获取轮廓 + Mat contour = new Mat(); + threshold.copyTo(contour); + + List contours = Lists.newArrayList(); + // 提取外部轮廓 + Imgproc.findContours(contour, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE); + + if (debug) { + Mat result = new Mat(); + inMat.copyTo(result); + Imgproc.drawContours(result, contours, -1, new Scalar(0, 0, 255, 255)); + Imgcodecs.imwrite(tempPath + debugMap.get("plateContours") + "_plateContours.jpg", result); + } + + + Vector rt = new Vector(); + for (int i = 0; i < contours.size(); i++) { + Rect mr = Imgproc.boundingRect(contours.get(i)); + /*if(debug) { + Mat mat = new Mat(threshold, mr); + Imgcodecs.imwrite(tempPath + debugMap.get("plateRect") + "_plateRect_" + i + ".jpg", mat); + }*/ + if (checkCharSizes(mr)) { + rt.add(mr); + } + } + if(null == rt || rt.size() <= 0) { + return; + } + Vector sorted = new Vector(); + sortRect(rt, sorted); + + String plate = ""; + Vector dst = new Vector(); + + for (int i = 0; i < sorted.size(); i++) { + Mat img_crop = new Mat(threshold, sorted.get(i)); + img_crop = preprocessChar(img_crop); + dst.add(img_crop); + if(debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("plateCrop") + "_plateCrop_" + i + ".jpg", img_crop); + } + + Mat f = features(img_crop, Constant.predictSize); + + // 字符预测 + Mat output = new Mat(1, 140, CvType.CV_32F); + int index = (int) ann.predict(f, output, 0); + + if (index < Constant.numCharacter) { + plate += String.valueOf(Constant.strCharacters[index]); + } else { + String s = Constant.strChinese[index - Constant.numCharacter]; + plate += Constant.KEY_CHINESE_MAP.get(s); + } + } + System.err.println("===>" + plate); + + return; + } + + /** + * 字符预处理: 统一每个字符的大小 + * @param in + * @return + */ + final static int CHAR_SIZE = 20; + private static Mat preprocessChar(Mat in) { + int h = in.rows(); + int w = in.cols(); + Mat transformMat = Mat.eye(2, 3, CvType.CV_32F); + int m = Math.max(w, h); + transformMat.put(0, 2, (m - w) / 2f); + transformMat.put(1, 2, (m - h) / 2f); + + Mat warpImage = new Mat(m, m, in.type()); + Imgproc.warpAffine(in, warpImage, transformMat, warpImage.size(), Imgproc.INTER_LINEAR, Core.BORDER_CONSTANT, new Scalar(0)); + + Mat resized = new Mat(CHAR_SIZE, CHAR_SIZE, CvType.CV_8UC3); + Imgproc.resize(warpImage, resized, resized.size(), 0, 0, Imgproc.INTER_CUBIC); + + return resized; + } + + + + /** + * 字符尺寸验证;去掉尺寸不符合的图块 + * 此处计算宽高比意义不大,因为字符 1 的宽高比干扰就已经很大了 + * @param r + * @return + */ + public static Boolean checkCharSizes(Rect r) { + float minHeight = 15f; + float maxHeight = 35f; + double charAspect = r.size().width / r.size().height; + return charAspect <1 && minHeight <= r.size().height && r.size().height < maxHeight; + } + + + + /** + * 将Rect按位置从左到右进行排序 + * @param vecRect + * @param out + * @return + */ + public static void sortRect(Vector vecRect, Vector out) { + Map map = Maps.newHashMap(); + for (int i = 0; i < vecRect.size(); ++i) { + map.put(vecRect.get(i).x, i); + } + Set set = map.keySet(); + Object[] arr = set.toArray(); + Arrays.sort(arr); + for (Object key : arr) { + out.add(vecRect.get(map.get(key))); + } + return; + } + + + + public static float[] projectedHistogram(final Mat img, Direction direction) { + int sz = 0; + switch (direction) { + case HORIZONTAL: + sz = img.rows(); + break; + + case VERTICAL: + sz = img.cols(); + break; + + default: + break; + } + + // 统计这一行或一列中,非零元素的个数,并保存到nonZeroMat中 + float[] nonZeroMat = new float[sz]; + Core.extractChannel(img, img, 0); + for (int j = 0; j < sz; j++) { + Mat data = (direction == Direction.HORIZONTAL) ? img.row(j) : img.col(j); + int count = Core.countNonZero(data); + nonZeroMat[j] = count; + } + // Normalize histogram + float max = 0; + for (int j = 0; j < nonZeroMat.length; ++j) { + max = Math.max(max, nonZeroMat[j]); + } + if (max > 0) { + for (int j = 0; j < nonZeroMat.length; ++j) { + nonZeroMat[j] /= max; + } + } + return nonZeroMat; + } + + + public static Mat features(Mat in, int sizeData) { + + float[] vhist = projectedHistogram(in, Direction.VERTICAL); + float[] hhist = projectedHistogram(in, Direction.HORIZONTAL); + + Mat lowData = new Mat(); + if (sizeData > 0) { + Imgproc.resize(in, lowData, new Size(sizeData, sizeData)); + } + + int numCols = vhist.length + hhist.length + lowData.cols() * lowData.rows(); + Mat out = new Mat(1, numCols, CvType.CV_32F); + + int j = 0; + for (int i = 0; i < vhist.length; ++i, ++j) { + out.put(0, j, vhist[i]); + } + for (int i = 0; i < hhist.length; ++i, ++j) { + out.put(0, j, hhist[i]); + } + + for (int x = 0; x < lowData.cols(); x++) { + for (int y = 0; y < lowData.rows(); y++, ++j) { + double[] val = lowData.get(x, y); + out.put(0, j, val[0]); + } + } + return out; + } + + + + + /** + * 进行膨胀操作 + * @param inMat + * @return + */ + public static Mat dilate(Mat inMat) { + Mat result = inMat.clone(); + Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(2, 2)); + Imgproc.dilate(inMat, result, element); + return result; + } + + /** + * 进行腐蚀操作 + * @param inMat + * @return + */ + public static Mat erode(Mat inMat) { + Mat result = inMat.clone(); + Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(2, 2)); + Imgproc.erode(inMat, result, element); + return result; + } + + + /** + * 随机数平移 + * @param inMat + * @return + */ + public static Mat randTranslate(Mat inMat) { + Random rand = new Random(); + Mat result = inMat.clone(); + int ran_x = rand.nextInt(10000) % 5 - 2; // 控制在-2~3个像素范围内 + int ran_y = rand.nextInt(10000) % 5 - 2; + return translateImg(result, ran_x, ran_y); + } + + + /** + * 随机数旋转 + * @param inMat + * @return + */ + public static Mat randRotate(Mat inMat) { + Random rand = new Random(); + Mat result = inMat.clone(); + float angle = (float) (rand.nextInt(10000) % 15 - 7); // 旋转角度控制在-7~8°范围内 + return rotateImg(result, angle); + } + + + /** + * 平移 + * @param img + * @param offsetx + * @param offsety + * @return + */ + public static Mat translateImg(Mat img, int offsetx, int offsety){ + Mat dst = new Mat(); + //定义平移矩阵 + Mat trans_mat = Mat.zeros(2, 3, CvType.CV_32FC1); + trans_mat.put(0, 0, 1); + trans_mat.put(0, 2, offsetx); + trans_mat.put(1, 1, 1); + trans_mat.put(1, 2, offsety); + Imgproc.warpAffine(img, dst, trans_mat, img.size()); // 仿射变换 + return dst; + } + + + /** + * 旋转角度 + * @param source + * @param angle + * @return + */ + public static Mat rotateImg(Mat source, float angle){ + Point src_center = new Point(source.cols() / 2.0F, source.rows() / 2.0F); + Mat rot_mat = Imgproc.getRotationMatrix2D(src_center, angle, 1); + Mat dst = new Mat(); + // 仿射变换 可以考虑使用投影变换; 这里使用放射变换进行旋转,对于实际效果来说感觉意义不大,反而会干扰结果预测 + Imgproc.warpAffine(source, dst, rot_mat, source.size()); + return dst; + } + + + +}