Feat[utils]: Implement MatrixUtils, with methods to easily work with transform matrices

This commit is contained in:
artdeell 2024-01-10 20:27:29 +03:00 committed by Maksim Belov
parent 83fdf15379
commit 2cef7ccd49
3 changed files with 198 additions and 92 deletions

View File

@ -5,6 +5,8 @@ import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import net.kdt.pojavlaunch.utils.MatrixUtils;
public class BitmapCropBehaviour implements CropperBehaviour{
private final Matrix mTranslateInverse = new Matrix();
protected final Matrix mTranslateMatrix = new Matrix();
@ -32,7 +34,7 @@ public class BitmapCropBehaviour implements CropperBehaviour{
public void zoom(float zoomLevel, float midpointX, float midpointY) {
// Do this to avoid constantly inverting the same matrix on each touch event.
if(mTranslateInverseOutdated) {
inverse(mTranslateMatrix, mTranslateInverse);
MatrixUtils.inverse(mTranslateMatrix, mTranslateInverse);
mTranslateInverseOutdated = false;
}
float[] zoomCenter = new float[] {
@ -81,42 +83,32 @@ public class BitmapCropBehaviour implements CropperBehaviour{
public Bitmap crop(int targetMaxSide) {
Matrix imageInverse = new Matrix();
inverse(mImageMatrix, imageInverse);
MatrixUtils.inverse(mImageMatrix, imageInverse);
// By inverting the matrix we will effectively "divide" our rectangle by it, thus getting
// its two points on the surface of the bitmap. Math be cool indeed.
float[] src = new float[] {
mHostView.mSelectionRect.left,
mHostView.mSelectionRect.top,
mHostView.mSelectionRect.right,
mHostView.mSelectionRect.bottom
};
float[] dst = new float[4];
imageInverse.mapPoints(dst, 0, src, 0, 2);
Rect originalBitmapRect = new Rect(
(int)dst[0], (int)dst[1],
(int)dst[2], (int)dst[3]
);
// its two points on the bitmap's surface. Math be cool indeed.
Rect targetRect = new Rect();
MatrixUtils.transformRect(mHostView.mSelectionRect, targetRect, imageInverse);
// Pick the best dimensions for the crop result, shrinking the target if necessary.
int targetWidth, targetHeight;
int targetMinDimension = Math.min(originalBitmapRect.width(), originalBitmapRect.height());
int targetMinDimension = Math.min(targetRect.width(), targetRect.height());
if(targetMaxSide < targetMinDimension) {
float ratio = (float) targetMaxSide / targetMinDimension;
targetWidth = (int) (originalBitmapRect.width() * ratio);
targetHeight = (int) (originalBitmapRect.height() * ratio);
targetWidth = (int) (targetRect.width() * ratio);
targetHeight = (int) (targetRect.height() * ratio);
}else {
targetWidth = originalBitmapRect.width();
targetHeight = originalBitmapRect.height();
targetWidth = targetRect.width();
targetHeight = targetRect.height();
}
Bitmap croppedBitmap = Bitmap.createBitmap(
targetWidth, targetHeight,
mOriginalBitmap.getConfig()
);
// Draw the bitmap on the target. Doing this allows us to not bother with making sure
// that originalBitmapRect is fully contained within image bounds.
// that targetRect is fully contained within image bounds.
Canvas drawCanvas = new Canvas(croppedBitmap);
drawCanvas.drawBitmap(
mOriginalBitmap,
originalBitmapRect,
targetRect,
new Rect(0, 0, targetWidth, targetHeight),
null
);
@ -166,50 +158,4 @@ public class BitmapCropBehaviour implements CropperBehaviour{
mZoomMatrix.reset();
refresh();
}
/**
* Android's conditions for matrix inversion are wacky, and sometimes it just stops working out
* of the blue. So, when Android's accelerated matrix inverse dies, just invert by hand.
* @param source Source matrix
* @param destination The inverse of the source matrix
*/
protected void inverse(Matrix source, Matrix destination) {
if(source.invert(destination)) return;
float[] matrix = new float[9];
source.getValues(matrix);
inverseMatrix(matrix);
destination.setValues(matrix);
}
// This was made by ChatGPT and i have no clue what's happening here, but it works so eh
private static void inverseMatrix(float[] matrix) {
float determinant = matrix[0] * (matrix[4] * matrix[8] - matrix[5] * matrix[7])
- matrix[1] * (matrix[3] * matrix[8] - matrix[5] * matrix[6])
+ matrix[2] * (matrix[3] * matrix[7] - matrix[4] * matrix[6]);
if (determinant == 0) {
throw new IllegalArgumentException("Matrix is not invertible");
}
float invDet = 1 / determinant;
float temp0 = (matrix[4] * matrix[8] - matrix[5] * matrix[7]);
float temp1 = (matrix[2] * matrix[7] - matrix[1] * matrix[8]);
float temp2 = (matrix[1] * matrix[5] - matrix[2] * matrix[4]);
float temp3 = (matrix[5] * matrix[6] - matrix[3] * matrix[8]);
float temp4 = (matrix[0] * matrix[8] - matrix[2] * matrix[6]);
float temp5 = (matrix[2] * matrix[3] - matrix[0] * matrix[5]);
float temp6 = (matrix[3] * matrix[7] - matrix[4] * matrix[6]);
float temp7 = (matrix[1] * matrix[6] - matrix[0] * matrix[7]);
float temp8 = (matrix[0] * matrix[4] - matrix[1] * matrix[3]);
matrix[0] = temp0 * invDet;
matrix[1] = temp1 * invDet;
matrix[2] = temp2 * invDet;
matrix[3] = temp3 * invDet;
matrix[4] = temp4 * invDet;
matrix[5] = temp5 * invDet;
matrix[6] = temp6 * invDet;
matrix[7] = temp7 * invDet;
matrix[8] = temp8 * invDet;
}
}

View File

@ -11,6 +11,7 @@ import android.os.Handler;
import net.kdt.pojavlaunch.PojavApplication;
import net.kdt.pojavlaunch.modloaders.modpacks.SelfReferencingFuture;
import net.kdt.pojavlaunch.utils.MatrixUtils;
import java.util.concurrent.Future;
@ -38,7 +39,7 @@ public class RegionDecoderCropBehaviour extends BitmapCropBehaviour {
};
/**
* Decode a region from this Bitmap based on a subsection in the View coordinate space.
* Decoade a region from this Bitmap based on a subsection in the View coordinate space.
* @param targetDrawRect an output Rect. This Rect is the position at which the region must
* be rendered within subsectionRect.
* @param subsectionRect the subsection in View coordinate space. Note that this Rect is modified
@ -46,15 +47,15 @@ public class RegionDecoderCropBehaviour extends BitmapCropBehaviour {
* @return null if the resulting region is bigger than the original image
* null if the resulting region is completely out of the original image bounds
* null if the resulting region is smaller than 16x16 pixels
* null if a region decoding error has occurred
* null if a region decoding error has occured
* the resulting Bitmap region otherwise.
*/
private Bitmap decodeRegionBitmap(RectF targetDrawRect, RectF subsectionRect) {
RectF decoderRect = new RectF(0, 0, mBitmapDecoder.getWidth(), mBitmapDecoder.getHeight());
Matrix matrix = createDecoderImageMatrix();
Matrix inverse = new Matrix();
inverse(matrix, inverse);
transformRect(subsectionRect, inverse);
MatrixUtils.inverse(matrix, inverse);
MatrixUtils.transformRect(subsectionRect, inverse);
// If our current sub-section is bigger than the decoder rect, skip.
// We do this to avoid unnecessarily loading the image at full resolution.
if(subsectionRect.width() > decoderRect.width()
@ -187,25 +188,6 @@ public class RegionDecoderCropBehaviour extends BitmapCropBehaviour {
return decoderImageMatrix;
}
/**
* Transform the coordinates of the Rect using the supplied Matrix.
* @param rect the input/output Rect for this operation
* @param regionImageInverse the Matrix for transforming the Rect.
*/
private void transformRect(RectF rect, Matrix regionImageInverse) {
if(regionImageInverse.isIdentity()) return;
float[] inOutDecodeRect = new float[8];
inOutDecodeRect[0] = rect.left;
inOutDecodeRect[1] = rect.top;
inOutDecodeRect[2] = rect.right;
inOutDecodeRect[3] = rect.bottom;
regionImageInverse.mapPoints(inOutDecodeRect, 4, inOutDecodeRect, 0, 2);
rect.left = inOutDecodeRect[4];
rect.top = inOutDecodeRect[5];
rect.right = inOutDecodeRect[6];
rect.bottom = inOutDecodeRect[7];
}
@Override
public Bitmap crop(int targetMaxSide) {
RectF drawRect = new RectF();
@ -227,7 +209,7 @@ public class RegionDecoderCropBehaviour extends BitmapCropBehaviour {
float scaleRatio = (float)targetDimension / mHostView.mSelectionRect.width();
Matrix drawRectScaleMatrix = new Matrix();
drawRectScaleMatrix.setScale(scaleRatio, scaleRatio);
transformRect(drawRect, drawRectScaleMatrix);
MatrixUtils.transformRect(drawRect, drawRectScaleMatrix);
Bitmap returnBitmap = Bitmap.createBitmap(targetDimension, targetDimension, regionBitmap.getConfig());
Canvas canvas = new Canvas(returnBitmap);

View File

@ -0,0 +1,178 @@
package net.kdt.pojavlaunch.utils;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
@SuppressWarnings("unused")
public class MatrixUtils {
/**
* Transform the coordinates of the RectF using the supplied Matrix, and write the result back into
* the RectF
* @param inOutRect the RectF for this operation
* @param transformMatrix the Matrix for transforming the Rect.
*/
public static void transformRect(Rect inOutRect, Matrix transformMatrix) {
transformRect(inOutRect, inOutRect, transformMatrix);
}
/**
* Transform the coordinates of the RectF using the supplied Matrix, and write the result back into
* the RectF
* @param inOutRect the RectF for this operation
* @param transformMatrix the Matrix for transforming the Rect.
*/
public static void transformRect(RectF inOutRect, Matrix transformMatrix) {
transformRect(inOutRect, inOutRect, transformMatrix);
}
/**
* Transform the coordinates of the input RectF using the supplied Matrix, and write the result
* into the output Rect
* @param inRect the input RectF for this operation
* @param outRect the output Rect for this operation
* @param transformMatrix the Matrix for transforming the Rect.
*/
public static void transformRect(RectF inRect, Rect outRect, Matrix transformMatrix) {
float[] inOutDecodeRect = createInOutDecodeRect(transformMatrix);
if(inOutDecodeRect == null) return;
writeInputRect(inOutDecodeRect, inRect);
transformPoints(inOutDecodeRect, transformMatrix);
readOutputRect(inOutDecodeRect, outRect);
}
/**
* Transform the coordinates of the input Rect using the supplied Matrix, and write the result
* into the output RectF
* @param inRect the input Rect for this operation
* @param outRect the output RectF for this operation
* @param transformMatrix the Matrix for transforming the Rect.
*/
public static void transformRect(Rect inRect, RectF outRect, Matrix transformMatrix) {
float[] inOutDecodeRect = createInOutDecodeRect(transformMatrix);
if(inOutDecodeRect == null) return;
writeInputRect(inOutDecodeRect, inRect);
transformPoints(inOutDecodeRect, transformMatrix);
readOutputRect(inOutDecodeRect, outRect);
}
/**
* Transform the coordinates of the input Rect using the supplied Matrix, and write the result
* into the output Rect
* @param inRect the input Rect for this operation
* @param outRect the output Rect for this operation
* @param transformMatrix the Matrix for transforming the Rect.
*/
public static void transformRect(Rect inRect, Rect outRect, Matrix transformMatrix) {
float[] inOutDecodeRect = createInOutDecodeRect(transformMatrix);
if(inOutDecodeRect == null) return;
writeInputRect(inOutDecodeRect, inRect);
transformPoints(inOutDecodeRect, transformMatrix);
readOutputRect(inOutDecodeRect, outRect);
}
/**
* Transform the coordinates of the input RectF using the supplied Matrix, and write the result
* into the output RectF
* @param inRect the input RectF for this operation
* @param outRect the output RectF for this operation
* @param transformMatrix the Matrix for transforming the Rect.
*/
public static void transformRect(RectF inRect, RectF outRect, Matrix transformMatrix) {
float[] inOutDecodeRect = createInOutDecodeRect(transformMatrix);
if(inOutDecodeRect == null) return;
writeInputRect(inOutDecodeRect, inRect);
transformPoints(inOutDecodeRect, transformMatrix);
readOutputRect(inOutDecodeRect, outRect);
}
// The group of functions below are used as building blocks of the transformRect() functions
// in order to not repeat the same exact code a lot of times.
private static void writeInputRect(float[] inOutDecodeRect, RectF inRect) {
inOutDecodeRect[0] = inRect.left;
inOutDecodeRect[1] = inRect.top;
inOutDecodeRect[2] = inRect.right;
inOutDecodeRect[3] = inRect.bottom;
}
private static void writeInputRect(float[] inOutDecodeRect, Rect inRect) {
inOutDecodeRect[0] = inRect.left;
inOutDecodeRect[1] = inRect.top;
inOutDecodeRect[2] = inRect.right;
inOutDecodeRect[3] = inRect.bottom;
}
private static void readOutputRect(float[] inOutDecodeRect, RectF outRect) {
outRect.left = inOutDecodeRect[4];
outRect.top = inOutDecodeRect[5];
outRect.right = inOutDecodeRect[6];
outRect.bottom = inOutDecodeRect[7];
}
private static void readOutputRect(float[] inOutDecodeRect, Rect outRect) {
outRect.left = (int)inOutDecodeRect[4];
outRect.top = (int)inOutDecodeRect[5];
outRect.right = (int)inOutDecodeRect[6];
outRect.bottom = (int)inOutDecodeRect[7];
}
private static float[] createInOutDecodeRect(Matrix transformMatrix) {
if(transformMatrix.isIdentity()) return null;
// We need an array of 8 floats because each point is two floats,
// we need to transform two points and we need to have a separated input and output
return new float[8];
}
private static void transformPoints(float[] inOutDecodeRect, Matrix transformMatrix) {
transformMatrix.mapPoints(inOutDecodeRect, 4, inOutDecodeRect, 0, 2);
}
/**
* Invert the source matrix, and write the result into the destination matrix.
* Android's integrated Matrix.invert() has some unexpected conditions when the matrix
* can't be inverted, and in that case the method inverts the matrix by hand.
* @param source Source matrix
* @param destination The inverse of the source matrix
* @throws IllegalArgumentException when the matrix is not invertible
*/
public static void inverse(Matrix source, Matrix destination) throws IllegalArgumentException {
if(source.invert(destination)) return;
float[] matrix = new float[9];
source.getValues(matrix);
inverseMatrix(matrix);
destination.setValues(matrix);
}
// This was made by ChatGPT and i have no clue what's happening here, but it works so eh
private static void inverseMatrix(float[] matrix) {
float determinant = matrix[0] * (matrix[4] * matrix[8] - matrix[5] * matrix[7])
- matrix[1] * (matrix[3] * matrix[8] - matrix[5] * matrix[6])
+ matrix[2] * (matrix[3] * matrix[7] - matrix[4] * matrix[6]);
if (determinant == 0) {
throw new IllegalArgumentException("Matrix is not invertible");
}
float invDet = 1 / determinant;
float temp0 = (matrix[4] * matrix[8] - matrix[5] * matrix[7]);
float temp1 = (matrix[2] * matrix[7] - matrix[1] * matrix[8]);
float temp2 = (matrix[1] * matrix[5] - matrix[2] * matrix[4]);
float temp3 = (matrix[5] * matrix[6] - matrix[3] * matrix[8]);
float temp4 = (matrix[0] * matrix[8] - matrix[2] * matrix[6]);
float temp5 = (matrix[2] * matrix[3] - matrix[0] * matrix[5]);
float temp6 = (matrix[3] * matrix[7] - matrix[4] * matrix[6]);
float temp7 = (matrix[1] * matrix[6] - matrix[0] * matrix[7]);
float temp8 = (matrix[0] * matrix[4] - matrix[1] * matrix[3]);
matrix[0] = temp0 * invDet;
matrix[1] = temp1 * invDet;
matrix[2] = temp2 * invDet;
matrix[3] = temp3 * invDet;
matrix[4] = temp4 * invDet;
matrix[5] = temp5 * invDet;
matrix[6] = temp6 * invDet;
matrix[7] = temp7 * invDet;
matrix[8] = temp8 * invDet;
}
}