3454 lines
128 KiB
C++
3454 lines
128 KiB
C++
#include "DroneScrewServerPresenter.h"
|
||
#include "Version.h"
|
||
#include "VrLog.h"
|
||
#include "VrError.h"
|
||
|
||
#include <QCoreApplication>
|
||
#include <QDir>
|
||
#include <QDomDocument>
|
||
#include <QFile>
|
||
#include <QFileInfo>
|
||
#include <QImage>
|
||
#include <QJsonArray>
|
||
#include <QNetworkInterface>
|
||
#include <QStringList>
|
||
#include <QTextStream>
|
||
#include <QXmlStreamReader>
|
||
#include <algorithm>
|
||
#include <chrono>
|
||
#include <cstring>
|
||
#include <exception>
|
||
#include <memory>
|
||
#include <new>
|
||
#include <sstream>
|
||
#include <string>
|
||
#include <thread>
|
||
#include <utility>
|
||
|
||
#if defined(__linux__)
|
||
# if defined(__has_include)
|
||
# if __has_include(<rga/im2d.h>) && __has_include(<rga/RgaApi.h>)
|
||
# include <rga/im2d.h>
|
||
# include <rga/RgaApi.h>
|
||
# define DRONESCREW_HAS_RGA 1
|
||
# elif __has_include(<im2d.h>) && __has_include(<RgaApi.h>)
|
||
# include <im2d.h>
|
||
# include <RgaApi.h>
|
||
# define DRONESCREW_HAS_RGA 1
|
||
# endif
|
||
# endif
|
||
#endif
|
||
|
||
#ifndef DRONESCREW_HAS_RGA
|
||
# define DRONESCREW_HAS_RGA 0
|
||
#endif
|
||
|
||
namespace
|
||
{
|
||
constexpr double kPrecisionFrameRate = 2.0;
|
||
constexpr double kDistanceFrameRate = 30.0;
|
||
constexpr int kLiveStreamFrameRate = 30;
|
||
constexpr unsigned int kLiveStreamWidth = 1024;
|
||
constexpr unsigned int kLiveStreamHeight = 750;
|
||
constexpr int kRtspPullPort = 8554;
|
||
constexpr int kRtmpPushPort = 1935;
|
||
constexpr int kMvsPixelTypeMono8 = 0x01080001;
|
||
constexpr int kMvsAcqModeContinuous = 2;
|
||
constexpr unsigned int kSingleFrameWaitTimeoutMs = 10000;
|
||
constexpr int kGpioTriggerHighMs = 10;
|
||
constexpr int64_t kPrecisionBinningHorizontal = 1;
|
||
constexpr int64_t kPrecisionBinningVertical = 1;
|
||
constexpr int64_t kPrecisionDecimationHorizontal = 1;
|
||
constexpr int64_t kPrecisionDecimationVertical = 1;
|
||
constexpr int64_t kDistanceBinningHorizontal = 1;
|
||
constexpr int64_t kDistanceBinningVertical = 1;
|
||
constexpr int64_t kDistanceDecimationHorizontal = 4;
|
||
constexpr int64_t kDistanceDecimationVertical = 4;
|
||
constexpr int kDetectPipelinePrecision = 0;
|
||
constexpr int kDetectPipelineDistance = 1;
|
||
constexpr const char* kImageSaveRootDir = "/home/cat";
|
||
constexpr size_t kMaxImageSaveQueueDepth = 100;
|
||
constexpr size_t kImageSaveWorkerCount = 4;
|
||
|
||
const char* mvsSdkErrorName(int code)
|
||
{
|
||
switch (static_cast<unsigned int>(code))
|
||
{
|
||
case 0x00000000u: return "MV_OK";
|
||
case 0x80000000u: return "MV_E_HANDLE invalid handle";
|
||
case 0x80000001u: return "MV_E_SUPPORT not supported";
|
||
case 0x80000003u: return "MV_E_CALLORDER call order error";
|
||
case 0x80000004u: return "MV_E_PARAMETER incorrect parameter";
|
||
case 0x80000006u: return "MV_E_RESOURCE resource failed";
|
||
case 0x80000008u: return "MV_E_PRECONDITION precondition changed";
|
||
case 0x800000ffu: return "MV_E_UNKNOW unknown";
|
||
case 0x80000100u: return "MV_E_GC_GENERIC GenICam general error";
|
||
case 0x80000101u: return "MV_E_GC_ARGUMENT illegal parameter";
|
||
case 0x80000102u: return "MV_E_GC_RANGE out of range";
|
||
case 0x80000103u: return "MV_E_GC_PROPERTY property error";
|
||
case 0x80000104u: return "MV_E_GC_RUNTIME runtime error";
|
||
case 0x80000105u: return "MV_E_GC_LOGICAL logical error";
|
||
case 0x80000106u: return "MV_E_GC_ACCESS node access error";
|
||
case 0x80000107u: return "MV_E_GC_TIMEOUT timeout";
|
||
case 0x800001ffu: return "MV_E_GC_UNKNOW GenICam unknown";
|
||
case 0x80000200u: return "MV_E_NOT_IMPLEMENTED not implemented";
|
||
case 0x80000202u: return "MV_E_WRITE_PROTECT write protected";
|
||
case 0x80000204u: return "MV_E_BUSY busy";
|
||
case 0x80000303u: return "MV_E_USB_GENICAM USB GenICam error";
|
||
case 0x80000304u: return "MV_E_USB_BANDWIDTH insufficient bandwidth";
|
||
default: return "unknown";
|
||
}
|
||
}
|
||
|
||
const char* mvsFeatureInterfaceTypeName(EMvsFeatureInterfaceType type)
|
||
{
|
||
switch (type)
|
||
{
|
||
case EMvsFeatureInterfaceType::Value: return "Value";
|
||
case EMvsFeatureInterfaceType::Base: return "Base";
|
||
case EMvsFeatureInterfaceType::Integer: return "Integer";
|
||
case EMvsFeatureInterfaceType::Boolean: return "Boolean";
|
||
case EMvsFeatureInterfaceType::Command: return "Command";
|
||
case EMvsFeatureInterfaceType::Float: return "Float";
|
||
case EMvsFeatureInterfaceType::String: return "String";
|
||
case EMvsFeatureInterfaceType::Register: return "Register";
|
||
case EMvsFeatureInterfaceType::Category: return "Category";
|
||
case EMvsFeatureInterfaceType::Enumeration: return "Enumeration";
|
||
case EMvsFeatureInterfaceType::EnumEntry: return "EnumEntry";
|
||
case EMvsFeatureInterfaceType::Port: return "Port";
|
||
case EMvsFeatureInterfaceType::Unknown:
|
||
default: return "Unknown";
|
||
}
|
||
}
|
||
|
||
const char* mvsFeatureAccessModeName(EMvsFeatureAccessMode mode)
|
||
{
|
||
switch (mode)
|
||
{
|
||
case EMvsFeatureAccessMode::NotImplemented: return "NI";
|
||
case EMvsFeatureAccessMode::NotAvailable: return "NA";
|
||
case EMvsFeatureAccessMode::WriteOnly: return "WO";
|
||
case EMvsFeatureAccessMode::ReadOnly: return "RO";
|
||
case EMvsFeatureAccessMode::ReadWrite: return "RW";
|
||
case EMvsFeatureAccessMode::Undefined: return "Undefined";
|
||
case EMvsFeatureAccessMode::CycleDetect: return "CycleDetect";
|
||
case EMvsFeatureAccessMode::Unknown:
|
||
default: return "Unknown";
|
||
}
|
||
}
|
||
|
||
bool mvsFeatureWritable(EMvsFeatureAccessMode mode)
|
||
{
|
||
return mode == EMvsFeatureAccessMode::WriteOnly ||
|
||
mode == EMvsFeatureAccessMode::ReadWrite;
|
||
}
|
||
|
||
void logMvsFeatureNodeInfo(IMvsDevice* camera, const char* role, const char* name)
|
||
{
|
||
if (!camera || !name)
|
||
return;
|
||
|
||
MvsFeatureNodeInfo info;
|
||
const int r = camera->GetFeatureNodeInfo(name, info);
|
||
std::ostringstream oss;
|
||
oss << "[CAM] " << (role ? role : "") << ' ' << name
|
||
<< " node ret=" << r << "(0x" << std::hex << static_cast<unsigned int>(r)
|
||
<< std::dec << ' ' << mvsSdkErrorName(r) << ')'
|
||
<< " type=" << mvsFeatureInterfaceTypeName(info.interfaceType)
|
||
<< " access=" << mvsFeatureAccessModeName(info.accessMode)
|
||
<< " typeRet=" << info.interfaceRet << "(0x" << std::hex
|
||
<< static_cast<unsigned int>(info.interfaceRet) << std::dec << ' '
|
||
<< mvsSdkErrorName(info.interfaceRet) << ')'
|
||
<< " accessRet=" << info.accessRet << "(0x" << std::hex
|
||
<< static_cast<unsigned int>(info.accessRet) << std::dec << ' '
|
||
<< mvsSdkErrorName(info.accessRet) << ')';
|
||
|
||
if (info.interfaceType == EMvsFeatureInterfaceType::Integer)
|
||
{
|
||
oss << " intRet=" << info.intInfoRet << "(0x" << std::hex
|
||
<< static_cast<unsigned int>(info.intInfoRet) << std::dec << ' '
|
||
<< mvsSdkErrorName(info.intInfoRet) << ')'
|
||
<< " cur=" << info.intInfo.current
|
||
<< " min=" << info.intInfo.minimum
|
||
<< " max=" << info.intInfo.maximum
|
||
<< " inc=" << info.intInfo.increment;
|
||
}
|
||
else if (info.interfaceType == EMvsFeatureInterfaceType::Enumeration)
|
||
{
|
||
oss << " enumRet=" << info.enumInfoRet << "(0x" << std::hex
|
||
<< static_cast<unsigned int>(info.enumInfoRet) << std::dec << ' '
|
||
<< mvsSdkErrorName(info.enumInfoRet) << ')'
|
||
<< " cur=" << info.enumCurrent
|
||
<< " entries=";
|
||
for (size_t i = 0; i < info.enumEntries.size(); ++i)
|
||
{
|
||
if (i) oss << ',';
|
||
oss << info.enumEntries[i].value;
|
||
if (!info.enumEntries[i].symbolic.empty())
|
||
oss << ':' << info.enumEntries[i].symbolic;
|
||
}
|
||
}
|
||
|
||
if (r == 0)
|
||
LOG_INFO("%s\n", oss.str().c_str());
|
||
else
|
||
LOG_WARN("%s\n", oss.str().c_str());
|
||
}
|
||
|
||
unsigned int alignEven(unsigned int value)
|
||
{
|
||
if (value < 2) return 2;
|
||
return (value + 1u) & ~1u;
|
||
}
|
||
|
||
#if DRONESCREW_HAS_RGA
|
||
const char* rgaErrorText(IM_STATUS status)
|
||
{
|
||
const char* text = imStrError(status);
|
||
return text ? text : "unknown";
|
||
}
|
||
#endif
|
||
|
||
QString buildRtspAdvertiseUrl(int port, const QString& path)
|
||
{
|
||
// Advertise the external RTSP pull URL. The internal RTMP publish port is separate.
|
||
QString localIp = "0.0.0.0";
|
||
const auto addrs = QNetworkInterface::allAddresses();
|
||
for (const auto& a : addrs)
|
||
{
|
||
if (a.protocol() == QAbstractSocket::IPv4Protocol && !a.isLoopback())
|
||
{
|
||
localIp = a.toString();
|
||
break;
|
||
}
|
||
}
|
||
return QString("rtsp://%1:%2%3").arg(localIp).arg(port).arg(path);
|
||
}
|
||
|
||
void logDetectionResultFixed(const char* tag, int frameNo, const DroneScrewResult& result)
|
||
{
|
||
const char* resultTag = tag ? tag : "DETECT";
|
||
const char* msg = result.message.empty() ? "" : result.message.c_str();
|
||
|
||
LOG_INFO("[%s-result] frame #%d id=%llu success=%d code=%d boxes=%zu distances=%zu image=%dx%d msg=%s\n",
|
||
resultTag, frameNo,
|
||
static_cast<unsigned long long>(result.frameId),
|
||
result.success ? 1 : 0, result.errorCode,
|
||
result.boxes.size(), result.distances.size(),
|
||
result.imageWidth, result.imageHeight, msg);
|
||
|
||
for (size_t i = 0; i < result.boxes.size(); ++i)
|
||
{
|
||
const auto& b = result.boxes[i];
|
||
LOG_INFO("[%s-result] box[%zu] cls=%d score=%.3f x=%d y=%d w=%d h=%d\n",
|
||
resultTag, i, b.classId, b.score, b.x, b.y, b.width, b.height);
|
||
}
|
||
|
||
for (size_t i = 0; i < result.distances.size(); ++i)
|
||
{
|
||
const auto& d = result.distances[i];
|
||
LOG_INFO("[%s-result] distance[%zu] from=%d to=%d mm=%.3f\n",
|
||
resultTag, i, d.fromId, d.toId, d.distanceMm);
|
||
}
|
||
}
|
||
|
||
QString savedImageFileName(unsigned long long index, const QString& prefix)
|
||
{
|
||
return QStringLiteral("%1_%2Image.png").arg(index).arg(prefix);
|
||
}
|
||
|
||
bool saveDetectionResultText(const QString& dirPath,
|
||
unsigned long long imageIndex,
|
||
const DroneScrewResult& result)
|
||
{
|
||
const QString filePath = QDir(dirPath).filePath(
|
||
QStringLiteral("%1_result.txt").arg(imageIndex));
|
||
QFile file(filePath);
|
||
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
|
||
{
|
||
LOG_WARN("[SAVE] result save open failed: %s\n",
|
||
filePath.toStdString().c_str());
|
||
return false;
|
||
}
|
||
|
||
QTextStream out(&file);
|
||
out << "index:" << static_cast<qulonglong>(imageIndex) << '\n';
|
||
out << "leftImage:" << savedImageFileName(imageIndex, QStringLiteral("left")) << '\n';
|
||
out << "rightImage:" << savedImageFileName(imageIndex, QStringLiteral("right")) << '\n';
|
||
out << "frameId:" << static_cast<qulonglong>(result.frameId) << '\n';
|
||
out << "timestampUs:" << static_cast<qlonglong>(result.timestampUs) << '\n';
|
||
out << "success:" << (result.success ? 1 : 0) << '\n';
|
||
out << "errorCode:" << result.errorCode << '\n';
|
||
out << "message:" << QString::fromStdString(result.message) << '\n';
|
||
|
||
int physicalHeightCount = 0;
|
||
for (const auto& b : result.boxes)
|
||
{
|
||
if (b.hasPhysicalHeight)
|
||
++physicalHeightCount;
|
||
}
|
||
out << "physicalHeightCount:" << physicalHeightCount << '\n';
|
||
int physicalHeightIndex = 1;
|
||
for (const auto& b : result.boxes)
|
||
{
|
||
if (!b.hasPhysicalHeight)
|
||
continue;
|
||
out << "physicalHeight" << physicalHeightIndex << "Id:" << physicalHeightIndex << '\n';
|
||
out << "physicalHeight" << physicalHeightIndex << "Mm:"
|
||
<< QString::number(b.physicalHeightMm, 'f', 3) << '\n';
|
||
++physicalHeightIndex;
|
||
}
|
||
|
||
out << "rodSpacingCount:" << static_cast<int>(result.distances.size()) << '\n';
|
||
for (size_t i = 0; i < result.distances.size(); ++i)
|
||
{
|
||
const auto& d = result.distances[i];
|
||
const int index = static_cast<int>(i + 1);
|
||
out << "rodSpacing" << index << "FromId:" << (d.fromId + 1) << '\n';
|
||
out << "rodSpacing" << index << "ToId:" << (d.toId + 1) << '\n';
|
||
out << "rodSpacing" << index << "Mm:"
|
||
<< QString::number(d.distanceMm, 'f', 3) << '\n';
|
||
}
|
||
out.flush();
|
||
if (out.status() != QTextStream::Ok)
|
||
return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
double clampCameraFloatFeature(IMvsDevice* camera,
|
||
const char* role,
|
||
const char* featureName,
|
||
double requested)
|
||
{
|
||
if (!camera || !featureName)
|
||
return requested;
|
||
|
||
MvsFloatFeatureInfo info;
|
||
const int ret = camera->GetFloatFeatureInfo(featureName, info);
|
||
if (ret != 0)
|
||
{
|
||
LOG_WARN("[CAM] %s read %s range failed ret=%d(0x%08x %s), requested=%.2f\n",
|
||
role, featureName, ret, static_cast<unsigned int>(ret),
|
||
mvsSdkErrorName(ret), requested);
|
||
return requested;
|
||
}
|
||
|
||
if (info.maximum < info.minimum || info.maximum <= 0.0)
|
||
return requested;
|
||
|
||
const double clamped = std::min(std::max(requested, info.minimum), info.maximum);
|
||
if (clamped != requested)
|
||
{
|
||
LOG_WARN("[CAM] %s clamp %s requested=%.2f range=[%.2f, %.2f] cur=%.2f use=%.2f\n",
|
||
role, featureName, requested, info.minimum, info.maximum,
|
||
info.current, clamped);
|
||
}
|
||
else
|
||
{
|
||
LOG_INFO("[CAM] %s %s range=[%.2f, %.2f] cur=%.2f requested=%.2f\n",
|
||
role, featureName, info.minimum, info.maximum,
|
||
info.current, requested);
|
||
}
|
||
return clamped;
|
||
}
|
||
|
||
void appendUniqueDir(QStringList& dirs, const QString& dir)
|
||
{
|
||
if (dir.isEmpty())
|
||
return;
|
||
|
||
const QString absolute = QDir(dir).absolutePath();
|
||
if (!dirs.contains(absolute))
|
||
dirs.append(absolute);
|
||
}
|
||
|
||
QStringList calibrationBaseDirs(const QString& serverConfigPath)
|
||
{
|
||
QStringList dirs;
|
||
appendUniqueDir(dirs, QDir::currentPath());
|
||
appendUniqueDir(dirs, QCoreApplication::applicationDirPath());
|
||
if (!serverConfigPath.isEmpty())
|
||
appendUniqueDir(dirs, QFileInfo(serverConfigPath).absolutePath());
|
||
return dirs;
|
||
}
|
||
|
||
QString resolveCandidatePath(const QString& path, const QStringList& baseDirs)
|
||
{
|
||
const QString trimmed = path.trimmed();
|
||
if (trimmed.isEmpty())
|
||
return QString();
|
||
|
||
const QFileInfo direct(trimmed);
|
||
if (direct.isAbsolute())
|
||
return direct.absoluteFilePath();
|
||
|
||
QString fallback;
|
||
for (const QString& baseDir : baseDirs)
|
||
{
|
||
const QFileInfo candidate(QDir(baseDir).filePath(trimmed));
|
||
if (fallback.isEmpty())
|
||
fallback = candidate.absoluteFilePath();
|
||
if (candidate.exists())
|
||
return candidate.absoluteFilePath();
|
||
}
|
||
return fallback.isEmpty() ? QFileInfo(trimmed).absoluteFilePath() : fallback;
|
||
}
|
||
|
||
QString yamlValueAfterColon(QString line)
|
||
{
|
||
const int comment = line.indexOf('#');
|
||
if (comment >= 0)
|
||
line = line.left(comment);
|
||
|
||
const int colon = line.indexOf(':');
|
||
if (colon < 0)
|
||
return QString();
|
||
|
||
QString value = line.mid(colon + 1).trimmed();
|
||
if (value.size() >= 2)
|
||
{
|
||
const QChar first = value.at(0);
|
||
const QChar last = value.at(value.size() - 1);
|
||
if ((first == '"' && last == '"') || (first == '\'' && last == '\''))
|
||
value = value.mid(1, value.size() - 2);
|
||
}
|
||
return value;
|
||
}
|
||
|
||
QString readCalibrationXmlPathFromYaml(const QString& configPath)
|
||
{
|
||
QFile file(configPath);
|
||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||
return QString();
|
||
|
||
QTextStream in(&file);
|
||
bool inCalibration = false;
|
||
int calibrationIndent = -1;
|
||
while (!in.atEnd())
|
||
{
|
||
const QString raw = in.readLine();
|
||
int indent = 0;
|
||
while (indent < raw.size() && raw.at(indent).isSpace())
|
||
++indent;
|
||
|
||
QString line = raw.mid(indent);
|
||
const int comment = line.indexOf('#');
|
||
if (comment >= 0)
|
||
line = line.left(comment);
|
||
line = line.trimmed();
|
||
if (line.isEmpty())
|
||
continue;
|
||
|
||
if (line.startsWith("calibration:"))
|
||
{
|
||
inCalibration = true;
|
||
calibrationIndent = indent;
|
||
continue;
|
||
}
|
||
|
||
if (inCalibration && indent <= calibrationIndent && !line.startsWith("-"))
|
||
inCalibration = false;
|
||
|
||
if (inCalibration && line.startsWith("xml_path:"))
|
||
return yamlValueAfterColon(line);
|
||
}
|
||
return QString();
|
||
}
|
||
|
||
QString childText(const QDomElement& parent, const QString& name)
|
||
{
|
||
const QDomElement child = parent.firstChildElement(name);
|
||
return child.isNull() ? QString() : child.text().trimmed();
|
||
}
|
||
|
||
int childInt(const QDomElement& parent, const QString& name, int fallback = 0)
|
||
{
|
||
bool ok = false;
|
||
const int value = childText(parent, name).toInt(&ok);
|
||
return ok ? value : fallback;
|
||
}
|
||
|
||
double childDouble(const QDomElement& parent, const QString& name, double fallback = 0.0)
|
||
{
|
||
bool ok = false;
|
||
const double value = childText(parent, name).toDouble(&ok);
|
||
return ok ? value : fallback;
|
||
}
|
||
|
||
QJsonObject matrixElementToJson(const QDomElement& parent, const QString& name)
|
||
{
|
||
QJsonObject matrix;
|
||
const QDomElement element = parent.firstChildElement(name);
|
||
const int rows = childInt(element, "rows");
|
||
const int cols = childInt(element, "cols");
|
||
matrix["rows"] = rows;
|
||
matrix["cols"] = cols;
|
||
|
||
QJsonArray data;
|
||
if (!element.isNull())
|
||
{
|
||
const QString text = childText(element, "data").simplified();
|
||
const QStringList tokens = text.split(' ', Qt::SkipEmptyParts);
|
||
for (const QString& token : tokens)
|
||
{
|
||
bool ok = false;
|
||
const double value = token.toDouble(&ok);
|
||
if (ok)
|
||
data.append(value);
|
||
}
|
||
}
|
||
matrix["data"] = data;
|
||
matrix["valid"] = rows > 0 && cols > 0 && data.size() == rows * cols;
|
||
return matrix;
|
||
}
|
||
|
||
QJsonObject cameraCalibrationToJson(const QDomElement& root, const QString& name)
|
||
{
|
||
QJsonObject camera;
|
||
const QDomElement element = root.firstChildElement(name);
|
||
if (element.isNull())
|
||
{
|
||
camera["valid"] = false;
|
||
return camera;
|
||
}
|
||
|
||
camera["valid"] = childInt(element, "Valid") != 0;
|
||
camera["rms"] = childDouble(element, "RMS");
|
||
camera["imageWidth"] = childInt(element, "ImageWidth");
|
||
camera["imageHeight"] = childInt(element, "ImageHeight");
|
||
camera["cameraMatrix"] = matrixElementToJson(element, "CameraMatrix");
|
||
camera["distCoeffs"] = matrixElementToJson(element, "DistCoeffs");
|
||
return camera;
|
||
}
|
||
|
||
QJsonObject stereoCalibrationToJson(const QDomElement& root)
|
||
{
|
||
QJsonObject stereoJson;
|
||
const QDomElement stereo = root.firstChildElement("Stereo");
|
||
const QDomElement rectification = root.firstChildElement("Rectification");
|
||
if (stereo.isNull())
|
||
{
|
||
stereoJson["valid"] = false;
|
||
return stereoJson;
|
||
}
|
||
|
||
stereoJson["valid"] = true;
|
||
stereoJson["rms"] = childDouble(stereo, "RMS");
|
||
stereoJson["baselineMm"] = childDouble(stereo, "Baseline");
|
||
stereoJson["R"] = matrixElementToJson(stereo, "R");
|
||
stereoJson["T"] = matrixElementToJson(stereo, "T");
|
||
stereoJson["E"] = matrixElementToJson(stereo, "E");
|
||
stereoJson["F"] = matrixElementToJson(stereo, "F");
|
||
stereoJson["R1"] = matrixElementToJson(rectification, "R1");
|
||
stereoJson["R2"] = matrixElementToJson(rectification, "R2");
|
||
stereoJson["P1"] = matrixElementToJson(rectification, "P1");
|
||
stereoJson["P2"] = matrixElementToJson(rectification, "P2");
|
||
stereoJson["Q"] = matrixElementToJson(rectification, "Q");
|
||
return stereoJson;
|
||
}
|
||
}
|
||
|
||
DroneScrewServerPresenter::DroneScrewServerPresenter(QObject* parent)
|
||
: QObject(parent)
|
||
{
|
||
// 初始化 MvsImageData
|
||
m_leftImageData.pData = nullptr;
|
||
m_leftImageData.width = 0;
|
||
m_leftImageData.height = 0;
|
||
m_leftImageData.dataSize = 0;
|
||
m_leftImageData.pixelFormat = 0;
|
||
m_leftImageData.frameID = 0;
|
||
m_leftImageData.timestamp = 0;
|
||
|
||
m_rightImageData.pData = nullptr;
|
||
m_rightImageData.width = 0;
|
||
m_rightImageData.height = 0;
|
||
m_rightImageData.dataSize = 0;
|
||
m_rightImageData.pixelFormat = 0;
|
||
m_rightImageData.frameID = 0;
|
||
m_rightImageData.timestamp = 0;
|
||
|
||
m_pReconnectTimer = new QTimer(this);
|
||
m_pReconnectTimer->setInterval(2000);
|
||
connect(m_pReconnectTimer, &QTimer::timeout,
|
||
this, &DroneScrewServerPresenter::onCameraReconnectTimer);
|
||
}
|
||
|
||
DroneScrewServerPresenter::~DroneScrewServerPresenter()
|
||
{
|
||
deinitAll();
|
||
|
||
// 释放图像数据
|
||
if (m_leftImageData.pData != nullptr)
|
||
{
|
||
delete[] m_leftImageData.pData;
|
||
m_leftImageData.pData = nullptr;
|
||
}
|
||
if (m_rightImageData.pData != nullptr)
|
||
{
|
||
delete[] m_rightImageData.pData;
|
||
m_rightImageData.pData = nullptr;
|
||
}
|
||
}
|
||
|
||
bool DroneScrewServerPresenter::loadConfiguration(const QString& configFilePath)
|
||
{
|
||
m_configFilePath = configFilePath;
|
||
|
||
QFile f(configFilePath);
|
||
if (!f.open(QIODevice::ReadOnly))
|
||
{
|
||
LOG_WARN("DroneScrewServer: open config fail: %s\n",
|
||
configFilePath.toStdString().c_str());
|
||
return false;
|
||
}
|
||
|
||
QXmlStreamReader xml(&f);
|
||
while (!xml.atEnd() && !xml.hasError())
|
||
{
|
||
if (xml.readNext() == QXmlStreamReader::StartElement)
|
||
{
|
||
QString n = xml.name().toString();
|
||
if (n == "Camera")
|
||
{
|
||
auto a = xml.attributes();
|
||
int index = a.value("index").toInt();
|
||
QString role = a.value("role").toString();
|
||
QString serialNumber = a.value("serialNumber").toString();
|
||
double exposureTime = a.value("exposureTime").toDouble();
|
||
double gain = a.value("gain").toDouble();
|
||
|
||
const bool isLeftRole = (role == "left") || (role.isEmpty() && index == 0);
|
||
const bool isRightRole = (role == "right") || (role.isEmpty() && index == 1);
|
||
|
||
if (isLeftRole)
|
||
{
|
||
m_nLeftCameraIndex = index;
|
||
m_strLeftCameraSerial = serialNumber.toStdString();
|
||
m_leftExposureTime = exposureTime > 0 ? exposureTime : 10000.0;
|
||
m_leftGain = gain > 0 ? gain : 1.0;
|
||
LOG_INFO("Left camera config: index=%d, serial=%s, exposure=%.2f, gain=%.2f\n",
|
||
m_nLeftCameraIndex, m_strLeftCameraSerial.c_str(),
|
||
m_leftExposureTime, m_leftGain);
|
||
}
|
||
else if (isRightRole)
|
||
{
|
||
m_nRightCameraIndex = index;
|
||
m_strRightCameraSerial = serialNumber.toStdString();
|
||
m_rightExposureTime = exposureTime > 0 ? exposureTime : 10000.0;
|
||
m_rightGain = gain > 0 ? gain : 1.0;
|
||
LOG_INFO("Right camera config: index=%d, serial=%s, exposure=%.2f, gain=%.2f\n",
|
||
m_nRightCameraIndex, m_strRightCameraSerial.c_str(),
|
||
m_rightExposureTime, m_rightGain);
|
||
}
|
||
else
|
||
{
|
||
LOG_WARN("[CAM] ignore camera config: role=%s index=%d\n",
|
||
role.toStdString().c_str(), index);
|
||
}
|
||
}
|
||
else if (n == "Zmq")
|
||
{
|
||
auto a = xml.attributes();
|
||
m_zmqControlPort = a.value("controlPort").toInt();
|
||
m_zmqResultPort = a.value("resultPort").toInt();
|
||
m_zmqRawImagePort = a.value("rawImagePort").toInt();
|
||
if (!m_zmqControlPort) m_zmqControlPort = 5555;
|
||
if (!m_zmqResultPort) m_zmqResultPort = 5556;
|
||
if (m_zmqRawImagePort < 0) m_zmqRawImagePort = 0;
|
||
}
|
||
else if (n == "Rtsp")
|
||
{
|
||
auto a = xml.attributes();
|
||
m_rtspPort = a.value("port").toInt();
|
||
if (m_rtspPort <= 0) m_rtspPort = kRtspPullPort;
|
||
if (m_rtspPort == kRtmpPushPort)
|
||
{
|
||
LOG_WARN("[RTSP] configured pull port=%d is RTMP publish port, use RTSP pull port=%d\n",
|
||
m_rtspPort, kRtspPullPort);
|
||
m_rtspPort = kRtspPullPort;
|
||
}
|
||
m_rtspPath = a.value("path").toString();
|
||
if (m_rtspPath.isEmpty()) m_rtspPath = "/live/dronescrew";
|
||
m_pushBitrateKbps = a.value("bitrateKbps").toInt();
|
||
if (!m_pushBitrateKbps) m_pushBitrateKbps = 4096;
|
||
m_pushFps = a.value("fps").toInt();
|
||
if (m_pushFps < kLiveStreamFrameRate)
|
||
{
|
||
if (m_pushFps > 0)
|
||
{
|
||
LOG_WARN("[RTSP] configured fps=%d is legacy/too low, use live fps=%d\n",
|
||
m_pushFps, kLiveStreamFrameRate);
|
||
}
|
||
m_pushFps = kLiveStreamFrameRate;
|
||
}
|
||
m_liveStreamFps = m_pushFps;
|
||
const unsigned int streamWidth = a.value("streamWidth").toUInt();
|
||
const unsigned int streamHeight = a.value("streamHeight").toUInt();
|
||
if (streamWidth > 0 && streamHeight > 0)
|
||
{
|
||
m_liveStreamWidth = streamWidth;
|
||
m_liveStreamHeight = streamHeight;
|
||
}
|
||
const QString streamCamera =
|
||
a.value("streamCamera").toString().trimmed().toLower();
|
||
if (streamCamera == "left")
|
||
{
|
||
m_liveStreamCameraRole = "left";
|
||
}
|
||
else if (streamCamera == "right")
|
||
{
|
||
m_liveStreamCameraRole = "left";
|
||
LOG_WARN("[RTSP] streamCamera=right ignored, realtime stream uses left camera\n");
|
||
}
|
||
else if (!streamCamera.isEmpty())
|
||
{
|
||
LOG_WARN("[RTSP] invalid streamCamera=%s, keep %s\n",
|
||
streamCamera.toStdString().c_str(),
|
||
m_liveStreamCameraRole.c_str());
|
||
}
|
||
}
|
||
else if (n == "Algorithm")
|
||
{
|
||
auto a = xml.attributes();
|
||
m_algoParams.scoreThreshold = a.value("scoreThreshold").toFloat();
|
||
m_algoParams.nmsThreshold = a.value("nmsThreshold").toFloat();
|
||
m_algoParams.inputWidth = a.value("inputWidth").toInt();
|
||
m_algoParams.inputHeight = a.value("inputHeight").toInt();
|
||
const QString configPath = a.hasAttribute("configPath")
|
||
? a.value("configPath").toString()
|
||
: a.value("modelPath").toString();
|
||
m_algoParams.modelPath = configPath.toStdString();
|
||
m_algoParams.modelType = a.value("modelType").toInt();
|
||
if (a.hasAttribute("expectedBoltCount"))
|
||
m_algoParams.expectedBoltCount = a.value("expectedBoltCount").toInt();
|
||
}
|
||
else if (n == "Trigger")
|
||
{
|
||
auto a = xml.attributes();
|
||
if (a.hasAttribute("useIoTrigger"))
|
||
m_useIoTrigger = (a.value("useIoTrigger").toString() != "false" &&
|
||
a.value("useIoTrigger").toString() != "0");
|
||
if (a.hasAttribute("gpio"))
|
||
m_triggerGpio = a.value("gpio").toInt();
|
||
if (a.hasAttribute("source"))
|
||
m_triggerSource = a.value("source").toInt();
|
||
if (a.hasAttribute("activation"))
|
||
m_triggerActivation = a.value("activation").toInt();
|
||
}
|
||
}
|
||
}
|
||
f.close();
|
||
return !xml.hasError();
|
||
}
|
||
|
||
int DroneScrewServerPresenter::initAll()
|
||
{
|
||
// 1) 算法
|
||
m_pAlgo = IDroneScrewAlgo::CreateDefault();
|
||
if (m_pAlgo)
|
||
{
|
||
int r = m_pAlgo->Init(m_algoParams);
|
||
LOG_INFO("Algo init: %d, ver=%s\n", r, m_pAlgo->GetVersion().c_str());
|
||
}
|
||
|
||
// 2) 推流 URL 先按配置生成;推流对象等拿到相机实际尺寸后再初始化。
|
||
m_rtspAdvertiseUrl = buildRtspAdvertiseUrl(m_rtspPort, m_rtspPath);
|
||
LOG_INFO("RTSP configured: URL=%s fps=%d bitrate=%dKbps streamCamera=%s\n",
|
||
m_rtspAdvertiseUrl.toStdString().c_str(), m_pushFps,
|
||
m_pushBitrateKbps, m_liveStreamCameraRole.c_str());
|
||
|
||
// 3) 相机
|
||
if (tryConnectCamera() != 0)
|
||
{
|
||
LOG_WARN("Camera connect fail, start auto-reconnect timer\n");
|
||
m_pReconnectTimer->start();
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
void DroneScrewServerPresenter::deinitAll()
|
||
{
|
||
stopDetectionWork();
|
||
stopLiveStream();
|
||
stopGpioTriggerLoop();
|
||
stopImageSaveThread();
|
||
|
||
if (m_pReconnectTimer && m_pReconnectTimer->isActive())
|
||
m_pReconnectTimer->stop();
|
||
|
||
closeCamera();
|
||
|
||
releaseRtspPusher();
|
||
|
||
if (m_pAlgo)
|
||
{
|
||
m_pAlgo->UnInit();
|
||
m_pAlgo.reset();
|
||
}
|
||
}
|
||
|
||
int DroneScrewServerPresenter::tryConnectCamera()
|
||
{
|
||
LOG_DEBUG("========== 开始连接双目相机 ==========\n");
|
||
|
||
if (m_pLeftCamera) closeCamera();
|
||
|
||
// 失败时自动清理
|
||
auto fail = [this](const char* reason) {
|
||
LOG_DEBUG("[CAM] fail: %s\n", reason);
|
||
closeCamera();
|
||
return ERR_CODE(DEV_OPEN_ERR);
|
||
};
|
||
|
||
int ret = IMvsDevice::CreateObject(&m_pLeftCamera);
|
||
if (ret != 0 || !m_pLeftCamera) return fail("create left");
|
||
ret = IMvsDevice::CreateObject(&m_pRightCamera);
|
||
if (ret != 0 || !m_pRightCamera) return fail("create right");
|
||
|
||
ret = m_pLeftCamera->InitSDK();
|
||
if (ret != 0) return fail("InitSDK");
|
||
|
||
std::vector<MvsDeviceInfo> deviceList;
|
||
ret = m_pLeftCamera->EnumerateDevices(deviceList);
|
||
LOG_DEBUG("[CAM] enumerate: ret=%d count=%zu\n", ret, deviceList.size());
|
||
for (size_t i = 0; i < deviceList.size(); i++)
|
||
LOG_DEBUG("[CAM] [%zu] SN=%s model=%s\n", i,
|
||
deviceList[i].serialNumber.c_str(), deviceList[i].modelName.c_str());
|
||
|
||
if (deviceList.size() < 2)
|
||
return fail("only 1 camera");
|
||
|
||
// 逻辑左右目按配置的 serialNumber 或 index 绑定,允许现场交换安装方向。
|
||
auto configuredSerial = [&](const char* role,
|
||
unsigned int index,
|
||
const std::string& serial) -> std::string {
|
||
if (!serial.empty())
|
||
return serial;
|
||
if (index >= deviceList.size())
|
||
{
|
||
LOG_ERROR("[CAM] %s configured index=%u out of range, device count=%zu\n",
|
||
role, index, deviceList.size());
|
||
return std::string();
|
||
}
|
||
return deviceList[index].serialNumber;
|
||
};
|
||
|
||
const std::string leftSN =
|
||
configuredSerial("left", m_nLeftCameraIndex, m_strLeftCameraSerial);
|
||
const std::string rightSN =
|
||
configuredSerial("right", m_nRightCameraIndex, m_strRightCameraSerial);
|
||
if (leftSN.empty() || rightSN.empty())
|
||
return fail("camera config index/serial invalid");
|
||
if (leftSN == rightSN)
|
||
{
|
||
LOG_ERROR("[CAM] left/right resolved to same camera SN=%s\n", leftSN.c_str());
|
||
return fail("left/right same camera");
|
||
}
|
||
LOG_DEBUG("[CAM] left index=%u -> SN=%s\n", m_nLeftCameraIndex, leftSN.c_str());
|
||
LOG_DEBUG("[CAM] right index=%u -> SN=%s\n", m_nRightCameraIndex, rightSN.c_str());
|
||
|
||
ret = m_pLeftCamera->OpenDevice(leftSN);
|
||
LOG_DEBUG("[CAM] left OpenDevice ret=%d\n", ret);
|
||
if (ret != 0) return fail("left OpenDevice");
|
||
|
||
ret = m_pRightCamera->OpenDevice(rightSN);
|
||
LOG_DEBUG("[CAM] right OpenDevice ret=%d\n", ret);
|
||
if (ret != 0) return fail("right OpenDevice");
|
||
|
||
// Persist the resolved serials later so left/right binding survives device
|
||
// enumeration order changes after restart.
|
||
m_strLeftCameraSerial = leftSN;
|
||
m_strRightCameraSerial = rightSN;
|
||
|
||
ret = configureCamera(m_pLeftCamera, "left", m_useIoTrigger, kPrecisionFrameRate,
|
||
kPrecisionBinningHorizontal, kPrecisionBinningVertical);
|
||
if (ret != 0) return fail("configure left");
|
||
ret = configureCamera(m_pRightCamera, "right", m_useIoTrigger, kPrecisionFrameRate,
|
||
kPrecisionBinningHorizontal, kPrecisionBinningVertical);
|
||
if (ret != 0) return fail("configure right");
|
||
LOG_DEBUG("[CAM] mono8 continuous fps=%.1f trigger=%s left_exp=%.1f left_gain=%.1f right_exp=%.1f right_gain=%.1f\n",
|
||
kPrecisionFrameRate, m_useIoTrigger ? "io" : "off",
|
||
m_leftExposureTime, m_leftGain, m_rightExposureTime, m_rightGain);
|
||
|
||
ret = m_pLeftCamera->RegisterImageCallback(
|
||
[this](const MvsImageData& img) { this->leftCameraCallback(img); });
|
||
LOG_DEBUG("[CAM] left callback registered ret=%d\n", ret);
|
||
if (ret != 0) return fail("register left callback");
|
||
|
||
ret = m_pRightCamera->RegisterImageCallback(
|
||
[this](const MvsImageData& img) { this->rightCameraCallback(img); });
|
||
LOG_DEBUG("[CAM] right callback registered ret=%d\n", ret);
|
||
if (ret != 0) return fail("register right callback");
|
||
|
||
m_bCameraConnected = true;
|
||
resetFrameReadyFlags();
|
||
LOG_DEBUG("[CAM] 双目连接成功,等待 start/start_stream 进入采集\n");
|
||
return 0;
|
||
}
|
||
|
||
double DroneScrewServerPresenter::exposureForRole(const char* role) const
|
||
{
|
||
return (role && std::strcmp(role, "right") == 0) ? m_rightExposureTime : m_leftExposureTime;
|
||
}
|
||
|
||
int DroneScrewServerPresenter::applyExposureForRole(const char* role)
|
||
{
|
||
IMvsDevice* camera = cameraForRole(role);
|
||
if (!camera)
|
||
{
|
||
LOG_WARN("[CAM] %s exposure update skipped: camera not initialized\n",
|
||
role ? role : "");
|
||
return ERR_CODE(DEV_ARG_INVAILD);
|
||
}
|
||
|
||
const double requestedExpVal = exposureForRole(role);
|
||
const double expVal = clampCameraFloatFeature(camera, role, "ExposureTime", requestedExpVal);
|
||
const int ret = camera->SetExposureTime(expVal);
|
||
if (ret != 0)
|
||
{
|
||
LOG_WARN("[CAM] %s exposure update failed ret=%d(0x%08x %s) exp=%.2f requestedExp=%.2f\n",
|
||
role ? role : "", ret, static_cast<unsigned int>(ret), mvsSdkErrorName(ret),
|
||
expVal, requestedExpVal);
|
||
}
|
||
else
|
||
{
|
||
LOG_INFO("[CAM] %s exposure updated: exp=%.2f requestedExp=%.2f\n",
|
||
role ? role : "", expVal, requestedExpVal);
|
||
}
|
||
return ret;
|
||
}
|
||
|
||
int DroneScrewServerPresenter::applyCurrentExposureForRole(const char* role)
|
||
{
|
||
return applyExposureForRole(role);
|
||
}
|
||
|
||
double DroneScrewServerPresenter::gainForRole(const char* role) const
|
||
{
|
||
return (role && std::strcmp(role, "right") == 0) ? m_rightGain : m_leftGain;
|
||
}
|
||
|
||
IMvsDevice* DroneScrewServerPresenter::cameraForRole(const char* role) const
|
||
{
|
||
return (role && std::strcmp(role, "right") == 0) ? m_pRightCamera : m_pLeftCamera;
|
||
}
|
||
|
||
const char* DroneScrewServerPresenter::liveStreamCameraRole() const
|
||
{
|
||
return m_liveStreamCameraRole == "right" ? "right" : "left";
|
||
}
|
||
|
||
IMvsDevice* DroneScrewServerPresenter::liveStreamCamera() const
|
||
{
|
||
return cameraForRole(liveStreamCameraRole());
|
||
}
|
||
|
||
IMvsDevice* DroneScrewServerPresenter::nonLiveStreamCamera() const
|
||
{
|
||
return cameraForRole(std::strcmp(liveStreamCameraRole(), "right") == 0 ? "left" : "right");
|
||
}
|
||
|
||
bool DroneScrewServerPresenter::isLiveStreamCameraRole(const char* role) const
|
||
{
|
||
return role && std::strcmp(role, liveStreamCameraRole()) == 0;
|
||
}
|
||
|
||
int DroneScrewServerPresenter::configureCamera(IMvsDevice* camera,
|
||
const char* role,
|
||
bool ioTrigger,
|
||
double frameRate,
|
||
int64_t requestedBinningHorizontal,
|
||
int64_t requestedBinningVertical,
|
||
int64_t requestedDecimationHorizontal,
|
||
int64_t requestedDecimationVertical)
|
||
{
|
||
if (!camera) return ERR_CODE(DEV_ARG_INVAILD);
|
||
|
||
int ret = 0;
|
||
auto apply = [&](const char* name, int r) {
|
||
if (r != 0)
|
||
{
|
||
ret = ret == 0 ? r : ret;
|
||
LOG_WARN("[CAM] %s set %s failed ret=%d(0x%08x %s)\n",
|
||
role, name, r, static_cast<unsigned int>(r),
|
||
mvsSdkErrorName(r));
|
||
}
|
||
return r;
|
||
};
|
||
auto applyImageReduction = [&](const char* name, int64_t value) {
|
||
MvsFeatureNodeInfo nodeInfo;
|
||
int r = camera->GetFeatureNodeInfo(name, nodeInfo);
|
||
if (r != 0)
|
||
{
|
||
if (value != 1)
|
||
ret = ret == 0 ? r : ret;
|
||
LOG_WARN("[CAM] %s read %s node failed ret=%d(0x%08x %s)%s\n",
|
||
role, name, r, static_cast<unsigned int>(r), mvsSdkErrorName(r),
|
||
value == 1 ? " (ignored for 1x1)" : "");
|
||
logMvsFeatureNodeInfo(camera, role, name);
|
||
return r;
|
||
}
|
||
|
||
logMvsFeatureNodeInfo(camera, role, name);
|
||
|
||
if (!mvsFeatureWritable(nodeInfo.accessMode))
|
||
{
|
||
r = ERR_CODE(DEV_CTRL_ERR);
|
||
if (value != 1)
|
||
ret = ret == 0 ? r : ret;
|
||
LOG_WARN("[CAM] %s %s is not writable access=%s%s\n",
|
||
role, name, mvsFeatureAccessModeName(nodeInfo.accessMode),
|
||
value == 1 ? " (ignored for 1x1)" : "");
|
||
return r;
|
||
}
|
||
|
||
if (nodeInfo.interfaceType == EMvsFeatureInterfaceType::Integer)
|
||
{
|
||
r = camera->SetIntFeature(name, value);
|
||
}
|
||
else if (nodeInfo.interfaceType == EMvsFeatureInterfaceType::Enumeration)
|
||
{
|
||
std::string targetSymbolic;
|
||
unsigned int targetEnumValue = 0;
|
||
bool foundTarget = false;
|
||
const std::string targetValue = std::to_string(value);
|
||
const std::string targetSymbolicByName = std::string(name) + targetValue;
|
||
for (const MvsEnumEntryInfo& entry : nodeInfo.enumEntries)
|
||
{
|
||
if (entry.value == static_cast<unsigned int>(value) ||
|
||
entry.symbolic == targetValue ||
|
||
entry.symbolic == targetSymbolicByName)
|
||
{
|
||
targetSymbolic = entry.symbolic;
|
||
targetEnumValue = entry.value;
|
||
foundTarget = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!foundTarget)
|
||
{
|
||
r = ERR_CODE(DEV_CTRL_ERR);
|
||
if (value != 1)
|
||
ret = ret == 0 ? r : ret;
|
||
LOG_WARN("[CAM] %s %s has no enum entry for %lld\n",
|
||
role, name, static_cast<long long>(value));
|
||
return r;
|
||
}
|
||
|
||
if (!targetSymbolic.empty())
|
||
r = camera->SetEnumFeatureByString(name, targetSymbolic);
|
||
else
|
||
r = camera->SetEnumFeature(name, targetEnumValue);
|
||
}
|
||
else
|
||
{
|
||
r = ERR_CODE(DEV_CTRL_ERR);
|
||
if (value != 1)
|
||
ret = ret == 0 ? r : ret;
|
||
LOG_WARN("[CAM] %s %s unsupported node type=%s%s\n",
|
||
role, name, mvsFeatureInterfaceTypeName(nodeInfo.interfaceType),
|
||
value == 1 ? " (ignored for 1x1)" : "");
|
||
return r;
|
||
}
|
||
|
||
if (r != 0)
|
||
{
|
||
if (value != 1)
|
||
ret = ret == 0 ? r : ret;
|
||
LOG_WARN("[CAM] %s set %s=%lld failed ret=%d(0x%08x %s)%s\n",
|
||
role, name, static_cast<long long>(value),
|
||
r, static_cast<unsigned int>(r), mvsSdkErrorName(r),
|
||
value == 1 ? " (ignored for 1x1)" : "");
|
||
logMvsFeatureNodeInfo(camera, role, name);
|
||
}
|
||
else
|
||
{
|
||
LOG_INFO("[CAM] %s set %s=%lld ok via %s\n",
|
||
role, name, static_cast<long long>(value),
|
||
mvsFeatureInterfaceTypeName(nodeInfo.interfaceType));
|
||
}
|
||
return r;
|
||
};
|
||
|
||
apply("AcquisitionMode=Continuous", camera->SetEnumFeature("AcquisitionMode", kMvsAcqModeContinuous));
|
||
apply("PixelFormat=Mono8", camera->SetEnumFeature("PixelFormat", kMvsPixelTypeMono8));
|
||
logMvsFeatureNodeInfo(camera, role, "BinningSelector");
|
||
const bool requestDecimation = requestedDecimationHorizontal > 1 || requestedDecimationVertical > 1;
|
||
if (requestDecimation)
|
||
{
|
||
applyImageReduction("BinningHorizontal", requestedBinningHorizontal);
|
||
applyImageReduction("BinningVertical", requestedBinningVertical);
|
||
applyImageReduction("DecimationHorizontal", requestedDecimationHorizontal);
|
||
applyImageReduction("DecimationVertical", requestedDecimationVertical);
|
||
}
|
||
else
|
||
{
|
||
applyImageReduction("DecimationHorizontal", requestedDecimationHorizontal);
|
||
applyImageReduction("DecimationVertical", requestedDecimationVertical);
|
||
applyImageReduction("BinningHorizontal", requestedBinningHorizontal);
|
||
applyImageReduction("BinningVertical", requestedBinningVertical);
|
||
}
|
||
if (ioTrigger)
|
||
{
|
||
apply("TriggerSource=Line0", camera->SetEnumFeature("TriggerSource", m_triggerSource));
|
||
apply("TriggerActivation=RisingEdge", camera->SetEnumFeature("TriggerActivation", m_triggerActivation));
|
||
apply("TriggerMode=On", camera->SetTriggerMode(true));
|
||
// 外触发模式:帧率完全由触发信号决定,必须关闭帧率限制器。
|
||
// 否则左右相机各自的帧率限制器在触发抖动时可能丢弃【不同】的触发,
|
||
// 造成两路帧号错位 → 双目永久不同步。
|
||
apply("AcquisitionFrameRateEnable=false", camera->SetBoolFeature("AcquisitionFrameRateEnable", false));
|
||
}
|
||
else
|
||
{
|
||
apply("TriggerMode=Off", camera->SetTriggerMode(false));
|
||
// 非触发(自由运行)模式才用帧率限制控制采集速度
|
||
apply("AcquisitionFrameRateEnable=true", camera->SetBoolFeature("AcquisitionFrameRateEnable", true));
|
||
apply("AcquisitionFrameRate", camera->SetFrameRate(frameRate));
|
||
}
|
||
|
||
const double gainVal = gainForRole(role);
|
||
LOG_INFO("[CAM] %s applying config: gain=%.2f binning=%lldx%lld decimation=%lldx%lld\n",
|
||
role, gainVal,
|
||
static_cast<long long>(requestedBinningHorizontal),
|
||
static_cast<long long>(requestedBinningVertical),
|
||
static_cast<long long>(requestedDecimationHorizontal),
|
||
static_cast<long long>(requestedDecimationVertical));
|
||
apply("ExposureTime", applyExposureForRole(role));
|
||
apply("Gain", camera->SetGain(gainVal));
|
||
|
||
int64_t pixelFormat = 0;
|
||
int64_t acquisitionMode = 0;
|
||
int64_t triggerMode = 0;
|
||
int64_t triggerSource = 0;
|
||
int64_t triggerActivation = 0;
|
||
bool frameRateEnable = false;
|
||
double actualFrameRate = 0.0;
|
||
double exposureTime = 0.0;
|
||
double gain = 0.0;
|
||
unsigned int width = 0;
|
||
unsigned int height = 0;
|
||
if (camera->GetEnumFeature("PixelFormat", pixelFormat) == 0)
|
||
LOG_INFO("[CAM] %s PixelFormat=0x%llx\n", role, static_cast<long long>(pixelFormat));
|
||
if (camera->GetEnumFeature("AcquisitionMode", acquisitionMode) == 0)
|
||
LOG_INFO("[CAM] %s AcquisitionMode=%lld\n", role, static_cast<long long>(acquisitionMode));
|
||
if (camera->GetEnumFeature("TriggerMode", triggerMode) == 0)
|
||
LOG_INFO("[CAM] %s TriggerMode=%lld\n", role, static_cast<long long>(triggerMode));
|
||
if (ioTrigger && camera->GetEnumFeature("TriggerSource", triggerSource) == 0)
|
||
LOG_INFO("[CAM] %s TriggerSource=%lld\n", role, static_cast<long long>(triggerSource));
|
||
if (ioTrigger && camera->GetEnumFeature("TriggerActivation", triggerActivation) == 0)
|
||
LOG_INFO("[CAM] %s TriggerActivation=%lld\n", role, static_cast<long long>(triggerActivation));
|
||
if (camera->GetBoolFeature("AcquisitionFrameRateEnable", frameRateEnable) == 0)
|
||
LOG_INFO("[CAM] %s AcquisitionFrameRateEnable=%d\n", role, frameRateEnable ? 1 : 0);
|
||
if (camera->GetFrameRate(actualFrameRate) == 0)
|
||
LOG_INFO("[CAM] %s AcquisitionFrameRate=%.2f\n", role, actualFrameRate);
|
||
if (camera->GetExposureTime(exposureTime) == 0)
|
||
LOG_INFO("[CAM] %s ExposureTime=%.2f\n", role, exposureTime);
|
||
if (camera->GetGain(gain) == 0)
|
||
LOG_INFO("[CAM] %s Gain=%.2f\n", role, gain);
|
||
if (!requestDecimation)
|
||
{
|
||
logMvsFeatureNodeInfo(camera, role, "BinningHorizontal");
|
||
logMvsFeatureNodeInfo(camera, role, "BinningVertical");
|
||
}
|
||
else
|
||
{
|
||
LOG_INFO("[CAM] %s skip BinningHorizontal/Vertical post-read while decimation=%lldx%lld\n",
|
||
role,
|
||
static_cast<long long>(requestedDecimationHorizontal),
|
||
static_cast<long long>(requestedDecimationVertical));
|
||
}
|
||
logMvsFeatureNodeInfo(camera, role, "DecimationHorizontal");
|
||
logMvsFeatureNodeInfo(camera, role, "DecimationVertical");
|
||
if (camera->GetWidth(width) == 0 && camera->GetHeight(height) == 0)
|
||
LOG_INFO("[CAM] %s Size=%ux%u\n", role, width, height);
|
||
|
||
if (ret != 0)
|
||
LOG_WARN("[CAM] %s configure failed ret=%d(0x%08x %s)\n",
|
||
role, ret, static_cast<unsigned int>(ret), mvsSdkErrorName(ret));
|
||
else
|
||
LOG_INFO("[CAM] %s configure ok\n", role);
|
||
|
||
return ret;
|
||
}
|
||
|
||
bool DroneScrewServerPresenter::areCamerasGrabbing()
|
||
{
|
||
return m_pLeftCamera && m_pRightCamera &&
|
||
m_pLeftCamera->IsAcquisitioning() &&
|
||
m_pRightCamera->IsAcquisitioning();
|
||
}
|
||
|
||
int DroneScrewServerPresenter::prepareDetectionAcquisition(double frameRate,
|
||
int64_t binningHorizontal,
|
||
int64_t binningVertical,
|
||
int64_t decimationHorizontal,
|
||
int64_t decimationVertical,
|
||
const char* tag)
|
||
{
|
||
if (!m_pLeftCamera || !m_pRightCamera)
|
||
return ERR_CODE(DRONESCREW_ERR_CAMERA_NOT_CONNECTED);
|
||
|
||
if (m_pLeftCamera->IsAcquisitioning())
|
||
{
|
||
const int ret = m_pLeftCamera->StopAcquisition();
|
||
LOG_DEBUG("[DETECT] left StopAcquisition before configure ret=%d\n", ret);
|
||
}
|
||
if (m_pRightCamera->IsAcquisitioning())
|
||
{
|
||
const int ret = m_pRightCamera->StopAcquisition();
|
||
LOG_DEBUG("[DETECT] right StopAcquisition before configure ret=%d\n", ret);
|
||
}
|
||
|
||
int ret = configureCamera(m_pLeftCamera, "left", m_useIoTrigger, frameRate,
|
||
binningHorizontal, binningVertical,
|
||
decimationHorizontal, decimationVertical);
|
||
if (ret != 0)
|
||
{
|
||
LOG_ERROR("[%s] configure left failed sdkRet=%d(0x%08x %s)\n",
|
||
tag ? tag : "DETECT", ret, static_cast<unsigned int>(ret),
|
||
mvsSdkErrorName(ret));
|
||
return ERR_CODE(DRONESCREW_ERR_DETECT_CONFIG_LEFT);
|
||
}
|
||
|
||
ret = configureCamera(m_pRightCamera, "right", m_useIoTrigger, frameRate,
|
||
binningHorizontal, binningVertical,
|
||
decimationHorizontal, decimationVertical);
|
||
if (ret != 0)
|
||
{
|
||
LOG_ERROR("[%s] configure right failed sdkRet=%d(0x%08x %s)\n",
|
||
tag ? tag : "DETECT", ret, static_cast<unsigned int>(ret),
|
||
mvsSdkErrorName(ret));
|
||
return ERR_CODE(DRONESCREW_ERR_DETECT_CONFIG_RIGHT);
|
||
}
|
||
|
||
resetFrameReadyFlags();
|
||
|
||
ret = m_pLeftCamera->StartAcquisition();
|
||
LOG_DEBUG("[DETECT] left StartAcquisition sdkRet=%d(0x%08x %s)\n",
|
||
ret, static_cast<unsigned int>(ret), mvsSdkErrorName(ret));
|
||
if (ret != 0)
|
||
{
|
||
LOG_ERROR("[DETECT] left StartAcquisition failed sdkRet=%d(0x%08x %s)\n",
|
||
ret, static_cast<unsigned int>(ret), mvsSdkErrorName(ret));
|
||
return ERR_CODE(DRONESCREW_ERR_DETECT_START_LEFT);
|
||
}
|
||
|
||
ret = m_pRightCamera->StartAcquisition();
|
||
LOG_DEBUG("[DETECT] right StartAcquisition sdkRet=%d(0x%08x %s)\n",
|
||
ret, static_cast<unsigned int>(ret), mvsSdkErrorName(ret));
|
||
if (ret != 0)
|
||
{
|
||
LOG_ERROR("[DETECT] right StartAcquisition failed sdkRet=%d(0x%08x %s)\n",
|
||
ret, static_cast<unsigned int>(ret), mvsSdkErrorName(ret));
|
||
if (m_pLeftCamera->IsAcquisitioning())
|
||
m_pLeftCamera->StopAcquisition();
|
||
return ERR_CODE(DRONESCREW_ERR_DETECT_START_RIGHT);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
int DroneScrewServerPresenter::prepareLiveStreamAcquisition()
|
||
{
|
||
IMvsDevice* streamCamera = liveStreamCamera();
|
||
IMvsDevice* otherCamera = nonLiveStreamCamera();
|
||
const char* streamRole = liveStreamCameraRole();
|
||
if (!streamCamera)
|
||
return ERR_CODE(DRONESCREW_ERR_CAMERA_NOT_CONNECTED);
|
||
|
||
if (otherCamera && otherCamera->IsAcquisitioning())
|
||
{
|
||
const int ret = otherCamera->StopAcquisition();
|
||
LOG_DEBUG("[LIVE] non-stream camera StopAcquisition role=%s ret=%d\n",
|
||
std::strcmp(streamRole, "right") == 0 ? "left" : "right", ret);
|
||
}
|
||
if (streamCamera->IsAcquisitioning())
|
||
{
|
||
const int ret = streamCamera->StopAcquisition();
|
||
LOG_DEBUG("[LIVE] stream camera StopAcquisition before configure role=%s ret=%d\n",
|
||
streamRole, ret);
|
||
}
|
||
|
||
int ret = configureCamera(streamCamera, streamRole, false,
|
||
static_cast<double>(m_liveStreamFps),
|
||
kDistanceBinningHorizontal,
|
||
kDistanceBinningVertical,
|
||
kDistanceDecimationHorizontal,
|
||
kDistanceDecimationVertical);
|
||
if (ret != 0)
|
||
{
|
||
LOG_ERROR("[LIVE] configure stream camera role=%s failed sdkRet=%d(0x%08x %s)\n",
|
||
streamRole, ret, static_cast<unsigned int>(ret),
|
||
mvsSdkErrorName(ret));
|
||
return ERR_CODE(DRONESCREW_ERR_LIVE_CONFIG_LEFT);
|
||
}
|
||
|
||
resetFrameReadyFlags();
|
||
|
||
ret = streamCamera->StartAcquisition();
|
||
LOG_DEBUG("[LIVE] stream camera role=%s StartAcquisition sdkRet=%d(0x%08x %s)\n",
|
||
streamRole, ret, static_cast<unsigned int>(ret), mvsSdkErrorName(ret));
|
||
if (ret != 0)
|
||
{
|
||
LOG_ERROR("[LIVE] stream camera role=%s StartAcquisition failed sdkRet=%d(0x%08x %s)\n",
|
||
streamRole, ret, static_cast<unsigned int>(ret), mvsSdkErrorName(ret));
|
||
return ERR_CODE(DRONESCREW_ERR_LIVE_START_LEFT);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
void DroneScrewServerPresenter::resetFrameReadyFlags()
|
||
{
|
||
QMutexLocker lk(&m_frameMutex);
|
||
m_bLeftImageReady = false;
|
||
m_bRightImageReady = false;
|
||
}
|
||
|
||
void DroneScrewServerPresenter::closeCamera()
|
||
{
|
||
if (m_pLeftCamera)
|
||
{
|
||
if (m_pLeftCamera->IsAcquisitioning())
|
||
m_pLeftCamera->StopAcquisition();
|
||
m_pLeftCamera->UnregisterImageCallback();
|
||
m_pLeftCamera->CloseDevice();
|
||
delete m_pLeftCamera;
|
||
m_pLeftCamera = nullptr;
|
||
LOG_INFO("Left camera closed\n");
|
||
}
|
||
|
||
if (m_pRightCamera)
|
||
{
|
||
if (m_pRightCamera->IsAcquisitioning())
|
||
m_pRightCamera->StopAcquisition();
|
||
m_pRightCamera->UnregisterImageCallback();
|
||
m_pRightCamera->CloseDevice();
|
||
delete m_pRightCamera;
|
||
m_pRightCamera = nullptr;
|
||
LOG_INFO("Right camera closed\n");
|
||
}
|
||
|
||
m_bCameraConnected = false;
|
||
}
|
||
|
||
int DroneScrewServerPresenter::initRtspPusher(unsigned int width, unsigned int height, unsigned int fps)
|
||
{
|
||
if (width == 0 || height == 0)
|
||
{
|
||
LOG_WARN("[RTSP] skip init: invalid frame size %ux%u\n", width, height);
|
||
return ERR_CODE(DEV_ARG_INVAILD);
|
||
}
|
||
|
||
// 每次 start_stream 都重建 muxer。FLV muxer 会记录上一轮 DTS,复用会导致重开流后
|
||
// "non monotonically increasing dts"。
|
||
releaseRtspPusher();
|
||
|
||
if (!IVrFFMediaPusher::CreateObject(&m_pPusher) || !m_pPusher)
|
||
{
|
||
LOG_ERROR("[RTSP] create pusher failed\n");
|
||
return ERR_CODE(DEV_OPEN_ERR);
|
||
}
|
||
|
||
VrFFPushConfig pc;
|
||
pc.width = width;
|
||
pc.height = height;
|
||
pc.pixelFormat = EFFPushPixelFormat::NV12; // Mono8 frames are scaled to NV12 before MPP H264 encoding.
|
||
pc.encodeType = EFFEncodeType::H264;
|
||
pc.fps = fps > 0 ? fps : static_cast<unsigned int>(kLiveStreamFrameRate);
|
||
pc.gop = std::max(1u, pc.fps / 2u); // Short GOP reduces RTSP join delay.
|
||
pc.bitrateKbps = m_pushBitrateKbps;
|
||
pc.rcMode = EFFRcMode::CBR;
|
||
pc.rtspHost = "127.0.0.1"; // 推流到本地 mediamtx
|
||
pc.rtspPath = m_rtspPath.toStdString();
|
||
pc.rtspPort = kRtmpPushPort; // RTMP publish port of local mediamtx
|
||
|
||
const int ret = m_pPusher->Init(pc);
|
||
if (ret != 0)
|
||
{
|
||
LOG_ERROR("[RTSP] pusher init failed ret=%d size=%ux%u\n", ret, width, height);
|
||
delete m_pPusher;
|
||
m_pPusher = nullptr;
|
||
return ret;
|
||
}
|
||
|
||
m_rtspWidth = width;
|
||
m_rtspHeight = height;
|
||
m_rtspAdvertiseUrl = buildRtspAdvertiseUrl(m_rtspPort, m_rtspPath);
|
||
LOG_INFO("[RTMP] pusher init ok size=%ux%u fps=%u gop=%u bitrate=%dKbps input=NV12 encoder=MPP-H264 push=rtmp://127.0.0.1:%d%s pull=%s\n",
|
||
m_rtspWidth, m_rtspHeight, pc.fps, pc.gop, m_pushBitrateKbps,
|
||
pc.rtspPort, pc.rtspPath.c_str(),
|
||
m_rtspAdvertiseUrl.toStdString().c_str());
|
||
return 0;
|
||
}
|
||
|
||
void DroneScrewServerPresenter::releaseRtspPusher()
|
||
{
|
||
std::lock_guard<std::mutex> pusherLock(m_rtspPusherMutex);
|
||
m_bRtspStarted = false;
|
||
if (m_pPusher)
|
||
{
|
||
m_pPusher->Stop();
|
||
m_pPusher->UnInit();
|
||
delete m_pPusher;
|
||
m_pPusher = nullptr;
|
||
}
|
||
m_rtspWidth = 0;
|
||
m_rtspHeight = 0;
|
||
m_rtspFrameBuffer.clear();
|
||
m_rtspScaleX.clear();
|
||
m_rtspScaleY.clear();
|
||
m_rtspScaleSrcWidth = 0;
|
||
m_rtspScaleSrcHeight = 0;
|
||
m_rtspScaleOutWidth = 0;
|
||
m_rtspScaleOutHeight = 0;
|
||
m_rtspUvInitialized = false;
|
||
m_rtspRgaScaleDisabled = false;
|
||
m_rtspRgaScaleLogged = false;
|
||
resetRtspPushState();
|
||
}
|
||
|
||
int DroneScrewServerPresenter::startRtspPusher()
|
||
{
|
||
std::lock_guard<std::mutex> pusherLock(m_rtspPusherMutex);
|
||
if (!m_pPusher)
|
||
return ERR_CODE(DRONESCREW_ERR_RTSP_INIT);
|
||
return m_pPusher->Start();
|
||
}
|
||
|
||
void DroneScrewServerPresenter::resetRtspPushState()
|
||
{
|
||
{
|
||
std::lock_guard<std::mutex> lk(m_rtspPushMutex);
|
||
m_rtspPushedFrameCounter = 0;
|
||
m_rtspLastPushRet = 0;
|
||
m_rtspLastPushFrameId = 0;
|
||
}
|
||
}
|
||
|
||
void DroneScrewServerPresenter::recordRtspPushResult(int ret, unsigned long long frameId)
|
||
{
|
||
bool logFirstSuccess = false;
|
||
bool logFirstFailure = false;
|
||
int64_t pushedCount = 0;
|
||
const bool pendingEncoderOutput = (ret == ERR_CODE(DEV_RESULT_EMPTY));
|
||
{
|
||
std::lock_guard<std::mutex> lk(m_rtspPushMutex);
|
||
pushedCount = m_rtspPushedFrameCounter.load();
|
||
const int previousRet = m_rtspLastPushRet.load();
|
||
m_rtspLastPushFrameId = frameId;
|
||
if (ret == 0)
|
||
{
|
||
m_rtspLastPushRet = 0;
|
||
++m_rtspPushedFrameCounter;
|
||
logFirstSuccess = (pushedCount == 0);
|
||
}
|
||
else if (!pendingEncoderOutput)
|
||
{
|
||
m_rtspLastPushRet = ret;
|
||
logFirstFailure = (!pendingEncoderOutput &&
|
||
pushedCount == 0 &&
|
||
previousRet != ret);
|
||
}
|
||
else if (previousRet == 0)
|
||
{
|
||
m_rtspLastPushRet = ret;
|
||
}
|
||
}
|
||
|
||
if (logFirstSuccess)
|
||
{
|
||
LOG_INFO("[RTMP] first frame pushed frame=%llu pull=%s\n",
|
||
frameId, m_rtspAdvertiseUrl.toStdString().c_str());
|
||
}
|
||
else if (logFirstFailure)
|
||
{
|
||
LOG_WARN("[RTMP] push failed before first frame ret=%d frame=%llu\n",
|
||
ret, frameId);
|
||
}
|
||
}
|
||
|
||
void DroneScrewServerPresenter::leftCameraCallback(const MvsImageData& img)
|
||
{
|
||
try
|
||
{
|
||
{
|
||
QMutexLocker lk(&m_frameMutex);
|
||
|
||
// 检查是否需要重新分配内存
|
||
if (m_leftImageData.dataSize != img.dataSize)
|
||
{
|
||
if (m_leftImageData.pData != nullptr)
|
||
{
|
||
delete[] m_leftImageData.pData;
|
||
m_leftImageData.pData = nullptr;
|
||
LOG_DEBUG("[CAM] left realloc: old=%zu new=%zu frame=%llu\n",
|
||
m_leftImageData.dataSize, img.dataSize, img.frameID);
|
||
}
|
||
|
||
if (img.dataSize > 0)
|
||
{
|
||
m_leftImageData.pData = new (std::nothrow) unsigned char[img.dataSize];
|
||
if (!m_leftImageData.pData)
|
||
{
|
||
m_leftImageData.dataSize = 0;
|
||
m_bLeftImageReady = false;
|
||
LOG_ERROR("[CAM] left alloc failed size=%zu frame=%llu\n",
|
||
img.dataSize, img.frameID);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 复制图像数据
|
||
m_leftImageData.width = img.width;
|
||
m_leftImageData.height = img.height;
|
||
m_leftImageData.dataSize = img.dataSize;
|
||
m_leftImageData.pixelFormat = img.pixelFormat;
|
||
m_leftImageData.frameID = img.frameID;
|
||
m_leftImageData.timestamp = img.timestamp;
|
||
|
||
if (m_leftImageData.pData != nullptr && img.pData != nullptr)
|
||
{
|
||
memcpy(m_leftImageData.pData, img.pData, img.dataSize);
|
||
}
|
||
|
||
m_bLeftImageReady = true;
|
||
}
|
||
|
||
static bool leftFirst = true;
|
||
if (leftFirst) {
|
||
std::ostringstream oss;
|
||
oss << std::this_thread::get_id();
|
||
LOG_DEBUG("[CAM] left first frame! id=%llu tid=%s\n", img.frameID, oss.str().c_str());
|
||
leftFirst = false;
|
||
}
|
||
static int leftFrameCnt = 0;
|
||
static int rtspPushCnt = 0;
|
||
static int rtspFailCnt = 0;
|
||
|
||
// 直播相机配置为左目时,收到左目立即推流(不等右目)。
|
||
if (isLiveStreamCameraRole("left") &&
|
||
m_bRtspStarted.load() && img.pData)
|
||
{
|
||
const int rtspRet = pushRtspFrame(img);
|
||
if (rtspRet == 0)
|
||
++rtspPushCnt;
|
||
else if (rtspRet != ERR_CODE(DEV_RESULT_EMPTY))
|
||
++rtspFailCnt;
|
||
}
|
||
|
||
if (++leftFrameCnt % 50 == 0) {
|
||
LOG_DEBUG("[CAM] left frame #%d id=%llu %dx%d size=%zu rtsp_push=%d rtsp_fail=%d\n",
|
||
leftFrameCnt, img.frameID, img.width, img.height, img.dataSize,
|
||
rtspPushCnt, rtspFailCnt);
|
||
rtspPushCnt = 0;
|
||
rtspFailCnt = 0;
|
||
}
|
||
}
|
||
catch (const std::bad_alloc& e)
|
||
{
|
||
LOG_ERROR("[CAM] left callback bad_alloc frame=%llu size=%zu: %s\n",
|
||
img.frameID, img.dataSize, e.what());
|
||
}
|
||
catch (const std::exception& e)
|
||
{
|
||
LOG_ERROR("[CAM] left callback exception frame=%llu: %s\n",
|
||
img.frameID, e.what());
|
||
}
|
||
catch (...)
|
||
{
|
||
LOG_ERROR("[CAM] left callback unknown exception frame=%llu\n", img.frameID);
|
||
}
|
||
}
|
||
|
||
void DroneScrewServerPresenter::rightCameraCallback(const MvsImageData& img)
|
||
{
|
||
try
|
||
{
|
||
{
|
||
QMutexLocker lk(&m_frameMutex);
|
||
|
||
// 检查是否需要重新分配内存
|
||
if (m_rightImageData.dataSize != img.dataSize)
|
||
{
|
||
if (m_rightImageData.pData != nullptr)
|
||
{
|
||
delete[] m_rightImageData.pData;
|
||
m_rightImageData.pData = nullptr;
|
||
LOG_DEBUG("[CAM] right realloc: old=%zu new=%zu frame=%llu\n",
|
||
m_rightImageData.dataSize, img.dataSize, img.frameID);
|
||
}
|
||
|
||
if (img.dataSize > 0)
|
||
{
|
||
m_rightImageData.pData = new (std::nothrow) unsigned char[img.dataSize];
|
||
if (!m_rightImageData.pData)
|
||
{
|
||
m_rightImageData.dataSize = 0;
|
||
m_bRightImageReady = false;
|
||
LOG_ERROR("[CAM] right alloc failed size=%zu frame=%llu\n",
|
||
img.dataSize, img.frameID);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 复制图像数据
|
||
m_rightImageData.width = img.width;
|
||
m_rightImageData.height = img.height;
|
||
m_rightImageData.dataSize = img.dataSize;
|
||
m_rightImageData.pixelFormat = img.pixelFormat;
|
||
m_rightImageData.frameID = img.frameID;
|
||
m_rightImageData.timestamp = img.timestamp;
|
||
|
||
if (m_rightImageData.pData != nullptr && img.pData != nullptr)
|
||
{
|
||
memcpy(m_rightImageData.pData, img.pData, img.dataSize);
|
||
}
|
||
|
||
m_bRightImageReady = true;
|
||
}
|
||
static bool rightFirst = true;
|
||
if (rightFirst) {
|
||
std::ostringstream oss;
|
||
oss << std::this_thread::get_id();
|
||
LOG_DEBUG("[CAM] right first frame! id=%llu tid=%s\n", img.frameID, oss.str().c_str());
|
||
rightFirst = false;
|
||
}
|
||
static int rightFrameCnt = 0;
|
||
static int rtspPushCnt = 0;
|
||
static int rtspFailCnt = 0;
|
||
|
||
if (isLiveStreamCameraRole("right") &&
|
||
m_bRtspStarted.load() && img.pData)
|
||
{
|
||
const int rtspRet = pushRtspFrame(img);
|
||
if (rtspRet == 0)
|
||
++rtspPushCnt;
|
||
else if (rtspRet != ERR_CODE(DEV_RESULT_EMPTY))
|
||
++rtspFailCnt;
|
||
}
|
||
|
||
if (++rightFrameCnt % 50 == 0)
|
||
{
|
||
LOG_DEBUG("[CAM] right frame #%d id=%llu %dx%d size=%zu rtsp_push=%d rtsp_fail=%d\n",
|
||
rightFrameCnt, img.frameID, img.width, img.height, img.dataSize,
|
||
rtspPushCnt, rtspFailCnt);
|
||
rtspPushCnt = 0;
|
||
rtspFailCnt = 0;
|
||
}
|
||
}
|
||
catch (const std::bad_alloc& e)
|
||
{
|
||
LOG_ERROR("[CAM] right callback bad_alloc frame=%llu size=%zu: %s\n",
|
||
img.frameID, img.dataSize, e.what());
|
||
}
|
||
catch (const std::exception& e)
|
||
{
|
||
LOG_ERROR("[CAM] right callback exception frame=%llu: %s\n",
|
||
img.frameID, e.what());
|
||
}
|
||
catch (...)
|
||
{
|
||
LOG_ERROR("[CAM] right callback unknown exception frame=%llu\n", img.frameID);
|
||
}
|
||
}
|
||
|
||
void DroneScrewServerPresenter::onCameraReconnectTimer()
|
||
{
|
||
if (m_bCameraConnected.load())
|
||
{
|
||
m_pReconnectTimer->stop();
|
||
return;
|
||
}
|
||
LOG_INFO("Attempting to reconnect cameras...\n");
|
||
if (tryConnectCamera() == 0)
|
||
{
|
||
m_pReconnectTimer->stop();
|
||
LOG_INFO("Camera reconnection successful\n");
|
||
}
|
||
else
|
||
{
|
||
LOG_WARN("Camera reconnection failed, will retry in 2 seconds\n");
|
||
}
|
||
}
|
||
|
||
QJsonObject DroneScrewServerPresenter::getRuntimeInfo() const
|
||
{
|
||
QJsonObject info;
|
||
info["rtsp"] = m_rtspAdvertiseUrl;
|
||
info["controlPort"] = m_zmqControlPort;
|
||
info["resultPort"] = m_zmqResultPort;
|
||
info["rawImagePort"] = m_zmqRawImagePort;
|
||
const bool detecting = m_bIsDetecting.load();
|
||
const int pipelineMode = detecting ? m_detectPipelineMode.load() : -1;
|
||
const bool distanceMode = (pipelineMode == kDetectPipelineDistance);
|
||
const bool precisionMode = (pipelineMode == kDetectPipelinePrecision);
|
||
info["detectFps"] = distanceMode ? kDistanceFrameRate : kPrecisionFrameRate;
|
||
info["streamFps"] = m_liveStreamFps;
|
||
info["leftExposure"] = m_leftExposureTime;
|
||
info["rightExposure"] = m_rightExposureTime;
|
||
info["leftGain"] = m_leftGain;
|
||
info["rightGain"] = m_rightGain;
|
||
info["leftCameraIndex"] = static_cast<int>(m_nLeftCameraIndex);
|
||
info["rightCameraIndex"] = static_cast<int>(m_nRightCameraIndex);
|
||
info["leftCameraSerial"] = QString::fromStdString(m_strLeftCameraSerial);
|
||
info["rightCameraSerial"] = QString::fromStdString(m_strRightCameraSerial);
|
||
info["streamWidth"] = static_cast<int>(m_liveStreamWidth);
|
||
info["streamHeight"] = static_cast<int>(m_liveStreamHeight);
|
||
info["streamCamera"] = QString::fromStdString(m_liveStreamCameraRole);
|
||
info["binningHorizontal"] = static_cast<int>(distanceMode ? kDistanceBinningHorizontal
|
||
: kPrecisionBinningHorizontal);
|
||
info["binningVertical"] = static_cast<int>(distanceMode ? kDistanceBinningVertical
|
||
: kPrecisionBinningVertical);
|
||
info["decimationHorizontal"] = static_cast<int>(distanceMode ? kDistanceDecimationHorizontal
|
||
: kPrecisionDecimationHorizontal);
|
||
info["decimationVertical"] = static_cast<int>(distanceMode ? kDistanceDecimationVertical
|
||
: kPrecisionDecimationVertical);
|
||
info["detectionPipeline"] = distanceMode ? "distance" : (precisionMode ? "precision" : "idle");
|
||
info["detectTriggerMode"] = m_useIoTrigger ? "io" : "off";
|
||
info["streamTriggerMode"] = m_useIoTrigger ? "io" : "off";
|
||
info["detectMode"] = QString::fromStdString(detectMode());
|
||
info["mode"] = m_bLiveStreaming.load()
|
||
? "live_stream"
|
||
: (m_bIsDetecting.load() ? "detection" : "idle");
|
||
|
||
// 算法参数
|
||
info["algoScore"] = static_cast<double>(m_algoParams.scoreThreshold);
|
||
info["algoNms"] = static_cast<double>(m_algoParams.nmsThreshold);
|
||
info["algoWidth"] = m_algoParams.inputWidth;
|
||
info["algoHeight"] = m_algoParams.inputHeight;
|
||
info["algoModel"] = QString::fromStdString(m_algoParams.modelPath);
|
||
info["algoConfigPath"] = QString::fromStdString(m_algoParams.modelPath);
|
||
info["algoModelType"] = m_algoParams.modelType;
|
||
info["algoExpectedBoltCount"] = m_algoParams.expectedBoltCount;
|
||
|
||
return info;
|
||
}
|
||
|
||
QJsonObject DroneScrewServerPresenter::getCalibrationInfo() const
|
||
{
|
||
QJsonObject calibration;
|
||
|
||
QStringList configBases = calibrationBaseDirs(m_configFilePath);
|
||
QString algoConfigPath = QString::fromStdString(m_algoParams.modelPath);
|
||
if (algoConfigPath.trimmed().isEmpty())
|
||
algoConfigPath = "config/config.rk3588.yaml";
|
||
|
||
const QString resolvedConfigPath = resolveCandidatePath(algoConfigPath, configBases);
|
||
calibration["algoConfigPath"] = resolvedConfigPath;
|
||
|
||
QString xmlPath = readCalibrationXmlPathFromYaml(resolvedConfigPath);
|
||
if (xmlPath.trimmed().isEmpty())
|
||
xmlPath = "calib/stereo_calib.xml";
|
||
|
||
QStringList xmlBases = configBases;
|
||
if (!resolvedConfigPath.isEmpty())
|
||
{
|
||
const QFileInfo configInfo(resolvedConfigPath);
|
||
appendUniqueDir(xmlBases, configInfo.absolutePath());
|
||
appendUniqueDir(xmlBases, QDir(configInfo.absolutePath()).absoluteFilePath(".."));
|
||
}
|
||
|
||
const QString resolvedXmlPath = resolveCandidatePath(xmlPath, xmlBases);
|
||
calibration["sourcePath"] = resolvedXmlPath;
|
||
|
||
QFile file(resolvedXmlPath);
|
||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||
{
|
||
calibration["valid"] = false;
|
||
calibration["message"] = QString("open calibration xml failed: %1").arg(file.errorString());
|
||
return calibration;
|
||
}
|
||
|
||
QDomDocument doc;
|
||
QString errorText;
|
||
int errorLine = 0;
|
||
int errorColumn = 0;
|
||
if (!doc.setContent(&file, &errorText, &errorLine, &errorColumn))
|
||
{
|
||
calibration["valid"] = false;
|
||
calibration["message"] = QString("parse calibration xml failed at %1:%2: %3")
|
||
.arg(errorLine)
|
||
.arg(errorColumn)
|
||
.arg(errorText);
|
||
return calibration;
|
||
}
|
||
|
||
const QDomElement root = doc.documentElement();
|
||
const QJsonObject left = cameraCalibrationToJson(root, "LeftCamera");
|
||
const QJsonObject right = cameraCalibrationToJson(root, "RightCamera");
|
||
const QJsonObject stereo = stereoCalibrationToJson(root);
|
||
|
||
calibration["left"] = left;
|
||
calibration["right"] = right;
|
||
calibration["stereo"] = stereo;
|
||
calibration["valid"] = left["valid"].toBool() &&
|
||
right["valid"].toBool() &&
|
||
stereo["valid"].toBool();
|
||
calibration["message"] = calibration["valid"].toBool() ? "ok" : "calibration xml incomplete";
|
||
return calibration;
|
||
}
|
||
|
||
int DroneScrewServerPresenter::startDetectionWork()
|
||
{
|
||
if (m_bLiveStreaming.load())
|
||
{
|
||
LOG_WARN("[DETECT] start rejected: live stream is running\n");
|
||
emit statusChanged(QStringLiteral("live stream is running"));
|
||
return ERR_CODE(DRONESCREW_ERR_MODE_CONFLICT);
|
||
}
|
||
|
||
if (m_bIsDetecting.load())
|
||
{
|
||
LOG_DEBUG("Detection already started\n");
|
||
return 0;
|
||
}
|
||
|
||
if (m_bLiveStreaming.load())
|
||
{
|
||
LOG_WARN("[DETECT] start rejected: live stream is running\n");
|
||
emit statusChanged(QStringLiteral("实时传图中,不能启动检测"));
|
||
return ERR_CODE(DRONESCREW_ERR_MODE_CONFLICT);
|
||
}
|
||
|
||
if (!m_pLeftCamera || !m_pRightCamera)
|
||
{
|
||
LOG_DEBUG("Cameras not connected\n");
|
||
emit statusChanged(QStringLiteral("双目相机未连接"));
|
||
return ERR_CODE(DRONESCREW_ERR_CAMERA_NOT_CONNECTED);
|
||
}
|
||
|
||
m_detectPipelineMode = kDetectPipelinePrecision;
|
||
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
|
||
|
||
const int acqRet = prepareDetectionAcquisition(kPrecisionFrameRate,
|
||
kPrecisionBinningHorizontal,
|
||
kPrecisionBinningVertical,
|
||
kPrecisionDecimationHorizontal,
|
||
kPrecisionDecimationVertical,
|
||
"PRECISION");
|
||
if (acqRet != 0)
|
||
{
|
||
emit statusChanged(QStringLiteral("双目相机采集启动失败"));
|
||
return acqRet;
|
||
}
|
||
|
||
// 停止 RTSP 推流(检测和推流互斥)
|
||
if (m_pPusher)
|
||
{
|
||
LOG_DEBUG("[DETECT] stopping RTSP pusher (detection mode)\n");
|
||
releaseRtspPusher();
|
||
}
|
||
m_bRtspStarted = false;
|
||
resetFrameReadyFlags();
|
||
|
||
m_bThreadExit = false;
|
||
m_bIsDetecting = true;
|
||
startImageSaveThread(imageSaveModeName());
|
||
startGpioTriggerLoop();
|
||
m_detectThread = std::thread(&DroneScrewServerPresenter::detectThreadFunc, this);
|
||
emit statusChanged(QStringLiteral("开始持续检测(双目)"));
|
||
LOG_INFO("Detection started (binocular mode, fps=%.1f trigger=%s)\n",
|
||
kPrecisionFrameRate, m_useIoTrigger ? "io" : "off");
|
||
return 0;
|
||
}
|
||
|
||
int DroneScrewServerPresenter::stopDetectionWork()
|
||
{
|
||
if (m_bLiveStreaming.load())
|
||
{
|
||
LOG_INFO("[DETECT] stop ignored: live stream keeps binocular detection running\n");
|
||
return 0;
|
||
}
|
||
|
||
if (!m_bIsDetecting.load())
|
||
{
|
||
stopGpioTriggerLoop();
|
||
stopImageSaveThread();
|
||
LOG_WARN("Detection not started\n");
|
||
return 0;
|
||
}
|
||
|
||
stopGpioTriggerLoop();
|
||
m_bThreadExit = true;
|
||
if (m_detectThread.joinable())
|
||
m_detectThread.join();
|
||
stopImageSaveThread();
|
||
m_bIsDetecting = false;
|
||
m_detectPipelineMode = kDetectPipelinePrecision;
|
||
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
|
||
|
||
emit statusChanged(QStringLiteral("停止持续检测"));
|
||
LOG_DEBUG("Detection stopped\n");
|
||
return 0;
|
||
}
|
||
|
||
int DroneScrewServerPresenter::startLiveStream()
|
||
{
|
||
if (m_bLiveStreaming.load())
|
||
{
|
||
LOG_DEBUG("[LIVE] already started\n");
|
||
return 0;
|
||
}
|
||
|
||
releaseRtspPusher();
|
||
|
||
if (m_bIsDetecting.load())
|
||
{
|
||
LOG_WARN("[LIVE] start rejected: detection is running\n");
|
||
emit statusChanged(QStringLiteral("检测中,不能启动实时传图"));
|
||
return ERR_CODE(DRONESCREW_ERR_MODE_CONFLICT);
|
||
}
|
||
|
||
IMvsDevice* streamCamera = liveStreamCamera();
|
||
const char* streamRole = liveStreamCameraRole();
|
||
auto stopLiveCameras = [this]() {
|
||
if (m_pLeftCamera && m_pLeftCamera->IsAcquisitioning())
|
||
m_pLeftCamera->StopAcquisition();
|
||
if (m_pRightCamera && m_pRightCamera->IsAcquisitioning())
|
||
m_pRightCamera->StopAcquisition();
|
||
};
|
||
if (!streamCamera)
|
||
{
|
||
LOG_DEBUG("[LIVE] stream camera role=%s not connected\n", streamRole);
|
||
emit statusChanged(QStringLiteral("实时传图相机未连接"));
|
||
return ERR_CODE(DRONESCREW_ERR_CAMERA_NOT_CONNECTED);
|
||
}
|
||
|
||
m_detectPipelineMode = kDetectPipelineDistance;
|
||
m_activeTriggerFps = static_cast<int>(kDistanceFrameRate);
|
||
|
||
const int acqRet = prepareDetectionAcquisition(kDistanceFrameRate,
|
||
kDistanceBinningHorizontal,
|
||
kDistanceBinningVertical,
|
||
kDistanceDecimationHorizontal,
|
||
kDistanceDecimationVertical,
|
||
"DISTANCE");
|
||
if (acqRet != 0)
|
||
{
|
||
emit statusChanged(QStringLiteral("实时传图相机采集启动失败"));
|
||
releaseRtspPusher();
|
||
m_detectPipelineMode = kDetectPipelinePrecision;
|
||
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
|
||
return acqRet;
|
||
}
|
||
|
||
const unsigned int outWidth = alignEven(m_liveStreamWidth > 0 ? m_liveStreamWidth : kLiveStreamWidth);
|
||
const unsigned int outHeight = alignEven(m_liveStreamHeight > 0 ? m_liveStreamHeight : kLiveStreamHeight);
|
||
m_liveStreamWidth = outWidth;
|
||
m_liveStreamHeight = outHeight;
|
||
int ret = initRtspPusher(outWidth, outHeight, static_cast<unsigned int>(m_liveStreamFps));
|
||
if (ret != 0 || !m_pPusher)
|
||
{
|
||
LOG_ERROR("[LIVE] RTSP pusher init failed ret=%d\n", ret);
|
||
stopLiveCameras();
|
||
releaseRtspPusher();
|
||
m_detectPipelineMode = kDetectPipelinePrecision;
|
||
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
|
||
return ERR_CODE(DRONESCREW_ERR_RTSP_INIT);
|
||
}
|
||
|
||
ret = startRtspPusher();
|
||
if (ret != 0)
|
||
{
|
||
LOG_ERROR("[LIVE] RTSP pusher start failed ret=%d\n", ret);
|
||
m_bRtspStarted = false;
|
||
stopLiveCameras();
|
||
releaseRtspPusher();
|
||
m_detectPipelineMode = kDetectPipelinePrecision;
|
||
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
|
||
return ERR_CODE(DRONESCREW_ERR_RTSP_START);
|
||
}
|
||
|
||
m_rtspFrameCounter = 0; // 重置帧计数器
|
||
resetRtspPushState();
|
||
m_bRtspStarted = true;
|
||
m_bLiveStreaming = true;
|
||
m_bThreadExit = false;
|
||
m_bIsDetecting = true;
|
||
startGpioTriggerLoop();
|
||
m_detectThread = std::thread(&DroneScrewServerPresenter::detectThreadFunc, this);
|
||
emit statusChanged(QStringLiteral("开始实时传图"));
|
||
LOG_INFO("[LIVE] started streamCamera=%s fps=%d out=%ux%u url=%s\n",
|
||
streamRole, m_liveStreamFps, m_rtspWidth, m_rtspHeight,
|
||
m_rtspAdvertiseUrl.toStdString().c_str());
|
||
return 0;
|
||
}
|
||
|
||
int DroneScrewServerPresenter::stopLiveStream()
|
||
{
|
||
if (!m_bLiveStreaming.load())
|
||
{
|
||
if (m_pPusher)
|
||
releaseRtspPusher();
|
||
else
|
||
m_bRtspStarted = false;
|
||
m_rtspFrameCounter = 0; // 重置计数器,防止下次开流时 DTS 残留
|
||
return 0;
|
||
}
|
||
|
||
m_bLiveStreaming = false;
|
||
m_bRtspStarted = false;
|
||
stopGpioTriggerLoop();
|
||
m_bThreadExit = true;
|
||
if (m_detectThread.joinable())
|
||
m_detectThread.join();
|
||
stopImageSaveThread();
|
||
m_bIsDetecting = false;
|
||
m_detectPipelineMode = kDetectPipelinePrecision;
|
||
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
|
||
m_rtspFrameCounter = 0; // 重置计数器,下次开流从 0 开始
|
||
IMvsDevice* streamCamera = liveStreamCamera();
|
||
const char* streamRole = liveStreamCameraRole();
|
||
if (streamCamera && streamCamera->IsAcquisitioning())
|
||
{
|
||
const int ret = streamCamera->StopAcquisition();
|
||
LOG_DEBUG("[LIVE] stream camera role=%s StopAcquisition ret=%d\n",
|
||
streamRole, ret);
|
||
}
|
||
if (m_pLeftCamera && m_pLeftCamera != streamCamera && m_pLeftCamera->IsAcquisitioning())
|
||
{
|
||
const int ret = m_pLeftCamera->StopAcquisition();
|
||
LOG_DEBUG("[LIVE] left StopAcquisition ret=%d\n", ret);
|
||
}
|
||
if (m_pRightCamera && m_pRightCamera != streamCamera && m_pRightCamera->IsAcquisitioning())
|
||
{
|
||
const int ret = m_pRightCamera->StopAcquisition();
|
||
LOG_DEBUG("[LIVE] right StopAcquisition ret=%d\n", ret);
|
||
}
|
||
|
||
if (m_pPusher)
|
||
{
|
||
LOG_DEBUG("[LIVE] releasing RTMP pusher\n");
|
||
releaseRtspPusher();
|
||
}
|
||
|
||
m_detectPipelineMode = kDetectPipelinePrecision;
|
||
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
|
||
|
||
if (m_pLeftCamera)
|
||
configureCamera(m_pLeftCamera, "left", m_useIoTrigger, kPrecisionFrameRate,
|
||
kPrecisionBinningHorizontal, kPrecisionBinningVertical);
|
||
if (m_pRightCamera)
|
||
configureCamera(m_pRightCamera, "right", m_useIoTrigger, kPrecisionFrameRate,
|
||
kPrecisionBinningHorizontal, kPrecisionBinningVertical);
|
||
|
||
emit statusChanged(QStringLiteral("停止实时传图"));
|
||
LOG_INFO("[LIVE] stopped\n");
|
||
return 0;
|
||
}
|
||
|
||
int DroneScrewServerPresenter::swapCameraRoles()
|
||
{
|
||
if (m_bIsDetecting.load() || m_bLiveStreaming.load())
|
||
{
|
||
LOG_WARN("[CAM] swap rejected: stop detection/live stream first\n");
|
||
return ERR_CODE(DRONESCREW_ERR_MODE_CONFLICT);
|
||
}
|
||
|
||
if (m_pReconnectTimer)
|
||
m_pReconnectTimer->stop();
|
||
|
||
closeCamera();
|
||
|
||
std::swap(m_strLeftCameraSerial, m_strRightCameraSerial);
|
||
std::swap(m_nLeftCameraIndex, m_nRightCameraIndex);
|
||
std::swap(m_leftExposureTime, m_rightExposureTime);
|
||
std::swap(m_leftGain, m_rightGain);
|
||
m_liveStreamCameraRole = "left";
|
||
resetFrameReadyFlags();
|
||
|
||
const int ret = tryConnectCamera();
|
||
if (ret != 0)
|
||
{
|
||
LOG_ERROR("[CAM] swap reconnect failed ret=%d\n", ret);
|
||
if (m_pReconnectTimer)
|
||
m_pReconnectTimer->start();
|
||
return ret;
|
||
}
|
||
|
||
if (!saveConfiguration())
|
||
{
|
||
LOG_ERROR("[CAM] swap save configuration failed\n");
|
||
return ERR_CODE(FILE_ERR_WRITE);
|
||
}
|
||
|
||
LOG_INFO("[CAM] swapped roles: left index=%u serial=%s, right index=%u serial=%s, streamCamera=%s\n",
|
||
m_nLeftCameraIndex, m_strLeftCameraSerial.c_str(),
|
||
m_nRightCameraIndex, m_strRightCameraSerial.c_str(),
|
||
m_liveStreamCameraRole.c_str());
|
||
emit statusChanged(QStringLiteral("左右目已交换"));
|
||
return 0;
|
||
}
|
||
|
||
bool DroneScrewServerPresenter::saveConfiguration()
|
||
{
|
||
if (m_configFilePath.isEmpty())
|
||
{
|
||
LOG_WARN("[CONFIG] save skipped: config path empty\n");
|
||
return false;
|
||
}
|
||
|
||
QFile in(m_configFilePath);
|
||
QDomDocument doc;
|
||
if (in.open(QIODevice::ReadOnly))
|
||
{
|
||
QString errMsg;
|
||
int errLine = 0;
|
||
int errCol = 0;
|
||
if (!doc.setContent(&in, false, &errMsg, &errLine, &errCol))
|
||
{
|
||
LOG_WARN("[CONFIG] parse failed before save: %s line=%d col=%d\n",
|
||
errMsg.toStdString().c_str(), errLine, errCol);
|
||
doc.clear();
|
||
}
|
||
in.close();
|
||
}
|
||
|
||
if (doc.isNull())
|
||
{
|
||
QDomProcessingInstruction pi =
|
||
doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\"");
|
||
doc.appendChild(pi);
|
||
doc.appendChild(doc.createElement("DroneScrewServerConfig"));
|
||
}
|
||
|
||
QDomElement root = doc.documentElement();
|
||
if (root.isNull())
|
||
{
|
||
root = doc.createElement("DroneScrewServerConfig");
|
||
doc.appendChild(root);
|
||
}
|
||
|
||
auto ensureCamera = [&](const QString& role, int index) -> QDomElement {
|
||
QDomNodeList nodes = root.elementsByTagName("Camera");
|
||
for (int i = 0; i < nodes.count(); ++i)
|
||
{
|
||
QDomElement e = nodes.at(i).toElement();
|
||
if (e.attribute("role") == role || e.attribute("index").toInt() == index)
|
||
return e;
|
||
}
|
||
QDomElement e = doc.createElement("Camera");
|
||
root.appendChild(e);
|
||
return e;
|
||
};
|
||
|
||
QDomElement left = ensureCamera("left", static_cast<int>(m_nLeftCameraIndex));
|
||
left.setAttribute("role", "left");
|
||
left.setAttribute("index", static_cast<int>(m_nLeftCameraIndex));
|
||
left.setAttribute("serialNumber", QString::fromStdString(m_strLeftCameraSerial));
|
||
left.setAttribute("exposureTime", QString::number(m_leftExposureTime, 'f', 3));
|
||
left.setAttribute("gain", QString::number(m_leftGain, 'f', 3));
|
||
|
||
QDomElement right = ensureCamera("right", static_cast<int>(m_nRightCameraIndex));
|
||
right.setAttribute("role", "right");
|
||
right.setAttribute("index", static_cast<int>(m_nRightCameraIndex));
|
||
right.setAttribute("serialNumber", QString::fromStdString(m_strRightCameraSerial));
|
||
right.setAttribute("exposureTime", QString::number(m_rightExposureTime, 'f', 3));
|
||
right.setAttribute("gain", QString::number(m_rightGain, 'f', 3));
|
||
|
||
QDomElement rtsp = root.firstChildElement("Rtsp");
|
||
if (rtsp.isNull())
|
||
{
|
||
rtsp = doc.createElement("Rtsp");
|
||
root.appendChild(rtsp);
|
||
}
|
||
rtsp.setAttribute("streamCamera", QString::fromStdString(m_liveStreamCameraRole));
|
||
|
||
QDomElement trigger = root.firstChildElement("Trigger");
|
||
if (trigger.isNull())
|
||
{
|
||
trigger = doc.createElement("Trigger");
|
||
root.appendChild(trigger);
|
||
}
|
||
trigger.setAttribute("useIoTrigger", m_useIoTrigger ? "true" : "false");
|
||
trigger.setAttribute("gpio", m_triggerGpio);
|
||
trigger.setAttribute("source", m_triggerSource);
|
||
trigger.setAttribute("activation", m_triggerActivation);
|
||
|
||
QDomElement algorithm = root.firstChildElement("Algorithm");
|
||
if (algorithm.isNull())
|
||
{
|
||
algorithm = doc.createElement("Algorithm");
|
||
root.appendChild(algorithm);
|
||
}
|
||
algorithm.setAttribute("scoreThreshold", QString::number(m_algoParams.scoreThreshold, 'f', 3));
|
||
algorithm.setAttribute("nmsThreshold", QString::number(m_algoParams.nmsThreshold, 'f', 3));
|
||
algorithm.setAttribute("inputWidth", m_algoParams.inputWidth);
|
||
algorithm.setAttribute("inputHeight", m_algoParams.inputHeight);
|
||
algorithm.setAttribute("modelType", m_algoParams.modelType);
|
||
algorithm.setAttribute("configPath", QString::fromStdString(m_algoParams.modelPath));
|
||
algorithm.setAttribute("expectedBoltCount", m_algoParams.expectedBoltCount);
|
||
|
||
QFileInfo fi(m_configFilePath);
|
||
if (!fi.absolutePath().isEmpty())
|
||
QDir().mkpath(fi.absolutePath());
|
||
|
||
QFile out(m_configFilePath);
|
||
if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
|
||
{
|
||
LOG_WARN("[CONFIG] open for save failed: %s\n",
|
||
m_configFilePath.toStdString().c_str());
|
||
return false;
|
||
}
|
||
|
||
QTextStream ts(&out);
|
||
doc.save(ts, 4);
|
||
out.close();
|
||
LOG_INFO("[CONFIG] saved runtime config to %s\n",
|
||
m_configFilePath.toStdString().c_str());
|
||
return true;
|
||
}
|
||
|
||
int DroneScrewServerPresenter::pulseGpioTrigger(bool logOk)
|
||
{
|
||
if (!m_useIoTrigger)
|
||
return 0;
|
||
|
||
#if defined(__linux__)
|
||
const QString gpio = QString::number(m_triggerGpio);
|
||
const QString gpioDir = QString("/sys/class/gpio/gpio%1").arg(gpio);
|
||
|
||
auto writeText = [](const QString& path, const QByteArray& data) -> bool {
|
||
QFile f(path);
|
||
if (!f.open(QIODevice::WriteOnly | QIODevice::Text))
|
||
return false;
|
||
return f.write(data) == data.size();
|
||
};
|
||
|
||
if (!QFileInfo::exists(gpioDir))
|
||
{
|
||
if (!writeText("/sys/class/gpio/export", gpio.toUtf8()))
|
||
{
|
||
LOG_WARN("[GPIO] export gpio=%s failed\n", gpio.toStdString().c_str());
|
||
return ERR_CODE(DRONESCREW_ERR_GPIO_EXPORT);
|
||
}
|
||
for (int i = 0; i < 50 && !QFileInfo::exists(gpioDir); ++i)
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||
}
|
||
|
||
if (!writeText(gpioDir + "/direction", "out"))
|
||
{
|
||
LOG_WARN("[GPIO] set direction failed gpio=%s\n", gpio.toStdString().c_str());
|
||
return ERR_CODE(DRONESCREW_ERR_GPIO_DIRECTION);
|
||
}
|
||
|
||
if (!writeText(gpioDir + "/value", "1"))
|
||
{
|
||
LOG_WARN("[GPIO] set value=1 failed gpio=%s\n", gpio.toStdString().c_str());
|
||
return ERR_CODE(DRONESCREW_ERR_GPIO_VALUE);
|
||
}
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(kGpioTriggerHighMs));
|
||
if (!writeText(gpioDir + "/value", "0"))
|
||
{
|
||
LOG_WARN("[GPIO] set value=0 failed gpio=%s\n", gpio.toStdString().c_str());
|
||
return ERR_CODE(DRONESCREW_ERR_GPIO_VALUE);
|
||
}
|
||
if (logOk)
|
||
LOG_DEBUG("[GPIO] pulse gpio=%s ok high=%dms\n",
|
||
gpio.toStdString().c_str(), kGpioTriggerHighMs);
|
||
return 0;
|
||
#else
|
||
if (logOk)
|
||
LOG_WARN("[GPIO] io trigger requested but ignored on non-linux platform\n");
|
||
return 0;
|
||
#endif
|
||
}
|
||
|
||
void DroneScrewServerPresenter::startGpioTriggerLoop()
|
||
{
|
||
if (!m_useIoTrigger)
|
||
return;
|
||
|
||
stopGpioTriggerLoop();
|
||
m_bTriggerThreadExit = false;
|
||
m_triggerThread = std::thread(&DroneScrewServerPresenter::triggerThreadFunc, this);
|
||
}
|
||
|
||
void DroneScrewServerPresenter::stopGpioTriggerLoop()
|
||
{
|
||
m_bTriggerThreadExit = true;
|
||
if (m_triggerThread.joinable())
|
||
m_triggerThread.join();
|
||
}
|
||
|
||
void DroneScrewServerPresenter::triggerThreadFunc()
|
||
{
|
||
const int fps = std::max(1, m_activeTriggerFps.load());
|
||
{
|
||
std::ostringstream oss;
|
||
oss << std::this_thread::get_id();
|
||
LOG_DEBUG("[GPIO] trigger thread started gpio=%d fps=%.1f high=%dms tid=%s\n",
|
||
m_triggerGpio, static_cast<double>(fps), kGpioTriggerHighMs, oss.str().c_str());
|
||
}
|
||
|
||
try
|
||
{
|
||
const int periodMs = std::max(kGpioTriggerHighMs + 1,
|
||
static_cast<int>(1000.0 / static_cast<double>(fps)));
|
||
const auto period = std::chrono::milliseconds(periodMs);
|
||
auto nextPulseAt = std::chrono::steady_clock::now();
|
||
int failCnt = 0;
|
||
while (!m_bTriggerThreadExit.load())
|
||
{
|
||
const int ret = pulseGpioTrigger(false);
|
||
if (ret != 0)
|
||
{
|
||
++failCnt;
|
||
if (failCnt == 1 || failCnt % 50 == 0)
|
||
LOG_WARN("[GPIO] trigger pulse failed ret=%d count=%d\n", ret, failCnt);
|
||
}
|
||
else
|
||
{
|
||
failCnt = 0;
|
||
}
|
||
|
||
nextPulseAt += period;
|
||
for (;;)
|
||
{
|
||
if (m_bTriggerThreadExit.load())
|
||
break;
|
||
|
||
const auto now = std::chrono::steady_clock::now();
|
||
if (now >= nextPulseAt)
|
||
break;
|
||
|
||
const auto remain =
|
||
std::chrono::duration_cast<std::chrono::milliseconds>(nextPulseAt - now);
|
||
const auto sleepMs = std::min<std::chrono::milliseconds>(
|
||
remain, std::chrono::milliseconds(5));
|
||
if (sleepMs.count() <= 0)
|
||
break;
|
||
std::this_thread::sleep_for(sleepMs);
|
||
}
|
||
|
||
if (std::chrono::steady_clock::now() > nextPulseAt + period)
|
||
nextPulseAt = std::chrono::steady_clock::now();
|
||
}
|
||
}
|
||
catch (const std::exception& e)
|
||
{
|
||
LOG_ERROR("[GPIO] trigger thread exception: %s\n", e.what());
|
||
}
|
||
catch (...)
|
||
{
|
||
LOG_ERROR("[GPIO] trigger thread unknown exception\n");
|
||
}
|
||
|
||
LOG_DEBUG("[GPIO] trigger thread stopped gpio=%d\n", m_triggerGpio);
|
||
}
|
||
|
||
bool DroneScrewServerPresenter::waitAndCopyLatestBinocular(MvsImageData& leftImg,
|
||
MvsImageData& rightImg,
|
||
unsigned int timeoutMs)
|
||
{
|
||
const auto deadline = std::chrono::steady_clock::now() +
|
||
std::chrono::milliseconds(timeoutMs);
|
||
|
||
while (std::chrono::steady_clock::now() < deadline)
|
||
{
|
||
{
|
||
QMutexLocker lk(&m_frameMutex);
|
||
if (m_bLeftImageReady && m_bRightImageReady &&
|
||
m_leftImageData.pData && m_rightImageData.pData &&
|
||
m_leftImageData.dataSize > 0 && m_rightImageData.dataSize > 0)
|
||
{
|
||
leftImg = m_leftImageData;
|
||
rightImg = m_rightImageData;
|
||
std::unique_ptr<unsigned char[]> leftBuf(new (std::nothrow) unsigned char[leftImg.dataSize]);
|
||
std::unique_ptr<unsigned char[]> rightBuf(new (std::nothrow) unsigned char[rightImg.dataSize]);
|
||
if (!leftBuf || !rightBuf)
|
||
{
|
||
leftImg.pData = nullptr;
|
||
rightImg.pData = nullptr;
|
||
LOG_ERROR("[SINGLE] alloc latest binocular failed left=%u right=%u\n",
|
||
m_leftImageData.dataSize, m_rightImageData.dataSize);
|
||
return false;
|
||
}
|
||
std::memcpy(leftBuf.get(), m_leftImageData.pData, leftImg.dataSize);
|
||
std::memcpy(rightBuf.get(), m_rightImageData.pData, rightImg.dataSize);
|
||
leftImg.pData = leftBuf.release();
|
||
rightImg.pData = rightBuf.release();
|
||
m_bLeftImageReady = false;
|
||
m_bRightImageReady = false;
|
||
return true;
|
||
}
|
||
}
|
||
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
QString DroneScrewServerPresenter::imageSaveModeName() const
|
||
{
|
||
if (m_detectPipelineMode.load() == kDetectPipelineDistance)
|
||
return QStringLiteral("distance");
|
||
|
||
return QStringLiteral("precision");
|
||
}
|
||
|
||
void DroneScrewServerPresenter::startImageSaveThread(const QString& modeName)
|
||
{
|
||
stopImageSaveThread();
|
||
|
||
const QDateTime now = QDateTime::currentDateTime();
|
||
const QString dateDir = now.toString(QStringLiteral("yyyyMMdd"));
|
||
const QString sessionName = QStringLiteral("%1_%2")
|
||
.arg(modeName,
|
||
now.toString(QStringLiteral("HHmmss_zzz")));
|
||
const QString sessionDir =
|
||
QDir(QDir(QString::fromLatin1(kImageSaveRootDir)).filePath(dateDir)).filePath(sessionName);
|
||
if (!QDir().mkpath(sessionDir))
|
||
{
|
||
LOG_WARN("[SAVE] create image save dir failed: %s\n",
|
||
sessionDir.toStdString().c_str());
|
||
}
|
||
|
||
{
|
||
std::lock_guard<std::mutex> lk(m_imageSaveMutex);
|
||
m_imageSaveQueue.clear();
|
||
m_imageSaveSessionDir = sessionDir;
|
||
m_imageSaveDropped = 0;
|
||
m_imageSaveThreadExit = false;
|
||
}
|
||
|
||
m_imageSaveThreads.reserve(kImageSaveWorkerCount);
|
||
for (size_t i = 0; i < kImageSaveWorkerCount; ++i)
|
||
{
|
||
m_imageSaveThreads.emplace_back(&DroneScrewServerPresenter::imageSaveThreadFunc,
|
||
this,
|
||
static_cast<int>(i));
|
||
}
|
||
LOG_INFO("[SAVE] image save workers started count=%zu mode=%s dir=%s\n",
|
||
m_imageSaveThreads.size(),
|
||
modeName.toStdString().c_str(),
|
||
sessionDir.toStdString().c_str());
|
||
}
|
||
|
||
void DroneScrewServerPresenter::stopImageSaveThread()
|
||
{
|
||
const bool shouldJoin = !m_imageSaveThreads.empty();
|
||
size_t pendingJobs = 0;
|
||
{
|
||
std::lock_guard<std::mutex> lk(m_imageSaveMutex);
|
||
if (!shouldJoin)
|
||
{
|
||
m_imageSaveQueue.clear();
|
||
m_imageSaveSessionDir.clear();
|
||
m_imageSaveThreadExit = false;
|
||
return;
|
||
}
|
||
pendingJobs = m_imageSaveQueue.size();
|
||
m_imageSaveThreadExit = true;
|
||
}
|
||
|
||
if (pendingJobs > 0)
|
||
{
|
||
LOG_INFO("[SAVE] stop requested, flush pending save jobs=%zu\n",
|
||
pendingJobs);
|
||
}
|
||
|
||
m_imageSaveCv.notify_all();
|
||
for (std::thread& worker : m_imageSaveThreads)
|
||
{
|
||
if (worker.joinable())
|
||
worker.join();
|
||
}
|
||
m_imageSaveThreads.clear();
|
||
|
||
{
|
||
std::lock_guard<std::mutex> lk(m_imageSaveMutex);
|
||
m_imageSaveQueue.clear();
|
||
m_imageSaveSessionDir.clear();
|
||
m_imageSaveThreadExit = false;
|
||
}
|
||
}
|
||
|
||
void DroneScrewServerPresenter::enqueueImageSave(const MvsImageData& leftImg,
|
||
const MvsImageData& rightImg,
|
||
const DroneScrewResult& result,
|
||
unsigned long long index)
|
||
{
|
||
auto dropIfQueueFull = [this, index]() -> bool {
|
||
if (m_imageSaveQueue.size() < kMaxImageSaveQueueDepth)
|
||
return false;
|
||
|
||
++m_imageSaveDropped;
|
||
if (m_imageSaveDropped == 1 || (m_imageSaveDropped % 50) == 0)
|
||
{
|
||
LOG_WARN("[SAVE] image save queue full, drop index=%llu dropped=%llu pending=%zu\n",
|
||
index, m_imageSaveDropped, m_imageSaveQueue.size());
|
||
}
|
||
return true;
|
||
};
|
||
|
||
{
|
||
std::lock_guard<std::mutex> lk(m_imageSaveMutex);
|
||
if (m_imageSaveThreadExit.load() || m_imageSaveSessionDir.isEmpty())
|
||
return;
|
||
if (dropIfQueueFull())
|
||
return;
|
||
}
|
||
|
||
auto copyMono8 = [](const MvsImageData& src, ImageSaveBuffer& dst) -> bool {
|
||
if (!src.pData || src.width == 0 || src.height == 0 ||
|
||
src.pixelFormat != kMvsPixelTypeMono8)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const size_t expectedSize =
|
||
static_cast<size_t>(src.width) * static_cast<size_t>(src.height);
|
||
if (src.dataSize < expectedSize)
|
||
return false;
|
||
|
||
dst.width = src.width;
|
||
dst.height = src.height;
|
||
dst.pixelFormat = src.pixelFormat;
|
||
dst.data.resize(expectedSize);
|
||
std::memcpy(dst.data.data(), src.pData, expectedSize);
|
||
dst.valid = true;
|
||
return true;
|
||
};
|
||
|
||
ImageSaveJob job;
|
||
job.index = index;
|
||
job.result = result;
|
||
job.hasResult = true;
|
||
if (!copyMono8(leftImg, job.left) || !copyMono8(rightImg, job.right))
|
||
{
|
||
LOG_WARN("[SAVE] skip image save: unsupported image pair index=%llu left=%ux%u pf=0x%x right=%ux%u pf=0x%x\n",
|
||
index, leftImg.width, leftImg.height, leftImg.pixelFormat,
|
||
rightImg.width, rightImg.height, rightImg.pixelFormat);
|
||
return;
|
||
}
|
||
|
||
{
|
||
std::lock_guard<std::mutex> lk(m_imageSaveMutex);
|
||
if (m_imageSaveThreadExit.load() || m_imageSaveSessionDir.isEmpty())
|
||
return;
|
||
|
||
if (dropIfQueueFull())
|
||
return;
|
||
|
||
job.dirPath = m_imageSaveSessionDir;
|
||
m_imageSaveQueue.emplace_back(std::move(job));
|
||
}
|
||
m_imageSaveCv.notify_one();
|
||
}
|
||
|
||
void DroneScrewServerPresenter::imageSaveThreadFunc(int workerIndex)
|
||
{
|
||
LOG_DEBUG("[SAVE] worker #%d entered\n", workerIndex);
|
||
for (;;)
|
||
{
|
||
ImageSaveJob job;
|
||
{
|
||
std::unique_lock<std::mutex> lk(m_imageSaveMutex);
|
||
m_imageSaveCv.wait(lk, [this]() {
|
||
return m_imageSaveThreadExit.load() || !m_imageSaveQueue.empty();
|
||
});
|
||
|
||
if (m_imageSaveQueue.empty())
|
||
{
|
||
if (m_imageSaveThreadExit.load())
|
||
break;
|
||
continue;
|
||
}
|
||
|
||
job = std::move(m_imageSaveQueue.front());
|
||
m_imageSaveQueue.pop_front();
|
||
}
|
||
|
||
if (job.dirPath.isEmpty())
|
||
continue;
|
||
QDir().mkpath(job.dirPath);
|
||
|
||
auto saveMono8 = [&job](const ImageSaveBuffer& img, const QString& prefix) {
|
||
if (!img.valid || img.data.empty())
|
||
return;
|
||
|
||
QImage q(img.data.data(),
|
||
static_cast<int>(img.width),
|
||
static_cast<int>(img.height),
|
||
static_cast<int>(img.width),
|
||
QImage::Format_Grayscale8);
|
||
const QString filePath = QDir(job.dirPath).filePath(
|
||
savedImageFileName(job.index, prefix));
|
||
if (!q.save(filePath, "PNG"))
|
||
{
|
||
LOG_WARN("[SAVE] image save failed: %s\n",
|
||
filePath.toStdString().c_str());
|
||
}
|
||
};
|
||
|
||
saveMono8(job.left, QStringLiteral("left"));
|
||
saveMono8(job.right, QStringLiteral("right"));
|
||
if (job.hasResult)
|
||
saveDetectionResultText(job.dirPath, job.index, job.result);
|
||
}
|
||
LOG_DEBUG("[SAVE] worker #%d stopped\n", workerIndex);
|
||
}
|
||
|
||
DroneScrewResult DroneScrewServerPresenter::runSingleDetection()
|
||
{
|
||
DroneScrewResult result;
|
||
|
||
auto releaseImage = [](MvsImageData& img) {
|
||
delete[] img.pData;
|
||
img.pData = nullptr;
|
||
img.dataSize = 0;
|
||
};
|
||
|
||
auto fail = [&](int code, const std::string& msg) {
|
||
result.success = false;
|
||
result.errorCode = code;
|
||
result.message = msg;
|
||
LOG_ERROR("[SINGLE] %s\n", msg.c_str());
|
||
logDetectionResultFixed("SINGLE", 0, result);
|
||
emit detectionResult(result);
|
||
return result;
|
||
};
|
||
|
||
if (!m_pLeftCamera || !m_pRightCamera)
|
||
{
|
||
return fail(ERR_CODE(DRONESCREW_ERR_CAMERA_NOT_CONNECTED), "cameras not connected");
|
||
}
|
||
|
||
{
|
||
MvsImageData leftImg{};
|
||
MvsImageData rightImg{};
|
||
bool usedCachedFrame = false;
|
||
|
||
if (m_bIsDetecting.load())
|
||
{
|
||
QMutexLocker lk(&m_frameMutex);
|
||
if (!m_leftImageData.pData || m_leftImageData.dataSize == 0 ||
|
||
!m_rightImageData.pData || m_rightImageData.dataSize == 0)
|
||
{
|
||
return fail(ERR_CODE(DRONESCREW_ERR_CACHED_FRAME_NOT_READY),
|
||
"cached binocular image not ready");
|
||
}
|
||
|
||
leftImg = m_leftImageData;
|
||
rightImg = m_rightImageData;
|
||
std::unique_ptr<unsigned char[]> leftBuf(new (std::nothrow) unsigned char[leftImg.dataSize]);
|
||
std::unique_ptr<unsigned char[]> rightBuf(new (std::nothrow) unsigned char[rightImg.dataSize]);
|
||
if (!leftBuf || !rightBuf)
|
||
{
|
||
return fail(ERR_CODE(DATA_ERR_MEM), "copy cached frame memory allocation failed");
|
||
}
|
||
memcpy(leftBuf.get(), m_leftImageData.pData, leftImg.dataSize);
|
||
memcpy(rightBuf.get(), m_rightImageData.pData, rightImg.dataSize);
|
||
leftImg.pData = leftBuf.release();
|
||
rightImg.pData = rightBuf.release();
|
||
usedCachedFrame = true;
|
||
}
|
||
else
|
||
{
|
||
const bool leftGrabbing = m_pLeftCamera->IsAcquisitioning();
|
||
const bool rightGrabbing = m_pRightCamera->IsAcquisitioning();
|
||
if (!leftGrabbing || !rightGrabbing)
|
||
{
|
||
LOG_ERROR("[SINGLE] acquisition not started: left=%d right=%d\n",
|
||
leftGrabbing ? 1 : 0, rightGrabbing ? 1 : 0);
|
||
if (!leftGrabbing)
|
||
return fail(ERR_CODE(DRONESCREW_ERR_LEFT_ACQ_NOT_STARTED),
|
||
"left camera acquisition not started");
|
||
return fail(ERR_CODE(DRONESCREW_ERR_RIGHT_ACQ_NOT_STARTED),
|
||
"right camera acquisition not started");
|
||
}
|
||
|
||
resetFrameReadyFlags();
|
||
|
||
const int gpioRet = pulseGpioTrigger();
|
||
if (gpioRet != 0)
|
||
{
|
||
LOG_ERROR("[SINGLE] gpio trigger failed ret=%d\n", gpioRet);
|
||
return fail(ERR_CODE(DRONESCREW_ERR_GPIO_TRIGGER), "gpio trigger failed");
|
||
}
|
||
|
||
if (!waitAndCopyLatestBinocular(leftImg, rightImg, kSingleFrameWaitTimeoutMs))
|
||
return fail(ERR_CODE(DRONESCREW_ERR_BINOCULAR_FRAME_TIMEOUT),
|
||
"wait binocular callback frame timeout");
|
||
}
|
||
|
||
DroneScrewInputImage inLeft, inRight;
|
||
|
||
inLeft.data = leftImg.pData;
|
||
inLeft.width = leftImg.width;
|
||
inLeft.height = leftImg.height;
|
||
inLeft.stride = leftImg.width;
|
||
inLeft.pixelFormat = 0;
|
||
inLeft.frameId = leftImg.frameID;
|
||
inLeft.timestampUs = leftImg.timestamp;
|
||
|
||
inRight.data = rightImg.pData;
|
||
inRight.width = rightImg.width;
|
||
inRight.height = rightImg.height;
|
||
inRight.stride = rightImg.width;
|
||
inRight.pixelFormat = 0;
|
||
inRight.frameId = rightImg.frameID;
|
||
inRight.timestampUs = rightImg.timestamp;
|
||
|
||
if (!m_pAlgo)
|
||
{
|
||
result.frameId = leftImg.frameID;
|
||
result.timestampUs = static_cast<int64_t>(leftImg.timestamp);
|
||
result.imageWidth = static_cast<int>(leftImg.width);
|
||
result.imageHeight = static_cast<int>(leftImg.height);
|
||
result.success = false;
|
||
result.errorCode = ERR_CODE(DRONESCREW_ERR_ALGO_NOT_INIT);
|
||
result.message = "algorithm not initialized";
|
||
}
|
||
else
|
||
{
|
||
LOG_INFO("[SINGLE] algo input: left=%llu %ux%u size=%u right=%llu %ux%u size=%u\n",
|
||
inLeft.frameId, inLeft.width, inLeft.height, leftImg.dataSize,
|
||
inRight.frameId, inRight.width, inRight.height, rightImg.dataSize);
|
||
|
||
const auto t0 = std::chrono::steady_clock::now();
|
||
const int algoRet = m_pAlgo->Detect(inLeft, inRight, result);
|
||
const auto t1 = std::chrono::steady_clock::now();
|
||
const double elapsedMs = std::chrono::duration<double, std::milli>(t1 - t0).count();
|
||
|
||
if (result.frameId == 0) result.frameId = leftImg.frameID;
|
||
if (result.timestampUs == 0) result.timestampUs = static_cast<int64_t>(leftImg.timestamp);
|
||
if (result.imageWidth == 0) result.imageWidth = static_cast<int>(leftImg.width);
|
||
if (result.imageHeight == 0) result.imageHeight = static_cast<int>(leftImg.height);
|
||
|
||
LOG_INFO("[SINGLE] algo output: ret=%d success=%d boxes=%zu elapsed=%.1fms\n",
|
||
algoRet, result.success ? 1 : 0, result.boxes.size(), elapsedMs);
|
||
|
||
for (size_t bi = 0; bi < result.boxes.size(); ++bi)
|
||
{
|
||
const auto& b = result.boxes[bi];
|
||
LOG_DEBUG("[SINGLE] box[%zu] cls=%d score=%.3f x=%d y=%d w=%d h=%d\n",
|
||
bi, b.classId, b.score, b.x, b.y, b.width, b.height);
|
||
}
|
||
|
||
if (algoRet != 0)
|
||
{
|
||
result.success = false;
|
||
result.errorCode = algoRet;
|
||
if (result.message.empty())
|
||
result.message = "algorithm Detect failed: " + std::to_string(algoRet);
|
||
}
|
||
}
|
||
|
||
logDetectionResultFixed("SINGLE", 1, result);
|
||
emit detectionResult(result);
|
||
|
||
releaseImage(leftImg);
|
||
releaseImage(rightImg);
|
||
LOG_INFO("[SINGLE] completed frame=%llu source=%s success=%d code=%d\n",
|
||
result.frameId, usedCachedFrame ? "cache" : "callback",
|
||
result.success ? 1 : 0, result.errorCode);
|
||
return result;
|
||
}
|
||
|
||
}
|
||
|
||
void DroneScrewServerPresenter::handleSetExposure(double exposureTime)
|
||
{
|
||
m_leftExposureTime = exposureTime;
|
||
m_rightExposureTime = exposureTime;
|
||
if (m_pLeftCamera)
|
||
{
|
||
applyCurrentExposureForRole("left");
|
||
LOG_INFO("Left camera base exposure set to %.2f\n", exposureTime);
|
||
}
|
||
if (m_pRightCamera)
|
||
{
|
||
applyCurrentExposureForRole("right");
|
||
LOG_INFO("Right camera base exposure set to %.2f\n", exposureTime);
|
||
}
|
||
saveConfiguration();
|
||
}
|
||
|
||
void DroneScrewServerPresenter::handleSetGain(double gain)
|
||
{
|
||
m_leftGain = gain;
|
||
m_rightGain = gain;
|
||
if (m_pLeftCamera)
|
||
{
|
||
m_pLeftCamera->SetGain(gain);
|
||
LOG_INFO("Left camera gain set to %.2f\n", gain);
|
||
}
|
||
if (m_pRightCamera)
|
||
{
|
||
m_pRightCamera->SetGain(gain);
|
||
LOG_INFO("Right camera gain set to %.2f\n", gain);
|
||
}
|
||
saveConfiguration();
|
||
}
|
||
|
||
void DroneScrewServerPresenter::handleSetLeftExposure(double exposureTime)
|
||
{
|
||
m_leftExposureTime = exposureTime;
|
||
if (m_pLeftCamera)
|
||
{
|
||
applyCurrentExposureForRole("left");
|
||
LOG_INFO("Left camera base exposure set to %.2f\n", exposureTime);
|
||
}
|
||
else
|
||
{
|
||
LOG_WARN("Left camera not initialized\n");
|
||
}
|
||
saveConfiguration();
|
||
}
|
||
|
||
void DroneScrewServerPresenter::handleSetRightExposure(double exposureTime)
|
||
{
|
||
m_rightExposureTime = exposureTime;
|
||
if (m_pRightCamera)
|
||
{
|
||
applyCurrentExposureForRole("right");
|
||
LOG_INFO("Right camera base exposure set to %.2f\n", exposureTime);
|
||
}
|
||
else
|
||
{
|
||
LOG_WARN("Right camera not initialized\n");
|
||
}
|
||
saveConfiguration();
|
||
}
|
||
|
||
void DroneScrewServerPresenter::handleSetLeftGain(double gain)
|
||
{
|
||
m_leftGain = gain;
|
||
if (m_pLeftCamera)
|
||
{
|
||
m_pLeftCamera->SetGain(gain);
|
||
LOG_INFO("Left camera gain set to %.2f\n", gain);
|
||
}
|
||
else
|
||
{
|
||
LOG_WARN("Left camera not initialized\n");
|
||
}
|
||
saveConfiguration();
|
||
}
|
||
|
||
void DroneScrewServerPresenter::handleSetRightGain(double gain)
|
||
{
|
||
m_rightGain = gain;
|
||
if (m_pRightCamera)
|
||
{
|
||
m_pRightCamera->SetGain(gain);
|
||
LOG_INFO("Right camera gain set to %.2f\n", gain);
|
||
}
|
||
else
|
||
{
|
||
LOG_WARN("Right camera not initialized\n");
|
||
}
|
||
saveConfiguration();
|
||
}
|
||
|
||
void DroneScrewServerPresenter::setDetectMode(const std::string& mode)
|
||
{
|
||
if (mode == "mono" || mode == "binocular")
|
||
{
|
||
{
|
||
std::lock_guard<std::mutex> lk(m_detectModeMutex);
|
||
m_detectMode = mode;
|
||
}
|
||
LOG_INFO("[DETECT] detectMode set to: %s\n", mode.c_str());
|
||
}
|
||
else
|
||
{
|
||
const std::string current = detectMode();
|
||
LOG_WARN("[DETECT] unknown detectMode: %s, keep current: %s\n",
|
||
mode.c_str(), current.c_str());
|
||
}
|
||
}
|
||
|
||
std::string DroneScrewServerPresenter::detectMode() const
|
||
{
|
||
std::lock_guard<std::mutex> lk(m_detectModeMutex);
|
||
return m_detectMode;
|
||
}
|
||
|
||
void DroneScrewServerPresenter::handleUpdateAlgoParams(const DroneScrewAlgoParams& params)
|
||
{
|
||
m_algoParams = params;
|
||
if (m_pAlgo) m_pAlgo->UpdateParams(params);
|
||
saveConfiguration();
|
||
}
|
||
|
||
void DroneScrewServerPresenter::detectThreadFunc()
|
||
{
|
||
{
|
||
std::ostringstream oss;
|
||
oss << std::this_thread::get_id();
|
||
LOG_DEBUG("[DETECT] thread started (binocular mode) tid=%s\n", oss.str().c_str());
|
||
}
|
||
|
||
int frameCnt = 0;
|
||
unsigned long long savedFrameCnt = 0;
|
||
bool haveProcessedFrame = false;
|
||
unsigned long long lastProcessedLeftFrameId = 0;
|
||
unsigned long long lastProcessedRightFrameId = 0;
|
||
auto nextWaitLog = std::chrono::steady_clock::now();
|
||
try
|
||
{
|
||
while (!m_bThreadExit.load())
|
||
{
|
||
try
|
||
{
|
||
// 等待双目图像都准备好
|
||
bool leftReady = false;
|
||
bool rightReady = false;
|
||
bool pairValid = false;
|
||
bool pairChanged = false;
|
||
unsigned long long currentLeftFrameId = 0;
|
||
unsigned long long currentRightFrameId = 0;
|
||
|
||
// 取出双目图像(深拷贝)
|
||
MvsImageData leftImg, rightImg;
|
||
std::unique_ptr<unsigned char[]> leftBuf;
|
||
std::unique_ptr<unsigned char[]> rightBuf;
|
||
{
|
||
QMutexLocker lk(&m_frameMutex);
|
||
|
||
leftReady = m_bLeftImageReady;
|
||
rightReady = m_bRightImageReady;
|
||
currentLeftFrameId = m_leftImageData.frameID;
|
||
currentRightFrameId = m_rightImageData.frameID;
|
||
pairValid =
|
||
leftReady && rightReady &&
|
||
m_leftImageData.pData && m_rightImageData.pData &&
|
||
m_leftImageData.dataSize > 0 && m_rightImageData.dataSize > 0;
|
||
pairChanged =
|
||
pairValid &&
|
||
(!haveProcessedFrame ||
|
||
currentLeftFrameId != lastProcessedLeftFrameId ||
|
||
currentRightFrameId != lastProcessedRightFrameId);
|
||
|
||
if (pairChanged)
|
||
{
|
||
leftImg = m_leftImageData;
|
||
rightImg = m_rightImageData;
|
||
|
||
// Deep-copy the latest valid pair. Do not clear ready flags here;
|
||
// continuous detection advances by frame-id changes.
|
||
leftBuf.reset(new unsigned char[leftImg.dataSize]);
|
||
memcpy(leftBuf.get(), m_leftImageData.pData, leftImg.dataSize);
|
||
leftImg.pData = leftBuf.get();
|
||
|
||
rightBuf.reset(new unsigned char[rightImg.dataSize]);
|
||
memcpy(rightBuf.get(), m_rightImageData.pData, rightImg.dataSize);
|
||
rightImg.pData = rightBuf.get();
|
||
}
|
||
}
|
||
|
||
if (!pairValid || !pairChanged)
|
||
{
|
||
const auto now = std::chrono::steady_clock::now();
|
||
if (now >= nextWaitLog)
|
||
{
|
||
LOG_DEBUG("[DETECT] waiting latest pair: leftReady=%d rightReady=%d left=%llu right=%llu last=%llu/%llu changed=%d\n",
|
||
leftReady ? 1 : 0, rightReady ? 1 : 0,
|
||
currentLeftFrameId, currentRightFrameId,
|
||
lastProcessedLeftFrameId, lastProcessedRightFrameId,
|
||
pairChanged ? 1 : 0);
|
||
nextWaitLog = now + std::chrono::seconds(1);
|
||
}
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||
continue;
|
||
}
|
||
|
||
haveProcessedFrame = true;
|
||
lastProcessedLeftFrameId = leftImg.frameID;
|
||
lastProcessedRightFrameId = rightImg.frameID;
|
||
|
||
// Publish the display frame before detection work. detectMode only controls raw transport.
|
||
if (m_bRawPubEnabled.load())
|
||
{
|
||
const int rawPipelineMode = m_detectPipelineMode.load();
|
||
const bool rawSingleImage = (detectMode() == "mono");
|
||
LOG_DEBUG("[DETECT] frame #%d before rawImageReady emit, left=%llu right=%llu singleRaw=%d pipeline=%d\n",
|
||
frameCnt + 1, leftImg.frameID, rightImg.frameID,
|
||
rawSingleImage ? 1 : 0, rawPipelineMode);
|
||
if (rawSingleImage)
|
||
{
|
||
MvsImageData emptyRight{};
|
||
emit rawImageReady(leftImg, emptyRight);
|
||
}
|
||
else
|
||
{
|
||
emit rawImageReady(leftImg, rightImg);
|
||
}
|
||
LOG_DEBUG("[DETECT] frame #%d after rawImageReady emit\n", frameCnt + 1);
|
||
}
|
||
|
||
#if 0
|
||
|
||
// ZMQ 原始图像 PUB(在检测之前发送,避免阻塞)
|
||
if (m_bRawPubEnabled.load())
|
||
{
|
||
const bool isMono = (detectMode() == "mono");
|
||
LOG_DEBUG("[DETECT] frame #%d before rawImageReady emit, left=%llu right=%llu mono=%d\n",
|
||
frameCnt + 1, leftImg.frameID, rightImg.frameID, isMono ? 1 : 0);
|
||
if (isMono)
|
||
{
|
||
// mono 模式:仅发送左目图像,右目传空
|
||
MvsImageData emptyRight{};
|
||
emit rawImageReady(leftImg, emptyRight);
|
||
}
|
||
else
|
||
{
|
||
emit rawImageReady(leftImg, rightImg);
|
||
}
|
||
LOG_DEBUG("[DETECT] frame #%d after rawImageReady emit\n", frameCnt + 1);
|
||
}
|
||
|
||
// 调用算法进行检测(双目输入)
|
||
#endif
|
||
|
||
DroneScrewResult result;
|
||
DroneScrewInputImage inLeft, inRight;
|
||
|
||
inLeft.data = leftImg.pData;
|
||
inLeft.width = leftImg.width;
|
||
inLeft.height = leftImg.height;
|
||
inLeft.stride = leftImg.width;
|
||
inLeft.pixelFormat = 0;
|
||
inLeft.frameId = leftImg.frameID;
|
||
inLeft.timestampUs = leftImg.timestamp;
|
||
|
||
inRight.data = rightImg.pData;
|
||
inRight.width = rightImg.width;
|
||
inRight.height = rightImg.height;
|
||
inRight.stride = rightImg.width;
|
||
inRight.pixelFormat = 0;
|
||
inRight.frameId = rightImg.frameID;
|
||
inRight.timestampUs = rightImg.timestamp;
|
||
|
||
result.frameId = leftImg.frameID;
|
||
result.timestampUs = static_cast<int64_t>(leftImg.timestamp);
|
||
result.imageWidth = static_cast<int>(leftImg.width);
|
||
result.imageHeight = static_cast<int>(leftImg.height);
|
||
|
||
const int pipelineMode = m_detectPipelineMode.load();
|
||
const bool distanceMode = (pipelineMode == kDetectPipelineDistance);
|
||
if (distanceMode)
|
||
{
|
||
if (m_pAlgo)
|
||
{
|
||
try
|
||
{
|
||
const auto t0 = std::chrono::steady_clock::now();
|
||
const int algoRet = m_pAlgo->DetectDistance(inLeft, inRight, result);
|
||
const auto t1 = std::chrono::steady_clock::now();
|
||
const double elapsedMs =
|
||
std::chrono::duration<double, std::milli>(t1 - t0).count();
|
||
|
||
if (algoRet != 0)
|
||
{
|
||
result.success = false;
|
||
result.errorCode = algoRet;
|
||
if (result.message.empty())
|
||
result.message = "algorithm DetectDistance failed: " +
|
||
std::to_string(algoRet);
|
||
}
|
||
|
||
if (frameCnt == 0)
|
||
{
|
||
LOG_INFO("[DISTANCE] first frame input: left=%llu %ux%u size=%u right=%llu %ux%u size=%u\n",
|
||
inLeft.frameId, inLeft.width, inLeft.height, leftImg.dataSize,
|
||
inRight.frameId, inRight.width, inRight.height, rightImg.dataSize);
|
||
LOG_INFO("[DISTANCE] first frame output: ret=%d success=%d code=%d boxes=%zu distances=%zu elapsed=%.1fms msg=%s\n",
|
||
algoRet, result.success ? 1 : 0, result.errorCode,
|
||
result.boxes.size(), result.distances.size(),
|
||
elapsedMs, result.message.c_str());
|
||
}
|
||
else if (elapsedMs > 50.0)
|
||
{
|
||
LOG_WARN("[DISTANCE] frame #%d algo slow: elapsed=%.1fms left=%llu %ux%u success=%d code=%d distances=%zu msg=%s\n",
|
||
frameCnt + 1, elapsedMs, leftImg.frameID,
|
||
leftImg.width, leftImg.height, result.success ? 1 : 0,
|
||
result.errorCode, result.distances.size(),
|
||
result.message.c_str());
|
||
}
|
||
}
|
||
catch (const std::exception& e)
|
||
{
|
||
LOG_ERROR("[DISTANCE] algo exception frame=%llu: %s\n", leftImg.frameID, e.what());
|
||
result.success = false;
|
||
result.errorCode = -1;
|
||
result.message = std::string("Distance algorithm exception: ") + e.what();
|
||
}
|
||
catch (...)
|
||
{
|
||
LOG_ERROR("[DISTANCE] algo unknown exception frame=%llu\n", leftImg.frameID);
|
||
result.success = false;
|
||
result.errorCode = -1;
|
||
result.message = "Distance algorithm unknown exception";
|
||
}
|
||
}
|
||
else
|
||
{
|
||
result.success = false;
|
||
result.errorCode = ERR_CODE(DRONESCREW_ERR_ALGO_NOT_INIT);
|
||
result.message = "algorithm not initialized";
|
||
}
|
||
}
|
||
else
|
||
{
|
||
LOG_DEBUG("[DETECT] frame #%d before algo Detect, left=%llu right=%llu\n",
|
||
frameCnt + 1, leftImg.frameID, rightImg.frameID);
|
||
|
||
if (m_pAlgo)
|
||
{
|
||
try
|
||
{
|
||
const auto t0 = std::chrono::steady_clock::now();
|
||
const int algoRet = m_pAlgo->Detect(inLeft, inRight, result);
|
||
const auto t1 = std::chrono::steady_clock::now();
|
||
const double elapsedMs = std::chrono::duration<double, std::milli>(t1 - t0).count();
|
||
|
||
if (result.frameId == 0) result.frameId = leftImg.frameID;
|
||
if (result.timestampUs == 0) result.timestampUs = static_cast<int64_t>(leftImg.timestamp);
|
||
if (result.imageWidth == 0) result.imageWidth = static_cast<int>(leftImg.width);
|
||
if (result.imageHeight == 0) result.imageHeight = static_cast<int>(leftImg.height);
|
||
|
||
if (algoRet != 0)
|
||
{
|
||
result.success = false;
|
||
result.errorCode = algoRet;
|
||
if (result.message.empty())
|
||
result.message = "algorithm Detect failed: " + std::to_string(algoRet);
|
||
}
|
||
|
||
// 首帧打印 algo 输入输出详情
|
||
if (frameCnt == 0)
|
||
{
|
||
LOG_INFO("[DETECT] first frame algo input: left=%llu %ux%u size=%u right=%llu %ux%u size=%u\n",
|
||
inLeft.frameId, inLeft.width, inLeft.height, leftImg.dataSize,
|
||
inRight.frameId, inRight.width, inRight.height, rightImg.dataSize);
|
||
LOG_INFO("[DETECT] first frame algo output: ret=%d success=%d code=%d boxes=%zu distances=%zu elapsed=%.1fms msg=%s\n",
|
||
algoRet, result.success ? 1 : 0, result.errorCode,
|
||
result.boxes.size(), result.distances.size(),
|
||
elapsedMs, result.message.c_str());
|
||
for (size_t bi = 0; bi < result.boxes.size(); ++bi)
|
||
{
|
||
const auto& b = result.boxes[bi];
|
||
LOG_INFO("[DETECT] box[%zu] cls=%d score=%.3f x=%d y=%d w=%d h=%d\n",
|
||
bi, b.classId, b.score, b.x, b.y, b.width, b.height);
|
||
}
|
||
}
|
||
else if (elapsedMs > 100.0)
|
||
{
|
||
// 耗时异常帧:打印告警
|
||
LOG_WARN("[DETECT] frame #%d algo slow: elapsed=%.1fms left=%llu %ux%u success=%d code=%d boxes=%zu distances=%zu msg=%s\n",
|
||
frameCnt + 1, elapsedMs,
|
||
leftImg.frameID, leftImg.width, leftImg.height,
|
||
result.success ? 1 : 0, result.errorCode,
|
||
result.boxes.size(), result.distances.size(),
|
||
result.message.c_str());
|
||
}
|
||
}
|
||
catch (const std::exception& e)
|
||
{
|
||
LOG_ERROR("[DETECT] algo exception frame=%llu: %s\n", leftImg.frameID, e.what());
|
||
result.success = false;
|
||
result.errorCode = -1;
|
||
result.message = std::string("Algorithm exception: ") + e.what();
|
||
}
|
||
catch (...)
|
||
{
|
||
LOG_ERROR("[DETECT] algo unknown exception frame=%llu\n", leftImg.frameID);
|
||
result.success = false;
|
||
result.errorCode = -1;
|
||
result.message = "Algorithm unknown exception";
|
||
}
|
||
}
|
||
else
|
||
{
|
||
result.success = false;
|
||
result.errorCode = ERR_CODE(DRONESCREW_ERR_ALGO_NOT_INIT);
|
||
result.message = "algorithm not initialized";
|
||
}
|
||
}
|
||
|
||
if (result.frameId == 0) result.frameId = leftImg.frameID;
|
||
if (result.timestampUs == 0) result.timestampUs = static_cast<int64_t>(leftImg.timestamp);
|
||
result.imageWidth = static_cast<int>(leftImg.width);
|
||
result.imageHeight = static_cast<int>(leftImg.height);
|
||
|
||
logDetectionResultFixed(distanceMode ? "DISTANCE" : "DETECT",
|
||
frameCnt + 1, result);
|
||
|
||
LOG_DEBUG("[DETECT] frame #%d after %s pipeline, success=%d code=%d boxes=%zu distances=%zu\n",
|
||
frameCnt + 1, distanceMode ? "distance" : "precision",
|
||
result.success ? 1 : 0, result.errorCode,
|
||
result.boxes.size(), result.distances.size());
|
||
|
||
if (!distanceMode)
|
||
enqueueImageSave(leftImg, rightImg, result, ++savedFrameCnt);
|
||
|
||
emit detectionResult(result);
|
||
|
||
LOG_DEBUG("[DETECT] frame #%d after detectionResult emit\n", frameCnt + 1);
|
||
|
||
frameCnt++;
|
||
if (frameCnt % 100 == 0)
|
||
{
|
||
// 打印最近检测到的目标摘要
|
||
std::ostringstream boxSummary;
|
||
for (size_t bi = 0; bi < result.boxes.size() && bi < 5; ++bi)
|
||
{
|
||
if (bi > 0) boxSummary << ", ";
|
||
boxSummary << "cls=" << result.boxes[bi].classId
|
||
<< " score=" << result.boxes[bi].score;
|
||
}
|
||
if (result.boxes.size() > 5)
|
||
boxSummary << "... +" << (result.boxes.size() - 5);
|
||
|
||
LOG_INFO("[DETECT] frame #%d id=%llu rawPub=%d boxes=%zu success=%d [%s]\n",
|
||
frameCnt, leftImg.frameID, m_bRawPubEnabled.load() ? 1 : 0,
|
||
result.boxes.size(), result.success ? 1 : 0,
|
||
boxSummary.str().c_str());
|
||
}
|
||
|
||
LOG_DEBUG("[DETECT] frame #%d loop end, checking exit flag=%d\n",
|
||
frameCnt, m_bThreadExit.load() ? 1 : 0);
|
||
}
|
||
catch (const std::bad_alloc& e)
|
||
{
|
||
LOG_ERROR("[DETECT] memory allocation failed: %s\n", e.what());
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||
}
|
||
catch (const std::exception& e)
|
||
{
|
||
LOG_ERROR("[DETECT] exception: %s\n", e.what());
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||
}
|
||
catch (...)
|
||
{
|
||
LOG_ERROR("[DETECT] unknown exception\n");
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||
}
|
||
} // end while
|
||
|
||
LOG_DEBUG("[DETECT] thread stopped, total frames=%d\n", frameCnt);
|
||
}
|
||
catch (const std::exception& e)
|
||
{
|
||
LOG_ERROR("[DETECT] FATAL: outer exception (thread will exit): %s\n", e.what());
|
||
}
|
||
catch (...)
|
||
{
|
||
LOG_ERROR("[DETECT] FATAL: outer unknown exception (thread will exit)\n");
|
||
}
|
||
}
|
||
|
||
bool DroneScrewServerPresenter::tryRgaScaleMonoToY(const MvsImageData& img,
|
||
unsigned char* dstY,
|
||
unsigned int outWidth,
|
||
unsigned int outHeight)
|
||
{
|
||
#if DRONESCREW_HAS_RGA
|
||
if (!img.pData || !dstY || img.width == 0 || img.height == 0 ||
|
||
outWidth == 0 || outHeight == 0)
|
||
return false;
|
||
|
||
rga_buffer_t src = wrapbuffer_virtualaddr(
|
||
const_cast<unsigned char*>(img.pData),
|
||
static_cast<int>(img.width),
|
||
static_cast<int>(img.height),
|
||
RK_FORMAT_YCbCr_400,
|
||
static_cast<int>(img.width),
|
||
static_cast<int>(img.height));
|
||
rga_buffer_t dst = wrapbuffer_virtualaddr(
|
||
dstY,
|
||
static_cast<int>(outWidth),
|
||
static_cast<int>(outHeight),
|
||
RK_FORMAT_YCbCr_400,
|
||
static_cast<int>(outWidth),
|
||
static_cast<int>(outHeight));
|
||
|
||
const IM_STATUS status = imresize(src, dst);
|
||
if (status == IM_STATUS_SUCCESS)
|
||
{
|
||
if (!m_rtspRgaScaleLogged)
|
||
{
|
||
LOG_INFO("[RGA] live scale Mono8/Y %ux%u -> %ux%u enabled\n",
|
||
img.width, img.height, outWidth, outHeight);
|
||
m_rtspRgaScaleLogged = true;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
if (!m_rtspRgaScaleDisabled)
|
||
{
|
||
LOG_WARN("[RGA] live scale failed status=%d (%s), fallback to CPU\n",
|
||
static_cast<int>(status), rgaErrorText(status));
|
||
m_rtspRgaScaleDisabled = true;
|
||
}
|
||
return false;
|
||
#else
|
||
(void)img;
|
||
(void)dstY;
|
||
(void)outWidth;
|
||
(void)outHeight;
|
||
return false;
|
||
#endif
|
||
}
|
||
|
||
int DroneScrewServerPresenter::pushRtspFrame(const MvsImageData& img)
|
||
{
|
||
try
|
||
{
|
||
std::lock_guard<std::mutex> pusherLock(m_rtspPusherMutex);
|
||
if (!m_pPusher) return ERR_CODE(DRONESCREW_ERR_RTSP_INIT);
|
||
if (!m_bLiveStreaming.load() || !m_bRtspStarted.load()) return 0;
|
||
if (!img.pData || img.dataSize == 0 || img.width == 0 || img.height == 0)
|
||
{
|
||
const int ret = ERR_CODE(DATA_ERR_INVALID);
|
||
recordRtspPushResult(ret, img.frameID);
|
||
return ret;
|
||
}
|
||
|
||
const unsigned int outWidth = m_rtspWidth > 0 ? m_rtspWidth : img.width;
|
||
const unsigned int outHeight = m_rtspHeight > 0 ? m_rtspHeight : img.height;
|
||
if (outWidth == 0 || outHeight == 0)
|
||
{
|
||
const int ret = ERR_CODE(DATA_ERR_INVALID);
|
||
recordRtspPushResult(ret, img.frameID);
|
||
return ret;
|
||
}
|
||
|
||
if (img.pixelFormat == kMvsPixelTypeMono8)
|
||
{
|
||
const size_t monoSize = static_cast<size_t>(img.width) * static_cast<size_t>(img.height);
|
||
if (img.dataSize < monoSize)
|
||
{
|
||
const int ret = ERR_CODE(DATA_ERR_LEN);
|
||
recordRtspPushResult(ret, img.frameID);
|
||
return ret;
|
||
}
|
||
|
||
const size_t ySize = static_cast<size_t>(outWidth) *
|
||
static_cast<size_t>(outHeight);
|
||
const size_t uvSize = ySize / 2;
|
||
const size_t frameSize = ySize + uvSize;
|
||
if (m_rtspFrameBuffer.size() != frameSize)
|
||
{
|
||
m_rtspFrameBuffer.resize(frameSize);
|
||
m_rtspUvInitialized = false;
|
||
}
|
||
|
||
const unsigned char* src = img.pData;
|
||
unsigned char* dstY = m_rtspFrameBuffer.data();
|
||
|
||
const bool sameSize = (outWidth == img.width && outHeight == img.height);
|
||
const bool scaledByRga =
|
||
!sameSize &&
|
||
!m_rtspRgaScaleDisabled &&
|
||
tryRgaScaleMonoToY(img, dstY, outWidth, outHeight);
|
||
|
||
if (!scaledByRga)
|
||
{
|
||
if (m_rtspScaleSrcWidth != img.width ||
|
||
m_rtspScaleSrcHeight != img.height ||
|
||
m_rtspScaleOutWidth != outWidth ||
|
||
m_rtspScaleOutHeight != outHeight ||
|
||
m_rtspScaleX.size() != outWidth ||
|
||
m_rtspScaleY.size() != outHeight)
|
||
{
|
||
m_rtspScaleX.resize(outWidth);
|
||
m_rtspScaleY.resize(outHeight);
|
||
for (unsigned int x = 0; x < outWidth; ++x)
|
||
{
|
||
m_rtspScaleX[x] =
|
||
static_cast<unsigned int>((static_cast<uint64_t>(x) * img.width) / outWidth);
|
||
}
|
||
for (unsigned int y = 0; y < outHeight; ++y)
|
||
{
|
||
m_rtspScaleY[y] =
|
||
static_cast<unsigned int>((static_cast<uint64_t>(y) * img.height) / outHeight);
|
||
}
|
||
m_rtspScaleSrcWidth = img.width;
|
||
m_rtspScaleSrcHeight = img.height;
|
||
m_rtspScaleOutWidth = outWidth;
|
||
m_rtspScaleOutHeight = outHeight;
|
||
}
|
||
|
||
if (sameSize)
|
||
{
|
||
std::memcpy(dstY, src, ySize);
|
||
}
|
||
else
|
||
{
|
||
for (unsigned int y = 0; y < outHeight; ++y)
|
||
{
|
||
const unsigned char* srcRow =
|
||
src + static_cast<size_t>(m_rtspScaleY[y]) * img.width;
|
||
unsigned char* dstRow = dstY + static_cast<size_t>(y) * outWidth;
|
||
for (unsigned int x = 0; x < outWidth; ++x)
|
||
{
|
||
dstRow[x] = srcRow[m_rtspScaleX[x]];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!m_rtspUvInitialized)
|
||
{
|
||
std::memset(m_rtspFrameBuffer.data() + ySize, 128, uvSize);
|
||
m_rtspUvInitialized = true;
|
||
}
|
||
|
||
// 计算 PTS(微秒):按实际直播 fps 递增,避免 RTMP 时间戳被误放大到检测帧率。
|
||
const int64_t frameIdx = m_rtspFrameCounter.fetch_add(1, std::memory_order_relaxed);
|
||
const double streamFps = m_liveStreamFps > 0
|
||
? static_cast<double>(m_liveStreamFps)
|
||
: static_cast<double>(kLiveStreamFrameRate);
|
||
const int64_t ptsUs = static_cast<int64_t>(
|
||
(static_cast<double>(frameIdx) / streamFps) * 1000000.0
|
||
);
|
||
const int ret = m_pPusher->PushFrame(m_rtspFrameBuffer.data(),
|
||
m_rtspFrameBuffer.size(),
|
||
ptsUs);
|
||
recordRtspPushResult(ret, img.frameID);
|
||
return ret;
|
||
}
|
||
|
||
const int ret = ERR_CODE(DATA_ERR_INVALID);
|
||
recordRtspPushResult(ret, img.frameID);
|
||
return ret;
|
||
}
|
||
catch (const std::bad_alloc& e)
|
||
{
|
||
LOG_ERROR("[RTSP] push memory allocation failed frame=%llu src=%ux%u: %s\n",
|
||
img.frameID, img.width, img.height, e.what());
|
||
const int ret = ERR_CODE(DATA_ERR_MEM);
|
||
recordRtspPushResult(ret, img.frameID);
|
||
return ret;
|
||
}
|
||
catch (const std::exception& e)
|
||
{
|
||
LOG_ERROR("[RTSP] push exception frame=%llu src=%ux%u: %s\n",
|
||
img.frameID, img.width, img.height, e.what());
|
||
const int ret = ERR_CODE(DATA_ERR_INVALID);
|
||
recordRtspPushResult(ret, img.frameID);
|
||
return ret;
|
||
}
|
||
catch (...)
|
||
{
|
||
LOG_ERROR("[RTSP] push unknown exception frame=%llu src=%ux%u\n",
|
||
img.frameID, img.width, img.height);
|
||
const int ret = ERR_CODE(DATA_ERR_INVALID);
|
||
recordRtspPushResult(ret, img.frameID);
|
||
return ret;
|
||
}
|
||
}
|