jfjoch_viewer: Add ROI to calibration window (doesn't work very well, but as an experimental feature why not)
This commit is contained in:
@@ -116,49 +116,127 @@ void JFJochSimpleImageViewer::wheelEvent(QWheelEvent *event) {
|
||||
}
|
||||
}
|
||||
|
||||
void JFJochSimpleImageViewer::mousePressEvent(QMouseEvent *event) {
|
||||
void JFJochSimpleImageViewer::mousePressEvent(QMouseEvent* event) {
|
||||
if (!scene() || !has_image_) return;
|
||||
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
panning_ = true;
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
last_mouse_pos_ = event->pos();
|
||||
const QPointF scenePos = mapToScene(event->pos());
|
||||
|
||||
// With Shift -> ROI interactions (draw/resize/move); without -> pan
|
||||
if (event->modifiers() & Qt::Modifier::SHIFT) {
|
||||
// Determine if we are over an existing ROI handle
|
||||
active_handle_ = hitTestROIHandle(scenePos, 4.0 / std::sqrt(std::max(1e-4, scale_factor_)));
|
||||
if (active_handle_ != ResizeHandle::None && active_handle_ != ResizeHandle::Inside) {
|
||||
mouse_event_type_ = MouseEventType::ResizingROI;
|
||||
roi_start_pos_ = roi_box_.topLeft();
|
||||
roi_end_pos_ = roi_box_.bottomRight();
|
||||
setCursor(Qt::SizeAllCursor);
|
||||
} else if (roi_box_.contains(scenePos)) {
|
||||
mouse_event_type_ = MouseEventType::MovingROI;
|
||||
last_mouse_pos_ = event->pos();
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
} else {
|
||||
mouse_event_type_ = MouseEventType::DrawingROI;
|
||||
roi_start_pos_ = RoundPoint(scenePos);
|
||||
roi_end_pos_ = roi_start_pos_;
|
||||
setCursor(Qt::CrossCursor);
|
||||
}
|
||||
} else {
|
||||
mouse_event_type_ = MouseEventType::Panning;
|
||||
panning_ = true;
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
last_mouse_pos_ = event->pos();
|
||||
}
|
||||
}
|
||||
|
||||
QGraphicsView::mousePressEvent(event);
|
||||
}
|
||||
|
||||
void JFJochSimpleImageViewer::mouseMoveEvent(QMouseEvent *event) {
|
||||
void JFJochSimpleImageViewer::mouseMoveEvent(QMouseEvent* event) {
|
||||
if (!scene() || !has_image_) return;
|
||||
if (panning_) {
|
||||
const QPointF delta = mapToScene(event->pos()) - mapToScene(last_mouse_pos_);
|
||||
last_mouse_pos_ = event->pos();
|
||||
translate(delta.x(), delta.y());
|
||||
// labels positions are in scene coords, scene items re-used; no special handling
|
||||
} else {
|
||||
auto coord = mapToScene(event->pos());
|
||||
if ((coord.x() >= 0)
|
||||
&& (coord.x() < image_->image.GetWidth())
|
||||
&& (coord.y() >= 0)
|
||||
&& (coord.y() < image_->image.GetHeight())) {
|
||||
emit writeStatusBar(QString("x=%1 y=%2 %3 ")
|
||||
.arg(coord.x(), 0, 'f', 1)
|
||||
.arg(coord.y(), 0, 'f', 1)
|
||||
.arg(image_values_[int(coord.x()) + int(coord.y()) * image_->image.GetWidth()]
|
||||
), 6000);
|
||||
} else
|
||||
emit writeStatusBar("", 6000);
|
||||
|
||||
const QPointF scenePos = mapToScene(event->pos());
|
||||
|
||||
switch (mouse_event_type_) {
|
||||
case MouseEventType::Panning: {
|
||||
const QPointF delta = mapToScene(event->pos()) - mapToScene(last_mouse_pos_);
|
||||
last_mouse_pos_ = event->pos();
|
||||
translate(delta.x(), delta.y());
|
||||
updateScene();
|
||||
break;
|
||||
}
|
||||
case MouseEventType::DrawingROI: {
|
||||
roi_end_pos_ = RoundPoint(scenePos);
|
||||
updateROI();
|
||||
break;
|
||||
}
|
||||
case MouseEventType::MovingROI: {
|
||||
const QPointF delta = mapToScene(event->pos()) - mapToScene(last_mouse_pos_);
|
||||
last_mouse_pos_ = event->pos();
|
||||
roi_box_.translate(delta);
|
||||
updateROI();
|
||||
break;
|
||||
}
|
||||
case MouseEventType::ResizingROI: {
|
||||
// Modify the corresponding edges based on active_handle_
|
||||
QRectF r = roi_box_;
|
||||
switch (active_handle_) {
|
||||
case ResizeHandle::Left: r.setLeft(scenePos.x()); break;
|
||||
case ResizeHandle::Right: r.setRight(scenePos.x()); break;
|
||||
case ResizeHandle::Top: r.setTop(scenePos.y()); break;
|
||||
case ResizeHandle::Bottom: r.setBottom(scenePos.y()); break;
|
||||
case ResizeHandle::TopLeft: r.setTop(scenePos.y()); r.setLeft(scenePos.x()); break;
|
||||
case ResizeHandle::TopRight: r.setTop(scenePos.y()); r.setRight(scenePos.x()); break;
|
||||
case ResizeHandle::BottomLeft: r.setBottom(scenePos.y()); r.setLeft(scenePos.x()); break;
|
||||
case ResizeHandle::BottomRight:r.setBottom(scenePos.y()); r.setRight(scenePos.x()); break;
|
||||
default: break;
|
||||
}
|
||||
roi_box_ = r.normalized();
|
||||
updateROI();
|
||||
break;
|
||||
}
|
||||
case MouseEventType::None: {
|
||||
// Hover feedback / status bar display
|
||||
if ((scenePos.x() >= 0)
|
||||
&& (scenePos.x() < image_->image.GetWidth())
|
||||
&& (scenePos.y() >= 0)
|
||||
&& (scenePos.y() < image_->image.GetHeight())) {
|
||||
const auto ix = int(scenePos.x());
|
||||
const auto iy = int(scenePos.y());
|
||||
const auto idx = iy * int(image_->image.GetWidth()) + ix;
|
||||
if (idx >= 0 && idx < int(image_values_.size()))
|
||||
emit writeStatusBar(QString("x=%1 y=%2 I=%3")
|
||||
.arg(scenePos.x(), 0, 'f', 1)
|
||||
.arg(scenePos.y(), 0, 'f', 1)
|
||||
.arg(image_values_[size_t(idx)]), 3000);
|
||||
} else {
|
||||
emit writeStatusBar("", 1000);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QGraphicsView::mouseMoveEvent(event);
|
||||
}
|
||||
|
||||
void JFJochSimpleImageViewer::mouseReleaseEvent(QMouseEvent *event) {
|
||||
void JFJochSimpleImageViewer::mouseReleaseEvent(QMouseEvent* event) {
|
||||
if (!scene() || !has_image_) return;
|
||||
if (event->button() == Qt::LeftButton && panning_) {
|
||||
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
if (mouse_event_type_ == MouseEventType::DrawingROI) {
|
||||
roi_end_pos_ = RoundPoint(mapToScene(event->pos()));
|
||||
updateROI();
|
||||
}
|
||||
panning_ = false;
|
||||
mouse_event_type_ = MouseEventType::None;
|
||||
active_handle_ = ResizeHandle::None;
|
||||
setCursor(Qt::ArrowCursor);
|
||||
}
|
||||
|
||||
QGraphicsView::mouseReleaseEvent(event);
|
||||
}
|
||||
|
||||
|
||||
void JFJochSimpleImageViewer::resizeEvent(QResizeEvent *event) {
|
||||
QGraphicsView::resizeEvent(event);
|
||||
// Nothing special; keep current transform and scene
|
||||
@@ -293,6 +371,8 @@ void JFJochSimpleImageViewer::updateScene() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawROI(scene());
|
||||
}
|
||||
|
||||
template<class T>
|
||||
@@ -329,3 +409,167 @@ void JFJochSimpleImageViewer::renderImage(QImage &qimg, const uint8_t *input) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
QPointF JFJochSimpleImageViewer::RoundPoint(const QPointF& p) {
|
||||
return QPointF(qRound(p.x()), qRound(p.y()));
|
||||
}
|
||||
|
||||
JFJochSimpleImageViewer::ResizeHandle
|
||||
JFJochSimpleImageViewer::hitTestROIHandle(const QPointF& scenePos, qreal tol) const {
|
||||
if (roi_box_.isNull() || roi_box_.width() <= 0 || roi_box_.height() <= 0)
|
||||
return ResizeHandle::None;
|
||||
|
||||
const QRectF r = roi_box_;
|
||||
const QPointF tl = r.topLeft();
|
||||
const QPointF tr = r.topRight();
|
||||
const QPointF bl = r.bottomLeft();
|
||||
const QPointF br = r.bottomRight();
|
||||
|
||||
auto nearPt = [&](const QPointF& a, const QPointF& b, qreal t) {
|
||||
return std::abs(a.x() - b.x()) <= t && std::abs(a.y() - b.y()) <= t;
|
||||
};
|
||||
|
||||
if (nearPt(scenePos, tl, tol)) return ResizeHandle::TopLeft;
|
||||
if (nearPt(scenePos, tr, tol)) return ResizeHandle::TopRight;
|
||||
if (nearPt(scenePos, bl, tol)) return ResizeHandle::BottomLeft;
|
||||
if (nearPt(scenePos, br, tol)) return ResizeHandle::BottomRight;
|
||||
|
||||
// Edges
|
||||
if (std::abs(scenePos.x() - r.left()) <= tol && scenePos.y() >= r.top() - tol && scenePos.y() <= r.bottom() + tol)
|
||||
return ResizeHandle::Left;
|
||||
if (std::abs(scenePos.x() - r.right()) <= tol && scenePos.y() >= r.top() - tol && scenePos.y() <= r.bottom() + tol)
|
||||
return ResizeHandle::Right;
|
||||
if (std::abs(scenePos.y() - r.top()) <= tol && scenePos.x() >= r.left() - tol && scenePos.x() <= r.right() + tol)
|
||||
return ResizeHandle::Top;
|
||||
if (std::abs(scenePos.y() - r.bottom()) <= tol && scenePos.x() >= r.left() - tol && scenePos.x() <= r.right() + tol)
|
||||
return ResizeHandle::Bottom;
|
||||
|
||||
if (r.contains(scenePos)) return ResizeHandle::Inside;
|
||||
return ResizeHandle::None;
|
||||
}
|
||||
|
||||
void JFJochSimpleImageViewer::updateROI() {
|
||||
// Normalize box if drawing
|
||||
if (mouse_event_type_ == MouseEventType::DrawingROI) {
|
||||
roi_box_ = QRectF(roi_start_pos_, roi_end_pos_).normalized();
|
||||
}
|
||||
|
||||
// Clamp ROI to image bounds
|
||||
if (has_image_) {
|
||||
const QRectF bounds(0.0, 0.0,
|
||||
image_->image.GetWidth(),
|
||||
image_->image.GetHeight());
|
||||
roi_box_ = roi_box_.intersected(bounds);
|
||||
}
|
||||
|
||||
updateScene();
|
||||
}
|
||||
|
||||
void JFJochSimpleImageViewer::drawROI(QGraphicsScene* scn) {
|
||||
if (roi_box_.isNull() || roi_box_.width() <= 0 || roi_box_.height() <= 0) return;
|
||||
|
||||
QPen pen(Qt::yellow, 2);
|
||||
pen.setStyle(Qt::DashLine);
|
||||
pen.setCosmetic(true);
|
||||
scn->addRect(roi_box_, pen);
|
||||
|
||||
// Small corner handles
|
||||
const qreal handleSize = 3.0 / std::sqrt(std::max(1e-4, scale_factor_));
|
||||
auto addHandle = [&](const QPointF& p) {
|
||||
scn->addRect(QRectF(p.x() - handleSize, p.y() - handleSize, 2 * handleSize, 2 * handleSize),
|
||||
QPen(Qt::yellow, 1), QBrush(Qt::yellow));
|
||||
};
|
||||
addHandle(roi_box_.topLeft());
|
||||
addHandle(roi_box_.topRight());
|
||||
addHandle(roi_box_.bottomLeft());
|
||||
addHandle(roi_box_.bottomRight());
|
||||
|
||||
// Compute stats inside integer pixel ROI
|
||||
QRect roi_px = roi_box_.toRect().normalized();
|
||||
ROIStats stats = computeROIStats(roi_px);
|
||||
|
||||
if (stats.valid()) {
|
||||
// Compose text
|
||||
QString text = QString("min %1 max %2 avg %3 std %4 n=%5")
|
||||
.arg(stats.min, 0, 'f', 2)
|
||||
.arg(stats.max, 0, 'f', 2)
|
||||
.arg(stats.avg, 0, 'f', 2)
|
||||
.arg(stats.stddev, 0, 'f', 2)
|
||||
.arg(qulonglong(stats.count));
|
||||
|
||||
// Choose background based on local luminance under top-left pixel of ROI
|
||||
const int W = int(image_->image.GetWidth());
|
||||
const int H = int(image_->image.GetHeight());
|
||||
int px = std::clamp(roi_px.left(), 0, std::max(0, W - 1));
|
||||
int py = std::clamp(roi_px.top(), 0, std::max(0, H - 1));
|
||||
const rgb bgc = image_rgb_[size_t(py) * size_t(W) + size_t(px)];
|
||||
const bool darkText = luminance(bgc) > 128.0;
|
||||
|
||||
QFont font("Arial");
|
||||
const qreal f = std::clamp(scale_factor_, 0.5, 50.0);
|
||||
font.setPointSizeF(14.0 / std::sqrt(f));
|
||||
|
||||
auto* t = scn->addText(text, font);
|
||||
t->setDefaultTextColor(darkText ? Qt::black : Qt::white);
|
||||
t->setZValue(20.0);
|
||||
|
||||
// Place near top-left of ROI, slightly above
|
||||
QPointF pos = roi_box_.topLeft() + QPointF(2.0, -16.0 / std::sqrt(f));
|
||||
t->setPos(pos);
|
||||
}
|
||||
}
|
||||
|
||||
JFJochSimpleImageViewer::ROIStats
|
||||
JFJochSimpleImageViewer::computeROIStats(const QRect& roi_px) const {
|
||||
ROIStats s;
|
||||
if (!has_image_) return s;
|
||||
|
||||
const int W = int(image_->image.GetWidth());
|
||||
const int H = int(image_->image.GetHeight());
|
||||
|
||||
const int x0 = std::clamp(roi_px.left(), 0, std::max(0, W - 1));
|
||||
const int x1 = std::clamp(roi_px.right(), 0, std::max(0, W - 1));
|
||||
const int y0 = std::clamp(roi_px.top(), 0, std::max(0, H - 1));
|
||||
const int y1 = std::clamp(roi_px.bottom(), 0, std::max(0, H - 1));
|
||||
if (x1 < x0 || y1 < y0) return s;
|
||||
|
||||
// First pass: min, max, sum
|
||||
double sum = 0.0L;
|
||||
s.min = std::numeric_limits<double>::infinity();
|
||||
s.max = -std::numeric_limits<double>::infinity();
|
||||
s.count = 0;
|
||||
|
||||
for (int y = y0; y <= y1; ++y) {
|
||||
const size_t base = size_t(y) * size_t(W);
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
const size_t idx = base + size_t(x);
|
||||
const double v = image_values_[idx];
|
||||
if (std::isnan(v)) continue; // skip invalid
|
||||
if (v < s.min) s.min = v;
|
||||
if (v > s.max) s.max = v;
|
||||
sum += v;
|
||||
++s.count;
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.valid()) return s;
|
||||
|
||||
s.avg = double(sum / s.count);
|
||||
|
||||
// Second pass: variance
|
||||
double var_sum = 0.0L;
|
||||
for (int y = y0; y <= y1; ++y) {
|
||||
const size_t base = size_t(y) * size_t(W);
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
const size_t idx = base + size_t(x);
|
||||
const double v = image_values_[idx];
|
||||
if (std::isnan(v)) continue;
|
||||
const double d = v - s.avg;
|
||||
var_sum += d * d;
|
||||
}
|
||||
}
|
||||
const double variance = (s.count > 1) ? (var_sum / s.count) : 0.0L;
|
||||
s.stddev = std::sqrt(double(variance));
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public slots:
|
||||
signals:
|
||||
void foregroundChanged(float v);
|
||||
void backgroundChanged(float v);
|
||||
void writeStatusBar(QString string, int timeout);
|
||||
void writeStatusBar(QString string, int timeout_ms = 0);
|
||||
|
||||
protected:
|
||||
void wheelEvent(QWheelEvent* event) override;
|
||||
@@ -44,15 +44,26 @@ private:
|
||||
QColor color;
|
||||
};
|
||||
|
||||
struct ROIStats {
|
||||
double min = std::numeric_limits<double>::infinity();
|
||||
double max = -std::numeric_limits<double>::infinity();
|
||||
double avg = std::numeric_limits<double>::quiet_NaN();
|
||||
double stddev = std::numeric_limits<double>::quiet_NaN();
|
||||
size_t count = 0;
|
||||
bool valid() const { return std::isfinite(min) && std::isfinite(max) && count > 0; }
|
||||
};
|
||||
|
||||
void renderImage(); // builds pixmap_ from current image_ + settings
|
||||
void updateScene(); // clears scene, adds pixmap + labels
|
||||
void updateScene(); // clears scene, adds pixmap + labels + ROI (if any)
|
||||
void drawROI(QGraphicsScene* scn); // draw box and stats
|
||||
ROIStats computeROIStats(const QRect& roi_px) const;
|
||||
|
||||
// Image and rendering state
|
||||
std::shared_ptr<const SimpleImage> image_;
|
||||
bool has_image_ = false;
|
||||
QPixmap pixmap_;
|
||||
std::vector<rgb> image_rgb_; // intermediate RGB buffer to feed QImage
|
||||
std::vector<int64_t> image_values_; // original scalar per pixel (for label text)
|
||||
std::vector<double> image_values_; // original scalar values per pixel (for labels and min/max)
|
||||
|
||||
// View/interaction
|
||||
double scale_factor_ = 1.0;
|
||||
@@ -63,9 +74,28 @@ private:
|
||||
float background_ = 0.0f;
|
||||
float foreground_ = 10.0f;
|
||||
ColorScale color_scale_;
|
||||
|
||||
bool auto_fgbg = true;
|
||||
bool show_labels_ = false;
|
||||
std::vector<Label> labels_;
|
||||
|
||||
template <class T>
|
||||
void renderImage(QImage &qimg, const uint8_t *input);
|
||||
|
||||
// ROI (box) state
|
||||
enum class MouseEventType { None, Panning, DrawingROI, MovingROI, ResizingROI };
|
||||
MouseEventType mouse_event_type_ = MouseEventType::None;
|
||||
|
||||
QRectF roi_box_; // in scene/image pixel coordinates
|
||||
QPointF roi_start_pos_; // for drawing/resizing
|
||||
QPointF roi_end_pos_; // for drawing/resizing
|
||||
|
||||
// Resizing which edge/corner
|
||||
enum class ResizeHandle { None, Left, Right, Top, Bottom, TopLeft, TopRight, BottomLeft, BottomRight, Inside };
|
||||
ResizeHandle active_handle_ = ResizeHandle::None;
|
||||
|
||||
// Helpers
|
||||
static QPointF RoundPoint(const QPointF& p);
|
||||
ResizeHandle hitTestROIHandle(const QPointF& scenePos, qreal tol = 3.0) const;
|
||||
void updateROI(); // normalize and request repaint
|
||||
|
||||
bool auto_fgbg = true;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user