1、使用BufferedImage 的 ImageIO.read 读取本地图片信息,将其放入缓冲区以便操作
2、将图片垂直等分几份 分区分段扫描以提高准确率
3、通过getRGB()方式获得像素矩阵 : 此方式为沿width方向扫描,来依次向上扫描此区域 统计连续的黑色像素的高度
4、获得每段的数据数组,去掉极端数据(明显不属于一个字体的高度) 用来去掉 标点,------,和一些数学图形
5、用一个较大的数组去存下扫描的所有数据(要符合一个字体的合适的高度范围,比如最低20,最高200)
5、每段去平均值组成一个新的数组,得到平均值
6、判断该平均值 是否大于一个字体的 最大高度, 大于 :将图片选中90度,再次扫描
7、 取较大数组里的众数
8、如果 一个众数也没有,返回平均
9、有众数,如果有多个,得到多个众数取平均,最终获得的众数如果与平均值相差很大,依次向后面取,这个差值暂定30
代码:
package testDemo;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.imageio.ImageIO;
import javax.print.attribute.Size2DSyntax;
/**
* 识别图片文字高度 (返回0 表示无效图片)
* @author huangxq
*
*/
public class TruncatImgStudy {
private final int part =10;//需要切割成几份扫描
private final float minFontHeight = 25; // 允许最小误差
private final float maxFontHeight = 200;//允许最大误差舍弃
private final int twoCycles=2;//是否二次循环过:用于上传过来的图片是倒过来的
public static void main(String [] args){
TruncatImgStudy JustStudy = new TruncatImgStudy();
JustStudy.initImage("F:/错题图片/1.jpg");
}
/**
* 得到文字行与图片高度
*/
public int getFontHeight(int cycles,BufferedImage bimg){
if(null == bimg){
return 0;
}
//用来求众数:
ArrayList<Integer> multipleList = new ArrayList<>();
//计众数和
int multipleSum = 0;
//将该图片垂直等分几段
List<BufferedImage> cutImage = randomCut(bimg,part);
if(null == cutImage || cutImage.isEmpty()){
return 0;//未切割成功
}
//用来求最终平均值【判断是否需要旋转扫描】
int[] lastHignNums = new int[cutImage.size()];
for(int j=0;j<cutImage.size();j++){
BufferedImage tempBimg = cutImage.get(j);
//获取所有不为全白行的高度坐标
int[] notWhiteRow = this.getBlackRow(tempBimg);
// 计算所有连续行的行数即文字高度
int[] hignSpace = this.getAllHeight(notWhiteRow);
//去掉极端数据(即:不满足一个字体的合适的最大最小高度 如:黑点)
int[] newArray = this.cutInvalidNum(hignSpace,minFontHeight);
//若该段未扫描到一个字体,跳出本次循环
if(null == newArray || newArray.length<=0){
continue;
}
//将该段统计到的符合合理高度的数据都存到【众数集合】
for (int i : newArray) {
//这里指考虑最高高度,最低高度前面已经剔除了
if(i< this.maxFontHeight){
multipleList.add(i);
multipleSum +=i;
}
}
lastHignNums[j] = this.getAverage(newArray);
}
//该数组去极端值【有的段没有扫到字体,去掉该段的0记录】
lastHignNums = this.cutInvalidNum(lastHignNums, minFontHeight);
int lastAvg = this.getAverage(lastHignNums);
//平均值与一个字体高度偏差太高,旋转过来扫描
if(lastAvg<= this.minFontHeight || lastAvg>= this.maxFontHeight){
//扫描次数超过两次,已经扫描过了,直接返回0,为无效图片
if(cycles >= this.twoCycles){
return 0;
}
//第一次旋转,执行旋转
cycles++;
return this.rotateImage(bimg);
}
//返回众数
//一个字体高度都没统计出来,表示图片无效
if(null == multipleList || multipleList.isEmpty()){
return 0;
}
//去除极端数据取平均数
int multipleAvg =multipleSum/multipleList.size();
int result = this.multipleNum(multipleList,multipleAvg,this.minFontHeight,this.maxFontHeight);
return result;
}
/**
* 把一张图片垂直等分
*
* @param bimg
* @param time
* @return
*/
private List<BufferedImage> randomCut(BufferedImage bimg, int time) {
if(null == bimg || time<=0){
return Collections.emptyList();
}
ArrayList list = new ArrayList();
int width = bimg.getWidth();
int sPerTime = width / time;
for (int i = 0; i < time; i++) {
BufferedImage newBuffer = bimg.getSubimage(i * sPerTime, 0, sPerTime, bimg.getHeight());
list.add(newBuffer);
}
return list;
}
/**
* 获取拥有黑色像素的高度坐标 沿width方向扫描
*
* @return
*/
private int[] getBlackRow(BufferedImage bimg) {
if(null == bimg){
return null;
}
int [][]data = new int[bimg.getWidth()][bimg.getHeight()];
int[] startRow = new int[bimg.getHeight()];
int n = 0;
//通过getRGB()方式获得像素矩阵 : 此方式为沿width方向扫描
for(int y=0;y<bimg.getHeight();y++){
for(int x=0;x<bimg.getWidth();x++){
data[x][y]=bimg.getRGB(x,y);
if (!("ffffffff".equals(String.format("%x",data[x][y])))){// 该行像素不是 ("#ffffffff")即全白 就归类为一个字
startRow[n] = y;//统计该字体高度
n++;
break;//一行只记一个
}
}
}
return startRow;
}
/**
* 计算所有连续行的高度即文字的高度
*/
public int[] getAllHeight(int[] startRow){
if(null == startRow || startRow.length<=0){
return null;
}
int k = 0;
//用来计一个字的向上延展有多少像素
int[] hignSpace = new int[startRow.length];
int temp = 0;
for (int i=0;i<startRow.length;i++) {
//除第一行,需要连续
if (i != startRow.length-1 && !((startRow[i+1]-startRow[i]) == 1)){
if (k ==0){
hignSpace[k] = startRow[i] - startRow[0];
}else {
hignSpace[k] = startRow[i] - temp;
}
temp = startRow[i+1];
k++;
}
}
return hignSpace;
}
/**
* 去除指定误差外的无效数据
*
* @param oldArr
* @param t
* @return
*/
private int[] cutInvalidNum(int[] oldArr, float min) {
if (null == oldArr || oldArr.length <= 0) {
return null;
}
int zero = 0;
for (int i = 0; i < oldArr.length; i++) {
if (oldArr[i] < min) {
zero++;
}
}
// 定义新的数组 长度是 原来旧的数组的长度减去(要去掉的极端数据)的个数
int newArr[] = new int[oldArr.length - zero];
int j = 0; // 新数组的索引
for (int i = 0; i < oldArr.length; i++) { // 遍历原来旧的数组
if (oldArr[i] >= min) {
newArr[j] = oldArr[i]; // 赋值给新的数组
j++;
}
}
Arrays.sort(newArr);
return newArr;
}
/**
* 取平均值
*
* @param
*/
private int getAverage(int[] array) {
if (null == array || array.length <= 0) {
return 0;
}
int leg = 0;
int sum = 0;
for (int i = 0; i < array.length; i++) {
if (0 != array[i]) {
sum = sum + array[i];
leg++;
}
}
if(leg<=0){
return 0;
}
return sum / leg;
}
/**
* 顺时针旋转90度(通过交换图像的整数像素RGB 值)
* @param BufferedImage bi
* @return
*/
public int rotateImage(BufferedImage bi) {
if(null == bi){
return 0;
}
int width = bi.getWidth();
int height = bi.getHeight();
BufferedImage bufferedImage = new BufferedImage(height, width, bi.getType());
for (int i = 0; i < width; i++){
for (int j = 0; j < height; j++){
bufferedImage.setRGB(height - 1 - j, width - 1 - i, bi.getRGB(i, j));
}
}
return getFontHeight(2,bufferedImage);
}
/**
* 取众数 如果一个众数也没有,返回改组数据平均值,如果众数不止一个,返回众数平均值
*
* @param list
* @return
*/
public int multipleNum(ArrayList<Integer> list,int multipleAvg,float minFontHeight,float maxFontHeight){
int result;
if(null == list || list.isEmpty()){
return 0;
}
//计value 重复次数:map集合 map<value,该value重复的个数>
Map<Integer, Integer> multiMap = new HashMap<>();
for (int i : list) {
if(multiMap.containsKey(i)){
multiMap.put(i, multiMap.get(i)+1);
}else{
multiMap.put(i, 1);
}
}
// 将 multiMap 中所有的键值对(键为数,值为数出现的频率)放入一个 ArrayList 按 value 重复次数排序
List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(multiMap.entrySet());
// 对 entries 按出现频率从大到小排序
Collections.sort(entries, new Comparator<Map.Entry<Integer, Integer>>() {
@Override
public int compare(Map.Entry<Integer, Integer> e1, Map.Entry<Integer, Integer> e2) {
return e2.getValue() - e1.getValue();
}
});
if(null == entries || entries.isEmpty()){
return 0;
}
//如果这个数组没有众数 返回平均数
if(entries.get(0).getValue()==1){
return multipleAvg;
}
return this.getSuitMultipleNum(entries,multipleAvg);
}
/**
* 获取合适的众数
* @param entries
* @param multipleAvg
* @return
*/
public int getSuitMultipleNum(List<Map.Entry<Integer, Integer>> entries,int multipleAvg){
int size = entries.size();
for(int i=0 ; i<size ;i++){
int result = 0;
int sumNum = 0;
List<Integer> modalNums = new ArrayList<>();
// 本来排序后第一个 entry 的键肯定是一个众数,如果这个众数不合理,依次向后取,越靠前越合理
int firstNum = entries.get(i).getKey();
modalNums.add(firstNum);
sumNum+=firstNum;
//处理有多个众数情况
//统计众数是否唯一
for (int j = (i+1); j < size; j++) {
// 如果之后的 entry 与第一个 entry 的 value 相等,那么这个 entry 的键也是众数
if (entries.get(j).getValue().equals(entries.get(i).getValue())) {
int tempNum = entries.get(j).getKey();
modalNums.add(tempNum);
sumNum+=tempNum;
} else {
break;
}
}
//如果数组为空,继续循环
if(null == modalNums || modalNums.isEmpty()){
continue;
}
if(modalNums.size()<=1){
//只有一个众数
result = modalNums.get(0);
}else{
//多个众数取平均值
result = sumNum/modalNums.size();
}
//这里经过找规律比较,暂定30
if(Math.abs((result - multipleAvg)) < 30){
return result;
}
}
//都没有
return multipleAvg;
}
/**
* 获取图片信息
* @param openUrl 文件路径
* @return
*/
public int initImage(String openUrl){
File file = new File(openUrl);
BufferedImage img=null;// 将图片缓冲到内存中,BufferedImage生成的图片在内存里有一个图像缓冲区
if(null!=file){
try {
img= ImageIO.read(file);
return getFontHeight(1,img);
} catch (IOException e) {
e.printStackTrace();
return 0;
}
}
return 0;
}
}