最近项目中需要做图像边缘识别矫正的功能,需要用到手动做细节调整,大概长成这个样子。
整个相机预览加识别过程涉及到一些识别算法和模型就不上整个链路了,把裁剪view单拿出来记录一下,大部分参考了SmartCropper这个项目,自己加入了局部放大的功能来解决手指调节边缘时看不准的尴尬局面
public class CropImageView extends AppCompatImageView {
private float density;
/**
* 画点的
*/
private Paint mPointPaint;
/**
* 画边框的
*/
private Paint mLinePaint;
/**
* 画遮罩的
*/
private Paint mMaskPaint;
/**
* 画提示线的
*/
private Paint mGuideLinePaint;
/**
* 画放大镜的
*/
private Paint enlargePaint = new Paint();
/**
* 画准星的
*/
private Paint mBeadPaint;
/**
* 四个顶点
*/
ArrayList<Point> mCropPoints;
/**
* 拖拽后的点
*/
private Point mDraggingPoint = null;
/**
* 遮罩透明度
*/
int mMaskAlpha = 86;
/**
* 是否显示提示线
*/
boolean mShowGuideLine = true;
/**
* 边框线和顶点的颜色
*/
int mColor = 0xFF1AAF54;
/**
* 画笔宽度
*/
int mLineWidth = 1;
/**
* 矩阵变换
*/
private float[] matrixValue = new float[9];
/**
* 缩放比例
*/
private float scaleX, scaleY;
/**
* 图片的位置
*/
private int actW, actH, actLeft, actTop;
private Xfermode maskXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
/**
* 四顶点路径
*/
private Path mPointLinePath = new Path();
/**
* 顶点半径
*/
private static final int POINT_RADIUS = 8;
/**
* 捕捉距离
*/
private static final int TOUCH_POINT_CATCH_DISTANCE = 20;
/**
* 原图截取圆的半径
*/
private int srcRadius = 50;
/**
* 放大倍数
*/
private float enlargeMultiple = 3f;
/**
* 放大镜bitmap
*/
private Bitmap enlargeBitmap;
/**
* 绘制放大镜的位置
*/
private int offsetLeft = 0;
private int offsetTop = 0;
public CropImageView(Context context) {
this(context, null);
}
public CropImageView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CropImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ScaleType scaleType = getScaleType();
if (scaleType == ScaleType.FIT_END || scaleType == ScaleType.FIT_START || scaleType == ScaleType.MATRIX) {
throw new RuntimeException("Image in CropImageView must be in center");
}
density = getResources().getDisplayMetrics().density;
initPaints();
}
private void initPaints() {
mPointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLinePaint.setColor(mColor);
mLinePaint.setStrokeWidth(dp2px(mLineWidth));
mLinePaint.setStyle(Paint.Style.STROKE);
mMaskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mMaskPaint.setColor(Color.BLACK);
mMaskPaint.setStyle(Paint.Style.FILL);
mGuideLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mGuideLinePaint.setColor(Color.WHITE);
mGuideLinePaint.setStyle(Paint.Style.FILL);
mGuideLinePaint.setStrokeWidth(dp2px(mLineWidth));
mBeadPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBeadPaint.setColor(getResources().getColor(R.color.colorPrimaryGreen));
mBeadPaint.setStyle(Paint.Style.STROKE);
mBeadPaint.setStrokeWidth(dp2px(2));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
getDrawablePosition();
onDrawCropPoint(canvas);
if (mDraggingPoint != null) {
onDrawMagnifier(canvas);
}
}
/**
* 得到图片的位置
*/
private void getDrawablePosition() {
Drawable drawable = getDrawable();
if (drawable != null) {
//获取矩阵参数
getImageMatrix().getValues(matrixValue);
//现在的图相对于原图的缩放值
scaleX = matrixValue[Matrix.MSCALE_X];
scaleY = matrixValue[Matrix.MSCALE_Y];
//得到图片原大小
int origW = drawable.getIntrinsicWidth();
int origH = drawable.getIntrinsicHeight();
actW = Math.round(origW * scaleX);
actH = Math.round(origH * scaleY);
actLeft = (getWidth() - actW) / 2;
actTop = (getHeight() - actH) / 2;
}
}
/**
* 绘制裁剪相关
*
* @param canvas
*/
protected void onDrawCropPoint(Canvas canvas) {
onDrawMask(canvas);
onDrawGuideLine(canvas);
onDrawLines(canvas);
onDrawPoints(canvas);
}
protected void onDrawMagnifier(Canvas canvas) {
canvas.drawBitmap(enlargeBitmap,offsetLeft,offsetTop, enlargePaint);
float enlargeRadius = srcRadius * enlargeMultiple;
enlargePaint.setStyle(Paint.Style.STROKE);
enlargePaint.setStrokeWidth(dp2px(2));
enlargePaint.setColor(Color.BLACK);
canvas.drawCircle(offsetLeft+enlargeRadius,offsetTop+enlargeRadius,enlargeRadius,enlargePaint);
//画准星
float centerX = offsetLeft+enlargeRadius;
float centerY = offsetTop+enlargeRadius;
int length = 20;
canvas.drawLine(centerX-length,centerY,centerX+length,centerY,mBeadPaint);
canvas.drawLine(centerX,centerY-length,centerX,centerY+length,mBeadPaint);
}
protected void onDrawMask(Canvas canvas) {
if (mMaskAlpha <= 0) {
return;
}
Path path = resetPointPath();
if (path != null) {
//新建一层绘制
int sc = canvas.saveLayer(actLeft, actTop, actLeft + actW, actTop + actH, mMaskPaint, Canvas.ALL_SAVE_FLAG);
mMaskPaint.setAlpha(mMaskAlpha);
canvas.drawRect(actLeft, actTop, actLeft + actW, actTop + actH, mMaskPaint);
mMaskPaint.setXfermode(maskXfermode);
mMaskPaint.setAlpha(255);
canvas.drawPath(path, mMaskPaint);
mMaskPaint.setXfermode(null);
canvas.restoreToCount(sc);
}
}
/**
* 绘制提示线
*
* @param canvas
*/
protected void onDrawGuideLine(Canvas canvas) {
if (!mShowGuideLine) {
return;
}
int widthStep = actW / 3;
int heightStep = actH / 3;
canvas.drawLine(actLeft + widthStep, actTop, actLeft + widthStep, actTop + actH, mGuideLinePaint);
canvas.drawLine(actLeft + widthStep * 2, actTop, actLeft + widthStep * 2, actTop + actH, mGuideLinePaint);
canvas.drawLine(actLeft, actTop + heightStep, actLeft + actW, actTop + heightStep, mGuideLinePaint);
canvas.drawLine(actLeft, actTop + heightStep * 2, actLeft + actW, actTop + heightStep * 2, mGuideLinePaint);
}
/**
* 绘制四顶点
*
* @param canvas
*/
protected void onDrawPoints(Canvas canvas) {
if (mCropPoints == null) {
return;
}
for (Point point : mCropPoints) {
mPointPaint.setColor(Color.WHITE);
mPointPaint.setStyle(Paint.Style.FILL);
mPointPaint.setAlpha(200);
canvas.drawCircle(getViewPointX(point), getViewPointY(point), dp2px(POINT_RADIUS), mPointPaint);
mPointPaint.setColor(mColor);
mPointPaint.setStrokeWidth(dp2px(mLineWidth));
mPointPaint.setAlpha(255);
mPointPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(getViewPointX(point), getViewPointY(point), dp2px(POINT_RADIUS), mPointPaint);
}
}
/**
* 绘制边框路径
*
* @param canvas
*/
protected void onDrawLines(Canvas canvas) {
Path path = resetPointPath();
if (path != null) {
canvas.drawPath(path, mLinePaint);
}
}
/**
* 重置缩放后边线路径
*
* @return
*/
private Path resetPointPath() {
if (mCropPoints == null || mCropPoints.size() != 4) {
return null;
}
mPointLinePath.reset();
Point lt = mCropPoints.get(0);
Point rt = mCropPoints.get(1);
Point rb = mCropPoints.get(2);
Point lb = mCropPoints.get(3);
mPointLinePath.moveTo(getViewPointX(lt), getViewPointY(lt));
mPointLinePath.lineTo(getViewPointX(rt), getViewPointY(rt));
mPointLinePath.lineTo(getViewPointX(rb), getViewPointY(rb));
mPointLinePath.lineTo(getViewPointX(lb), getViewPointY(lb));
mPointLinePath.close();
return mPointLinePath;
}
/**
* 返回顶点
*
* @return
*/
public ArrayList<Point> getCropPoints() {
return mCropPoints;
}
/**
* 设置顶点
*
* @param cropPoints
*/
public void setCropPoints(ArrayList<Point> cropPoints) {
this.mCropPoints = cropPoints;
invalidate();
}
/**
* 设置画笔颜色
*
* @param lineColor
*/
public void setColor(int lineColor) {
this.mColor = lineColor;
invalidate();
}
/**
* 设置边框线宽度
*
* @param lineWidth
*/
public void setLineWidth(int lineWidth) {
this.mLineWidth = lineWidth;
invalidate();
}
public void setMaskAlpha(int mMaskAlpha) {
mMaskAlpha = Math.min(Math.max(0, mMaskAlpha), 255);
this.mMaskAlpha = mMaskAlpha;
invalidate();
}
public void setShowGuideLine(boolean showGuideLine) {
this.mShowGuideLine = showGuideLine;
invalidate();
}
/**
* 获取图片的bitmap对象
*
* @return
*/
public Bitmap getBitmap() {
Bitmap bmp = null;
Drawable drawable = getDrawable();
if (drawable instanceof BitmapDrawable) {
bmp = ((BitmapDrawable) drawable).getBitmap();
}
return bmp;
}
/**
* 相对于CropImageView坐标系的x坐标
* @param point
* @return
*/
private int getViewPointX(Point point) {
return (int) (point.x * scaleX + actLeft);
}
/**
* 相对于CropImageView坐标系的y坐标
* @param point
* @return
*/
private int getViewPointY(Point point) {
return (int) (point.y * scaleY + actTop);
}
private int dp2px(float dp) {
return (int) (dp * density + 0.5f);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
boolean handle = true;
switch (action) {
case MotionEvent.ACTION_DOWN:
mDraggingPoint = getNearbyPoint(event);
if (mDraggingPoint == null) {
handle = false;
}
break;
case MotionEvent.ACTION_MOVE:
toImagePointSize(mDraggingPoint, event);
resetEnlarge(event);
invalidate();
break;
case MotionEvent.ACTION_UP:
mDraggingPoint = null;
enlargeBitmap = null;
invalidate();
break;
}
return handle || super.onTouchEvent(event);
}
/**
* 根据出点来判断要移动哪个顶点
*
* @param event
* @return
*/
private Point getNearbyPoint(MotionEvent event) {
if (mCropPoints == null || mCropPoints.size() == 0) {
return null;
}
float x = event.getX();
float y = event.getY();
for (Point p : mCropPoints) {
int px = getViewPointX(p);
int py = getViewPointY(p);
double distance = Math.sqrt(Math.pow(x - px, 2) + Math.pow(y - py, 2));
if (distance < dp2px(TOUCH_POINT_CATCH_DISTANCE)) {
return p;
}
}
return null;
}
/**
* 转换成对应图片坐标系的位置
* @param dragPoint
* @param event
*/
private void toImagePointSize(Point dragPoint, MotionEvent event) {
if (dragPoint == null) {
return;
}
float x = Math.min(Math.max(event.getX(), actLeft), actLeft + actW);
float y = Math.min(Math.max(event.getY(), actTop), actTop + actH);
dragPoint.x = (int) ((x - actLeft) / scaleX);
dragPoint.y = (int) ((y - actTop) / scaleY);
}
/**
* 重置放大区域
* @param event
*/
private void resetEnlarge(MotionEvent event) {
int x = (int)event.getX();
int y = (int)event.getY();
Point point = new Point();
point.x = x - actLeft;
point.y = y - actTop;
enlargeBitmap = getCroppedBitmap(point);
Matrix matrix = new Matrix();
matrix.postScale(enlargeMultiple,enlargeMultiple); //长和宽放大缩小的比例
enlargeBitmap = Bitmap.createBitmap(enlargeBitmap,0,0,enlargeBitmap.getWidth(),enlargeBitmap.getHeight(),matrix,true);
}
/**
* 裁剪出需要放大的范围
* @param point
* @return
*/
public Bitmap getCroppedBitmap(Point point) {
Bitmap scaleBitmap = Bitmap.createScaledBitmap(getBitmap(), actW, actH, false);
Bitmap output = Bitmap.createBitmap(srcRadius *2,
srcRadius *2, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
final int color = 0xff424242;
final Paint paint = new Paint();
final Rect src = new Rect(point.x- srcRadius, point.y- srcRadius, point.x+ srcRadius, point.y+ srcRadius);
paint.setAntiAlias(true);
canvas.drawARGB(0, 0, 0, 0);
paint.setColor(color);
canvas.drawCircle(srcRadius, srcRadius,
srcRadius, paint);
Rect dst = new Rect(0,0, srcRadius *2, srcRadius *2);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(scaleBitmap, src, dst, paint);
return output;
}
}
在此主要说下局部放大的思路(代码中的最后一个方法),我们需要以手指触碰屏幕的坐标为圆心,画出一个一定半径(自己掌握)的圆,把圆范围内的图片内容截取出来,再将这部分像素进行一定倍数的放大
首先需要新建一个bitmap对象来放裁剪下的内容,大小就是我们要裁剪的原型的外接正方形
Bitmap output = Bitmap.createBitmap(srcRadius *2,
srcRadius *2, Bitmap.Config.ARGB_8888);
根据这个bitmap新建画布,以及设置画笔,并将这个圆绘制到画布上,需要注意的是我们把这个圆画在了画布的左上角,距离画布的left和top没有任何偏移量
Canvas canvas = new Canvas(output);
final int color = 0xff424242;
final Paint paint = new Paint();
paint.setAntiAlias(true);
canvas.drawARGB(0, 0, 0, 0);
paint.setColor(color);
canvas.drawCircle(srcRadius, srcRadius,
srcRadius, paint);
随后开始在原bitmap上截取我们所需要的矩形区域,需要用到canvas的这个方法
public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst,
@Nullable Paint paint) {
super.drawBitmap(bitmap, src, dst, paint);
}
其中:
bitmap:为原图
src:为你要裁剪的矩形区域
dst:为你将src裁剪出来要放到的矩形区域(这里我们将dst的位置也放到了左上角且left和top没有任何偏移量,为的就是能让这矩形和上面的圆能够完全重合,否则裁剪不完整)
paint:为画笔
final Rect src = new Rect(point.x- srcRadius, point.y- srcRadius, point.x+ srcRadius, point.y+ srcRadius);
Rect dst = new Rect(0,0, srcRadius *2, srcRadius *2);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(scaleBitmap, src, dst, paint);
在这个里给画笔设置图层间的混合模式为 PorterDuff.Mode.SRC_IN,即取重合的部分
到此,我们就剪切出了我们想要的bitmap,接下来在将这个bitmap放大
enlargeBitmap = getCroppedBitmap(point);
Matrix matrix = new Matrix();
matrix.postScale(enlargeMultiple,enlargeMultiple); //长和宽放大缩小的比例
enlargeBitmap = Bitmap.createBitmap(enlargeBitmap,0,0,enlargeBitmap.getWidth(),enlargeBitmap.getHeight(),matrix,true);
最后在把放大后的bitmap绘制到你想要的位置就可以了