makcar/app/src/main/java/com/aros/apron/tools/ApronArucoDetect.java

494 lines
19 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.aros.apron.tools;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.aros.apron.constant.AMSConfig;
import com.aros.apron.entity.ArucoMarker;
import com.aros.apron.entity.ArucoMarkerDimensions;
import com.aros.apron.entity.Movement;
import com.aros.apron.manager.AlternateLandingManager;
import org.opencv.calib3d.Calib3d;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfInt;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.objdetect.ArucoDetector;
import org.opencv.objdetect.DetectorParameters;
import org.opencv.objdetect.Dictionary;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ApronArucoDetect {
//是否触发识别(如果丢失图传此值为false)
private boolean isTriggerSuccess;
//没识别到二维码
private boolean arucoNotFoundTag=false;
private boolean isStartAruco = false;
// public ExecutorService mThreadPool = Executors.newSingleThreadExecutor();
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Future<?> lastFuture = null;
private String TAG = getClass().getSimpleName();
Double resultYaw = 0.0;
//触发去备降点
private boolean triggerToAlternateLandingPoint;
long startTime;
long endTime;
private int sigleMarkerDetectFailsTimes;
//复降触发条件
private boolean dropTimesTag;
//复降次数
private int dropTimes=0;
//是否双挂
private boolean isDoublePayload;
// 【关键修改1】M400下视镜头右后方补偿量像素正值向右补偿
// 如果还往左偏就调大,往右偏就调小,建议范围 30-80
private static final double LENS_OFFSET_X = 15.0;
private static final double LENS_OFFSET_Y = 20.0;
// 【关键修改2】放宽居中判定阈值原40补偿后改为60更合理
private static final int CENTER_ERR_MAX = 40; // 居中误差px
public PIDControl pidControlX = null;
public PIDControl pidControlY = null;
//为了解决降落后不停浆问题增加记录开启速降时startFastStick之后的过滤次数
public int checkThrowingErrorsTimes;
public int getCheckThrowingErrorsTimes() {
return checkThrowingErrorsTimes;
}
public void setCheckThrowingErrorsTimes(int checkThrowingErrorsTimes) {
this.checkThrowingErrorsTimes = checkThrowingErrorsTimes;
}
public boolean isTriggerSuccess() {
return isTriggerSuccess;
}
public void setTriggerSuccess(boolean triggerSuccess) {
isTriggerSuccess = triggerSuccess;
}
public boolean isDoublePayload() {
return isDoublePayload;
}
public void setDoublePayload(boolean doublePayload) {
isDoublePayload = doublePayload;
}
private ApronArucoDetect() {
// 构造函数里也可以调用,但静态代码块更可靠
}
private static class OpenCVHelperHolder {
private static final ApronArucoDetect INSTANCE = new ApronArucoDetect();
static {
INSTANCE.init();
}
}
public static ApronArucoDetect getInstance() {
return OpenCVHelperHolder.INSTANCE;
}
private static final float SLOW_LAND_SPEED = -0.3f; // 远场慢降
private static final float SLOW_SUPER_SPEED = -0.1f; // 超近慢降
private static final int PIXEL_TRIGGER = 400; // 近场像素阈值
public void init() {
pidControlX = new PIDControl(0.6f, 0.015f, 0.12f, 0.05f, 2.0f, 0.05f);
pidControlY = new PIDControl(0.6f, 0.015f, 0.12f, 0.05f, 2.0f, 0.05f);
pidControlX.reset();
pidControlY.reset();
}
public void detectArucoTags(int height, int width, byte[] data, Dictionary dictionary) {
//这里说明图传正常
isTriggerSuccess = true;
//0未启用 虚拟摇杆启用原因 1异常拉高返航触发 2视觉降落触发 3手动触发
Movement.getInstance().setVirtualStickEnableReason(2);
//保证一帧进入同时 保证只有一次速降
if (isStartAruco || startFastStick) {
// LogUtil.log(TAG, "过滤:" + isStartAruco + startFastStick);
if (!isStartAruco && startFastStick) {
checkThrowingErrorsTimes++;
}
return;
}
isStartAruco = true;
/* 如果上一帧任务还没跑完,直接 cancel */
if (lastFuture != null && !lastFuture.isDone()) {
LogUtil.log(TAG, "break---");
lastFuture.cancel(true);
}
//LogUtil.log(TAG, "执行了");
lastFuture = executor.schedule(new Runnable() {
@Override
public void run() {
try {
/* ---------- 1. YUV → BGR → Gray ---------- */
Mat yuvMat = new Mat(height + height / 2, width, CvType.CV_8UC1);
yuvMat.put(0, 0, data);
Mat rgbMat = new Mat();
Imgproc.cvtColor(yuvMat, rgbMat, Imgproc.COLOR_YUV2BGR_I420);
// 灰度
Mat grayImgMat = new Mat();
Imgproc.cvtColor(rgbMat, grayImgMat, Imgproc.COLOR_BGR2GRAY);
/* 【OpenCV 4.9+】温和预处理(应对飞行模糊,提升识别率) */
Mat processedMat = fixedPreprocess(grayImgMat);
/* 2. 检测二维码OpenCV 4.12+ */
MatOfInt ids = new MatOfInt();
List<Mat> corners = new ArrayList<>();
Mat corner6 = new Mat();
// 【OpenCV 4.12+】创建 ArucoDetector
DetectorParameters parameters = new DetectorParameters();
ArucoDetector detector = new ArucoDetector(dictionary, parameters);
// 先用预处理图像检测
detector.detectMarkers(processedMat, corners, ids);
// 如果预处理检测不到,回退到原图
if (ids.empty()) {
corners.clear();
detector.detectMarkers(grayImgMat, corners, ids);
}
//b保留6号
ids = keepOnly6(ids, corners);
boolean marker6Found = false;
if (!ids.empty()) {
arucoNotFoundTag = false;
int[] idArray = ids.toArray();
int ultrasonicHeight = Movement.getInstance().getUltrasonicHeight();
// double flyingHeight = Movement.getInstance().getElevation();
// 如果只有一个而且是6
if (idArray.length == 1 && idArray[0] == 6) {
marker6Found = true;
corner6 = corners.get(0);
Point[] points = new Point[4];
for (int i = 0; i < 4; i++) {
double[] p = corner6.get(0, i);
points[i] = new Point(p[0], p[1]);
}
/* 像素宽:相邻两角点像素距离 */
double pixelWidth = calculateDistance(points[0], points[1]);
// 【关键修改3】降落判断加上镜头偏移补偿向右补偿
double centerX = (points[0].x + points[1].x + points[2].x + points[3].x) / 4.0;
double errX = Math.abs(centerX - rgbMat.width() / 2.0 + LENS_OFFSET_X);
// 修改为(向前补偿):
double centerY = (points[0].y + points[1].y + points[2].y + points[3].y) / 4.0;
double errY = Math.abs(centerY - rgbMat.height() / 2.0 - LENS_OFFSET_Y); ;
LogUtil.log(TAG, "像素" + (int) pixelWidth +
" errX=" + (int) errX + " errY=" + (int) errY + " (含偏移" + LENS_OFFSET_X + ")");
/* 近场 + 对准 + 像素够 → 立即速降 */
if (!startFastStick &&
pixelWidth >= PIXEL_TRIGGER &&
// flyingHeight <= 1 &&
ultrasonicHeight <= 4 &&
errX < CENTER_ERR_MAX &&
errY < CENTER_ERR_MAX) {
startFastStick = true;
handler.post(runnable);
LogUtil.log(TAG, "6号居中+近场触发速降 pixel=" + (int) pixelWidth +
" errX=" + (int) errX + " errY=" + (int) errY);
return;
}
}
if(marker6Found){
//执行位移修正
moveOnArucoDetected(new ArucoMarker(1, corner6, 0.24f), rgbMat.width(), rgbMat.height());
}
dropTimesTag = true;
} else {
LogUtil.log(TAG, "找不到了二维码");
if (!arucoNotFoundTag) {
startTime = System.currentTimeMillis();
arucoNotFoundTag = true;
}
endTime = System.currentTimeMillis();
long lostDuration = endTime - startTime;
//1s到8s内
if (lostDuration > 700 && lostDuration <= 8000) {
if (Movement.getInstance().getUltrasonicHeight()<=30) {
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, 0.3f);
if (dropTimes > Integer.parseInt(AMSConfig.getInstance().getAlternateLandingTimes())) {
LogUtil.log(TAG, "超过复降限制,去备降点");
AlternateLandingManager.getInstance().startTaskProcess(null);
return;
}
if (dropTimesTag) {
dropTimesTag = false;
dropTimes++;
LogUtil.log(TAG, "复降第:" + dropTimes + "");
}
} else {
LogUtil.log(TAG, "执行位移");
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -0.3f);
}
// 超过8s
} else if (lostDuration > 8000) {
LogUtil.log(TAG, "判定未识别到二维码,飞往备降点");
AlternateLandingManager.getInstance().startTaskProcess(null);
}
}
// 释放资源(安全版)
processedMat.release();
grayImgMat.release();
rgbMat.release();
yuvMat.release();
ids.release();
// 安全释放角点(避免空指针和重复释放)
if (!corners.isEmpty()) {
for (Mat c : corners) {
if (c != null) c.release();
}
}
isStartAruco = false;
} catch (Exception e) {
LogUtil.log(TAG, "Exception" + e); // 第一个日志
isStartAruco = false;
}
}
}, 0, TimeUnit.MILLISECONDS);
}
/**
* 只保留 id==6 的 marker
* 返回新的 MatOfInt旧对象已 releasecorners 列表同步
*/
private static MatOfInt keepOnly6(MatOfInt ids, List<Mat> corners) {
if (ids.empty()) return ids;
int[] idArr = ids.toArray();
List<Integer> keepIdx = new ArrayList<>();
for (int i = 0; i < idArr.length; i++) if (idArr[i] == 6) keepIdx.add(i);
// 1. 先把要保留的角点缓存到临时列表
List<Mat> tmpCorners = new ArrayList<>(keepIdx.size());
for (int i : keepIdx) tmpCorners.add(corners.get(i));
// 2. 再清空原列表
corners.clear();
// 3. 重建 ids 并回填 corners
ids.release();
int[] newIds = new int[keepIdx.size()];
for (int j = 0; j < keepIdx.size(); j++) newIds[j] = 6;
corners.addAll(tmpCorners); // 现在安全了
return new MatOfInt(newIds);
}
/**
* 【温和预处理】双边滤波 + CLAHE + 锐化(不做二值化)
*/
private Mat fixedPreprocess(Mat src) {
Mat result = new Mat();
try {
// 1. 双边滤波(去噪 + 保边)
Mat filtered = new Mat();
Imgproc.bilateralFilter(src, filtered, 9, 75, 75);
// 2. CLAHE对比度增强
Mat clahe = new Mat();
Imgproc.createCLAHE(4.0, new Size(8, 8)).apply(filtered, clahe);
filtered.release();
// 3. 锐化Unsharp Masking
Mat blurred = new Mat();
Imgproc.GaussianBlur(clahe, blurred, new Size(0, 0), 2.0);
Core.addWeighted(clahe, 1.5, blurred, -0.5, 0, result);
clahe.release();
blurred.release();
return result;
} catch (Exception e) {
LogUtil.log(TAG, "预处理失败: " + e.getMessage());
src.copyTo(result);
return result;
}
}
/**
* 61 * 计算两个点之间的欧几里得距离
* 62 * @param p1 第一个点
* 63 * @param p2 第二个点
* 64 * @return 两点之间的距离
* 65
*/
private double calculateDistance(Point p1, Point p2) {
double dx = p2.x - p1.x;
double dy = p2.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
}
public void setDetectedBigMarkers() {
startFastStick = false; // 停止速降
isStartAruco = false; // 允许下一帧再进检测
}
/**
* 只处理一个 ArucoMarker6 号码)
*/
private void moveOnArucoDetected(ArucoMarker marker, // ← 不再是 List
int imageWidth,
int imageHeight) {
/* 1. 取角点 */
Mat corner6 = marker.getConner(); // ← 直接拿,不用 get(0)
Point[] pts = new Point[4];
for (int i = 0; i < 4; i++) {
double[] p = corner6.get(0, i);
pts[i] = new Point(p[0], p[1]);
}
/* 2. 中心 + 误差(带镜头偏移补偿) */
double cx = (pts[0].x + pts[1].x + pts[2].x + pts[3].x) / 4.0;
double cy = (pts[0].y + pts[1].y + pts[2].y + pts[3].y) / 4.0;
Point screenCenter = new Point(imageWidth / 2.0, imageHeight / 2.0);
double errX = (cx - screenCenter.x) + LENS_OFFSET_X; // 向右补偿
double errY = (cy - screenCenter.y) - LENS_OFFSET_Y; // Y方向不变
/* 3. PID 微调水平 */
pidControlX.setInputFilterAll((float) (errX / 700.0));
pidControlY.setInputFilterAll((float) (-errY/ 700.0));
float vx = (float) Math.max(-0.25, Math.min(0.25, pidControlX.get_pid()));
float vy = (float) Math.max(-0.25, Math.min(0.25, pidControlY.get_pid()));
/* 4. 远场慢降:像素 < 1500 且 > 2 m */
double pixelWidth = Math.sqrt(Math.pow(pts[1].x - pts[0].x, 2) +
Math.pow(pts[1].y - pts[0].y, 2));
// double flyingHeight = Movement.getInstance().getElevation();
int currentHeight = Movement.getInstance().getUltrasonicHeight();
float vz;
if (currentHeight <= 4) {
vz = 0.0f; // 近场悬停,等速降
} else if (currentHeight <= 6) {
vz = SLOW_SUPER_SPEED; // -0.1f,过渡层
} else {
vz = SLOW_LAND_SPEED; // -0.3f,远场慢降
}
// if (flyingHeight <= 0.2) {
// vz = 0.0f;
// LogUtil.log(TAG, "相对高度" + flyingHeight + "强制悬停");
// }
/* 5. 输出 */
DroneHelper.getInstance().moveVxVyYawrateHeight(vx, vy, 0f, vz);
/* 6. 日志(加上偏移提示) */
LogUtil.log(TAG, "vx" + vx + "vy" + vy + "像素居中 errX=" + (int) errX + "(含偏移" + LENS_OFFSET_X + ") errY=" + (int) errY +
" pixelW=" + (int) pixelWidth + " vz=" + vz + "ultheight" + Movement.getInstance().getUltrasonicHeight());
}
public boolean startFastStick;
public boolean isStartFastStick() {
return startFastStick;
}
public void setStartFastStick(boolean startFastStick) {
this.startFastStick = startFastStick;
}
public boolean canLanding;
public boolean isCanLanding() {
return canLanding;
}
public void setCanLanding(boolean canLanding) {
//测试重置未识别和识别时间,避免刚触发识别就飞向备降点
startTime = 0;
endTime = 0;
this.canLanding = canLanding;
}
private int handlerCallbackCount = 0; // 记录回调次数
private Handler handler = new Handler(Looper.getMainLooper());
private Runnable runnable = new Runnable() {
@Override
public void run() {
performOperation();
if (handlerCallbackCount < 20) {
handler.postDelayed(this, 50); // 每 50 毫秒执行一次1 秒内执行 20 次
} else {
performNextStep();
}
}
};
private void performOperation() {
LogUtil.log(TAG, "快速下拉中..." + handlerCallbackCount);
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -4);
handlerCallbackCount++; // 增加计数器
}
private void performNextStep() {
canLanding = true;
handlerCallbackCount = 0;
dropTimes = 0;//手动测试避免多次累加后直接飞往备降点
LogUtil.log(TAG, "下拉完成:触发下一步自动降落");
handler.removeCallbacks(runnable); // 防止重复执行
}
}