https://blog.csdn.net/cjzjolly/article/details/109659319
接上文《java通过三角函数旋转矩阵2_提升速度》
通过上文我们不难发现实际上转换过后的矩阵,每行的斜率都是一样的,所以只要知道第一行起点和终点、最后一行的起点去终点,即可计算每行的起始x对比上一行要偏移的值ddx,另外也可以计算每行偏移的值ddy,即可优化出方法,这样只需计算一次三角函数和一次线条坐标,即可每次通过简单的偏移值累加就可以实现求得所有新的坐标值的目的:
public byte[] rotateMatrix2(byte data[], int width, int height, float rotateDegree) {
long startTimeStamp = System.currentTimeMillis();
int area[] = rotateAreaTransform(rotateDegree, width, height);
byte pixels[] = new byte[area[0] * area[1]];
int offsetX = (area[0] - width) / 2;
int offsetY = (area[1] - height) / 2;
//计算旋转后2个顶点
double firstRowXYStard[] = rotateCoordinateTransform(rotateDegree, 0, 0, width / 2, height / 2, true);
double firstRowXYEnd[] = rotateCoordinateTransform(rotateDegree, width, 0, width / 2, height / 2, true);
double lastRowXYStart[] = rotateCoordinateTransform(rotateDegree, 0, height, width / 2, height / 2, true);
double lastRowXYEnd[] = rotateCoordinateTransform(rotateDegree, width, height, width / 2, height / 2, true);
//计算最后一行与第一行的偏移值dx,算出行与行之间要进行x偏移值ddx 每行的斜率是一致的不用算
double ddx = (lastRowXYStart[0] - firstRowXYStard[0]) / height;
double ddy = (lastRowXYStart[1] - firstRowXYStard[1]) / height;
System.out.println("ddx :" + ddx + ", ddy:" + ddy); //如果ddx和ddy即使是整数也无法做到点对点,但ddy ddx任意一个为0可以做到锯齿的对齐,所以是信息密度上不达标
int[][] linePoints = drawLine((int) firstRowXYStard[0], (int) firstRowXYStard[1],
(int) firstRowXYEnd[0], (int) firstRowXYEnd[1], width);
for (int i = 0, row = 0; i < data.length * 2 / 3; i += width, row++) {
int j = i;
for (int[] pointXY : linePoints) {
int x = (int) Math.round(pointXY[0] + offsetX + ddx * row);
int y = (int) Math.round(pointXY[1] + offsetY + ddy * row);
if (x >= 0 && y >= 0 && x < area[0] && y < area[1]) {
pixels[y * area[0] + x] = data[j];
++j;
}
}
}
System.out.println("cycle end in " + (System.currentTimeMillis() - startTimeStamp) + " ms");
return pixels;
}
另外,通过上次的例子,不难发现因为旋转后的图片因为离散直线绘制方面的天然缺陷,导致锯齿咬合得比较差,出现很多没有信息的空洞,变得很难看,举个通俗的例子,就像俄罗斯方块两个方块之间没能找到完全吻合的块导致空洞出现一样:
因此可以通过每次填充值时,把附近1个临近单元格进行填充即可得到比较满意的视觉效果。其实就是在像素填充时增加如下代码:
if (x - 1 >= 0 && pixels[y * area[0] + x - 1] == 0) { //补足因信息不足产生的空隙区域
pixels[y * area[0] + x - 1] = data[j];
}
当然我是为了快,所以直接填充可能存在的空隙区域。如果希望画质更好一些,可以和周边像素求个平均值,使得空隙的像素像素过得更平滑
完整代码如下:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class MainForGrayImageRotate {
public static void main(String args[]) {
System.out.println("MainForGrayImageRotate");
try {
FileInputStream fis = new FileInputStream(new File("/media/chenjiezhu/work2/其他/Download/2048x1024_1605497440349.nv21"));
byte data[] = new byte[fis.available()];
fis.read(data);
fis.close();
new MainForGrayImageRotate().rotateMatrix2(data, 2048, 1024, 130);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return;
}
public byte[] rotateMatrix2(byte data[], int width, int height, float rotateDegree) {
long startTimeStamp = System.currentTimeMillis();
int area[] = rotateAreaTransform(rotateDegree, width, height);
byte pixels[] = new byte[area[0] * area[1]];
int offsetX = (area[0] - width) / 2;
int offsetY = (area[1] - height) / 2;
//计算旋转后2个顶点
double firstRowXYStard[] = rotateCoordinateTransform(rotateDegree, 0, 0, width / 2, height / 2, true);
double firstRowXYEnd[] = rotateCoordinateTransform(rotateDegree, width, 0, width / 2, height / 2, true);
double lastRowXYStart[] = rotateCoordinateTransform(rotateDegree, 0, height, width / 2, height / 2, true);
double lastRowXYEnd[] = rotateCoordinateTransform(rotateDegree, width, height, width / 2, height / 2, true);
//计算最后一行与第一行的偏移值dx,算出行与行之间要进行x偏移值ddx 每行的斜率是一致的不用算
double ddx = (lastRowXYStart[0] - firstRowXYStard[0]) / height;
double ddy = (lastRowXYStart[1] - firstRowXYStard[1]) / height;
System.out.println("ddx :" + ddx + ", ddy:" + ddy); //如果ddx和ddy即使是整数也无法做到点对点,但ddy ddx任意一个为0可以做到锯齿的对齐,所以是信息密度上不达标
int[][] linePoints = drawLine((int) firstRowXYStard[0], (int) firstRowXYStard[1],
(int) firstRowXYEnd[0], (int) firstRowXYEnd[1], width);
for (int i = 0, row = 0; i < data.length * 2 / 3; i += width, row++) {
int j = i;
for (int[] pointXY : linePoints) {
int x = (int) Math.round(pointXY[0] + offsetX + ddx * row);
int y = (int) Math.round(pointXY[1] + offsetY + ddy * row);
if (x >= 0 && y >= 0 && x < area[0] && y < area[1]) {
pixels[y * area[0] + x] = data[j];
if (x - 1 >= 0 && pixels[y * area[0] + x - 1] == 0) { //补足因信息不足产生的空隙区域
pixels[y * area[0] + x - 1] = data[j];
}
++j;
}
}
}
System.out.println("cycle end in " + (System.currentTimeMillis() - startTimeStamp) + " ms");
File file = new File("/media/chenjiezhu/work2/其他/Download/" + area[0] + "x" + area[1] + "_" + System.currentTimeMillis() + ".gray");
FileOutputStream fileOutputStream;
try {
fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(pixels);
fileOutputStream.flush();
fileOutputStream.close();
return pixels;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* @param centerPointX
* Rotate according to center pointX
* @param centerPointY
* Rotate according to center pointY
**/
private double[] rotateCoordinateTransform(float rotateDegree, int bmpX, int bmpY, int centerPointX,
int centerPointY, boolean ccw) {
double temp[] = new double[2];
double degreeToRadians = Math.toRadians(rotateDegree);
if (ccw) {
temp[0] = (Math.cos(degreeToRadians) * (bmpX - centerPointX) - Math.sin(degreeToRadians) * (bmpY - centerPointY));
temp[1] = (Math.sin(degreeToRadians) * (bmpX - centerPointX) + Math.cos(degreeToRadians) * (bmpY - centerPointY));
} else {
temp[0] = (Math.cos(degreeToRadians) * (bmpX - centerPointX) + Math.sin(degreeToRadians) * (bmpY - centerPointY));
temp[1] = (Math.sin(degreeToRadians) * (bmpX - centerPointX) - Math.cos(degreeToRadians) * (bmpY - centerPointY));
}
temp[0] += centerPointX;
temp[1] += centerPointY;
return temp;
}
private int[] rotateAreaTransform(float rotateDegree, int width, int height) { //有bug
double degreeToRadians = Math.toRadians(rotateDegree);
int newWidth = (int) Math.ceil(Math.abs(Math.cos(degreeToRadians) * width) + Math.abs(Math.sin(degreeToRadians) * height));
int newHeight = (int) Math.ceil(Math.abs(Math.cos(degreeToRadians) * height) + Math.abs(Math.sin(degreeToRadians) * width));
return new int[] { (int) (newWidth), (int) (newHeight)};
}
private int[][] drawLine(int x1, int y1, int x2, int y2, int length) {
int[][] pointList = new int[length][2];
int dx = Math.abs(x2 - x1),
dy = Math.abs(y2 - y1),
yy = 0;
if (dx < dy) {
yy = 1;
int temp = x1;
x1 = y1;
y1 = temp;
temp = x2;
x2 = y2;
y2 = temp;
temp = dx;
dx = dy;
dy = temp;
}
int ix = (x2 - x1) > 0 ? 1 : -1,
iy = (y2 - y1) > 0 ? 1 : -1,
cx = x1,//cjzmark 输入起始点和终结点,返回线条坐标点阵
cy = y1,
n2dy = dy * 2,
n2dydx = (dy - dx) * 2,
d = dy * 2 - dx;
if (yy > 0) { // 如果直线与 x 轴的夹角大于 45 度
for (int i = 0; cx != x2 && i < length; i++) {
if (d < 0) {
d += n2dy;
} else {
cy += iy;
d += n2dydx;
}
pointList[i] = new int[]{cy, cx};
cx += ix;
}
} else { // 如果直线与 x 轴的夹角小于 45 度
for (int i = 0; cx != x2 && i < length; i++) {
//while (cx != x2) {
if (d < 0) {
d += n2dy;
} else {
cy += iy;
d += n2dydx;
}
pointList[i] = new int[]{cx, cy};
cx += ix;
}
}
return pointList;
}
}
实际效果:
(输入2048x1024的画面只需90ms左右即处理完成):
在可以任意角度旋转灰度图的同时,不会产生肉眼可见的画质损失,可以用于嵌入式软件或者安卓APP通过旋转传入的二维码图像,提升多角度扫码效果等,或用于监控预览画面的换算。