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

1088 lines
43 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.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.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfInt;
import org.opencv.core.Point;
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.Arrays;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 融合视觉降落识别器(独立版本)
* 集成云台相机和下视相机的识别功能,实现相机切换逻辑
*
* 降落策略:
* 1. 先使用云台相机识别
* 2. 云台相机识别失败,切换到下视相机
* 3. 下视相机识别失败,切换回云台相机
* 4. 最多切换2次超过则触发备降
*/
public class MixedVisionLanding {
private static final String TAG_LOG = "MixedVisionLanding";
// 单例模式
private static class SingletonHolder {
private static final MixedVisionLanding INSTANCE = new MixedVisionLanding();
static { INSTANCE.init(); }
}
public static MixedVisionLanding getInstance() {
return SingletonHolder.INSTANCE;
}
// ========== 降落模式 ==========
public enum LandingMode {
GIMBAL_CAMERA, // 云台相机
DOWNWARD_CAMERA // 下视相机
}
// ========== 状态变量 ==========
private LandingMode currentMode = LandingMode.GIMBAL_CAMERA;
private int switchCount = 0;
private static final int MAX_SWITCH_COUNT = 2;
private boolean isLanding = false;
private boolean isDoublePayload = false;
private int startArucoType = 0; // 1执行机库二维码识别 2执行备降点二维码识别
// ========== 云台相机参数 ==========
private static double GIMBAL_LENS_OFFSET_X = 0;
private static double GIMBAL_LENS_OFFSET_Y = 0;
private static final int GIMBAL_CENTER_ERR_MAX = 50;
private static final int GIMBAL_PIXEL_TRIGGER = 360;
private static final int GIMBAL_TRIGGER_FRAME_THRESHOLD = 2;
// ========== 下视相机参数 ==========
private static double DOWNWARD_LENS_OFFSET_X = 0;
private static double DOWNWARD_LENS_OFFSET_Y = 0;
private static final int DOWNWARD_CENTER_ERR_MAX = 30;
private static final int DOWNWARD_PIXEL_TRIGGER = 360;
private static final int DOWNWARD_TRIGGER_FRAME_THRESHOLD = 2;
// ========== 云台相机状态 ==========
private boolean gimbalIsTriggerSuccess = false;
private boolean gimbalArucoNotFoundTag = false;
private boolean gimbalIsStartAruco = false;
private boolean gimbalStartFastStick = false;
private boolean gimbalCanLanding = false;
private int gimbalConsecutiveTriggerCount = 0;
private int gimbalFrameCounter = 0;
private int gimbalDropTimes = 0;
private boolean gimbalDropTimesTag = false;
private long gimbalStartTime = 0;
private long gimbalEndTime = 0;
private boolean gimbalIsYawAligned = false;
private int gimbalCurrentLandingMode = 0;
private int gimbalLastLandingCondition = 0;
private boolean gimbalSetMF = false;
// ========== 下视相机状态 ==========
private boolean downwardIsTriggerSuccess = false;
private boolean downwardArucoNotFoundTag = false;
private boolean downwardIsStartAruco = false;
private boolean downwardStartFastStick = false;
private boolean downwardCanLanding = false;
private int downwardConsecutiveTriggerCount = 0;
private int downwardFrameCounter = 0;
private int downwardDropTimes = 0;
private boolean downwardDropTimesTag = false;
private long downwardStartTime = 0;
private long downwardEndTime = 0;
private boolean downwardIsYawAligned = false;
// ========== 执行器 ==========
private ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
private Future<?> lastGimbalFuture = null;
private Future<?> lastDownwardFuture = null;
// ========== PID控制器 ==========
public PIDControl pidControlX = null;
public PIDControl pidControlY = null;
// ========== Handler ==========
private Handler handler = new Handler(Looper.getMainLooper());
private int handlerCallbackCount = 0;
// ========== 初始化 ==========
public void init() {
LogUtil.log(TAG_LOG, "初始化融合视觉降落识别器");
pidControlX = new PIDControl(0.7f, 0.02f, 0.2f, 0.05f, 2.5f, 0.1f);
pidControlY = new PIDControl(0.7f, 0.02f, 0.2f, 0.05f, 2.5f, 0.1f);
pidControlX.reset();
pidControlY.reset();
}
// ========== 公共方法 ==========
public void setDoublePayload(boolean doublePayload) {
isDoublePayload = doublePayload;
}
public void startLanding() {
synchronized (this) {
LogUtil.log(TAG_LOG, "开始融合视觉降落");
isLanding = true;
currentMode = LandingMode.GIMBAL_CAMERA;
switchCount = 0;
resetGimbalState();
resetDownwardState();
}
}
public void stopLanding() {
synchronized (this) {
LogUtil.log(TAG_LOG, "停止融合视觉降落");
isLanding = false;
startArucoType = 0;
resetGimbalState();
resetDownwardState();
}
}
public LandingMode getCurrentMode() {
return currentMode;
}
public int getSwitchCount() {
return switchCount;
}
public boolean isLanding() {
return isLanding;
}
public int getStartArucoType() {
return startArucoType;
}
public void setStartArucoType(int type) {
startArucoType = type;
}
// ========== 云台相机帧处理 ==========
public void processGimbalFrame(int height, int width, byte[] data, Dictionary dictionary) {
synchronized (this) {
if (!isLanding || currentMode != LandingMode.GIMBAL_CAMERA || startArucoType == 0) {
return;
}
gimbalIsTriggerSuccess = true;
Movement.getInstance().setVirtualStickEnableReason(2);
if (gimbalIsStartAruco || gimbalStartFastStick) {
return;
}
int currentFrame = gimbalFrameCounter++;
if (currentFrame % 3 != 0) {
return;
}
if (currentFrame % 30 != 0) {
DroneHelper.getInstance().setGimbalPitchdown();
}
gimbalIsStartAruco = true;
if (lastGimbalFuture != null && !lastGimbalFuture.isDone()) {
lastGimbalFuture.cancel(true);
}
int ultHeight = Movement.getInstance().getUltrasonicHeight();
updateGimbalLensOffset(ultHeight);
final int finalUltHeight = ultHeight;
lastGimbalFuture = executor.schedule(() -> {
try {
processGimbalFrameInternal(height, width, data, dictionary, finalUltHeight);
} catch (Exception e) {
LogUtil.log(TAG_LOG, "云台相机处理异常: " + e);
synchronized (MixedVisionLanding.this) {
gimbalIsStartAruco = false;
}
handleLandingFailure();
}
}, 0, TimeUnit.MILLISECONDS);
}
}
// ========== 下视相机帧处理 ==========
public void processDownwardFrame(int height, int width, byte[] data, Dictionary dictionary) {
synchronized (this) {
if (!isLanding || currentMode != LandingMode.DOWNWARD_CAMERA || startArucoType == 0) {
return;
}
downwardIsTriggerSuccess = true;
Movement.getInstance().setVirtualStickEnableReason(2);
if (downwardIsStartAruco || downwardStartFastStick) {
return;
}
int currentFrame = downwardFrameCounter++;
if (currentFrame % 2 != 0) {
return;
}
downwardIsStartAruco = true;
if (lastDownwardFuture != null && !lastDownwardFuture.isDone()) {
lastDownwardFuture.cancel(true);
}
int ultHeight = Movement.getInstance().getUltrasonicHeight();
updateDownwardLensOffset(ultHeight);
final int finalUltHeight = ultHeight;
lastDownwardFuture = executor.schedule(() -> {
try {
processDownwardFrameInternal(height, width, data, dictionary, finalUltHeight);
} catch (Exception e) {
LogUtil.log(TAG_LOG, "下视相机处理异常: " + e);
synchronized (MixedVisionLanding.this) {
downwardIsStartAruco = false;
}
handleLandingFailure();
}
}, 0, TimeUnit.MILLISECONDS);
}
}
// ========== 云台相机内部处理 ==========
private void processGimbalFrameInternal(int height, int width, byte[] data, Dictionary dictionary, int ultHeight) {
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<Mat> corners = new ArrayList<>();
DetectorParameters parameters = new DetectorParameters();
ArucoDetector detector = new ArucoDetector(dictionary, parameters);
detector.detectMarkers(grayImgMat, corners, ids);
if (!ids.empty()) {
gimbalArucoNotFoundTag = false;
gimbalDropTimesTag = true;
List<ArucoMarker> smallMarkers = new ArrayList<>();
List<ArucoMarker> bigMarkers1_4 = new ArrayList<>();
List<ArucoMarker> bigMarker5 = new ArrayList<>();
List<ArucoMarker> bigMarker6 = new ArrayList<>();
int[] idArray = ids.toArray();
List<Integer> excludeIds = isDoublePayload ?
Arrays.asList(12, 11, 21, 14, 13, 22) :
Arrays.asList(12, 11, 21, 20, 19, 25);
StringBuilder detectInfo = new StringBuilder();
for (int i = 0; i < idArray.length; i++) {
int id = idArray[i];
Mat corner = corners.get(i);
double pixelW = calculatePixelWidth(corner);
if (id >= 11 && id <= 25 && !excludeIds.contains(id)) {
smallMarkers.add(new ArucoMarker(id, corner, 0.03f));
detectInfo.append(String.format("小码%d(%.0fpx) ", id, pixelW));
} else if (id >= 1 && id <= 4) {
bigMarkers1_4.add(new ArucoMarker(id, corner, 0.18f));
detectInfo.append(String.format("大码%d(%.0fpx) ", id, pixelW));
} else if (id == 5) {
bigMarker5.add(new ArucoMarker(5, corner, 0.15f));
detectInfo.append(String.format("5号(%.0fpx) ", pixelW));
} else if (id == 6) {
bigMarker6.add(new ArucoMarker(6, corner, 0.24f));
detectInfo.append(String.format("6号(%.0fpx) ", pixelW));
}
}
if (detectInfo.length() > 0) {
LogUtil.log(TAG_LOG, "【云台检测到】" + detectInfo.toString());
}
// 优先级1小码模式
if (!smallMarkers.isEmpty() && ultHeight < 25) {
gimbalCurrentLandingMode = 1;
processGimbalSmallMarkers(smallMarkers, rgbMat.width(), rgbMat.height(), ultHeight);
}
// 优先级21-4号几何中心
else if (!bigMarkers1_4.isEmpty()) {
gimbalCurrentLandingMode = 2;
if (ultHeight > 30 && !gimbalIsYawAligned) {
processGimbalBigMarkersYawOnly(bigMarkers1_4, rgbMat.width(), rgbMat.height(), ultHeight);
} else {
processGimbalBigMarkersCenter(bigMarkers1_4, rgbMat.width(), rgbMat.height(), ultHeight);
}
}
// 优先级35/6号纯下降
else if ((!bigMarker5.isEmpty() || !bigMarker6.isEmpty())) {
gimbalCurrentLandingMode = 3;
ArucoMarker target = !bigMarker5.isEmpty() ? bigMarker5.get(0) : bigMarker6.get(0);
processGimbalLandingOnly(target, rgbMat.width(), rgbMat.height(), ultHeight);
} else {
handleGimbalLostMarker(ultHeight);
}
} else {
handleGimbalLostMarker(ultHeight);
}
grayImgMat.release();
rgbMat.release();
yuvMat.release();
ids.release();
if (!corners.isEmpty()) {
for (Mat c : corners) if (c != null) c.release();
}
gimbalIsStartAruco = false;
}
// ========== 下视相机内部处理 ==========
private void processDownwardFrameInternal(int height, int width, byte[] data, Dictionary dictionary, int ultHeight) {
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<Mat> corners = new ArrayList<>();
DetectorParameters parameters = new DetectorParameters();
ArucoDetector detector = new ArucoDetector(dictionary, parameters);
detector.detectMarkers(grayImgMat, corners, ids);
// 只保留6号码
ids = keepOnly6(ids, corners);
if (!ids.empty()) {
downwardArucoNotFoundTag = false;
downwardDropTimesTag = true;
int[] idArray = ids.toArray();
if (idArray.length == 1 && idArray[0] == 6) {
Mat 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 + DOWNWARD_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 + DOWNWARD_LENS_OFFSET_Y;
double errY = Math.abs(centerY - rgbMat.height() / 2.0);
LogUtil.log(TAG_LOG, "【下视检测到】6号 像素" + (int) pixelWidth + " errX=" + (int) errX + " errY=" + (int) errY);
// 速降判断
if (!downwardStartFastStick) {
if (pixelWidth >= DOWNWARD_PIXEL_TRIGGER &&
ultHeight <= 4 &&
errX < DOWNWARD_CENTER_ERR_MAX &&
errY < DOWNWARD_CENTER_ERR_MAX) {
downwardConsecutiveTriggerCount++;
LogUtil.log(TAG_LOG, "下视速降条件满足,累计帧数: " + downwardConsecutiveTriggerCount);
if (downwardConsecutiveTriggerCount >= DOWNWARD_TRIGGER_FRAME_THRESHOLD) {
downwardStartFastStick = true;
downwardConsecutiveTriggerCount = 0;
LogUtil.log(TAG_LOG, "【下视触发速降】");
handler.postDelayed(() -> handler.post(downwardRunnable), 300);
return;
}
} else {
if (downwardConsecutiveTriggerCount > 0) {
LogUtil.log(TAG_LOG, "下视速降条件中断,重置累计帧数");
downwardConsecutiveTriggerCount = 0;
}
}
}
// 旋转处理
if (ultHeight > 30 && !downwardIsYawAligned) {
double yawError = calculateYawErrorFromCorners(points);
double absYaw = Math.abs(yawError);
if (absYaw < 10.0) {
downwardIsYawAligned = true;
LogUtil.log(TAG_LOG, "【下视旋转到位】偏航已对准");
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, 0f);
} else {
float yawRate = calculateYawRate(yawError, ultHeight);
LogUtil.log(TAG_LOG, String.format("【下视执行】纯旋转 yawRate=%.1f avgYaw=%.1f", yawRate, yawError));
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, yawRate, 0f);
}
} else {
// 正常修正
moveOnArucoDetected(new ArucoMarker(1, corner6, 0.24f), rgbMat.width(), rgbMat.height());
}
}
} else {
handleDownwardLostMarker(ultHeight);
}
grayImgMat.release();
rgbMat.release();
yuvMat.release();
ids.release();
if (!corners.isEmpty()) {
for (Mat c : corners) if (c != null) c.release();
}
downwardIsStartAruco = false;
}
// ========== 云台相机小码处理 ==========
private void processGimbalSmallMarkers(List<ArucoMarker> markers, int imgWidth, int imgHeight, int ultHeight) {
if (markers == null || markers.size() < 3) {
LogUtil.log(TAG_LOG, "【云台小码数量不足】检测到" + (markers == null ? 0 : markers.size()) + "");
return;
}
double sumX = 0, sumY = 0;
int validCount = 0;
double totalPixelWidth = 0;
ArucoMarker target15 = null;
ArucoMarker target17 = null;
for (ArucoMarker marker : markers) {
Mat corner = marker.getConner();
totalPixelWidth += calculatePixelWidth(corner);
double cx = 0, cy = 0;
for (int i = 0; i < 4; i++) {
double[] p = corner.get(0, i);
cx += p[0];
cy += p[1];
}
cx /= 4.0;
cy /= 4.0;
sumX += cx;
sumY += cy;
validCount++;
int id = marker.getId();
if (id == 15) target15 = marker;
if (id == 17) target17 = marker;
}
if (validCount == 0) return;
double avgPixelWidth = totalPixelWidth / validCount;
double geoCenterX = sumX / validCount;
double geoCenterY = sumY / validCount;
double offsetX = geoCenterX - imgWidth / 2.0 - GIMBAL_LENS_OFFSET_X;
double offsetY = geoCenterY - imgHeight / 2.0 + GIMBAL_LENS_OFFSET_Y;
double absX = Math.abs(offsetX);
double absY = Math.abs(offsetY);
double z = ultHeight / 10.0;
// 特殊降落判断
boolean isSpecialLanding = false;
if (!isDoublePayload && target15 != null && ultHeight <= 4) {
Mat corner = target15.getConner();
double cx = 0, cy = 0;
for (int i = 0; i < 4; i++) {
double[] p = corner.get(0, i);
cx += p[0];
cy += p[1];
}
cx /= 4.0; cy /= 4.0;
double singleErrX = cx - imgWidth / 2.0 - GIMBAL_LENS_OFFSET_X;
double singleErrY = cy - imgHeight / 2.0 + GIMBAL_LENS_OFFSET_Y;
if (Math.abs(singleErrX) < 60 && Math.abs(singleErrY) < 60 && ultHeight <= 4) {
isSpecialLanding = true;
offsetX = singleErrX; offsetY = singleErrY;
absX = Math.abs(singleErrX); absY = Math.abs(singleErrY);
}
}
if (isDoublePayload && target17 != null && ultHeight <= 4) {
Mat corner = target17.getConner();
double cx = 0, cy = 0;
for (int i = 0; i < 4; i++) {
double[] p = corner.get(0, i);
cx += p[0];
cy += p[1];
}
cx /= 4.0; cy /= 4.0;
double singleErrX = cx - imgWidth / 2.0 - GIMBAL_LENS_OFFSET_X;
double singleErrY = cy - imgHeight / 2.0 + GIMBAL_LENS_OFFSET_Y;
if (Math.abs(singleErrX) < 60 && Math.abs(singleErrY) < 60 && ultHeight <= 4) {
isSpecialLanding = true;
offsetX = singleErrX; offsetY = singleErrY;
absX = Math.abs(singleErrX); absY = Math.abs(singleErrY);
}
}
// 连续帧判断
int currentCondition = 0;
if (isSpecialLanding) {
currentCondition = 1;
} else if (ultHeight <= 4 && absX < 60 && absY < 60 && validCount >= 8) {
currentCondition = 2;
}
if (currentCondition == 0 || currentCondition != gimbalLastLandingCondition) {
if (gimbalConsecutiveTriggerCount > 0) {
gimbalConsecutiveTriggerCount = 0;
LogUtil.log(TAG_LOG, "【云台计数器清零】条件变化");
}
gimbalLastLandingCondition = currentCondition;
}
if (currentCondition > 0) {
gimbalConsecutiveTriggerCount++;
LogUtil.log(TAG_LOG, "【云台计数器累加】count=" + gimbalConsecutiveTriggerCount);
if (gimbalConsecutiveTriggerCount >= GIMBAL_TRIGGER_FRAME_THRESHOLD) {
gimbalStartFastStick = true;
gimbalConsecutiveTriggerCount = 0;
gimbalLastLandingCondition = 0;
LogUtil.log(TAG_LOG, "【云台触发速降】");
handler.postDelayed(() -> handler.post(gimbalRunnable), 300);
return;
}
}
// PID控制
double outX = 0, outY = 0, outZ = 0;
if (z <= 0.4) {
pidControlX.setInputFilterAll((float)offsetX/1750);
pidControlY.setInputFilterAll(-(float)offsetY/1750);
outX = Math.max(-0.125, Math.min(0.125, pidControlX.get_pid()));
outY = Math.max(-0.125, Math.min(0.125, pidControlY.get_pid()));
} else if (z <= 0.7) {
pidControlX.setInputFilterAll((float)offsetX/1200);
pidControlY.setInputFilterAll(-(float)offsetY/1200);
outX = Math.max(-0.135, Math.min(0.135, pidControlX.get_pid()));
outY = Math.max(-0.135, Math.min(0.135, pidControlY.get_pid()));
outZ = (absX < 250 && absY < 250) ? -0.1 : 0;
} else if (z <= 1.0) {
pidControlX.setInputFilterAll((float)offsetX/1200);
pidControlY.setInputFilterAll(-(float)offsetY/1200);
outX = Math.max(-0.145, Math.min(0.145, pidControlX.get_pid()));
outY = Math.max(-0.175, Math.min(0.175, pidControlY.get_pid()));
outZ = (absX < 200 && absY < 200) ? -0.2 : 0;
} else if (z <= 1.5) {
pidControlX.setInputFilterAll((float)offsetX/1450);
pidControlY.setInputFilterAll(-(float)offsetY/1450);
outX = Math.max(-0.185, Math.min(0.185, pidControlX.get_pid()));
outY = Math.max(-0.185, Math.min(0.185, pidControlY.get_pid()));
outZ = (absX < 180 && absY < 180) ? -0.4 : 0;
} else if (z <= 2.0) {
pidControlX.setInputFilterAll((float)offsetX/1350);
pidControlY.setInputFilterAll(-(float)offsetY/1350);
outX = pidControlX.get_pid();
outY = pidControlY.get_pid();
outZ = (absX < 200 && absY < 200) ? -0.4 : 0;
} else if (z <= 3.0) {
pidControlX.setInputFilterAll((float)offsetX/1250);
pidControlY.setInputFilterAll(-(float)offsetY/1250);
outX = pidControlX.get_pid();
outY = pidControlY.get_pid();
outZ = (absX < 200 && absY < 200) ? -0.4 : 0;
} else {
pidControlX.setInputFilterAll((float)offsetX/850);
pidControlY.setInputFilterAll(-(float)offsetY/850);
outX = pidControlX.get_pid();
outY = pidControlY.get_pid();
outZ = (absX < 130 && absY < 130) ? -0.4 : 0;
}
LogUtil.log(TAG_LOG, String.format("【云台执行】小码 vx=%.3f vy=%.3f vz=%.3f pixel=%.0f ult=%d", outX, outY, outZ, avgPixelWidth, ultHeight));
DroneHelper.getInstance().moveVxVyYawrateHeight((float)outX, (float)outY, 0, (float)outZ);
}
// ========== 云台相机大码旋转处理 ==========
private void processGimbalBigMarkersYawOnly(List<ArucoMarker> markers, int imgWidth, int imgHeight, int ultHeight) {
double sumYaw = 0;
int count = 0;
for (ArucoMarker marker : markers) {
Point[] pts = new Point[4];
Mat corner = marker.getConner();
for (int i = 0; i < 4; i++) {
double[] p = corner.get(0, i);
pts[i] = new Point(p[0], p[1]);
}
sumYaw += calculateYawErrorFromCorners(pts);
count++;
}
double avgYaw = sumYaw / count;
double absYaw = Math.abs(avgYaw);
if (absYaw < 10.0) {
gimbalIsYawAligned = true;
LogUtil.log(TAG_LOG, "【云台旋转到位】偏航已对准");
return;
}
float yawRate = calculateYawRate(avgYaw, ultHeight);
LogUtil.log(TAG_LOG, String.format("【云台执行】纯旋转 yawRate=%.1f avgYaw=%.1f", yawRate, avgYaw));
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, yawRate, 0f);
}
// ========== 云台相机大码中心处理 ==========
private void processGimbalBigMarkersCenter(List<ArucoMarker> markers, int imgWidth, int imgHeight, int ultHeight) {
double sumX = 0, sumY = 0;
StringBuilder markerInfo = new StringBuilder();
for (ArucoMarker marker : markers) {
Mat corner = marker.getConner();
double cx = 0, cy = 0;
for (int i = 0; i < 4; i++) {
double[] p = corner.get(0, i);
cx += p[0];
cy += p[1];
}
sumX += cx / 4.0;
sumY += cy / 4.0;
double pixelW = calculatePixelWidth(corner);
markerInfo.append(String.format("[ID%d:%.0fpx]", marker.getId(), pixelW));
}
double geoCenterX = sumX / markers.size();
double geoCenterY = sumY / markers.size();
double offsetX = geoCenterX - imgWidth / 2.0;
double offsetY = geoCenterY - imgHeight / 2.0;
double absX = Math.abs(offsetX);
double absY = Math.abs(offsetY);
double z = ultHeight / 10.0;
double outX = 0, outY = 0, outZ = 0;
if (z >= 5.0) {
pidControlX.setInputFilterAll((float)offsetX/300);
pidControlY.setInputFilterAll(-(float)offsetY/300);
outX = Math.max(-0.20, Math.min(0.20, pidControlX.get_pid()));
outY = Math.max(-0.20, Math.min(0.20, pidControlY.get_pid()));
outZ = (absX < 100 && absY < 100) ? -0.4 : 0;
} else if (z >= 4) {
pidControlX.setInputFilterAll((float)offsetX/600);
pidControlY.setInputFilterAll(-(float)offsetY/600);
outX = Math.max(-0.20, Math.min(0.20, pidControlX.get_pid()));
outY = Math.max(-0.20, Math.min(0.20, pidControlY.get_pid()));
outZ = (absX < 120 && absY < 120) ? -0.4 : 0;
} else if (z >= 2) {
pidControlX.setInputFilterAll((float)offsetX/900);
pidControlY.setInputFilterAll(-(float)offsetY/900);
outX = Math.max(-0.20, Math.min(0.20, pidControlX.get_pid()));
outY = Math.max(-0.20, Math.min(0.20, pidControlY.get_pid()));
outZ = (absX < 140 && absY < 140) ? -0.4 : 0;
}
LogUtil.log(TAG_LOG, String.format("【云台执行】大码几何中心 vx=%.3f vy=%.3f vz=%.3f ult=%d %s", outX, outY, outZ, ultHeight, markerInfo.toString()));
DroneHelper.getInstance().moveVxVyYawrateHeight((float)outX, (float)outY, 0f, (float)outZ);
}
// ========== 云台相机纯下降处理 ==========
private void processGimbalLandingOnly(ArucoMarker marker, int imgWidth, int imgHeight, int ultHeight) {
Mat corner = marker.getConner();
double pixelWidth = calculatePixelWidth(corner);
float vz;
if (ultHeight >= 50) {
vz = -0.7f;
} else if (ultHeight <= 15) {
vz = 0.0f;
} else {
vz = 0.3f;
}
LogUtil.log(TAG_LOG, String.format("【云台执行】5/6号纯下降 vz=%.3f pixel=%.0f ult=%d", vz, pixelWidth, ultHeight));
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, vz);
}
// ========== 云台相机丢失处理 ==========
private void handleGimbalLostMarker(int ultHeight) {
if (!gimbalArucoNotFoundTag) {
gimbalStartTime = System.currentTimeMillis();
gimbalArucoNotFoundTag = true;
}
gimbalEndTime = System.currentTimeMillis();
long lostDuration = gimbalEndTime - gimbalStartTime;
if (lostDuration > 1000 && lostDuration <= 12000) {
if (ultHeight <= 20) {
LogUtil.log(TAG_LOG, "【云台执行】丢失上升 vz=3.0");
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, 3f);
if (gimbalDropTimes > Integer.parseInt(AMSConfig.getInstance().getAlternateLandingTimes())) {
LogUtil.log(TAG_LOG, "云台超过复降限制,切换相机");
handleLandingFailure();
return;
}
if (gimbalDropTimesTag) {
gimbalDropTimesTag = false;
gimbalDropTimes++;
LogUtil.log(TAG_LOG, "云台复降第:" + gimbalDropTimes + "");
}
} else {
LogUtil.log(TAG_LOG, "【云台执行】丢失下降 vz=-0.3");
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -0.3f);
}
} else if (lostDuration > 8000) {
LogUtil.log(TAG_LOG, "云台判定未识别到二维码,切换相机");
handleLandingFailure();
}
}
// ========== 下视相机丢失处理 ==========
private void handleDownwardLostMarker(int ultHeight) {
LogUtil.log(TAG_LOG, "下视找不到了二维码");
if (!downwardArucoNotFoundTag) {
downwardStartTime = System.currentTimeMillis();
downwardArucoNotFoundTag = true;
}
downwardEndTime = System.currentTimeMillis();
long lostDuration = downwardEndTime - downwardStartTime;
if (lostDuration > 1000 && lostDuration <= 12000) {
if (ultHeight <= 20) {
LogUtil.log(TAG_LOG, "【下视执行】丢失上升 vz=3.0");
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, 3f);
if (downwardDropTimes > Integer.parseInt(AMSConfig.getInstance().getAlternateLandingTimes())) {
LogUtil.log(TAG_LOG, "下视超过复降限制,切换相机");
handleLandingFailure();
return;
}
if (downwardDropTimesTag) {
downwardDropTimesTag = false;
downwardDropTimes++;
LogUtil.log(TAG_LOG, "下视复降第:" + downwardDropTimes + "");
}
} else {
LogUtil.log(TAG_LOG, "【下视执行】丢失下降 vz=-0.3");
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -0.3f);
}
} else if (lostDuration > 8000) {
LogUtil.log(TAG_LOG, "下视判定未识别到二维码,切换相机");
handleLandingFailure();
}
}
// ========== 下视相机移动控制 ==========
private void moveOnArucoDetected(ArucoMarker marker, int imgWidth, int imgHeight) {
Mat corner = marker.getConner();
Point[] points = new Point[4];
for (int i = 0; i < 4; i++) {
double[] p = corner.get(0, i);
points[i] = new Point(p[0], p[1]);
}
double centerX = (points[0].x + points[1].x + points[2].x + points[3].x) / 4.0 + DOWNWARD_LENS_OFFSET_X;
double centerY = (points[0].y + points[1].y + points[2].y + points[3].y) / 4.0 + DOWNWARD_LENS_OFFSET_Y;
double errX = centerX - imgWidth / 2.0;
double errY = centerY - imgHeight / 2.0;
double absX = Math.abs(errX);
double absY = Math.abs(errY);
int ultHeight = Movement.getInstance().getUltrasonicHeight();
double z = ultHeight / 10.0;
double outX = 0, outY = 0, outZ = 0;
if (z <= 0.4) {
pidControlX.setInputFilterAll((float)errX/1750);
pidControlY.setInputFilterAll(-(float)errY/1750);
outX = Math.max(-0.125, Math.min(0.125, pidControlX.get_pid()));
outY = Math.max(-0.125, Math.min(0.125, pidControlY.get_pid()));
} else if (z <= 0.7) {
pidControlX.setInputFilterAll((float)errX/1200);
pidControlY.setInputFilterAll(-(float)errY/1200);
outX = Math.max(-0.135, Math.min(0.135, pidControlX.get_pid()));
outY = Math.max(-0.135, Math.min(0.135, pidControlY.get_pid()));
outZ = (absX < 250 && absY < 250) ? -0.1 : 0;
} else if (z <= 1.0) {
pidControlX.setInputFilterAll((float)errX/1200);
pidControlY.setInputFilterAll(-(float)errY/1200);
outX = Math.max(-0.145, Math.min(0.145, pidControlX.get_pid()));
outY = Math.max(-0.175, Math.min(0.175, pidControlY.get_pid()));
outZ = (absX < 200 && absY < 200) ? -0.2 : 0;
} else if (z <= 1.5) {
pidControlX.setInputFilterAll((float)errX/1450);
pidControlY.setInputFilterAll(-(float)errY/1450);
outX = Math.max(-0.185, Math.min(0.185, pidControlX.get_pid()));
outY = Math.max(-0.185, Math.min(0.185, pidControlY.get_pid()));
outZ = (absX < 180 && absY < 180) ? -0.4 : 0;
} else if (z <= 2.0) {
pidControlX.setInputFilterAll((float)errX/1350);
pidControlY.setInputFilterAll(-(float)errY/1350);
outX = pidControlX.get_pid();
outY = pidControlY.get_pid();
outZ = (absX < 200 && absY < 200) ? -0.4 : 0;
} else if (z <= 3.0) {
pidControlX.setInputFilterAll((float)errX/1250);
pidControlY.setInputFilterAll(-(float)errY/1250);
outX = pidControlX.get_pid();
outY = pidControlY.get_pid();
outZ = (absX < 200 && absY < 200) ? -0.4 : 0;
} else {
pidControlX.setInputFilterAll((float)errX/850);
pidControlY.setInputFilterAll(-(float)errY/850);
outX = pidControlX.get_pid();
outY = pidControlY.get_pid();
outZ = (absX < 130 && absY < 130) ? -0.4 : 0;
}
LogUtil.log(TAG_LOG, String.format("【下视执行】移动 vx=%.3f vy=%.3f vz=%.3f ult=%d errX=%.0f errY=%.0f", outX, outY, outZ, ultHeight, absX, absY));
DroneHelper.getInstance().moveVxVyYawrateHeight((float)outX, (float)outY, 0f, (float)outZ);
}
// ========== 降落失败处理(切换相机) ==========
private void handleLandingFailure() {
synchronized (this) {
if (!isLanding) {
return;
}
switchCount++;
LogUtil.log(TAG_LOG, "切换相机,当前切换次数: " + switchCount);
// 切换到另一个相机
if (currentMode == LandingMode.GIMBAL_CAMERA) {
currentMode = LandingMode.DOWNWARD_CAMERA;
LogUtil.log(TAG_LOG, "切换到下视相机");
resetDownwardState();
} else {
currentMode = LandingMode.GIMBAL_CAMERA;
LogUtil.log(TAG_LOG, "切换到云台相机");
resetGimbalState();
}
// 如果达到最大切换次数,触发备降
if (switchCount >= MAX_SWITCH_COUNT) {
LogUtil.log(TAG_LOG, "达到最大切换次数,触发备降");
stopLanding();
AlternateLandingManager.getInstance().startTaskProcess(null);
Movement.getInstance().setAlternate(true);
}
}
}
// ========== 重置云台相机状态 ==========
private void resetGimbalState() {
gimbalIsTriggerSuccess = false;
gimbalArucoNotFoundTag = false;
gimbalIsStartAruco = false;
gimbalStartFastStick = false;
gimbalCanLanding = false;
gimbalConsecutiveTriggerCount = 0;
gimbalFrameCounter = 0;
gimbalDropTimes = 0;
gimbalDropTimesTag = false;
gimbalStartTime = 0;
gimbalEndTime = 0;
gimbalIsYawAligned = false;
gimbalCurrentLandingMode = 0;
gimbalLastLandingCondition = 0;
gimbalSetMF = false;
}
// ========== 重置下视相机状态 ==========
private void resetDownwardState() {
downwardIsTriggerSuccess = false;
downwardArucoNotFoundTag = false;
downwardIsStartAruco = false;
downwardStartFastStick = false;
downwardCanLanding = false;
downwardConsecutiveTriggerCount = 0;
downwardFrameCounter = 0;
downwardDropTimes = 0;
downwardDropTimesTag = false;
downwardStartTime = 0;
downwardEndTime = 0;
downwardIsYawAligned = false;
}
// ========== 更新云台相机镜头偏移 ==========
private void updateGimbalLensOffset(int ultHeight) {
if (isDoublePayload) {
if (ultHeight <= 5) {
GIMBAL_LENS_OFFSET_X = -120;
GIMBAL_LENS_OFFSET_Y = 120;
} else if (ultHeight < 10) {
GIMBAL_LENS_OFFSET_X = -80;
GIMBAL_LENS_OFFSET_Y = 80;
} else if (ultHeight < 20) {
GIMBAL_LENS_OFFSET_X = -60;
GIMBAL_LENS_OFFSET_Y = 60;
}
} else {
if (ultHeight <= 5) {
GIMBAL_LENS_OFFSET_X = 120;
GIMBAL_LENS_OFFSET_Y = 100;
} else if (ultHeight < 10) {
GIMBAL_LENS_OFFSET_X = 80;
GIMBAL_LENS_OFFSET_Y = 80;
} else if (ultHeight < 20) {
GIMBAL_LENS_OFFSET_X = 60;
GIMBAL_LENS_OFFSET_Y = 60;
}
}
}
// ========== 更新下视相机镜头偏移 ==========
private void updateDownwardLensOffset(int ultHeight) {
if (ultHeight >= 30) {
DOWNWARD_LENS_OFFSET_X = 0;
DOWNWARD_LENS_OFFSET_Y = 0;
} else if (ultHeight >= 20) {
DOWNWARD_LENS_OFFSET_X = 0;
DOWNWARD_LENS_OFFSET_Y = 0;
} else if (ultHeight >= 10) {
DOWNWARD_LENS_OFFSET_X = 20;
DOWNWARD_LENS_OFFSET_Y = 10;
} else if (ultHeight >= 5) {
DOWNWARD_LENS_OFFSET_X = 30;
DOWNWARD_LENS_OFFSET_Y = 20;
} else {
DOWNWARD_LENS_OFFSET_X = 55;
DOWNWARD_LENS_OFFSET_Y = 35;
}
}
// ========== 工具方法 ==========
private double calculatePixelWidth(Mat corner) {
try {
Point[] pts = new Point[4];
for (int i = 0; i < 4; i++) {
double[] p = corner.get(0, i);
pts[i] = new Point(p[0], p[1]);
}
double top = Math.sqrt(Math.pow(pts[1].x - pts[0].x, 2) + Math.pow(pts[1].y - pts[0].y, 2));
double bottom = Math.sqrt(Math.pow(pts[2].x - pts[3].x, 2) + Math.pow(pts[2].y - pts[3].y, 2));
return (top + bottom) / 2.0;
} catch (Exception e) {
return 0.0;
}
}
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);
}
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 float calculateYawRate(double yawError, int ultHeight) {
if (ultHeight < 10 || ultHeight > 50) return 0f;
double absError = Math.abs(yawError);
if (absError < 10.0) return 0f;
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;
return yawError > 0 ? targetRate : -targetRate;
}
private MatOfInt keepOnly6(MatOfInt ids, List<Mat> corners) {
if (ids.empty()) return ids;
int[] idArray = ids.toArray();
List<Integer> indicesToKeep = new ArrayList<>();
for (int i = 0; i < idArray.length; i++) {
if (idArray[i] == 6) {
indicesToKeep.add(i);
}
}
if (indicesToKeep.isEmpty()) {
ids.release();
return new MatOfInt();
}
int[] newIds = new int[indicesToKeep.size()];
List<Mat> newCorners = new ArrayList<>();
for (int i = 0; i < indicesToKeep.size(); i++) {
newIds[i] = 6;
newCorners.add(corners.get(indicesToKeep.get(i)));
}
corners.clear();
corners.addAll(newCorners);
MatOfInt newIdsMat = new MatOfInt();
newIdsMat.fromArray(newIds);
ids.release();
return newIdsMat;
}
// ========== 云台速降Runnable ==========
private Runnable gimbalRunnable = new Runnable() {
@Override
public void run() {
LogUtil.log(TAG_LOG, String.format("【云台执行】速降 vz=-5 count=%d/20", handlerCallbackCount));
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -5);
handlerCallbackCount++;
if (handlerCallbackCount < 20) {
handler.postDelayed(this, 50);
} else {
gimbalCanLanding = true;
handlerCallbackCount = 0;
gimbalDropTimes = 0;
}
}
};
// ========== 下视速降Runnable ==========
private Runnable downwardRunnable = new Runnable() {
@Override
public void run() {
LogUtil.log(TAG_LOG, String.format("【下视执行】速降 vz=-5 count=%d/20", handlerCallbackCount));
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -5);
handlerCallbackCount++;
if (handlerCallbackCount < 20) {
handler.postDelayed(this, 50);
} else {
downwardCanLanding = true;
handlerCallbackCount = 0;
downwardDropTimes = 0;
}
}
};
}