package com.aros.apron.tools; import android.os.Handler; import android.os.Looper; import com.aros.apron.constant.AMSConfig; import com.aros.apron.entity.ArucoMarker; 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.imgproc.Imgproc; import org.opencv.objdetect.ArucoDetector; import org.opencv.objdetect.DetectorParameters; import org.opencv.objdetect.Dictionary; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import dji.sdk.keyvalue.key.FlightControllerKey; import dji.sdk.keyvalue.key.KeyTools; import dji.v5.common.callback.CommonCallbacks; import dji.v5.common.error.IDJIError; import dji.v5.manager.KeyManager; /** * 2帧一处理:0处理-1跳过-2处理-3跳过 * 先预处理检测,失败再回退原图检测 */ public class ApronArucoDetect { private static final String TAG = "ApronArucoDetect"; // ========== 原有参数 ========== private static double LENS_OFFSET_X = 0; private static double LENS_OFFSET_Y = 0; private static final int CENTER_ERR_MAX = 30; private static final float SLOW_LAND_SPEED = -0.2f; private static final float SLOW_SUPER_SPEED = -0.1f; private static final int PIXEL_TRIGGER = 360; private static final int TRIGGER_FRAME_THRESHOLD = 2; // ========== 原有状态 ========== private boolean isTriggerSuccess; private boolean arucoNotFoundTag = false; private boolean isStartAruco = false; public boolean isStartAruco() { return isStartAruco; } public void setStartAruco(boolean startAruco) { isStartAruco = startAruco; } ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); Future lastFuture = null; private String TAG_LOG = 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; private int trycount = 0; public PIDControl pidControlX = null; public PIDControl pidControlY = null; public int checkThrowingErrorsTimes; private int consecutiveTriggerCount = 0; // ========== 【新增】2帧一处理计数器 ========== private int frameCounter = 0; // ========== 原有方法 ========== public int getCheckThrowingErrorsTimes() { return checkThrowingErrorsTimes; } public void setCheckThrowingErrorsTimes(int v) { checkThrowingErrorsTimes = v; } public boolean isTriggerSuccess() { return isTriggerSuccess; } public void setTriggerSuccess(boolean v) { isTriggerSuccess = v; } public boolean isDoublePayload() { return isDoublePayload; } public void setDoublePayload(boolean v) { isDoublePayload = v; } public boolean isStartFastStick() { return startFastStick; } public void setStartFastStick(boolean v) { startFastStick = v; } public boolean isCanLanding() { return canLanding; } public void setCanLanding(boolean v) { startTime = 0; endTime = 0; canLanding = v; } private ApronArucoDetect() {} private static class OpenCVHelperHolder { private static final ApronArucoDetect INSTANCE = new ApronArucoDetect(); static { INSTANCE.init(); } } public static ApronArucoDetect getInstance() { return OpenCVHelperHolder.INSTANCE; } public void init() { pidControlX = new PIDControl(0.65f, 0.02f, 0.2f, 0.05f, 2.5f, 0.1f); pidControlY = new PIDControl(0.65f, 0.02f, 0.2f, 0.05f, 2.5f, 0.1f); pidControlX.reset(); pidControlY.reset(); } public boolean startFastStick; public boolean canLanding; // ========== 【核心修改】主入口:2帧一处理 + 预处理回退 ========== public void detectArucoTags(int height, int width, byte[] data, Dictionary dictionary) { isTriggerSuccess = true; Movement.getInstance().setVirtualStickEnableReason(2); // 原有过滤 if (isStartAruco || startFastStick) { LogUtil.log(TAG_LOG, "过滤:" + isStartAruco + startFastStick); if (!isStartAruco && startFastStick) { checkThrowingErrorsTimes++; } return; } // ========== 【关键】2帧一处理:0处理-1跳过-2处理-3跳过 ========== int currentFrame = frameCounter++; boolean shouldProcess = (currentFrame % 2 == 0); // 偶数帧处理,奇数帧跳过 if (!shouldProcess) { // 【关键】跳过的帧:啥也不干,直接return,让飞机自己飘 LogUtil.log(TAG_LOG, "【跳过帧】" + currentFrame + " 让飞机自稳"); return; } LogUtil.log(TAG_LOG, "【处理帧】" + currentFrame + " 执行修正"); isStartAruco = true; if (lastFuture != null && !lastFuture.isDone()) { LogUtil.log(TAG_LOG, "break---"); lastFuture.cancel(true); } // 动态偏移(原有) int ultHeight = Movement.getInstance().getUltrasonicHeight(); if (ultHeight >= 30) { LENS_OFFSET_X = 0; LENS_OFFSET_Y = 0; } else if (ultHeight >= 20) { LENS_OFFSET_X = 0; LENS_OFFSET_Y = 0; } else if (ultHeight >= 10) { LENS_OFFSET_X = 20; LENS_OFFSET_Y = 10; } else if (ultHeight >= 5) { LENS_OFFSET_X = 30; LENS_OFFSET_Y = 20; } else { LENS_OFFSET_X = 55; LENS_OFFSET_Y = 35; } // ========== 【核心逻辑】先预处理检测,失败回退原图 ========== lastFuture = executor.schedule(new Runnable() { @Override public void run() { try { 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); MatOfInt ids = new MatOfInt(); List corners = new ArrayList<>(); Mat corner6 = new Mat(); DetectorParameters parameters = new DetectorParameters(); ArucoDetector detector = new ArucoDetector(dictionary, parameters); // ========== 【关键修改】第1次:预处理图检测 ========== Mat processedMat = fixedPreprocess(grayImgMat); detector.detectMarkers(processedMat, corners, ids); LogUtil.log(TAG_LOG, "预处理图检测: " + (ids.empty() ? "未检出" : "成功,数量=" + corners.size())); // ========== 【关键修改】第2次:如果失败,回退原图检测 ========== if (ids.empty()) { LogUtil.log(TAG_LOG, "预处理失败,回退原图检测"); corners.clear(); // 清空之前的结果 detector.detectMarkers(grayImgMat, corners, ids); LogUtil.log(TAG_LOG, "原图检测: " + (ids.empty() ? "仍未检出" : "成功,数量=" + corners.size())); } // 释放预处理图(必须!避免内存泄漏) processedMat.release(); // 只保留6号码 ids = keepOnly6(ids, corners); boolean marker6Found = false; if (!ids.empty()) { trycount = 0; arucoNotFoundTag = false; int[] idArray = ids.toArray(); int ultrasonicHeight = Movement.getInstance().getUltrasonicHeight(); 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]); double centerX = (points[0].x + points[1].x + points[2].x + points[3].x) / 4.0 + LENS_OFFSET_X; double errX = Math.abs(centerX - rgbMat.width() / 2.0); double centerY = (points[0].y + points[1].y + points[2].y + points[3].y) / 4.0 + LENS_OFFSET_Y; double errY = Math.abs(centerY - rgbMat.height() / 2.0); LogUtil.log(TAG_LOG, "像素" + (int) pixelWidth + " errX=" + (int) errX + " errY=" + (int) errY + " (含偏移" + LENS_OFFSET_X + ")"); // 速降判断(原有) if (!startFastStick) { if (pixelWidth >= PIXEL_TRIGGER && ultrasonicHeight <= 4 && errX < CENTER_ERR_MAX && errY < CENTER_ERR_MAX) { consecutiveTriggerCount++; LogUtil.log(TAG_LOG, "速降条件满足,累计帧数: " + consecutiveTriggerCount); if (consecutiveTriggerCount >= TRIGGER_FRAME_THRESHOLD) { startFastStick = true; consecutiveTriggerCount = 0; LogUtil.log(TAG_LOG, "【触发速降】连续满足" + TRIGGER_FRAME_THRESHOLD + "帧,pixel=" + (int) pixelWidth + " errX=" + (int) errX); handler.postDelayed(new Runnable() { @Override public void run() { handler.post(runnable); LogUtil.log(TAG_LOG, "延迟结束,开始速降"); } }, 300); return; } } else { if (consecutiveTriggerCount > 0) { LogUtil.log(TAG_LOG, "速降条件中断,重置累计帧数(原因:像素=" + (int)pixelWidth + " 超声=" + ultrasonicHeight + " errX=" + (int)errX + " errY=" + (int)errY + ")"); consecutiveTriggerCount = 0; } } } } // 原有修正逻辑 if (marker6Found) { moveOnArucoDetected(new ArucoMarker(1, corner6, 0.24f), rgbMat.width(), rgbMat.height()); } dropTimesTag = true; } else { // 原有丢失处理 LogUtil.log(TAG_LOG, "找不到了二维码"); if (!arucoNotFoundTag) { startTime = System.currentTimeMillis(); arucoNotFoundTag = true; } endTime = System.currentTimeMillis(); long lostDuration = endTime - startTime; if (lostDuration > 1000 && lostDuration <= 12000) { if (Movement.getInstance().getUltrasonicHeight() <= 20) { DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, 3f); if (dropTimes > Integer.parseInt(AMSConfig.getInstance().getAlternateLandingTimes())) { LogUtil.log(TAG_LOG, "超过复降限制,去备降点"); AlternateLandingManager.getInstance().startTaskProcess(null); Movement.getInstance().setAlternate(true); return; } if (dropTimesTag) { dropTimesTag = false; dropTimes++; LogUtil.log(TAG_LOG, "复降第:" + dropTimes + "次"); } } else { LogUtil.log(TAG_LOG, "执行位移"); DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -0.3f); } } else if (lostDuration > 8000) { LogUtil.log(TAG_LOG, "判定未识别到二维码,飞往备降点"); AlternateLandingManager.getInstance().startTaskProcess(null); Movement.getInstance().setAlternate(true); } } // 释放资源 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_LOG, "Exception" + e); isStartAruco = false; } } }, 0, TimeUnit.MILLISECONDS); } // ========== 以下全部原有方法,一点不改 ========== private static MatOfInt keepOnly6(MatOfInt ids, List corners) { if (ids.empty()) return ids; int[] idArr = ids.toArray(); List keepIdx = new ArrayList<>(); for (int i = 0; i < idArr.length; i++) if (idArr[i] == 6) keepIdx.add(i); List tmpCorners = new ArrayList<>(keepIdx.size()); for (int i : keepIdx) tmpCorners.add(corners.get(i)); corners.clear(); 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); } private Mat fixedPreprocess(Mat src) { Mat result = new Mat(); try { // 【新增】判断亮度,区分白天/夜景 Scalar meanValue = Core.mean(src); double meanBrightness = meanValue.val[0]; if (meanBrightness < 80) { // 夜景模式(FPV晚上通常<80) LogUtil.log(TAG_LOG, "【夜景预处理】亮度=" + (int)meanBrightness); // Step 1: 中值滤波(去椒盐噪点,比双边滤波更适合夜景) Mat denoised = new Mat(); Imgproc.medianBlur(src, denoised, 5); // 5x5,去噪且保边 // Step 2: 直方图均衡化(全局对比度拉伸,把黑灰白拉开) Mat equalized = new Mat(); Imgproc.equalizeHist(denoised, equalized); // Step 3: 轻度CLAHE(局部微调,避免过曝) Mat clahe = new Mat(); Imgproc.createCLAHE(2.0, new Size(8, 8)).apply(equalized, clahe); // 2.0比白天4.0保守 // Step 4: 夜景下降低锐化强度(避免放大残余噪点) Mat blurred = new Mat(); Imgproc.GaussianBlur(clahe, blurred, new Size(0, 0), 2.0); Mat detail = new Mat(); Core.subtract(clahe, blurred, detail); double amount = 0.6; // 【关键】夜景降到0.6,白天1.2会放大噪点 Core.multiply(detail, new Scalar(amount), detail); Core.add(clahe, detail, result); // 限制范围 Core.min(result, new Scalar(255), result); Core.max(result, new Scalar(0), result); // 释放 denoised.release(); equalized.release(); blurred.release(); detail.release(); clahe.release(); LogUtil.log(TAG_LOG, "夜景处理完成:中值滤波+均衡化+弱锐化(amount=" + amount + ")"); return result; } // 【原逻辑】白天模式(亮度>=80) LogUtil.log(TAG_LOG, "【白天预处理】亮度=" + (int)meanBrightness); // Step 1: 双边滤波(保边缘降噪) Mat filtered = new Mat(); Imgproc.bilateralFilter(src, filtered, 9, 75, 75); // Step 2: CLAHE 局部对比度增强 Mat clahe = new Mat(); Imgproc.createCLAHE(4.0, new Size(8, 8)).apply(filtered, clahe); filtered.release(); // Step 3: Unsharp Mask(反锐化掩膜) Mat blurred = new Mat(); Imgproc.GaussianBlur(clahe, blurred, new Size(0, 0), 2.0); Mat detail = new Mat(); Core.subtract(clahe, blurred, detail); double amount = 1.2; // 白天可以用1.2 Core.multiply(detail, new Scalar(amount), detail); Core.add(clahe, detail, result); Core.min(result, new Scalar(255), result); Core.max(result, new Scalar(0), result); blurred.release(); detail.release(); clahe.release(); LogUtil.log(TAG_LOG, "白天处理完成:双边滤波+CLAHE+UnsharpMask(amount=" + amount + ")"); return result; } catch (Exception e) { LogUtil.log(TAG_LOG, "预处理失败: " + e.getMessage()); src.copyTo(result); return result; } } 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; consecutiveTriggerCount = 0; frameCounter = 0; // 【新增】重置帧计数 } private double calculateYawErrorFromCorners(Point[] pts) { double dxTop = pts[1].x - pts[0].x; double dyTop = pts[1].y - pts[0].y; double angleTop = Math.toDegrees(Math.atan2(dyTop, dxTop)); double dxBottom = pts[2].x - pts[3].x; double dyBottom = pts[2].y - pts[3].y; double angleBottom = Math.toDegrees(Math.atan2(dyBottom, dxBottom)); double yawError = (angleTop + angleBottom) / 2.0; while (yawError > 180) yawError -= 360; while (yawError < -180) yawError += 360; return yawError; } private void moveOnArucoDetected(ArucoMarker marker, int imageWidth, int imageHeight) { Mat corner6 = marker.getConner(); 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]); } double cx = (pts[0].x + pts[1].x + pts[2].x + pts[3].x) / 4.0 + LENS_OFFSET_X; double cy = (pts[0].y + pts[1].y + pts[2].y + pts[3].y) / 4.0 + LENS_OFFSET_Y; Point screenCenter = new Point(imageWidth / 2.0, imageHeight / 2.0); double errX = (cx - screenCenter.x); double errY = (cy - screenCenter.y); int currentHeight = Movement.getInstance().getUltrasonicHeight(); double scaleFactor; if (currentHeight >= 50) { scaleFactor = 300.0; // 50m: 像素3, 大力修正 } else if (currentHeight >= 30) { scaleFactor = 500.0; // 30m: 像素5 } else if (currentHeight >= 20) { scaleFactor = 600.0; // 20m: 像素8 } else if (currentHeight >= 10) { scaleFactor = 700.0; // 10m: 像素16 } else { scaleFactor = 900.0; // 1m: 像素159, 温柔修正 } pidControlX.setInputFilterAll((float) (errX / scaleFactor)); pidControlY.setInputFilterAll((float) (-errY / scaleFactor)); float rawVx = pidControlX.get_pid(); float rawVy = pidControlY.get_pid(); float yawRate = 0f; float vx = (float) Math.max(-0.2, Math.min(0.2, rawVx)); float vy = (float) Math.max(-0.2, Math.min(0.2, rawVy)); double pixelWidth = Math.sqrt(Math.pow(pts[1].x - pts[0].x, 2) + Math.pow(pts[1].y - pts[0].y, 2)); if (currentHeight >= 10 && currentHeight <= 55) { double yawError = calculateYawErrorFromCorners(pts); double absError = Math.abs(yawError); if (absError > 10.0) { float targetRate; if (absError > 100.0) targetRate = 50.0f; else if (absError > 50.0) targetRate = 30.0f; else if (absError > 30.0) targetRate = 20.0f; else targetRate = 10.0f; yawRate = yawError > 0 ? targetRate : -targetRate; String speedLabel = absError > 100 ? "高速50" : absError > 50 ? "中速30" : absError > 30 ? "低速20" : "微调10"; LogUtil.log(TAG_LOG, "机头矫正:误差=" + (int)yawError + "° 转速=" + yawRate + "°/s [" + speedLabel + "]"); } else { yawRate = 0.0f; LogUtil.log(TAG_LOG, "机头已对准:偏航误差=" + (int)yawError + "°"); } } else { yawRate = 0.0f; } float vz; if (currentHeight <= 4) { vz = 0.0f; if (Math.abs(errX) > 120) { vx = rawVx > 0 ? 0.135f : -0.135f; } else if (Math.abs(errX) > 80) { vx = rawVx > 0 ? 0.09f : -0.09f; } else if (Math.abs(errX) > 60) { vx = rawVx > 0 ? 0.07f : -0.07f; } else if (Math.abs(errX) > 30) { vx = rawVx > 0 ? 0.05f : -0.05f; } else { vx = 0f; } if (Math.abs(errY) > 120) { vy = rawVy > 0 ? 0.135f : -0.135f; } else if (Math.abs(errY) > 80) { vy = rawVy > 0 ? 0.09f : -0.09f; } else if (Math.abs(errY) > 60) { vy = rawVy > 0 ? 0.07f : -0.07f; } else if (Math.abs(errY) > 30) { vy = rawVy > 0 ? 0.05f : -0.05f; } else { vy = 0f; // 【修正】 } } else if (currentHeight <= 8) { vz = SLOW_SUPER_SPEED; } else { vz = SLOW_LAND_SPEED; } DroneHelper.getInstance().moveVxVyYawrateHeight(vx, vy, yawRate, vz); LogUtil.log(TAG_LOG, "vx" + vx + "vy" + vy + " errX=" + (int) errX + " errY=" + (int) errY + " pixelW=" + (int) pixelWidth + " vz=" + vz + " ult=" + currentHeight + " yaw=" + yawRate); } 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); } else { performNextStep(); } } }; private void performOperation() { LogUtil.log(TAG_LOG, "快速下拉中..." + handlerCallbackCount); DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -5); handlerCallbackCount++; } private void performNextStep() { canLanding = true; handlerCallbackCount = 0; dropTimes = 0; handler.removeCallbacks(runnable); } }