Se mejora notablemente la calibracion usando tablero de ajedrez y se usa opencl para procesamiento
This commit is contained in:
parent
c13794ada0
commit
bf5e058030
6 changed files with 854 additions and 466 deletions
|
|
@ -9,17 +9,19 @@ set(CMAKE_AUTORCC ON)
|
|||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Widgets OpenGLWidgets OpenGL)
|
||||
find_package(OpenCV REQUIRED)
|
||||
|
||||
add_executable(weld_measure
|
||||
main.cpp
|
||||
main_window.h
|
||||
main_window.cpp
|
||||
weld_opengl_widget.h
|
||||
weld_opengl_widget.cpp
|
||||
measurement_widget.h
|
||||
measurement_widget.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(weld_measure PRIVATE
|
||||
Qt6::Widgets
|
||||
Qt6::OpenGLWidgets
|
||||
Qt6::OpenGL
|
||||
${OpenCV_LIBS}
|
||||
)
|
||||
|
|
|
|||
134
main_window.cpp
134
main_window.cpp
|
|
@ -8,6 +8,7 @@
|
|||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QtMath>
|
||||
#include <QTabWidget>
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent)
|
||||
|
|
@ -16,7 +17,6 @@ MainWindow::MainWindow(QWidget *parent)
|
|||
resize(1200, 800);
|
||||
|
||||
createUI();
|
||||
createDefaultSampleImage();
|
||||
}
|
||||
|
||||
void MainWindow::createUI()
|
||||
|
|
@ -46,23 +46,26 @@ void MainWindow::createUI()
|
|||
panelLayout->addWidget(grpFile);
|
||||
|
||||
// Calibration setup section
|
||||
QGroupBox *grpCalib = new QGroupBox("2. Calibración por Escala", panel);
|
||||
QGroupBox *grpCalib = new QGroupBox("2. Calibración", panel);
|
||||
QVBoxLayout *calibLayout = new QVBoxLayout(grpCalib);
|
||||
|
||||
QFormLayout *formCalib = new QFormLayout();
|
||||
m_spinCalibMm = new QDoubleSpinBox(grpCalib);
|
||||
m_spinCalibMm->setRange(0.001, 10000.0);
|
||||
m_spinCalibMm->setValue(10.0);
|
||||
m_spinCalibMm->setSuffix(" mm");
|
||||
m_spinCalibMm->setDecimals(3);
|
||||
formCalib->addRow("Largo Real Patrón:", m_spinCalibMm);
|
||||
calibLayout->addLayout(formCalib);
|
||||
QFormLayout *formGrid = new QFormLayout();
|
||||
m_spinSquareSize = new QDoubleSpinBox(grpCalib);
|
||||
m_spinSquareSize->setRange(0.1, 1000.0);
|
||||
m_spinSquareSize->setValue(30.0);
|
||||
m_spinSquareSize->setSuffix(" mm");
|
||||
m_spinSquareSize->setDecimals(1);
|
||||
|
||||
formGrid->addRow("Lado Cuadrado (Ajedrez):", m_spinSquareSize);
|
||||
calibLayout->addLayout(formGrid);
|
||||
|
||||
QPushButton *btnCalibChessboard = new QPushButton("Autocalibrar Ajedrez", grpCalib);
|
||||
btnCalibChessboard->setFixedHeight(30);
|
||||
btnCalibChessboard->setStyleSheet("font-weight: bold; background-color: #b71c1c; color: white; border-radius: 4px;");
|
||||
calibLayout->addWidget(btnCalibChessboard);
|
||||
|
||||
|
||||
m_btnCalibrate = new QPushButton("Calibrar Escala", grpCalib);
|
||||
m_btnCalibrate->setFixedHeight(34);
|
||||
m_btnCalibrate->setStyleSheet("font-weight: bold; background-color: #2b5b84; color: white; border-radius: 4px;");
|
||||
|
||||
calibLayout->addWidget(m_btnCalibrate);
|
||||
panelLayout->addWidget(grpCalib);
|
||||
|
||||
// Measurement readout section
|
||||
|
|
@ -80,59 +83,71 @@ void MainWindow::createUI()
|
|||
panelLayout->addStretch();
|
||||
|
||||
// OpenGL Central Viewport Widget
|
||||
m_glWidget = new WeldOpenGLWidget(centralWidget);
|
||||
m_glWidget = new MeasurementWidget(centralWidget);
|
||||
|
||||
mainLayout->addWidget(panel);
|
||||
mainLayout->addWidget(m_glWidget, 1);
|
||||
|
||||
// Connect signals & slots
|
||||
connect(btnOpen, &QPushButton::clicked, this, &MainWindow::openImage);
|
||||
connect(m_btnCalibrate, &QPushButton::clicked, this, &MainWindow::onCalibrateClicked);
|
||||
|
||||
connect(m_glWidget, &WeldOpenGLWidget::measurementUpdated, this, &MainWindow::updateMeasurementInfo);
|
||||
connect(m_glWidget, &MeasurementWidget::measurementUpdated, this, &MainWindow::updateMeasurementInfo);
|
||||
|
||||
// Connect progress signal to dynamically update the gradient color of the button!
|
||||
connect(m_glWidget, &MeasurementWidget::calibrationProgress, this, [btnCalibChessboard](int percent) {
|
||||
double ratio = percent / 100.0;
|
||||
btnCalibChessboard->setStyleSheet(QString(
|
||||
"font-weight: bold; color: white; border-radius: 4px;"
|
||||
"background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #2e7d32, stop:%1 #2e7d32, stop:%2 #b71c1c, stop:1 #b71c1c);"
|
||||
).arg(ratio).arg(qMin(1.0, ratio + 0.001)));
|
||||
});
|
||||
|
||||
// Autocalibrate using chessboard
|
||||
connect(btnCalibChessboard, &QPushButton::clicked, this, [this, btnCalibChessboard, btnOpen]() {
|
||||
btnCalibChessboard->setEnabled(false);
|
||||
btnOpen->setEnabled(false);
|
||||
|
||||
int gx = 0; // Pass 0 to auto-detect
|
||||
int gy = 0; // Pass 0 to auto-detect
|
||||
double sqSize = m_spinSquareSize->value();
|
||||
|
||||
bool success = m_glWidget->calibrateWithChessboard(gx, gy, sqSize);
|
||||
|
||||
if (success) {
|
||||
// Keep green when calibrated successfully
|
||||
btnCalibChessboard->setStyleSheet("font-weight: bold; background-color: #2e7d32; color: white; border-radius: 4px;");
|
||||
} else {
|
||||
// Return to red if calibration failed
|
||||
btnCalibChessboard->setStyleSheet("font-weight: bold; background-color: #b71c1c; color: white; border-radius: 4px;");
|
||||
}
|
||||
btnCalibChessboard->setEnabled(true);
|
||||
btnOpen->setEnabled(true);
|
||||
|
||||
void MainWindow::createDefaultSampleImage()
|
||||
{
|
||||
QImage img(1000, 700, QImage::Format_RGB32);
|
||||
img.fill(QColor(40, 42, 48));
|
||||
if (success) {
|
||||
int squaresX = gx + 1;
|
||||
int squaresY = gy + 1;
|
||||
|
||||
QPainter painter(&img);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
// Draw simulated weld seam
|
||||
for (int x = 100; x < 900; x += 6) {
|
||||
double y = 350 + qSin(x / 30.0) * 15 + qCos(x / 7.0) * 8;
|
||||
double r = 25 + qSin(x / 10.0) * 5;
|
||||
painter.setBrush(QColor(130, 135, 145));
|
||||
painter.setPen(QPen(QColor(170, 175, 185), 1.5));
|
||||
painter.drawEllipse(QPointF(x, y), r, r * 0.5);
|
||||
QMessageBox::information(this, "Calibración Exitosa",
|
||||
QString("Se detectó automáticamente un tablero de ajedrez.\n\n"
|
||||
"Dimensiones identificadas:\n"
|
||||
"- Intersecciones internas: %1 x %2\n"
|
||||
"- Cuadrados totales: %3 x %4\n"
|
||||
"- Medida del lado: %5 mm\n\n"
|
||||
"La distorsión del lente y la perspectiva han sido calibradas con éxito.")
|
||||
.arg(gx).arg(gy).arg(squaresX).arg(squaresY).arg(sqSize, 0, 'f', 1));
|
||||
} else {
|
||||
QMessageBox::warning(this, "Error de Calibración",
|
||||
QString("No se pudo detectar automáticamente ningún tablero de ajedrez en la imagen.\n\n"
|
||||
"Por favor verifica:\n"
|
||||
"1. Que el tablero esté bien iluminado y plano.\n"
|
||||
"2. Que el tablero esté completamente visible en la foto."));
|
||||
}
|
||||
});
|
||||
|
||||
// Draw reference pattern bar (200px = 10mm)
|
||||
int scaleX = 150;
|
||||
int scaleY = 560;
|
||||
int scaleLenPx = 200;
|
||||
|
||||
painter.setBrush(QColor(230, 230, 230));
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.drawRoundedRect(scaleX - 20, scaleY - 30, scaleLenPx + 40, 70, 8, 8);
|
||||
|
||||
QPen rulerPen(Qt::black, 4, Qt::SolidLine, Qt::SquareCap);
|
||||
painter.setPen(rulerPen);
|
||||
painter.drawLine(scaleX, scaleY, scaleX + scaleLenPx, scaleY);
|
||||
painter.drawLine(scaleX, scaleY - 15, scaleX, scaleY + 15);
|
||||
painter.drawLine(scaleX + scaleLenPx, scaleY - 15, scaleX + scaleLenPx, scaleY + 15);
|
||||
|
||||
painter.setFont(QFont("Arial", 11, QFont::Bold));
|
||||
painter.drawText(QRect(scaleX - 20, scaleY + 10, scaleLenPx + 40, 25), Qt::AlignCenter, "PATRÓN DE PRUEBA: 10.0 mm");
|
||||
|
||||
img.save("/home/sebastian/repos/weld_measure/sample_weld.png");
|
||||
m_glWidget->setImage(img);
|
||||
|
||||
// Position single line right over the sample pattern bar (200px = 10mm)
|
||||
m_glWidget->setLinePoints(QPointF(scaleX, scaleY), QPointF(scaleX + scaleLenPx, scaleY));
|
||||
m_glWidget->calibrateWithCurrentLine(10.0);
|
||||
// Reset calibration button color to red when a new image is loaded
|
||||
connect(m_glWidget, &MeasurementWidget::imageLoaded, this, [btnCalibChessboard]() {
|
||||
btnCalibChessboard->setStyleSheet("font-weight: bold; background-color: #b71c1c; color: white; border-radius: 4px;");
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::openImage()
|
||||
|
|
@ -145,15 +160,6 @@ void MainWindow::openImage()
|
|||
}
|
||||
}
|
||||
|
||||
void MainWindow::onCalibrateClicked()
|
||||
{
|
||||
double knownMm = m_spinCalibMm->value();
|
||||
m_glWidget->calibrateWithCurrentLine(knownMm);
|
||||
QMessageBox::information(this, "Calibración Exitosa",
|
||||
QString("Escala fijada correctamente:\n%1 mm/px\n\nAhora puede mover la misma línea para medir cualquier parte de la imagen.")
|
||||
.arg(m_glWidget->mmPerPixel(), 0, 'g', 5));
|
||||
}
|
||||
|
||||
|
||||
|
||||
void MainWindow::updateMeasurementInfo(double px, double mm)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
|
||||
#include <QMainWindow>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QSpinBox>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include "weld_opengl_widget.h"
|
||||
#include <QProgressBar>
|
||||
#include "measurement_widget.h"
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
|
|
@ -17,18 +19,16 @@ public:
|
|||
|
||||
private slots:
|
||||
void openImage();
|
||||
void onCalibrateClicked();
|
||||
void updateMeasurementInfo(double px, double mm);
|
||||
|
||||
private:
|
||||
void createUI();
|
||||
void createDefaultSampleImage();
|
||||
|
||||
WeldOpenGLWidget *m_glWidget = nullptr;
|
||||
MeasurementWidget *m_glWidget = nullptr;
|
||||
|
||||
QDoubleSpinBox *m_spinCalibMm = nullptr;
|
||||
QPushButton *m_btnCalibrate = nullptr;
|
||||
QLabel *m_lblMeasurement = nullptr;
|
||||
|
||||
QDoubleSpinBox *m_spinSquareSize = nullptr;
|
||||
};
|
||||
|
||||
#endif // MAIN_WINDOW_H
|
||||
|
|
|
|||
745
measurement_widget.cpp
Normal file
745
measurement_widget.cpp
Normal file
|
|
@ -0,0 +1,745 @@
|
|||
#include "measurement_widget.h"
|
||||
#include <QPainter>
|
||||
#include <QMouseEvent>
|
||||
#include <QWheelEvent>
|
||||
#include <QtMath>
|
||||
#include <QDebug>
|
||||
#include <QPolygonF>
|
||||
#include <QTransform>
|
||||
#include <opencv2/calib3d.hpp>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
#include <QCoreApplication>
|
||||
#include <QThread>
|
||||
|
||||
MeasurementWidget::MeasurementWidget(QWidget *parent)
|
||||
: QOpenGLWidget(parent)
|
||||
{
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
MeasurementWidget::~MeasurementWidget()
|
||||
{
|
||||
makeCurrent();
|
||||
if (m_texture) {
|
||||
delete m_texture;
|
||||
m_texture = nullptr;
|
||||
}
|
||||
doneCurrent();
|
||||
}
|
||||
|
||||
void MeasurementWidget::initializeGL()
|
||||
{
|
||||
initializeOpenGLFunctions();
|
||||
glClearColor(0.15f, 0.15f, 0.18f, 1.0f);
|
||||
}
|
||||
|
||||
void MeasurementWidget::resizeGL(int w, int h)
|
||||
{
|
||||
glViewport(0, 0, w, h);
|
||||
}
|
||||
|
||||
bool MeasurementWidget::loadImage(const QString &filePath)
|
||||
{
|
||||
QImage img(filePath);
|
||||
if (img.isNull()) return false;
|
||||
|
||||
setImage(img);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MeasurementWidget::setImage(const QImage &image)
|
||||
{
|
||||
m_image = image.convertToFormat(QImage::Format_RGBA8888);
|
||||
m_textureNeedsUpdate = true;
|
||||
|
||||
// Reset zoom and center image
|
||||
m_zoomLevel = 1.0;
|
||||
m_panOffset = QPointF(0, 0);
|
||||
|
||||
// Initialize single measurement/calibration line
|
||||
int w = m_image.width();
|
||||
int h = m_image.height();
|
||||
|
||||
m_head = QPointF(w * 0.3, h * 0.5);
|
||||
m_tail = QPointF(w * 0.7, h * 0.5);
|
||||
m_hasLine = true;
|
||||
|
||||
// Reset calibration parameters
|
||||
m_mmPerPixel = 0.0;
|
||||
m_isCalibrated = false;
|
||||
m_hasHomography = false;
|
||||
m_calibrationMode = false;
|
||||
m_activeCornerIdx = -1;
|
||||
m_calibCorners.clear();
|
||||
|
||||
emit imageLoaded(w, h);
|
||||
emit calibrationUpdated(m_mmPerPixel);
|
||||
emit measurementUpdated(measuredPx(), measuredMm());
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
|
||||
|
||||
double MeasurementWidget::measuredPx() const
|
||||
{
|
||||
if (!m_hasLine) return 0.0;
|
||||
return QLineF(m_head, m_tail).length();
|
||||
}
|
||||
|
||||
double MeasurementWidget::measuredMm() const
|
||||
{
|
||||
if (m_hasHomography) {
|
||||
QPointF h = m_head;
|
||||
QPointF t = m_tail;
|
||||
|
||||
if (m_hasLensCalibration) {
|
||||
std::vector<cv::Point2f> src = { cv::Point2f(m_head.x(), m_head.y()), cv::Point2f(m_tail.x(), m_tail.y()) };
|
||||
std::vector<cv::Point2f> dst;
|
||||
cv::undistortPoints(src, dst, m_cameraMatrix, m_distCoeffs, cv::Mat(), m_cameraMatrix);
|
||||
h = QPointF(dst[0].x, dst[0].y);
|
||||
t = QPointF(dst[1].x, dst[1].y);
|
||||
}
|
||||
|
||||
QPointF headReal = m_homography.map(h);
|
||||
QPointF tailReal = m_homography.map(t);
|
||||
return QLineF(headReal, tailReal).length();
|
||||
}
|
||||
return measuredPx() * m_mmPerPixel;
|
||||
}
|
||||
|
||||
void MeasurementWidget::resetLine()
|
||||
{
|
||||
if (!m_image.isNull()) {
|
||||
int w = m_image.width();
|
||||
int h = m_image.height();
|
||||
m_head = QPointF(w * 0.3, h * 0.5);
|
||||
m_tail = QPointF(w * 0.7, h * 0.5);
|
||||
m_hasLine = true;
|
||||
emit measurementUpdated(measuredPx(), measuredMm());
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void MeasurementWidget::setLinePoints(const QPointF &head, const QPointF &tail)
|
||||
{
|
||||
m_head = head;
|
||||
m_tail = tail;
|
||||
m_hasLine = true;
|
||||
emit measurementUpdated(measuredPx(), measuredMm());
|
||||
update();
|
||||
}
|
||||
|
||||
void MeasurementWidget::paintGL()
|
||||
{
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
if (m_image.isNull()) return;
|
||||
|
||||
makeCurrent();
|
||||
|
||||
if (m_textureNeedsUpdate) {
|
||||
if (m_texture) delete m_texture;
|
||||
m_texture = new QOpenGLTexture(m_image);
|
||||
m_texture->setMinificationFilter(QOpenGLTexture::Linear);
|
||||
m_texture->setMagnificationFilter(QOpenGLTexture::Linear);
|
||||
m_textureNeedsUpdate = false;
|
||||
}
|
||||
|
||||
// Render 2D image via QPainter over OpenGL viewport
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
painter.setRenderHint(QPainter::TextAntialiasing, true);
|
||||
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||
|
||||
// Calculate image destination rectangle with zoom and pan
|
||||
double widgetAspect = (double)width() / height();
|
||||
double imageAspect = (double)m_image.width() / m_image.height();
|
||||
|
||||
double drawW, drawH;
|
||||
if (widgetAspect > imageAspect) {
|
||||
drawH = height() * m_zoomLevel;
|
||||
drawW = drawH * imageAspect;
|
||||
} else {
|
||||
drawW = width() * m_zoomLevel;
|
||||
drawH = drawW / imageAspect;
|
||||
}
|
||||
|
||||
double drawX = (width() - drawW) / 2.0 + m_panOffset.x();
|
||||
double drawY = (height() - drawH) / 2.0 + m_panOffset.y();
|
||||
|
||||
QRectF targetRect(drawX, drawY, drawW, drawH);
|
||||
painter.drawImage(targetRect, m_image);
|
||||
|
||||
// Draw the single measurement/calibration line
|
||||
if (m_hasLine && !m_calibrationMode) {
|
||||
QColor color(255, 45, 45); // Smooth Vivid Red
|
||||
drawLineOverlay(painter, m_head, m_tail, color);
|
||||
}
|
||||
|
||||
if (m_calibrationMode) {
|
||||
// Draw the 4 corners and connecting lines
|
||||
QPolygonF wPoly;
|
||||
for (const auto &p : m_calibCorners) {
|
||||
wPoly << imageToWidgetCoords(p);
|
||||
}
|
||||
|
||||
// Draw lines connecting them to form a quad
|
||||
QPen polyPen(QColor(0, 191, 255), 2.0, Qt::DashLine); // Deep Sky Blue
|
||||
painter.setPen(polyPen);
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
if (wPoly.size() == 4) {
|
||||
painter.drawPolygon(wPoly);
|
||||
}
|
||||
|
||||
// Draw handles at each corner
|
||||
double handleRadius = 8.0;
|
||||
painter.setPen(QPen(QColor(0, 191, 255), 2.0));
|
||||
QStringList labels = {"1 (Sup-Izq)", "2 (Sup-Der)", "3 (Inf-Der)", "4 (Inf-Izq)"};
|
||||
for (int i = 0; i < wPoly.size(); ++i) {
|
||||
if (i == m_activeCornerIdx) {
|
||||
painter.setBrush(QBrush(QColor(255, 165, 0, 200))); // Orange when dragging
|
||||
} else {
|
||||
painter.setBrush(QBrush(QColor(0, 191, 255, 150)));
|
||||
}
|
||||
painter.drawEllipse(wPoly[i], handleRadius, handleRadius);
|
||||
|
||||
// Draw label
|
||||
painter.setFont(QFont("Arial", 8, QFont::Bold));
|
||||
painter.setPen(Qt::white);
|
||||
painter.drawText(wPoly[i] + QPointF(12, 4), labels[i]);
|
||||
painter.setPen(QPen(QColor(0, 191, 255), 2.0)); // Restore pen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MeasurementWidget::drawLineOverlay(QPainter &painter, const QPointF &imgHead, const QPointF &imgTail,
|
||||
const QColor &color)
|
||||
{
|
||||
QPointF wHead = imageToWidgetCoords(imgHead);
|
||||
QPointF wTail = imageToWidgetCoords(imgTail);
|
||||
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
// Pure single red line (1.0px thin stroke with antialiasing)
|
||||
QPen linePen(color, 1.0, Qt::SolidLine, Qt::RoundCap);
|
||||
painter.setPen(linePen);
|
||||
painter.drawLine(wHead, wTail);
|
||||
|
||||
// Compact drag handles (short perpendicular ticks)
|
||||
QLineF line(wHead, wTail);
|
||||
double dx = line.dx();
|
||||
double dy = line.dy();
|
||||
double len = line.length();
|
||||
if (len > 0.0) {
|
||||
double px = -dy / len;
|
||||
double py = dx / len;
|
||||
double tickHalfLen = 6.0; // short length perpendicular to measurement line
|
||||
|
||||
QPointF headP1 = wHead + QPointF(px * tickHalfLen, py * tickHalfLen);
|
||||
QPointF headP2 = wHead - QPointF(px * tickHalfLen, py * tickHalfLen);
|
||||
painter.drawLine(headP1, headP2);
|
||||
|
||||
QPointF tailP1 = wTail + QPointF(px * tickHalfLen, py * tickHalfLen);
|
||||
QPointF tailP2 = wTail - QPointF(px * tickHalfLen, py * tickHalfLen);
|
||||
painter.drawLine(tailP1, tailP2);
|
||||
}
|
||||
|
||||
// End labels
|
||||
painter.setFont(QFont("Arial", 8, QFont::Bold));
|
||||
painter.setPen(Qt::white);
|
||||
painter.drawText(wHead + QPointF(10, -6), "A");
|
||||
painter.drawText(wTail + QPointF(10, -6), "B");
|
||||
}
|
||||
|
||||
QPointF MeasurementWidget::widgetToImageCoords(const QPointF &widgetPos) const
|
||||
{
|
||||
if (m_image.isNull()) return QPointF();
|
||||
|
||||
double widgetAspect = (double)width() / height();
|
||||
double imageAspect = (double)m_image.width() / m_image.height();
|
||||
|
||||
double drawW, drawH;
|
||||
if (widgetAspect > imageAspect) {
|
||||
drawH = height() * m_zoomLevel;
|
||||
drawW = drawH * imageAspect;
|
||||
} else {
|
||||
drawW = width() * m_zoomLevel;
|
||||
drawH = drawW / imageAspect;
|
||||
}
|
||||
|
||||
double drawX = (width() - drawW) / 2.0 + m_panOffset.x();
|
||||
double drawY = (height() - drawH) / 2.0 + m_panOffset.y();
|
||||
|
||||
double normX = (widgetPos.x() - drawX) / drawW;
|
||||
double normY = (widgetPos.y() - drawY) / drawH;
|
||||
|
||||
return QPointF(normX * m_image.width(), normY * m_image.height());
|
||||
}
|
||||
|
||||
QPointF MeasurementWidget::imageToWidgetCoords(const QPointF &imgPos) const
|
||||
{
|
||||
if (m_image.isNull()) return QPointF();
|
||||
|
||||
double widgetAspect = (double)width() / height();
|
||||
double imageAspect = (double)m_image.width() / m_image.height();
|
||||
|
||||
double drawW, drawH;
|
||||
if (widgetAspect > imageAspect) {
|
||||
drawH = height() * m_zoomLevel;
|
||||
drawW = drawH * imageAspect;
|
||||
} else {
|
||||
drawW = width() * m_zoomLevel;
|
||||
drawH = drawW / imageAspect;
|
||||
}
|
||||
|
||||
double drawX = (width() - drawW) / 2.0 + m_panOffset.x();
|
||||
double drawY = (height() - drawH) / 2.0 + m_panOffset.y();
|
||||
|
||||
double normX = imgPos.x() / m_image.width();
|
||||
double normY = imgPos.y() / m_image.height();
|
||||
|
||||
return QPointF(drawX + normX * drawW, drawY + normY * drawH);
|
||||
}
|
||||
|
||||
MeasurementWidget::DragHandle MeasurementWidget::hitTestLine(const QPointF &imgPos, const QPointF &head, const QPointF &tail) const
|
||||
{
|
||||
QPointF wPos = imageToWidgetCoords(imgPos);
|
||||
QPointF wHead = imageToWidgetCoords(head);
|
||||
QPointF wTail = imageToWidgetCoords(tail);
|
||||
|
||||
const double handleRadius = 15.0; // Hit test tolerance in pixels
|
||||
|
||||
if (QLineF(wPos, wHead).length() <= handleRadius) return DragHandle::Head;
|
||||
if (QLineF(wPos, wTail).length() <= handleRadius) return DragHandle::Tail;
|
||||
|
||||
// Check if clicked near line body
|
||||
QLineF line(wHead, wTail);
|
||||
double lineLen = line.length();
|
||||
if (lineLen > 0) {
|
||||
double u = ((wPos.x() - wHead.x()) * (wTail.x() - wHead.x()) + (wPos.y() - wHead.y()) * (wTail.y() - wHead.y())) / (lineLen * lineLen);
|
||||
if (u >= 0.0 && u <= 1.0) {
|
||||
QPointF proj = wHead + u * (wTail - wHead);
|
||||
if (QLineF(wPos, proj).length() <= handleRadius) {
|
||||
return DragHandle::Body;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DragHandle::None;
|
||||
}
|
||||
|
||||
void MeasurementWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (m_image.isNull()) return;
|
||||
|
||||
if (m_calibrationMode) {
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
QPointF imgPos = widgetToImageCoords(event->position());
|
||||
QPointF wPos = event->position();
|
||||
m_activeCornerIdx = -1;
|
||||
const double handleRadius = 15.0;
|
||||
for (int i = 0; i < m_calibCorners.size(); ++i) {
|
||||
QPointF wCorner = imageToWidgetCoords(m_calibCorners[i]);
|
||||
if (QLineF(wPos, wCorner).length() <= handleRadius) {
|
||||
m_activeCornerIdx = i;
|
||||
m_dragStartImgPos = imgPos;
|
||||
update();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (m_activeCornerIdx == -1) {
|
||||
m_isPanning = true;
|
||||
m_lastMousePos = event->pos();
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event->button() == Qt::RightButton) {
|
||||
m_isPanning = true;
|
||||
m_lastMousePos = event->pos();
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
QPointF imgPos = widgetToImageCoords(event->position());
|
||||
m_activeHandle = hitTestLine(imgPos, m_head, m_tail);
|
||||
if (m_activeHandle != DragHandle::None) {
|
||||
m_dragStartImgPos = imgPos;
|
||||
m_dragStartHead = m_head;
|
||||
m_dragStartTail = m_tail;
|
||||
update();
|
||||
} else {
|
||||
// Clicked background -> Pan image on left click drag
|
||||
m_isPanning = true;
|
||||
m_lastMousePos = event->pos();
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->button() == Qt::RightButton) {
|
||||
m_isPanning = true;
|
||||
m_lastMousePos = event->pos();
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void MeasurementWidget::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (m_isPanning) {
|
||||
QPoint delta = event->pos() - m_lastMousePos;
|
||||
m_panOffset += QPointF(delta.x(), delta.y());
|
||||
m_lastMousePos = event->pos();
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
QPointF imgPos = widgetToImageCoords(event->position());
|
||||
|
||||
if (m_calibrationMode) {
|
||||
if (m_activeCornerIdx != -1) {
|
||||
double x = qBound(0.0, imgPos.x(), (double)m_image.width());
|
||||
double y = qBound(0.0, imgPos.y(), (double)m_image.height());
|
||||
m_calibCorners[m_activeCornerIdx] = QPointF(x, y);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
// Update cursor on hover
|
||||
QPointF wPos = event->position();
|
||||
bool overCorner = false;
|
||||
const double handleRadius = 15.0;
|
||||
for (int i = 0; i < m_calibCorners.size(); ++i) {
|
||||
QPointF wCorner = imageToWidgetCoords(m_calibCorners[i]);
|
||||
if (QLineF(wPos, wCorner).length() <= handleRadius) {
|
||||
overCorner = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (overCorner) {
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
} else {
|
||||
setCursor(Qt::OpenHandCursor);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_activeHandle != DragHandle::None) {
|
||||
if (m_activeHandle == DragHandle::Head) {
|
||||
m_head = imgPos;
|
||||
} else if (m_activeHandle == DragHandle::Tail) {
|
||||
m_tail = imgPos;
|
||||
} else if (m_activeHandle == DragHandle::Body) {
|
||||
QPointF delta = imgPos - m_dragStartImgPos;
|
||||
m_head = m_dragStartHead + delta;
|
||||
m_tail = m_dragStartTail + delta;
|
||||
}
|
||||
emit measurementUpdated(measuredPx(), measuredMm());
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update cursor feedback based on hover
|
||||
DragHandle hover = hitTestLine(imgPos, m_head, m_tail);
|
||||
if (hover == DragHandle::Head || hover == DragHandle::Tail) {
|
||||
setCursor(Qt::CrossCursor);
|
||||
} else if (hover == DragHandle::Body) {
|
||||
setCursor(Qt::SizeAllCursor);
|
||||
} else {
|
||||
setCursor(Qt::OpenHandCursor);
|
||||
}
|
||||
}
|
||||
|
||||
void MeasurementWidget::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if (m_calibrationMode) {
|
||||
if (m_isPanning && (event->button() == Qt::LeftButton || event->button() == Qt::RightButton)) {
|
||||
m_isPanning = false;
|
||||
setCursor(Qt::OpenHandCursor);
|
||||
}
|
||||
if (event->button() == Qt::LeftButton && m_activeCornerIdx != -1) {
|
||||
m_activeCornerIdx = -1;
|
||||
setCursor(Qt::OpenHandCursor);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_isPanning && (event->button() == Qt::LeftButton || event->button() == Qt::RightButton)) {
|
||||
m_isPanning = false;
|
||||
QPointF imgPos = widgetToImageCoords(event->position());
|
||||
DragHandle hover = hitTestLine(imgPos, m_head, m_tail);
|
||||
if (hover == DragHandle::Head || hover == DragHandle::Tail) setCursor(Qt::CrossCursor);
|
||||
else if (hover == DragHandle::Body) setCursor(Qt::SizeAllCursor);
|
||||
else setCursor(Qt::OpenHandCursor);
|
||||
}
|
||||
|
||||
if (event->button() == Qt::LeftButton && m_activeHandle != DragHandle::None) {
|
||||
m_activeHandle = DragHandle::None;
|
||||
QPointF imgPos = widgetToImageCoords(event->position());
|
||||
DragHandle hover = hitTestLine(imgPos, m_head, m_tail);
|
||||
if (hover == DragHandle::Head || hover == DragHandle::Tail) setCursor(Qt::CrossCursor);
|
||||
else if (hover == DragHandle::Body) setCursor(Qt::SizeAllCursor);
|
||||
else setCursor(Qt::OpenHandCursor);
|
||||
}
|
||||
}
|
||||
|
||||
void MeasurementWidget::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
double zoomFactor = 1.15;
|
||||
if (event->angleDelta().y() < 0) {
|
||||
zoomFactor = 1.0 / zoomFactor;
|
||||
}
|
||||
|
||||
double newZoom = m_zoomLevel * zoomFactor;
|
||||
if (newZoom < 0.2) newZoom = 0.2;
|
||||
if (newZoom > 25.0) newZoom = 25.0;
|
||||
|
||||
QPointF mousePos = event->position();
|
||||
QPointF imgPosBefore = widgetToImageCoords(mousePos);
|
||||
|
||||
m_zoomLevel = newZoom;
|
||||
|
||||
QPointF widgetPosAfter = imageToWidgetCoords(imgPosBefore);
|
||||
m_panOffset += (mousePos - widgetPosAfter);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void MeasurementWidget::setCalibrationMode(bool active)
|
||||
{
|
||||
if (m_image.isNull()) return;
|
||||
m_calibrationMode = active;
|
||||
if (active && m_calibCorners.isEmpty()) {
|
||||
double w = m_image.width();
|
||||
double h = m_image.height();
|
||||
m_calibCorners.clear();
|
||||
m_calibCorners.append(QPointF(w * 0.1, h * 0.1));
|
||||
m_calibCorners.append(QPointF(w * 0.9, h * 0.1));
|
||||
m_calibCorners.append(QPointF(w * 0.9, h * 0.9));
|
||||
m_calibCorners.append(QPointF(w * 0.1, h * 0.9));
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void MeasurementWidget::cancelCalibrationMode()
|
||||
{
|
||||
m_calibrationMode = false;
|
||||
m_activeCornerIdx = -1;
|
||||
update();
|
||||
}
|
||||
|
||||
void MeasurementWidget::applyPerspectiveCalibration(double realX, double realY)
|
||||
{
|
||||
if (m_image.isNull() || m_calibCorners.size() != 4) return;
|
||||
|
||||
QPolygonF srcQuad;
|
||||
srcQuad << m_calibCorners[0] << m_calibCorners[1] << m_calibCorners[2] << m_calibCorners[3];
|
||||
|
||||
QPolygonF destQuad;
|
||||
destQuad << QPointF(0, 0) << QPointF(realX, 0) << QPointF(realX, realY) << QPointF(0, realY);
|
||||
|
||||
if (QTransform::quadToQuad(srcQuad, destQuad, m_homography)) {
|
||||
m_hasHomography = true;
|
||||
m_isCalibrated = true;
|
||||
|
||||
m_calibrationMode = false;
|
||||
m_activeCornerIdx = -1;
|
||||
|
||||
// Calculate a representative/average scale factor for compatibility
|
||||
double imgW = QLineF(m_calibCorners[0], m_calibCorners[1]).length();
|
||||
if (imgW > 0) {
|
||||
m_mmPerPixel = realX / imgW;
|
||||
}
|
||||
|
||||
emit calibrationUpdated(m_mmPerPixel);
|
||||
emit measurementUpdated(measuredPx(), measuredMm());
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void MeasurementWidget::resetCalibration()
|
||||
{
|
||||
m_mmPerPixel = 0.0;
|
||||
m_isCalibrated = false;
|
||||
m_hasHomography = false;
|
||||
m_hasLensCalibration = false;
|
||||
m_calibrationMode = false;
|
||||
m_activeCornerIdx = -1;
|
||||
m_calibCorners.clear();
|
||||
|
||||
emit calibrationUpdated(m_mmPerPixel);
|
||||
emit measurementUpdated(measuredPx(), measuredMm());
|
||||
update();
|
||||
}
|
||||
|
||||
bool MeasurementWidget::calibrateWithChessboard(int &innerCornersX, int &innerCornersY, double squareSizeMm)
|
||||
{
|
||||
if (m_image.isNull()) return false;
|
||||
|
||||
// Convert QImage to cv::Mat
|
||||
cv::Mat mat(m_image.height(), m_image.width(), CV_8UC4,
|
||||
const_cast<uchar*>(m_image.bits()),
|
||||
m_image.bytesPerLine());
|
||||
|
||||
cv::Mat gray;
|
||||
cv::cvtColor(mat, gray, cv::COLOR_RGBA2GRAY);
|
||||
|
||||
// If the image is very high resolution, resize it for the initial corner detection search
|
||||
// to reduce high-frequency printer noise/banding and speed up detection significantly (using 600px width).
|
||||
cv::Mat graySearch = gray;
|
||||
double scale = 1.0;
|
||||
if (gray.cols > 600) {
|
||||
scale = 600.0 / gray.cols;
|
||||
int newHeight = static_cast<int>(gray.rows * scale);
|
||||
cv::resize(gray, graySearch, cv::Size(600, newHeight), 0, 0, cv::INTER_AREA);
|
||||
}
|
||||
|
||||
std::vector<cv::Point2f> searchCorners;
|
||||
bool found = false;
|
||||
int bestX = 0;
|
||||
int bestY = 0;
|
||||
|
||||
if (innerCornersX > 0 && innerCornersY > 0) {
|
||||
// Try user specified dimensions
|
||||
cv::Size patternSize(innerCornersX, innerCornersY);
|
||||
found = cv::findChessboardCorners(graySearch, patternSize, searchCorners,
|
||||
cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);
|
||||
if (!found) {
|
||||
cv::Mat graySearchInv = 255 - graySearch;
|
||||
found = cv::findChessboardCorners(graySearchInv, patternSize, searchCorners,
|
||||
cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);
|
||||
}
|
||||
if (found) {
|
||||
bestX = innerCornersX;
|
||||
bestY = innerCornersY;
|
||||
}
|
||||
} else {
|
||||
// Auto-detect size: scan common sizes from largest to smallest (10 down to 4)
|
||||
int totalSteps = (10 - 4 + 1) * (10 - 4 + 1);
|
||||
int step = 0;
|
||||
for (int cx = 10; cx >= 4; --cx) {
|
||||
for (int cy = 10; cy >= 4; --cy) {
|
||||
step++;
|
||||
emit calibrationProgress(step * 100 / totalSteps);
|
||||
QCoreApplication::processEvents();
|
||||
QThread::msleep(8); // Tiny sleep for smooth visual progress bar
|
||||
|
||||
cv::Size patternSize(cx, cy);
|
||||
found = cv::findChessboardCorners(graySearch, patternSize, searchCorners,
|
||||
cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);
|
||||
if (!found) {
|
||||
cv::Mat graySearchInv = 255 - graySearch;
|
||||
found = cv::findChessboardCorners(graySearchInv, patternSize, searchCorners,
|
||||
cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);
|
||||
}
|
||||
if (found) {
|
||||
bestX = cx;
|
||||
bestY = cy;
|
||||
emit calibrationProgress(100);
|
||||
QCoreApplication::processEvents();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Assign detected dimensions back to reference variables to update UI
|
||||
innerCornersX = bestX;
|
||||
innerCornersY = bestY;
|
||||
|
||||
// Scale corner coordinates back to the original image resolution
|
||||
std::vector<cv::Point2f> corners;
|
||||
for (const auto &p : searchCorners) {
|
||||
corners.push_back(cv::Point2f(p.x / scale, p.y / scale));
|
||||
}
|
||||
|
||||
// Refine corner positions on the original high-res image
|
||||
cv::cornerSubPix(gray, corners, cv::Size(11, 11), cv::Size(-1, -1),
|
||||
cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.1));
|
||||
|
||||
// Prepare calibration points
|
||||
std::vector<std::vector<cv::Point3f>> objectPoints;
|
||||
std::vector<std::vector<cv::Point2f>> imagePoints;
|
||||
|
||||
std::vector<cv::Point3f> obj;
|
||||
for (int i = 0; i < innerCornersY; ++i) {
|
||||
for (int j = 0; j < innerCornersX; ++j) {
|
||||
obj.push_back(cv::Point3f(j * squareSizeMm, i * squareSizeMm, 0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
objectPoints.push_back(obj);
|
||||
imagePoints.push_back(corners);
|
||||
|
||||
// Calibrate camera
|
||||
m_cameraMatrix = cv::Mat::eye(3, 3, CV_64F);
|
||||
m_distCoeffs = cv::Mat::zeros(5, 1, CV_64F);
|
||||
std::vector<cv::Mat> rvecs, tvecs;
|
||||
|
||||
double rms = cv::calibrateCamera(objectPoints, imagePoints,
|
||||
cv::Size(m_image.width(), m_image.height()),
|
||||
m_cameraMatrix, m_distCoeffs, rvecs, tvecs,
|
||||
cv::CALIB_FIX_K3);
|
||||
|
||||
qDebug() << "Calibration RMS error:" << rms;
|
||||
|
||||
// Extreme corners:
|
||||
// Top-Left (index 0) -> maps to (0, 0)
|
||||
// Top-Right (index innerCornersX - 1) -> maps to ((innerCornersX-1)*sq, 0)
|
||||
// Bottom-Right (index innerCornersX * innerCornersY - 1) -> maps to ((innerCornersX-1)*sq, (innerCornersY-1)*sq)
|
||||
// Bottom-Left (index innerCornersX * (innerCornersY - 1)) -> maps to (0, (innerCornersY-1)*sq)
|
||||
|
||||
std::vector<cv::Point2f> srcPointsDistorted = {
|
||||
corners[0],
|
||||
corners[innerCornersX - 1],
|
||||
corners[innerCornersX * innerCornersY - 1],
|
||||
corners[innerCornersX * (innerCornersY - 1)]
|
||||
};
|
||||
|
||||
// Undistort these 4 points
|
||||
std::vector<cv::Point2f> srcPointsUndistorted;
|
||||
cv::undistortPoints(srcPointsDistorted, srcPointsUndistorted, m_cameraMatrix, m_distCoeffs, cv::Mat(), m_cameraMatrix);
|
||||
|
||||
QPolygonF srcQuad;
|
||||
srcQuad << QPointF(srcPointsUndistorted[0].x, srcPointsUndistorted[0].y)
|
||||
<< QPointF(srcPointsUndistorted[1].x, srcPointsUndistorted[1].y)
|
||||
<< QPointF(srcPointsUndistorted[2].x, srcPointsUndistorted[2].y)
|
||||
<< QPointF(srcPointsUndistorted[3].x, srcPointsUndistorted[3].y);
|
||||
|
||||
double realX = (innerCornersX - 1) * squareSizeMm;
|
||||
double realY = (innerCornersY - 1) * squareSizeMm;
|
||||
|
||||
QPolygonF destQuad;
|
||||
destQuad << QPointF(0, 0) << QPointF(realX, 0) << QPointF(realX, realY) << QPointF(0, realY);
|
||||
|
||||
if (QTransform::quadToQuad(srcQuad, destQuad, m_homography)) {
|
||||
m_hasHomography = true;
|
||||
m_hasLensCalibration = true;
|
||||
m_isCalibrated = true;
|
||||
|
||||
m_calibrationMode = false;
|
||||
m_activeCornerIdx = -1;
|
||||
|
||||
// Compute average scale
|
||||
double imgW = QLineF(srcQuad[0], srcQuad[1]).length();
|
||||
if (imgW > 0) {
|
||||
m_mmPerPixel = realX / imgW;
|
||||
}
|
||||
|
||||
emit calibrationUpdated(m_mmPerPixel);
|
||||
emit measurementUpdated(measuredPx(), measuredMm());
|
||||
update();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
#ifndef WELD_OPENGL_WIDGET_H
|
||||
#define WELD_OPENGL_WIDGET_H
|
||||
#ifndef MEASUREMENT_WIDGET_H
|
||||
#define MEASUREMENT_WIDGET_H
|
||||
|
||||
#include <QOpenGLWidget>
|
||||
#include <QOpenGLFunctions>
|
||||
|
|
@ -7,8 +7,9 @@
|
|||
#include <QPointF>
|
||||
#include <QLineF>
|
||||
#include <QOpenGLTexture>
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
class WeldOpenGLWidget : public QOpenGLWidget, protected QOpenGLFunctions
|
||||
class MeasurementWidget : public QOpenGLWidget, protected QOpenGLFunctions
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
|
|
@ -20,13 +21,12 @@ public:
|
|||
Body
|
||||
};
|
||||
|
||||
explicit WeldOpenGLWidget(QWidget *parent = nullptr);
|
||||
~WeldOpenGLWidget() override;
|
||||
explicit MeasurementWidget(QWidget *parent = nullptr);
|
||||
~MeasurementWidget() override;
|
||||
|
||||
bool loadImage(const QString &filePath);
|
||||
void setImage(const QImage &image);
|
||||
|
||||
void calibrateWithCurrentLine(double knownMm);
|
||||
bool isCalibrated() const { return m_isCalibrated; }
|
||||
double mmPerPixel() const { return m_mmPerPixel; }
|
||||
double measuredPx() const;
|
||||
|
|
@ -35,10 +35,18 @@ public:
|
|||
void resetLine();
|
||||
void setLinePoints(const QPointF &head, const QPointF &tail);
|
||||
|
||||
void setCalibrationMode(bool active);
|
||||
bool isCalibrationMode() const { return m_calibrationMode; }
|
||||
void applyPerspectiveCalibration(double realX, double realY);
|
||||
void cancelCalibrationMode();
|
||||
void resetCalibration();
|
||||
bool calibrateWithChessboard(int &innerCornersX, int &innerCornersY, double squareSizeMm);
|
||||
|
||||
signals:
|
||||
void calibrationUpdated(double mmPerPx);
|
||||
void measurementUpdated(double px, double mm);
|
||||
void imageLoaded(int width, int height);
|
||||
void calibrationProgress(int percent);
|
||||
|
||||
protected:
|
||||
void initializeGL() override;
|
||||
|
|
@ -71,6 +79,20 @@ private:
|
|||
double m_mmPerPixel = 0.0;
|
||||
bool m_isCalibrated = false;
|
||||
|
||||
// Homography matrix for perspective calibration
|
||||
QTransform m_homography;
|
||||
bool m_hasHomography = false;
|
||||
|
||||
// Lens Calibration Matrix & Coefficients
|
||||
cv::Mat m_cameraMatrix;
|
||||
cv::Mat m_distCoeffs;
|
||||
bool m_hasLensCalibration = false;
|
||||
|
||||
// Perspective Calibration state
|
||||
bool m_calibrationMode = false;
|
||||
QList<QPointF> m_calibCorners;
|
||||
int m_activeCornerIdx = -1;
|
||||
|
||||
// Mouse interaction state
|
||||
DragHandle m_activeHandle = DragHandle::None;
|
||||
QPointF m_dragStartImgPos;
|
||||
|
|
@ -84,4 +106,4 @@ private:
|
|||
QPoint m_lastMousePos;
|
||||
};
|
||||
|
||||
#endif // WELD_OPENGL_WIDGET_H
|
||||
#endif // MEASUREMENT_WIDGET_H
|
||||
|
|
@ -1,387 +0,0 @@
|
|||
#include "weld_opengl_widget.h"
|
||||
#include <QPainter>
|
||||
#include <QMouseEvent>
|
||||
#include <QWheelEvent>
|
||||
#include <QtMath>
|
||||
#include <QDebug>
|
||||
|
||||
WeldOpenGLWidget::WeldOpenGLWidget(QWidget *parent)
|
||||
: QOpenGLWidget(parent)
|
||||
{
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
WeldOpenGLWidget::~WeldOpenGLWidget()
|
||||
{
|
||||
makeCurrent();
|
||||
if (m_texture) {
|
||||
delete m_texture;
|
||||
m_texture = nullptr;
|
||||
}
|
||||
doneCurrent();
|
||||
}
|
||||
|
||||
void WeldOpenGLWidget::initializeGL()
|
||||
{
|
||||
initializeOpenGLFunctions();
|
||||
glClearColor(0.15f, 0.15f, 0.18f, 1.0f);
|
||||
}
|
||||
|
||||
void WeldOpenGLWidget::resizeGL(int w, int h)
|
||||
{
|
||||
glViewport(0, 0, w, h);
|
||||
}
|
||||
|
||||
bool WeldOpenGLWidget::loadImage(const QString &filePath)
|
||||
{
|
||||
QImage img(filePath);
|
||||
if (img.isNull()) return false;
|
||||
|
||||
setImage(img);
|
||||
return true;
|
||||
}
|
||||
|
||||
void WeldOpenGLWidget::setImage(const QImage &image)
|
||||
{
|
||||
m_image = image.convertToFormat(QImage::Format_RGBA8888);
|
||||
m_textureNeedsUpdate = true;
|
||||
|
||||
// Reset zoom and center image
|
||||
m_zoomLevel = 1.0;
|
||||
m_panOffset = QPointF(0, 0);
|
||||
|
||||
// Initialize single measurement/calibration line
|
||||
int w = m_image.width();
|
||||
int h = m_image.height();
|
||||
|
||||
m_head = QPointF(w * 0.3, h * 0.5);
|
||||
m_tail = QPointF(w * 0.7, h * 0.5);
|
||||
m_hasLine = true;
|
||||
|
||||
emit imageLoaded(w, h);
|
||||
emit calibrationUpdated(m_mmPerPixel);
|
||||
emit measurementUpdated(measuredPx(), measuredMm());
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void WeldOpenGLWidget::calibrateWithCurrentLine(double knownMm)
|
||||
{
|
||||
double px = measuredPx();
|
||||
if (px > 0.0 && knownMm > 0.0) {
|
||||
m_mmPerPixel = knownMm / px;
|
||||
m_isCalibrated = true;
|
||||
}
|
||||
emit calibrationUpdated(m_mmPerPixel);
|
||||
emit measurementUpdated(measuredPx(), measuredMm());
|
||||
update();
|
||||
}
|
||||
|
||||
double WeldOpenGLWidget::measuredPx() const
|
||||
{
|
||||
if (!m_hasLine) return 0.0;
|
||||
return QLineF(m_head, m_tail).length();
|
||||
}
|
||||
|
||||
double WeldOpenGLWidget::measuredMm() const
|
||||
{
|
||||
return measuredPx() * m_mmPerPixel;
|
||||
}
|
||||
|
||||
void WeldOpenGLWidget::resetLine()
|
||||
{
|
||||
if (!m_image.isNull()) {
|
||||
int w = m_image.width();
|
||||
int h = m_image.height();
|
||||
m_head = QPointF(w * 0.3, h * 0.5);
|
||||
m_tail = QPointF(w * 0.7, h * 0.5);
|
||||
m_hasLine = true;
|
||||
emit measurementUpdated(measuredPx(), measuredMm());
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void WeldOpenGLWidget::setLinePoints(const QPointF &head, const QPointF &tail)
|
||||
{
|
||||
m_head = head;
|
||||
m_tail = tail;
|
||||
m_hasLine = true;
|
||||
emit measurementUpdated(measuredPx(), measuredMm());
|
||||
update();
|
||||
}
|
||||
|
||||
void WeldOpenGLWidget::paintGL()
|
||||
{
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
if (m_image.isNull()) return;
|
||||
|
||||
makeCurrent();
|
||||
|
||||
if (m_textureNeedsUpdate) {
|
||||
if (m_texture) delete m_texture;
|
||||
m_texture = new QOpenGLTexture(m_image);
|
||||
m_texture->setMinificationFilter(QOpenGLTexture::Linear);
|
||||
m_texture->setMagnificationFilter(QOpenGLTexture::Linear);
|
||||
m_textureNeedsUpdate = false;
|
||||
}
|
||||
|
||||
// Render 2D image via QPainter over OpenGL viewport
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
painter.setRenderHint(QPainter::TextAntialiasing, true);
|
||||
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||
|
||||
// Calculate image destination rectangle with zoom and pan
|
||||
double widgetAspect = (double)width() / height();
|
||||
double imageAspect = (double)m_image.width() / m_image.height();
|
||||
|
||||
double drawW, drawH;
|
||||
if (widgetAspect > imageAspect) {
|
||||
drawH = height() * m_zoomLevel;
|
||||
drawW = drawH * imageAspect;
|
||||
} else {
|
||||
drawW = width() * m_zoomLevel;
|
||||
drawH = drawW / imageAspect;
|
||||
}
|
||||
|
||||
double drawX = (width() - drawW) / 2.0 + m_panOffset.x();
|
||||
double drawY = (height() - drawH) / 2.0 + m_panOffset.y();
|
||||
|
||||
QRectF targetRect(drawX, drawY, drawW, drawH);
|
||||
painter.drawImage(targetRect, m_image);
|
||||
|
||||
// Draw the single measurement/calibration line
|
||||
if (m_hasLine) {
|
||||
QColor color(255, 45, 45); // Smooth Vivid Red
|
||||
drawLineOverlay(painter, m_head, m_tail, color);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
void WeldOpenGLWidget::drawLineOverlay(QPainter &painter, const QPointF &imgHead, const QPointF &imgTail,
|
||||
const QColor &color)
|
||||
{
|
||||
QPointF wHead = imageToWidgetCoords(imgHead);
|
||||
QPointF wTail = imageToWidgetCoords(imgTail);
|
||||
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
// Pure single red line (1.0px thin stroke with antialiasing)
|
||||
QPen linePen(color, 1.0, Qt::SolidLine, Qt::RoundCap);
|
||||
painter.setPen(linePen);
|
||||
painter.drawLine(wHead, wTail);
|
||||
|
||||
// Compact drag handles (short perpendicular ticks)
|
||||
QLineF line(wHead, wTail);
|
||||
double dx = line.dx();
|
||||
double dy = line.dy();
|
||||
double len = line.length();
|
||||
if (len > 0.0) {
|
||||
double px = -dy / len;
|
||||
double py = dx / len;
|
||||
double tickHalfLen = 6.0; // short length perpendicular to measurement line
|
||||
|
||||
QPointF headP1 = wHead + QPointF(px * tickHalfLen, py * tickHalfLen);
|
||||
QPointF headP2 = wHead - QPointF(px * tickHalfLen, py * tickHalfLen);
|
||||
painter.drawLine(headP1, headP2);
|
||||
|
||||
QPointF tailP1 = wTail + QPointF(px * tickHalfLen, py * tickHalfLen);
|
||||
QPointF tailP2 = wTail - QPointF(px * tickHalfLen, py * tickHalfLen);
|
||||
painter.drawLine(tailP1, tailP2);
|
||||
}
|
||||
|
||||
// End labels
|
||||
painter.setFont(QFont("Arial", 8, QFont::Bold));
|
||||
painter.setPen(Qt::white);
|
||||
painter.drawText(wHead + QPointF(10, -6), "A");
|
||||
painter.drawText(wTail + QPointF(10, -6), "B");
|
||||
}
|
||||
|
||||
QPointF WeldOpenGLWidget::widgetToImageCoords(const QPointF &widgetPos) const
|
||||
{
|
||||
if (m_image.isNull()) return QPointF();
|
||||
|
||||
double widgetAspect = (double)width() / height();
|
||||
double imageAspect = (double)m_image.width() / m_image.height();
|
||||
|
||||
double drawW, drawH;
|
||||
if (widgetAspect > imageAspect) {
|
||||
drawH = height() * m_zoomLevel;
|
||||
drawW = drawH * imageAspect;
|
||||
} else {
|
||||
drawW = width() * m_zoomLevel;
|
||||
drawH = drawW / imageAspect;
|
||||
}
|
||||
|
||||
double drawX = (width() - drawW) / 2.0 + m_panOffset.x();
|
||||
double drawY = (height() - drawH) / 2.0 + m_panOffset.y();
|
||||
|
||||
double normX = (widgetPos.x() - drawX) / drawW;
|
||||
double normY = (widgetPos.y() - drawY) / drawH;
|
||||
|
||||
return QPointF(normX * m_image.width(), normY * m_image.height());
|
||||
}
|
||||
|
||||
QPointF WeldOpenGLWidget::imageToWidgetCoords(const QPointF &imgPos) const
|
||||
{
|
||||
if (m_image.isNull()) return QPointF();
|
||||
|
||||
double widgetAspect = (double)width() / height();
|
||||
double imageAspect = (double)m_image.width() / m_image.height();
|
||||
|
||||
double drawW, drawH;
|
||||
if (widgetAspect > imageAspect) {
|
||||
drawH = height() * m_zoomLevel;
|
||||
drawW = drawH * imageAspect;
|
||||
} else {
|
||||
drawW = width() * m_zoomLevel;
|
||||
drawH = drawW / imageAspect;
|
||||
}
|
||||
|
||||
double drawX = (width() - drawW) / 2.0 + m_panOffset.x();
|
||||
double drawY = (height() - drawH) / 2.0 + m_panOffset.y();
|
||||
|
||||
double normX = imgPos.x() / m_image.width();
|
||||
double normY = imgPos.y() / m_image.height();
|
||||
|
||||
return QPointF(drawX + normX * drawW, drawY + normY * drawH);
|
||||
}
|
||||
|
||||
WeldOpenGLWidget::DragHandle WeldOpenGLWidget::hitTestLine(const QPointF &imgPos, const QPointF &head, const QPointF &tail) const
|
||||
{
|
||||
QPointF wPos = imageToWidgetCoords(imgPos);
|
||||
QPointF wHead = imageToWidgetCoords(head);
|
||||
QPointF wTail = imageToWidgetCoords(tail);
|
||||
|
||||
const double handleRadius = 15.0; // Hit test tolerance in pixels
|
||||
|
||||
if (QLineF(wPos, wHead).length() <= handleRadius) return DragHandle::Head;
|
||||
if (QLineF(wPos, wTail).length() <= handleRadius) return DragHandle::Tail;
|
||||
|
||||
// Check if clicked near line body
|
||||
QLineF line(wHead, wTail);
|
||||
double lineLen = line.length();
|
||||
if (lineLen > 0) {
|
||||
double u = ((wPos.x() - wHead.x()) * (wTail.x() - wHead.x()) + (wPos.y() - wHead.y()) * (wTail.y() - wHead.y())) / (lineLen * lineLen);
|
||||
if (u >= 0.0 && u <= 1.0) {
|
||||
QPointF proj = wHead + u * (wTail - wHead);
|
||||
if (QLineF(wPos, proj).length() <= handleRadius) {
|
||||
return DragHandle::Body;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DragHandle::None;
|
||||
}
|
||||
|
||||
void WeldOpenGLWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (m_image.isNull()) return;
|
||||
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
QPointF imgPos = widgetToImageCoords(event->position());
|
||||
m_activeHandle = hitTestLine(imgPos, m_head, m_tail);
|
||||
if (m_activeHandle != DragHandle::None) {
|
||||
m_dragStartImgPos = imgPos;
|
||||
m_dragStartHead = m_head;
|
||||
m_dragStartTail = m_tail;
|
||||
update();
|
||||
} else {
|
||||
// Clicked background -> Pan image on left click drag
|
||||
m_isPanning = true;
|
||||
m_lastMousePos = event->pos();
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->button() == Qt::RightButton) {
|
||||
m_isPanning = true;
|
||||
m_lastMousePos = event->pos();
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void WeldOpenGLWidget::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (m_isPanning) {
|
||||
QPoint delta = event->pos() - m_lastMousePos;
|
||||
m_panOffset += QPointF(delta.x(), delta.y());
|
||||
m_lastMousePos = event->pos();
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
QPointF imgPos = widgetToImageCoords(event->position());
|
||||
|
||||
if (m_activeHandle != DragHandle::None) {
|
||||
if (m_activeHandle == DragHandle::Head) {
|
||||
m_head = imgPos;
|
||||
} else if (m_activeHandle == DragHandle::Tail) {
|
||||
m_tail = imgPos;
|
||||
} else if (m_activeHandle == DragHandle::Body) {
|
||||
QPointF delta = imgPos - m_dragStartImgPos;
|
||||
m_head = m_dragStartHead + delta;
|
||||
m_tail = m_dragStartTail + delta;
|
||||
}
|
||||
emit measurementUpdated(measuredPx(), measuredMm());
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update cursor feedback based on hover
|
||||
DragHandle hover = hitTestLine(imgPos, m_head, m_tail);
|
||||
if (hover == DragHandle::Head || hover == DragHandle::Tail) {
|
||||
setCursor(Qt::CrossCursor);
|
||||
} else if (hover == DragHandle::Body) {
|
||||
setCursor(Qt::SizeAllCursor);
|
||||
} else {
|
||||
setCursor(Qt::OpenHandCursor);
|
||||
}
|
||||
}
|
||||
|
||||
void WeldOpenGLWidget::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if (m_isPanning && (event->button() == Qt::LeftButton || event->button() == Qt::RightButton)) {
|
||||
m_isPanning = false;
|
||||
QPointF imgPos = widgetToImageCoords(event->position());
|
||||
DragHandle hover = hitTestLine(imgPos, m_head, m_tail);
|
||||
if (hover == DragHandle::Head || hover == DragHandle::Tail) setCursor(Qt::CrossCursor);
|
||||
else if (hover == DragHandle::Body) setCursor(Qt::SizeAllCursor);
|
||||
else setCursor(Qt::OpenHandCursor);
|
||||
}
|
||||
|
||||
if (event->button() == Qt::LeftButton && m_activeHandle != DragHandle::None) {
|
||||
m_activeHandle = DragHandle::None;
|
||||
QPointF imgPos = widgetToImageCoords(event->position());
|
||||
DragHandle hover = hitTestLine(imgPos, m_head, m_tail);
|
||||
if (hover == DragHandle::Head || hover == DragHandle::Tail) setCursor(Qt::CrossCursor);
|
||||
else if (hover == DragHandle::Body) setCursor(Qt::SizeAllCursor);
|
||||
else setCursor(Qt::OpenHandCursor);
|
||||
}
|
||||
}
|
||||
|
||||
void WeldOpenGLWidget::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
double zoomFactor = 1.15;
|
||||
if (event->angleDelta().y() < 0) {
|
||||
zoomFactor = 1.0 / zoomFactor;
|
||||
}
|
||||
|
||||
double newZoom = m_zoomLevel * zoomFactor;
|
||||
if (newZoom < 0.2) newZoom = 0.2;
|
||||
if (newZoom > 25.0) newZoom = 25.0;
|
||||
|
||||
QPointF mousePos = event->position();
|
||||
QPointF imgPosBefore = widgetToImageCoords(mousePos);
|
||||
|
||||
m_zoomLevel = newZoom;
|
||||
|
||||
QPointF widgetPosAfter = imageToWidgetCoords(imgPosBefore);
|
||||
m_panOffset += (mousePos - widgetPosAfter);
|
||||
|
||||
update();
|
||||
}
|
||||
Loading…
Reference in a new issue