From dbc75322fc1314741cc02f48c5cfd5ec6f3fd7f1 Mon Sep 17 00:00:00 2001 From: Alexander Rath Date: Thu, 28 Dec 2023 18:07:23 +0100 Subject: [PATCH 01/21] support for statistics over crops --- include/tev/ImageCanvas.h | 34 +++++++++++++++ include/tev/ImageViewer.h | 1 + include/tev/UberShader.h | 10 ++++- src/ImageCanvas.cpp | 91 ++++++++++++++++++++++++++++++++++----- src/ImageViewer.cpp | 27 +++++++++++- src/UberShader.cpp | 48 +++++++++++++++++++-- 6 files changed, 192 insertions(+), 19 deletions(-) diff --git a/include/tev/ImageCanvas.h b/include/tev/ImageCanvas.h index 1606ca7..8479b67 100644 --- a/include/tev/ImageCanvas.h +++ b/include/tev/ImageCanvas.h @@ -94,6 +94,34 @@ class ImageCanvas : public nanogui::Canvas { mMetric = metric; } + bool isCropped() const { + return mIsCropped; + } + + void enableCrop() { + mIsCropped = true; + } + + void disableCrop() { + mIsCropped = false; + } + + nanogui::Vector2i cropMin() const { + return mCropMin; + } + + void setCropMin(const nanogui::Vector2i &cropMin) { + mCropMin = cropMin; + } + + nanogui::Vector2i cropMax() const { + return mCropMin; + } + + void setCropMax(const nanogui::Vector2i &cropMax) { + mCropMax = cropMax; + } + static float applyMetric(float value, float reference, EMetric metric); float applyMetric(float value, float reference) const { return applyMetric(value, reference, mMetric); @@ -141,6 +169,9 @@ class ImageCanvas : public nanogui::Canvas { std::shared_ptr reference, const std::string& requestedChannelGroup, EMetric metric, + bool isCropped, + nanogui::Vector2i cropMin, + nanogui::Vector2i cropMax, int priority ); @@ -174,6 +205,9 @@ class ImageCanvas : public nanogui::Canvas { ETonemap mTonemap = SRGB; EMetric mMetric = Error; + bool mIsCropped = false; + nanogui::Vector2i mCropMin; + nanogui::Vector2i mCropMax; std::map>>> mCanvasStatistics; std::map> mImageIdToCanvasStatisticsKey; diff --git a/include/tev/ImageViewer.h b/include/tev/ImageViewer.h index 7826db4..ce519df 100644 --- a/include/tev/ImageViewer.h +++ b/include/tev/ImageViewer.h @@ -255,6 +255,7 @@ class ImageViewer : public nanogui::Screen { bool mIsDraggingSidebar = false; bool mIsDraggingImage = false; bool mIsDraggingImageButton = false; + bool mIsCroppingImage = false; size_t mDraggedImageButtonId; nanogui::Vector2f mDraggingStartPosition; diff --git a/include/tev/UberShader.h b/include/tev/UberShader.h index d957dbe..354d36d 100644 --- a/include/tev/UberShader.h +++ b/include/tev/UberShader.h @@ -27,7 +27,10 @@ class UberShader { float offset, float gamma, bool clipToLdr, - ETonemap tonemap + ETonemap tonemap, + bool isCropped, + const nanogui::Vector2f& cropMin, + const nanogui::Vector2f& cropMax ); // Draws a difference between a reference and an image. @@ -43,7 +46,10 @@ class UberShader { float gamma, bool clipToLdr, ETonemap tonemap, - EMetric metric + EMetric metric, + bool isCropped, + const nanogui::Vector2f& cropMin, + const nanogui::Vector2f& cropMax ); const nanogui::Color& backgroundColor() { diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index f35b7c7..7fe6247 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -60,6 +60,17 @@ void ImageCanvas::draw_contents() { return; } + nanogui::Vector2f cropMin = mCropMin; + nanogui::Vector2f cropMax = mCropMax; + + if (cropMin.x() > cropMax.x()) std::swap(cropMin.x(), cropMax.x()); + if (cropMin.y() > cropMax.y()) std::swap(cropMin.y(), cropMax.y()); + + cropMin.x() /= mImage->size().x(); + cropMin.y() /= mImage->size().y(); + cropMax.x() /= mImage->size().x(); + cropMax.y() /= mImage->size().y(); + if (!mReference || ctrlHeld || image == mReference.get()) { mShader->draw( 2.0f * inverse(Vector2f{m_size}) / mPixelRatio, @@ -72,7 +83,10 @@ void ImageCanvas::draw_contents() { mOffset, mGamma, mClipToLdr, - mTonemap + mTonemap, + mIsCropped, + cropMin, + cropMax ); return; } @@ -93,7 +107,10 @@ void ImageCanvas::draw_contents() { mGamma, mClipToLdr, mTonemap, - mMetric + mMetric, + mIsCropped, + cropMin, + cropMax ); } @@ -700,6 +717,21 @@ shared_ptr>> ImageCanvas::canvasStatistics() { string key = mReference ? fmt::format("{}-{}-{}-{}", mImage->id(), channels, mReference->id(), (int)mMetric) : fmt::format("{}-{}", mImage->id(), channels); + + auto isCropped = mIsCropped; + auto cropMin = mCropMin; + auto cropMax = mCropMax; + if (cropMin.x() > cropMax.x()) std::swap(cropMin.x(), cropMax.x()); + if (cropMin.y() > cropMax.y()) std::swap(cropMin.y(), cropMax.y()); + + if (isCropped) { + key += std::string("-crop") + + "-" + std::to_string(mCropMin.x()) + + "-" + std::to_string(mCropMin.y()) + + "-" + std::to_string(mCropMax.x()) + + "-" + std::to_string(mCropMax.y()) + ; + } auto iter = mCanvasStatistics.find(key); if (iter != end(mCanvasStatistics)) { @@ -728,9 +760,17 @@ shared_ptr>> ImageCanvas::canvasStatistics() { mReference->setStaleIdCallback([this](int id) { purgeCanvasStatistics(id); }); } - invokeTaskDetached([image, reference, requestedChannelGroup, metric, priority, p=std::move(promise)]() mutable -> Task { + invokeTaskDetached([ + image, reference, requestedChannelGroup, metric, + isCropped, cropMin, cropMax, + priority, p=std::move(promise) + ]() mutable -> Task { co_await ThreadPool::global().enqueueCoroutine(priority); - p.set_value(co_await computeCanvasStatistics(image, reference, requestedChannelGroup, metric, priority)); + p.set_value(co_await computeCanvasStatistics( + image, reference, requestedChannelGroup, metric, + isCropped, cropMin, cropMax, + priority + )); }); return mCanvasStatistics.at(key); @@ -807,6 +847,9 @@ Task> ImageCanvas::computeCanvasStatistics( std::shared_ptr reference, const string& requestedChannelGroup, EMetric metric, + bool isCropped, + Vector2i cropMin, + Vector2i cropMax, int priority ) { auto flattened = channelsFromImages(image, reference, requestedChannelGroup, metric, priority); @@ -834,15 +877,38 @@ Task> ImageCanvas::computeCanvasStatistics( int nChannels = result->nChannels = alphaChannel ? (int)flattened.size() - 1 : (int)flattened.size(); + if (!isCropped) { + cropMin = Vector2i(0, 0); + cropMax = image->size(); + } + + cropMin = { + clamp(cropMin.x(), 0, image->size().x()), + clamp(cropMin.y(), 0, image->size().y()) + }; + cropMax = { + clamp(cropMax.x(), 0, image->size().x()), + clamp(cropMax.y(), 0, image->size().y()) + }; + + int stride = image->size().x(); + int pixelCount = 0; for (int i = 0; i < nChannels; ++i) { const auto& channel = flattened[i]; - auto [cmin, cmax, cmean] = channel.minMaxMean(); - mean += cmean; - maximum = max(maximum, cmax); - minimum = min(minimum, cmin); + for (int y = cropMin.y(); y < cropMax.y(); ++y) { + for (int x = cropMin.x(); x < cropMax.x(); ++x) { + auto v = channel.at(Vector2i{x, y}); + if (!isnan(v)) { + mean += v; + maximum = max(maximum, v); + minimum = min(minimum, v); + pixelCount++; + } + } + } } - result->mean = nChannels > 0 ? (mean / nChannels) : 0; + result->mean = pixelCount > 0 ? (mean / pixelCount) : 0; result->maximum = maximum; result->minimum = minimum; @@ -878,7 +944,8 @@ Task> ImageCanvas::computeCanvasStatistics( co_return result; } - auto numPixels = image->numPixels(); + auto cropSize = cropMax - cropMin; + auto numPixels = cropSize.x() * cropSize.y(); std::vector indices(numPixels * nChannels); vector> tasks; @@ -886,7 +953,9 @@ Task> ImageCanvas::computeCanvasStatistics( const auto& channel = flattened[i]; tasks.emplace_back( ThreadPool::global().parallelForAsync(0, numPixels, [&, i](size_t j) { - indices[j + i * numPixels] = valToBin(channel.eval(j)); + int x = (j % cropSize.x()) + cropMin.x(); + int y = (j / cropSize.y()) + cropMin.y(); + indices[j + i * numPixels] = valToBin(channel.at(Vector2i{x, y})); }, priority) ); } diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 48eb608..13df173 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -519,8 +519,26 @@ bool ImageViewer::mouse_button_event(const nanogui::Vector2i &p, int button, boo mDraggingStartPosition = nanogui::Vector2f(p); return true; } else if (mImageCanvas->contains(p)) { - mIsDraggingImage = true; - mDraggingStartPosition = nanogui::Vector2f(p); + if ((modifiers & 4) != 0) { + if (button == 0) { + // control + left click, start crop + auto rel = mouse_pos() - mImageCanvas->position(); + auto imageCoords = mImageCanvas->getImageCoords(*mCurrentImage, {rel.x(), rel.y()}); + + mIsCroppingImage = true; + mImageCanvas->setCropMin(imageCoords); + mImageCanvas->setCropMax(imageCoords); + mImageCanvas->enableCrop(); + } else { + // control + right click, disable crop + mIsCroppingImage = false; + mImageCanvas->disableCrop(); + } + } else { + mIsDraggingImage = true; + mDraggingStartPosition = nanogui::Vector2f(p); + mIsCroppingImage = false; + } return true; } } else { @@ -531,6 +549,7 @@ bool ImageViewer::mouse_button_event(const nanogui::Vector2i &p, int button, boo mIsDraggingSidebar = false; mIsDraggingImage = false; mIsDraggingImageButton = false; + mIsCroppingImage = false; } return false; @@ -605,6 +624,10 @@ bool ImageViewer::mouse_motion_event( dynamic_cast(buttons[mDraggedImageButtonId])->set_position( relMousePos - nanogui::Vector2i(mDraggingStartPosition) ); + } else if (mIsCroppingImage) { + auto relMousePos = mouse_pos() - mImageCanvas->position(); + auto imageCoords = mImageCanvas->getImageCoords(*mCurrentImage, {relMousePos.x(), relMousePos.y()}); + mImageCanvas->setCropMax(imageCoords); } return false; diff --git a/src/UberShader.cpp b/src/UberShader.cpp index 15960e3..f198aa6 100644 --- a/src/UberShader.cpp +++ b/src/UberShader.cpp @@ -73,6 +73,10 @@ UberShader::UberShader(RenderPass* renderPass) { uniform int tonemap; uniform int metric; + uniform bool isCropped; + uniform vec2 cropMin; + uniform vec2 cropMax; + uniform vec4 bgColor; varying vec2 checkerUv; @@ -164,7 +168,17 @@ UberShader::UberShader(RenderPass* renderPass) { return; } + float cropAlpha = 1.f; + if (isCropped) { + if (imageUv.x < cropMin.x + || imageUv.x > cropMax.x + || imageUv.y < cropMin.y + || imageUv.y > cropMax.y) + cropAlpha = 0.3f; + } + vec4 imageVal = sample(image, imageUv); + imageVal.a = imageVal.a * cropAlpha; if (!hasReference) { gl_FragColor = vec4( applyTonemap(applyExposureAndOffset(imageVal.rgb), vec4(checker, 1.0 - imageVal.a)), @@ -175,6 +189,7 @@ UberShader::UberShader(RenderPass* renderPass) { } vec4 referenceVal = sample(reference, referenceUv); + referenceVal.a = referenceVal.a * cropAlpha; vec3 difference = imageVal.rgb - referenceVal.rgb; float alpha = (imageVal.a + referenceVal.a) * 0.5; @@ -324,6 +339,9 @@ UberShader::UberShader(RenderPass* renderPass) { const constant bool& clipToLdr, const constant int& tonemap, const constant int& metric, + const constant bool& isCropped, + const constant float2& cropMin, + const constant float2& cropMax, const constant float4& bgColor ) { float3 darkGray = float3(0.5f, 0.5f, 0.5f); @@ -335,7 +353,17 @@ UberShader::UberShader(RenderPass* renderPass) { return float4(checker, 1.0f); } + float cropAlpha = 1.f; + if (isCropped) { + if (vert.imageUv.x < cropMin.x + || vert.imageUv.x > cropMax.x + || vert.imageUv.y < cropMin.y + || vert.imageUv.y > cropMax.y) + cropAlpha = 0.3f; + } + float4 imageVal = sample(image, image_sampler, vert.imageUv); + imageVal.a = imageVal.a * cropAlpha; if (!hasReference) { float4 color = float4( applyTonemap( @@ -354,6 +382,7 @@ UberShader::UberShader(RenderPass* renderPass) { } float4 referenceVal = sample(reference, reference_sampler, vert.referenceUv); + referenceVal.a = referenceVal.a * cropAlpha; float3 difference = imageVal.rgb - referenceVal.rgb; float alpha = (imageVal.a + referenceVal.a) * 0.5f; @@ -411,7 +440,8 @@ void UberShader::draw(const Vector2f& pixelSize, const Vector2f& checkerSize) { pixelSize, checkerSize, nullptr, Matrix3f{0.0f}, 0.0f, 0.0f, 0.0f, false, - ETonemap::SRGB + ETonemap::SRGB, + false, Vector2f{0.f}, Vector2f{0.f} ); } @@ -424,14 +454,18 @@ void UberShader::draw( float offset, float gamma, bool clipToLdr, - ETonemap tonemap + ETonemap tonemap, + bool isCropped, + const Vector2f& cropMin, + const Vector2f& cropMax ) { draw( pixelSize, checkerSize, textureImage, transformImage, nullptr, Matrix3f{0.0f}, exposure, offset, gamma, clipToLdr, - tonemap, EMetric::Error + tonemap, EMetric::Error, + isCropped, cropMin, cropMax ); } @@ -447,7 +481,10 @@ void UberShader::draw( float gamma, bool clipToLdr, ETonemap tonemap, - EMetric metric + EMetric metric, + bool isCropped, + const nanogui::Vector2f& cropMin, + const nanogui::Vector2f& cropMax ) { bool hasImage = textureImage; if (!hasImage) { @@ -467,6 +504,9 @@ void UberShader::draw( mShader->set_uniform("hasImage", hasImage); mShader->set_uniform("hasReference", hasReference); mShader->set_uniform("clipToLdr", clipToLdr); + mShader->set_uniform("isCropped", isCropped); + mShader->set_uniform("cropMin", cropMin); + mShader->set_uniform("cropMax", cropMax); mShader->begin(); mShader->draw_array(Shader::PrimitiveType::Triangle, 0, 6, true); From 9d150a947059f91ebdd21c0e7cf828095c06bfe3 Mon Sep 17 00:00:00 2001 From: Alexander Rath Date: Thu, 28 Dec 2023 19:04:23 +0100 Subject: [PATCH 02/21] cleanup --- src/ImageCanvas.cpp | 23 +++++++++++------------ src/ImageViewer.cpp | 4 ++-- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index 7fe6247..1754cd2 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -877,21 +877,20 @@ Task> ImageCanvas::computeCanvasStatistics( int nChannels = result->nChannels = alphaChannel ? (int)flattened.size() - 1 : (int)flattened.size(); - if (!isCropped) { + if (isCropped) { + cropMin = { + clamp(cropMin.x(), 0, image->size().x()), + clamp(cropMin.y(), 0, image->size().y()) + }; + cropMax = { + clamp(cropMax.x(), 0, image->size().x()), + clamp(cropMax.y(), 0, image->size().y()) + }; + } else { cropMin = Vector2i(0, 0); cropMax = image->size(); } - cropMin = { - clamp(cropMin.x(), 0, image->size().x()), - clamp(cropMin.y(), 0, image->size().y()) - }; - cropMax = { - clamp(cropMax.x(), 0, image->size().x()), - clamp(cropMax.y(), 0, image->size().y()) - }; - - int stride = image->size().x(); int pixelCount = 0; for (int i = 0; i < nChannels; ++i) { const auto& channel = flattened[i]; @@ -945,7 +944,7 @@ Task> ImageCanvas::computeCanvasStatistics( } auto cropSize = cropMax - cropMin; - auto numPixels = cropSize.x() * cropSize.y(); + auto numPixels = size_t(cropSize.x()) * size_t(cropSize.y()); std::vector indices(numPixels * nChannels); vector> tasks; diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 13df173..9c25a72 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -521,7 +521,7 @@ bool ImageViewer::mouse_button_event(const nanogui::Vector2i &p, int button, boo } else if (mImageCanvas->contains(p)) { if ((modifiers & 4) != 0) { if (button == 0) { - // control + left click, start crop + // alt/option + left click, start crop auto rel = mouse_pos() - mImageCanvas->position(); auto imageCoords = mImageCanvas->getImageCoords(*mCurrentImage, {rel.x(), rel.y()}); @@ -530,7 +530,7 @@ bool ImageViewer::mouse_button_event(const nanogui::Vector2i &p, int button, boo mImageCanvas->setCropMax(imageCoords); mImageCanvas->enableCrop(); } else { - // control + right click, disable crop + // alt/option + right click, disable crop mIsCroppingImage = false; mImageCanvas->disableCrop(); } From 39e7ee2d3cf1d49e4deb6b32638e598d3239b3ad Mon Sep 17 00:00:00 2001 From: Alexander Rath Date: Tue, 2 Jan 2024 17:23:45 +0100 Subject: [PATCH 03/21] use std::optional for cropping --- include/tev/ImageCanvas.h | 60 ++++++++++++++--------------- include/tev/ImageViewer.h | 1 + include/tev/UberShader.h | 10 ++--- src/ImageCanvas.cpp | 81 ++++++++++++--------------------------- src/ImageViewer.cpp | 15 +++----- src/UberShader.cpp | 23 +++++------ 6 files changed, 78 insertions(+), 112 deletions(-) diff --git a/include/tev/ImageCanvas.h b/include/tev/ImageCanvas.h index 8479b67..0b76a33 100644 --- a/include/tev/ImageCanvas.h +++ b/include/tev/ImageCanvas.h @@ -6,10 +6,12 @@ #include #include #include +#include #include #include +#include namespace tev { @@ -53,6 +55,11 @@ class ImageCanvas : public nanogui::Canvas { float applyExposureAndOffset(float value) const; void setImage(std::shared_ptr image) { + if (!mImage || !image || mImage->size() != image->size()) { + // only keep crop active if image resolution has stayed the same + setCrop(std::nullopt); + } + mImage = image; } @@ -94,32 +101,29 @@ class ImageCanvas : public nanogui::Canvas { mMetric = metric; } - bool isCropped() const { - return mIsCropped; - } - - void enableCrop() { - mIsCropped = true; - } - - void disableCrop() { - mIsCropped = false; - } - - nanogui::Vector2i cropMin() const { - return mCropMin; - } - - void setCropMin(const nanogui::Vector2i &cropMin) { - mCropMin = cropMin; - } - - nanogui::Vector2i cropMax() const { - return mCropMin; + void setCrop(const std::optional& crop) { + if (!crop.has_value()) { + mCrop = std::nullopt; + return; + } + + // sanitize the input crop + Box2i clean; + for (size_t dim = 0; dim < clean.min.Size; dim++) { + // order input crop, add one to maximum to make box inclusive + clean.min[dim] = std::min(crop->min[dim], crop->max[dim]); + clean.max[dim] = std::max(crop->min[dim], crop->max[dim]) + 1; + + // clamp to image extents + clean.min[dim] = std::max(0, std::min(mImage->size()[dim], clean.min[dim])); + clean.max[dim] = std::max(0, std::min(mImage->size()[dim], clean.max[dim])); + } + + mCrop = clean; } - void setCropMax(const nanogui::Vector2i &cropMax) { - mCropMax = cropMax; + std::optional getCrop() { + return mCrop; } static float applyMetric(float value, float reference, EMetric metric); @@ -169,9 +173,7 @@ class ImageCanvas : public nanogui::Canvas { std::shared_ptr reference, const std::string& requestedChannelGroup, EMetric metric, - bool isCropped, - nanogui::Vector2i cropMin, - nanogui::Vector2i cropMax, + const Box2i& region, int priority ); @@ -205,9 +207,7 @@ class ImageCanvas : public nanogui::Canvas { ETonemap mTonemap = SRGB; EMetric mMetric = Error; - bool mIsCropped = false; - nanogui::Vector2i mCropMin; - nanogui::Vector2i mCropMax; + std::optional mCrop; std::map>>> mCanvasStatistics; std::map> mImageIdToCanvasStatisticsKey; diff --git a/include/tev/ImageViewer.h b/include/tev/ImageViewer.h index ce519df..91b683a 100644 --- a/include/tev/ImageViewer.h +++ b/include/tev/ImageViewer.h @@ -258,6 +258,7 @@ class ImageViewer : public nanogui::Screen { bool mIsCroppingImage = false; size_t mDraggedImageButtonId; + nanogui::Vector2i mCroppingStartCoordinates; nanogui::Vector2f mDraggingStartPosition; size_t mClipboardIndex = 0; diff --git a/include/tev/UberShader.h b/include/tev/UberShader.h index 354d36d..2e3105f 100644 --- a/include/tev/UberShader.h +++ b/include/tev/UberShader.h @@ -7,6 +7,8 @@ #include #include +#include + namespace tev { class UberShader { @@ -28,9 +30,7 @@ class UberShader { float gamma, bool clipToLdr, ETonemap tonemap, - bool isCropped, - const nanogui::Vector2f& cropMin, - const nanogui::Vector2f& cropMax + const std::optional& crop ); // Draws a difference between a reference and an image. @@ -47,9 +47,7 @@ class UberShader { bool clipToLdr, ETonemap tonemap, EMetric metric, - bool isCropped, - const nanogui::Vector2f& cropMin, - const nanogui::Vector2f& cropMax + const std::optional& crop ); const nanogui::Color& backgroundColor() { diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index 1754cd2..4d7977f 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -60,17 +60,6 @@ void ImageCanvas::draw_contents() { return; } - nanogui::Vector2f cropMin = mCropMin; - nanogui::Vector2f cropMax = mCropMax; - - if (cropMin.x() > cropMax.x()) std::swap(cropMin.x(), cropMax.x()); - if (cropMin.y() > cropMax.y()) std::swap(cropMin.y(), cropMax.y()); - - cropMin.x() /= mImage->size().x(); - cropMin.y() /= mImage->size().y(); - cropMax.x() /= mImage->size().x(); - cropMax.y() /= mImage->size().y(); - if (!mReference || ctrlHeld || image == mReference.get()) { mShader->draw( 2.0f * inverse(Vector2f{m_size}) / mPixelRatio, @@ -84,9 +73,7 @@ void ImageCanvas::draw_contents() { mGamma, mClipToLdr, mTonemap, - mIsCropped, - cropMin, - cropMax + mCrop ); return; } @@ -108,9 +95,7 @@ void ImageCanvas::draw_contents() { mClipToLdr, mTonemap, mMetric, - mIsCropped, - cropMin, - cropMax + mCrop ); } @@ -717,19 +702,13 @@ shared_ptr>> ImageCanvas::canvasStatistics() { string key = mReference ? fmt::format("{}-{}-{}-{}", mImage->id(), channels, mReference->id(), (int)mMetric) : fmt::format("{}-{}", mImage->id(), channels); - - auto isCropped = mIsCropped; - auto cropMin = mCropMin; - auto cropMax = mCropMax; - if (cropMin.x() > cropMax.x()) std::swap(cropMin.x(), cropMax.x()); - if (cropMin.y() > cropMax.y()) std::swap(cropMin.y(), cropMax.y()); - - if (isCropped) { + + if (mCrop.has_value()) { key += std::string("-crop") - + "-" + std::to_string(mCropMin.x()) - + "-" + std::to_string(mCropMin.y()) - + "-" + std::to_string(mCropMax.x()) - + "-" + std::to_string(mCropMax.y()) + + "-" + std::to_string(mCrop->min.x()) + + "-" + std::to_string(mCrop->min.y()) + + "-" + std::to_string(mCrop->max.x()) + + "-" + std::to_string(mCrop->max.y()) ; } @@ -760,16 +739,22 @@ shared_ptr>> ImageCanvas::canvasStatistics() { mReference->setStaleIdCallback([this](int id) { purgeCanvasStatistics(id); }); } + Box2i region; + if (mCrop.has_value()) { + region = mCrop.value(); + } else { + region.min = Vector2i{0}; + region.max = image->size(); + } + invokeTaskDetached([ image, reference, requestedChannelGroup, metric, - isCropped, cropMin, cropMax, - priority, p=std::move(promise) + region, priority, p=std::move(promise) ]() mutable -> Task { co_await ThreadPool::global().enqueueCoroutine(priority); p.set_value(co_await computeCanvasStatistics( image, reference, requestedChannelGroup, metric, - isCropped, cropMin, cropMax, - priority + region, priority )); }); @@ -847,9 +832,7 @@ Task> ImageCanvas::computeCanvasStatistics( std::shared_ptr reference, const string& requestedChannelGroup, EMetric metric, - bool isCropped, - Vector2i cropMin, - Vector2i cropMax, + const Box2i& region, int priority ) { auto flattened = channelsFromImages(image, reference, requestedChannelGroup, metric, priority); @@ -877,25 +860,11 @@ Task> ImageCanvas::computeCanvasStatistics( int nChannels = result->nChannels = alphaChannel ? (int)flattened.size() - 1 : (int)flattened.size(); - if (isCropped) { - cropMin = { - clamp(cropMin.x(), 0, image->size().x()), - clamp(cropMin.y(), 0, image->size().y()) - }; - cropMax = { - clamp(cropMax.x(), 0, image->size().x()), - clamp(cropMax.y(), 0, image->size().y()) - }; - } else { - cropMin = Vector2i(0, 0); - cropMax = image->size(); - } - int pixelCount = 0; for (int i = 0; i < nChannels; ++i) { const auto& channel = flattened[i]; - for (int y = cropMin.y(); y < cropMax.y(); ++y) { - for (int x = cropMin.x(); x < cropMax.x(); ++x) { + for (int y = region.min.y(); y < region.max.y(); ++y) { + for (int x = region.min.x(); x < region.max.x(); ++x) { auto v = channel.at(Vector2i{x, y}); if (!isnan(v)) { mean += v; @@ -943,8 +912,8 @@ Task> ImageCanvas::computeCanvasStatistics( co_return result; } - auto cropSize = cropMax - cropMin; - auto numPixels = size_t(cropSize.x()) * size_t(cropSize.y()); + auto regionSize = region.size(); + auto numPixels = size_t(regionSize.x()) * size_t(regionSize.y()); std::vector indices(numPixels * nChannels); vector> tasks; @@ -952,8 +921,8 @@ Task> ImageCanvas::computeCanvasStatistics( const auto& channel = flattened[i]; tasks.emplace_back( ThreadPool::global().parallelForAsync(0, numPixels, [&, i](size_t j) { - int x = (j % cropSize.x()) + cropMin.x(); - int y = (j / cropSize.y()) + cropMin.y(); + int x = (j % regionSize.x()) + region.min.x(); + int y = (j / regionSize.y()) + region.min.y(); indices[j + i * numPixels] = valToBin(channel.at(Vector2i{x, y})); }, priority) ); diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 9c25a72..7c22f27 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -521,18 +521,13 @@ bool ImageViewer::mouse_button_event(const nanogui::Vector2i &p, int button, boo } else if (mImageCanvas->contains(p)) { if ((modifiers & 4) != 0) { if (button == 0) { - // alt/option + left click, start crop + // alt/option + left drag to crop auto rel = mouse_pos() - mImageCanvas->position(); auto imageCoords = mImageCanvas->getImageCoords(*mCurrentImage, {rel.x(), rel.y()}); mIsCroppingImage = true; - mImageCanvas->setCropMin(imageCoords); - mImageCanvas->setCropMax(imageCoords); - mImageCanvas->enableCrop(); - } else { - // alt/option + right click, disable crop - mIsCroppingImage = false; - mImageCanvas->disableCrop(); + mCroppingStartCoordinates = imageCoords; + mImageCanvas->setCrop(std::nullopt); // single click disables crop } } else { mIsDraggingImage = true; @@ -627,7 +622,9 @@ bool ImageViewer::mouse_motion_event( } else if (mIsCroppingImage) { auto relMousePos = mouse_pos() - mImageCanvas->position(); auto imageCoords = mImageCanvas->getImageCoords(*mCurrentImage, {relMousePos.x(), relMousePos.y()}); - mImageCanvas->setCropMax(imageCoords); + + // we do not need to worry about min/max ordering here, as setCrop sanitizes the input for us + mImageCanvas->setCrop(Box2i{mCroppingStartCoordinates, imageCoords}); } return false; diff --git a/src/UberShader.cpp b/src/UberShader.cpp index f198aa6..24bcef8 100644 --- a/src/UberShader.cpp +++ b/src/UberShader.cpp @@ -441,7 +441,7 @@ void UberShader::draw(const Vector2f& pixelSize, const Vector2f& checkerSize) { nullptr, Matrix3f{0.0f}, 0.0f, 0.0f, 0.0f, false, ETonemap::SRGB, - false, Vector2f{0.f}, Vector2f{0.f} + std::nullopt ); } @@ -455,9 +455,7 @@ void UberShader::draw( float gamma, bool clipToLdr, ETonemap tonemap, - bool isCropped, - const Vector2f& cropMin, - const Vector2f& cropMax + const std::optional& crop ) { draw( pixelSize, checkerSize, @@ -465,7 +463,7 @@ void UberShader::draw( nullptr, Matrix3f{0.0f}, exposure, offset, gamma, clipToLdr, tonemap, EMetric::Error, - isCropped, cropMin, cropMax + crop ); } @@ -482,9 +480,7 @@ void UberShader::draw( bool clipToLdr, ETonemap tonemap, EMetric metric, - bool isCropped, - const nanogui::Vector2f& cropMin, - const nanogui::Vector2f& cropMax + const std::optional& crop ) { bool hasImage = textureImage; if (!hasImage) { @@ -504,9 +500,14 @@ void UberShader::draw( mShader->set_uniform("hasImage", hasImage); mShader->set_uniform("hasReference", hasReference); mShader->set_uniform("clipToLdr", clipToLdr); - mShader->set_uniform("isCropped", isCropped); - mShader->set_uniform("cropMin", cropMin); - mShader->set_uniform("cropMax", cropMax); + mShader->set_uniform("isCropped", crop.has_value()); + if (crop.has_value()) { + mShader->set_uniform("cropMin", Vector2f{crop->min} / Vector2f{textureImage->size()}); + mShader->set_uniform("cropMax", Vector2f{crop->max} / Vector2f{textureImage->size()}); + } else { + mShader->set_uniform("cropMin", Vector2f{0}); + mShader->set_uniform("cropMax", Vector2f{1}); + } mShader->begin(); mShader->draw_array(Shader::PrimitiveType::Triangle, 0, 6, true); From dccab90c1b11a22a75e8e5bcb4308b7500547dd1 Mon Sep 17 00:00:00 2001 From: Alexander Rath Date: Tue, 2 Jan 2024 17:31:51 +0100 Subject: [PATCH 04/21] improve mean accuracy --- src/ImageCanvas.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index 4d7977f..400826a 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -837,7 +837,7 @@ Task> ImageCanvas::computeCanvasStatistics( ) { auto flattened = channelsFromImages(image, reference, requestedChannelGroup, metric, priority); - float mean = 0; + double mean = 0; float maximum = -numeric_limits::infinity(); float minimum = numeric_limits::infinity(); @@ -876,7 +876,7 @@ Task> ImageCanvas::computeCanvasStatistics( } } - result->mean = pixelCount > 0 ? (mean / pixelCount) : 0; + result->mean = pixelCount > 0 ? float(mean / pixelCount) : 0; result->maximum = maximum; result->minimum = minimum; From 1dada73c9565437a1c5bdcda599d11842e5fab6e Mon Sep 17 00:00:00 2001 From: Alexander Rath Date: Tue, 2 Jan 2024 17:40:45 +0100 Subject: [PATCH 05/21] fix broken build on Linux --- include/tev/UberShader.h | 1 + src/UberShader.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/include/tev/UberShader.h b/include/tev/UberShader.h index 2e3105f..cbe7512 100644 --- a/include/tev/UberShader.h +++ b/include/tev/UberShader.h @@ -8,6 +8,7 @@ #include #include +#include namespace tev { diff --git a/src/UberShader.cpp b/src/UberShader.cpp index 24bcef8..b86aa25 100644 --- a/src/UberShader.cpp +++ b/src/UberShader.cpp @@ -168,13 +168,13 @@ UberShader::UberShader(RenderPass* renderPass) { return; } - float cropAlpha = 1.f; + float cropAlpha = 1.0; if (isCropped) { if (imageUv.x < cropMin.x || imageUv.x > cropMax.x || imageUv.y < cropMin.y || imageUv.y > cropMax.y) - cropAlpha = 0.3f; + cropAlpha = 0.3; } vec4 imageVal = sample(image, imageUv); From 9bb331a1c1757d84efcdab64ab19f870df192df7 Mon Sep 17 00:00:00 2001 From: Alexander Rath Date: Tue, 2 Jan 2024 18:56:28 +0100 Subject: [PATCH 06/21] fix unraveling of index --- src/ImageCanvas.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index 400826a..92d04d2 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -922,7 +922,7 @@ Task> ImageCanvas::computeCanvasStatistics( tasks.emplace_back( ThreadPool::global().parallelForAsync(0, numPixels, [&, i](size_t j) { int x = (j % regionSize.x()) + region.min.x(); - int y = (j / regionSize.y()) + region.min.y(); + int y = (j / regionSize.x()) + region.min.y(); indices[j + i * numPixels] = valToBin(channel.at(Vector2i{x, y})); }, priority) ); From 873cfacfefa940284e2bef06e40f231e1caaf6c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Wed, 3 Jan 2024 08:19:27 +0100 Subject: [PATCH 07/21] Keep crop active, even if image resolution changes This can come in handy when different images have different data windows but the same display windows. Or, alternatively, if the user has many images open that they want to compare with a few non-resolution-matching images inbetween. --- include/tev/ImageCanvas.h | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/include/tev/ImageCanvas.h b/include/tev/ImageCanvas.h index 0b76a33..fa46762 100644 --- a/include/tev/ImageCanvas.h +++ b/include/tev/ImageCanvas.h @@ -55,11 +55,6 @@ class ImageCanvas : public nanogui::Canvas { float applyExposureAndOffset(float value) const; void setImage(std::shared_ptr image) { - if (!mImage || !image || mImage->size() != image->size()) { - // only keep crop active if image resolution has stayed the same - setCrop(std::nullopt); - } - mImage = image; } @@ -106,19 +101,19 @@ class ImageCanvas : public nanogui::Canvas { mCrop = std::nullopt; return; } - + // sanitize the input crop Box2i clean; for (size_t dim = 0; dim < clean.min.Size; dim++) { // order input crop, add one to maximum to make box inclusive clean.min[dim] = std::min(crop->min[dim], crop->max[dim]); clean.max[dim] = std::max(crop->min[dim], crop->max[dim]) + 1; - + // clamp to image extents clean.min[dim] = std::max(0, std::min(mImage->size()[dim], clean.min[dim])); clean.max[dim] = std::max(0, std::min(mImage->size()[dim], clean.max[dim])); } - + mCrop = clean; } From 60b0d2c0e99ae59bc844b61bb17f20e0002779a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Wed, 3 Jan 2024 08:48:09 +0100 Subject: [PATCH 08/21] Fix crash when setting a crop region without having an image loaded Also cleans up the crop box sanitization code by separating it into inflation, +1, and intersection. --- include/tev/Box.h | 14 ++++++++++++-- include/tev/ImageCanvas.h | 19 +------------------ src/ImageCanvas.cpp | 7 ++----- src/ImageViewer.cpp | 10 +++++++--- 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/include/tev/Box.h b/include/tev/Box.h index be01080..786fa78 100644 --- a/include/tev/Box.h +++ b/include/tev/Box.h @@ -5,7 +5,6 @@ #include - namespace tev { template @@ -20,8 +19,15 @@ struct Box { template Box(const Box& other) : min{other.min}, max{other.max} {} + Box(const std::vector& points) : Box() { + for (const auto& point : points) { + min = nanogui::min(min, point); + max = nanogui::max(max, point); + } + } + Vector size() const { - return max - min; + return nanogui::max(max - min, Vector{(T)0}); } Vector middle() const { @@ -44,6 +50,10 @@ struct Box { return result; } + Box intersect(const Box& other) const { + return {nanogui::max(min, other.min), nanogui::min(max, other.max)}; + } + bool operator==(const Box& other) const { return min == other.min && max == other.max; } diff --git a/include/tev/ImageCanvas.h b/include/tev/ImageCanvas.h index fa46762..e59277d 100644 --- a/include/tev/ImageCanvas.h +++ b/include/tev/ImageCanvas.h @@ -97,24 +97,7 @@ class ImageCanvas : public nanogui::Canvas { } void setCrop(const std::optional& crop) { - if (!crop.has_value()) { - mCrop = std::nullopt; - return; - } - - // sanitize the input crop - Box2i clean; - for (size_t dim = 0; dim < clean.min.Size; dim++) { - // order input crop, add one to maximum to make box inclusive - clean.min[dim] = std::min(crop->min[dim], crop->max[dim]); - clean.max[dim] = std::max(crop->min[dim], crop->max[dim]) + 1; - - // clamp to image extents - clean.min[dim] = std::max(0, std::min(mImage->size()[dim], clean.min[dim])); - clean.max[dim] = std::max(0, std::min(mImage->size()[dim], clean.max[dim])); - } - - mCrop = clean; + mCrop = crop; } std::optional getCrop() { diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index 92d04d2..d7d89a4 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -739,12 +739,9 @@ shared_ptr>> ImageCanvas::canvasStatistics() { mReference->setStaleIdCallback([this](int id) { purgeCanvasStatistics(id); }); } - Box2i region; + Box2i region = {image->size()}; if (mCrop.has_value()) { - region = mCrop.value(); - } else { - region.min = Vector2i{0}; - region.max = image->size(); + region = region.intersect(mCrop.value()); } invokeTaskDetached([ diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 7c22f27..18ceed7 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -524,7 +524,7 @@ bool ImageViewer::mouse_button_event(const nanogui::Vector2i &p, int button, boo // alt/option + left drag to crop auto rel = mouse_pos() - mImageCanvas->position(); auto imageCoords = mImageCanvas->getImageCoords(*mCurrentImage, {rel.x(), rel.y()}); - + mIsCroppingImage = true; mCroppingStartCoordinates = imageCoords; mImageCanvas->setCrop(std::nullopt); // single click disables crop @@ -622,9 +622,13 @@ bool ImageViewer::mouse_motion_event( } else if (mIsCroppingImage) { auto relMousePos = mouse_pos() - mImageCanvas->position(); auto imageCoords = mImageCanvas->getImageCoords(*mCurrentImage, {relMousePos.x(), relMousePos.y()}); - + + // sanitize the input crop + Box2i crop = {{mCroppingStartCoordinates, imageCoords}}; + crop.max += Vector2i{1}; + // we do not need to worry about min/max ordering here, as setCrop sanitizes the input for us - mImageCanvas->setCrop(Box2i{mCroppingStartCoordinates, imageCoords}); + mImageCanvas->setCrop(crop); } return false; From 6c2d7db970cd6e0628758b3018a6ea86a244d564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Wed, 3 Jan 2024 09:19:56 +0100 Subject: [PATCH 09/21] Define crop window relative to the image's display window Previously, it was defined relative to the data window, which is inaccurate when actually making use of OpenEXR's display<->data distinction. --- include/tev/Box.h | 4 ++++ include/tev/ImageCanvas.h | 1 + include/tev/ImageViewer.h | 2 +- src/ImageCanvas.cpp | 21 ++++++++++++++++++--- src/ImageViewer.cpp | 39 +++++++++++++++++---------------------- src/UberShader.cpp | 2 +- 6 files changed, 42 insertions(+), 27 deletions(-) diff --git a/include/tev/Box.h b/include/tev/Box.h index 786fa78..dd77759 100644 --- a/include/tev/Box.h +++ b/include/tev/Box.h @@ -54,6 +54,10 @@ struct Box { return {nanogui::max(min, other.min), nanogui::min(max, other.max)}; } + Box translate(const Vector& offset) const { + return {min + offset, max + offset}; + } + bool operator==(const Box& other) const { return min == other.min && max == other.max; } diff --git a/include/tev/ImageCanvas.h b/include/tev/ImageCanvas.h index e59277d..fff7324 100644 --- a/include/tev/ImageCanvas.h +++ b/include/tev/ImageCanvas.h @@ -67,6 +67,7 @@ class ImageCanvas : public nanogui::Canvas { } nanogui::Vector2i getImageCoords(const Image& image, nanogui::Vector2i mousePos); + nanogui::Vector2i getDisplayWindowCoords(const Image& image, nanogui::Vector2i mousePos); void getValuesAtNanoPos(nanogui::Vector2i nanoPos, std::vector& result, const std::vector& channels); std::vector getValuesAtNanoPos(nanogui::Vector2i nanoPos, const std::vector& channels) { diff --git a/include/tev/ImageViewer.h b/include/tev/ImageViewer.h index 91b683a..6387713 100644 --- a/include/tev/ImageViewer.h +++ b/include/tev/ImageViewer.h @@ -259,7 +259,7 @@ class ImageViewer : public nanogui::Screen { size_t mDraggedImageButtonId; nanogui::Vector2i mCroppingStartCoordinates; - nanogui::Vector2f mDraggingStartPosition; + nanogui::Vector2i mDraggingStartPosition; size_t mClipboardIndex = 0; diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index d7d89a4..aae6ab2 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -60,6 +60,11 @@ void ImageCanvas::draw_contents() { return; } + optional imageSpaceCrop = nullopt; + if (mCrop.has_value()) { + imageSpaceCrop = mCrop.value().translate(image->displayWindow().min - image->dataWindow().min); + } + if (!mReference || ctrlHeld || image == mReference.get()) { mShader->draw( 2.0f * inverse(Vector2f{m_size}) / mPixelRatio, @@ -73,7 +78,7 @@ void ImageCanvas::draw_contents() { mGamma, mClipToLdr, mTonemap, - mCrop + imageSpaceCrop ); return; } @@ -95,7 +100,7 @@ void ImageCanvas::draw_contents() { mClipToLdr, mTonemap, mMetric, - mCrop + imageSpaceCrop ); } @@ -477,6 +482,11 @@ Vector2i ImageCanvas::getImageCoords(const Image& image, Vector2i nanoPos) { }; } +Vector2i ImageCanvas::getDisplayWindowCoords(const Image& image, Vector2i nanoPos) { + Vector2f imageCoords = getImageCoords(image, nanoPos); + return imageCoords + Vector2f(image.dataWindow().min - image.displayWindow().min); +} + void ImageCanvas::getValuesAtNanoPos(Vector2i nanoPos, vector& result, const vector& channels) { result.clear(); if (!mImage) { @@ -739,11 +749,16 @@ shared_ptr>> ImageCanvas::canvasStatistics() { mReference->setStaleIdCallback([this](int id) { purgeCanvasStatistics(id); }); } - Box2i region = {image->size()}; + // The user specifies a crop region in display window coordinates. + // First, intersect this crop window with the image's extent, then translate + // the crop window to the image's data window for canvas statistics computation. + Box2i region = image->dataWindow(); if (mCrop.has_value()) { region = region.intersect(mCrop.value()); } + region = region.translate(image->displayWindow().min - image->dataWindow().min); + invokeTaskDetached([ image, reference, requestedChannelGroup, metric, region, priority, p=std::move(promise) diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 18ceed7..a4fc40f 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -479,7 +479,7 @@ ImageViewer::ImageViewer( updateLayout(); } -bool ImageViewer::mouse_button_event(const nanogui::Vector2i &p, int button, bool down, int modifiers) { +bool ImageViewer::mouse_button_event(const nanogui::Vector2i& p, int button, bool down, int modifiers) { redraw(); // Check if the user performed mousedown on an imagebutton so we can mark it as being dragged. @@ -495,7 +495,7 @@ bool ImageViewer::mouse_button_event(const nanogui::Vector2i &p, int button, boo if (imgButton->contains(relMousePos) && !imgButton->textBoxVisible()) { mDraggedImageButtonId = i; mIsDraggingImageButton = true; - mDraggingStartPosition = nanogui::Vector2f(relMousePos - imgButton->position()); + mDraggingStartPosition = relMousePos - imgButton->position(); break; } } @@ -516,24 +516,16 @@ bool ImageViewer::mouse_button_event(const nanogui::Vector2i &p, int button, boo if (down && !mIsDraggingImageButton) { if (canDragSidebarFrom(p)) { mIsDraggingSidebar = true; - mDraggingStartPosition = nanogui::Vector2f(p); + mDraggingStartPosition = p; return true; } else if (mImageCanvas->contains(p)) { - if ((modifiers & 4) != 0) { - if (button == 0) { - // alt/option + left drag to crop - auto rel = mouse_pos() - mImageCanvas->position(); - auto imageCoords = mImageCanvas->getImageCoords(*mCurrentImage, {rel.x(), rel.y()}); - - mIsCroppingImage = true; - mCroppingStartCoordinates = imageCoords; - mImageCanvas->setCrop(std::nullopt); // single click disables crop - } - } else { - mIsDraggingImage = true; - mDraggingStartPosition = nanogui::Vector2f(p); - mIsCroppingImage = false; + mIsCroppingImage = modifiers & 4; + if (mIsCroppingImage) { + mImageCanvas->setCrop(std::nullopt); // single click disables crop } + + mIsDraggingImage = !mIsCroppingImage; + mDraggingStartPosition = p; return true; } } else { @@ -594,7 +586,7 @@ bool ImageViewer::mouse_motion_event( // If middle mouse button is held, zoom in-out with up-down mouse movement if ((button & 4) != 0) { - mImageCanvas->scale(relativeMovement.y() / 10.0f, {mDraggingStartPosition.x(), mDraggingStartPosition.y()}); + mImageCanvas->scale(relativeMovement.y() / 10.0f, Vector2f{mDraggingStartPosition}); } } else if (mIsDraggingImageButton) { auto& buttons = mImageButtonContainer->children(); @@ -617,14 +609,17 @@ bool ImageViewer::mouse_motion_event( } dynamic_cast(buttons[mDraggedImageButtonId])->set_position( - relMousePos - nanogui::Vector2i(mDraggingStartPosition) + relMousePos - mDraggingStartPosition ); } else if (mIsCroppingImage) { - auto relMousePos = mouse_pos() - mImageCanvas->position(); - auto imageCoords = mImageCanvas->getImageCoords(*mCurrentImage, {relMousePos.x(), relMousePos.y()}); + Vector2i relStartMousePos = (absolute_position() + mDraggingStartPosition) - mImageCanvas->absolute_position(); + Vector2i relMousePos = (absolute_position() + p) - mImageCanvas->absolute_position(); + + auto startImageCoords = mImageCanvas->getDisplayWindowCoords(*mCurrentImage, relStartMousePos); + auto imageCoords = mImageCanvas->getDisplayWindowCoords(*mCurrentImage, relMousePos); // sanitize the input crop - Box2i crop = {{mCroppingStartCoordinates, imageCoords}}; + Box2i crop = {{startImageCoords, imageCoords}}; crop.max += Vector2i{1}; // we do not need to worry about min/max ordering here, as setCrop sanitizes the input for us diff --git a/src/UberShader.cpp b/src/UberShader.cpp index b86aa25..c7f3e37 100644 --- a/src/UberShader.cpp +++ b/src/UberShader.cpp @@ -353,7 +353,7 @@ UberShader::UberShader(RenderPass* renderPass) { return float4(checker, 1.0f); } - float cropAlpha = 1.f; + float cropAlpha = 1.0f; if (isCropped) { if (vert.imageUv.x < cropMin.x || vert.imageUv.x > cropMax.x From 0b5bb28a10963f593da581ed8d9a9f9df4ecee47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Wed, 3 Jan 2024 09:33:52 +0100 Subject: [PATCH 10/21] Draw a box around the stats crop window --- include/tev/Common.h | 1 + src/ImageCanvas.cpp | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/include/tev/Common.h b/include/tev/Common.h index 2641470..b6e23d0 100644 --- a/include/tev/Common.h +++ b/include/tev/Common.h @@ -396,5 +396,6 @@ void redrawWindow(); static const nanogui::Color IMAGE_COLOR = {0.35f, 0.35f, 0.8f, 1.0f}; static const nanogui::Color REFERENCE_COLOR = {0.7f, 0.4f, 0.4f, 1.0f}; +static const nanogui::Color CROP_COLOR = {0.2f, 0.5f, 0.2f, 1.0f}; } diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index aae6ab2..2bb111c 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -215,8 +215,8 @@ void ImageCanvas::drawCoordinateSystem(NVGcontext* ctx) { nvgFontSize(ctx, fontSize); nvgTextAlign(ctx, (right ? NVG_ALIGN_RIGHT : NVG_ALIGN_LEFT) | (top ? NVG_ALIGN_BOTTOM : NVG_ALIGN_TOP)); float textWidth = nvgTextBounds(ctx, 0, 0, name.c_str(), nullptr, nullptr); - float textAlpha = max(min(1.0f, (((topRight.x() - topLeft.x()) / textWidth) - 2.0f)), 0.0f); - float regionAlpha = max(min(1.0f, (((topRight.x() - topLeft.x()) / textWidth) - 1.5f) * 2), 0.0f); + float textAlpha = max(min(1.0f, (((topRight.x() - topLeft.x() - textWidth - 5) / 30))), 0.0f); + float regionAlpha = max(min(1.0f, (((topRight.x() - topLeft.x() - textWidth - 5) / 30))), 0.0f); Color textColor = Color(190, 255); textColor.a() = textAlpha; @@ -278,6 +278,9 @@ void ImageCanvas::drawCoordinateSystem(NVGcontext* ctx) { drawWindow(mImage->displayWindow(), Color(0.3f, 1.0f), mImage->displayWindow().min.y() <= mImage->dataWindow().min.y(), false, "", flags); } + if (mCrop.has_value()) { + drawWindow(mCrop.value(), CROP_COLOR, false, false, "Stats crop", flags); + } }; // Draw all labels after the regions to ensure no occlusion @@ -439,6 +442,7 @@ void ImageCanvas::draw(NVGcontext* ctx) { // If the coordinate system is in any sort of way non-trivial, or if a hotkey is held, draw it! if ( glfwGetKey(screen()->glfw_window(), GLFW_KEY_B) || + mCrop.has_value() || mImage->dataWindow() != mImage->displayWindow() || mImage->displayWindow().min != Vector2i{0} || (mReference && (mReference->dataWindow() != mImage->dataWindow() || mReference->displayWindow() != mImage->displayWindow())) From 214bad22f3587cdbadd86a576621406674b4e85d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Wed, 3 Jan 2024 09:37:58 +0100 Subject: [PATCH 11/21] Don't begin a crop region unless the user has dragged a bit in screen space --- src/ImageViewer.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index a4fc40f..332a8cc 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -615,6 +615,14 @@ bool ImageViewer::mouse_motion_event( Vector2i relStartMousePos = (absolute_position() + mDraggingStartPosition) - mImageCanvas->absolute_position(); Vector2i relMousePos = (absolute_position() + p) - mImageCanvas->absolute_position(); + // Require a minimum movement of 3 (nanogui space) pixels to start cropping. + // Since this is measured in nanogui / screen space and not image space, this + // does not prevent the cropping of smaller image regions. Just zoom in before + // cropping smaller regions than 3 image pixels. + if (norm(relStartMousePos - relMousePos) < 3) { + return false; + } + auto startImageCoords = mImageCanvas->getDisplayWindowCoords(*mCurrentImage, relStartMousePos); auto imageCoords = mImageCanvas->getDisplayWindowCoords(*mCurrentImage, relMousePos); From a5290dbb1eec4d8c86a5310c2f1b3d22d28d3ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Wed, 3 Jan 2024 10:42:05 +0100 Subject: [PATCH 12/21] Fold drag modes into an enum --- include/tev/ImageCanvas.h | 4 +- include/tev/ImageViewer.h | 15 ++-- src/ImageViewer.cpp | 173 ++++++++++++++++++++------------------ 3 files changed, 104 insertions(+), 88 deletions(-) diff --git a/include/tev/ImageCanvas.h b/include/tev/ImageCanvas.h index fff7324..7171b1e 100644 --- a/include/tev/ImageCanvas.h +++ b/include/tev/ImageCanvas.h @@ -3,10 +3,10 @@ #pragma once -#include +#include #include #include -#include +#include #include diff --git a/include/tev/ImageViewer.h b/include/tev/ImageViewer.h index 6387713..b19c462 100644 --- a/include/tev/ImageViewer.h +++ b/include/tev/ImageViewer.h @@ -252,14 +252,17 @@ class ImageViewer : public nanogui::Screen { HelpWindow* mHelpWindow = nullptr; - bool mIsDraggingSidebar = false; - bool mIsDraggingImage = false; - bool mIsDraggingImageButton = false; - bool mIsCroppingImage = false; - size_t mDraggedImageButtonId; + enum class EMouseDragType { + None, + ImageDrag, + ImageCrop, + ImageButtonDrag, + SidebarDrag, + }; - nanogui::Vector2i mCroppingStartCoordinates; nanogui::Vector2i mDraggingStartPosition; + EMouseDragType mDragType = EMouseDragType::None; + size_t mDraggedImageButtonId; size_t mClipboardIndex = 0; diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 332a8cc..9ec62cd 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -493,9 +493,9 @@ bool ImageViewer::mouse_button_event(const nanogui::Vector2i& p, int button, boo for (size_t i = 0; i < buttons.size(); ++i) { const auto* imgButton = dynamic_cast(buttons[i]); if (imgButton->contains(relMousePos) && !imgButton->textBoxVisible()) { - mDraggedImageButtonId = i; - mIsDraggingImageButton = true; mDraggingStartPosition = relMousePos - imgButton->position(); + mDragType = EMouseDragType::ImageButtonDrag; + mDraggedImageButtonId = i; break; } } @@ -513,30 +513,26 @@ bool ImageViewer::mouse_button_event(const nanogui::Vector2i& p, int button, boo } } - if (down && !mIsDraggingImageButton) { + bool isDraggingImageButton = mDragType == EMouseDragType::ImageButtonDrag; + if (down && !isDraggingImageButton) { + mDraggingStartPosition = p; if (canDragSidebarFrom(p)) { - mIsDraggingSidebar = true; - mDraggingStartPosition = p; + mDragType = EMouseDragType::SidebarDrag; return true; } else if (mImageCanvas->contains(p)) { - mIsCroppingImage = modifiers & 4; - if (mIsCroppingImage) { - mImageCanvas->setCrop(std::nullopt); // single click disables crop + mDragType = (modifiers & 4) ? EMouseDragType::ImageCrop : EMouseDragType::ImageDrag; + if (mDragType == EMouseDragType::ImageCrop) { + mImageCanvas->setCrop(std::nullopt); // alt + single click disables crop } - mIsDraggingImage = !mIsCroppingImage; - mDraggingStartPosition = p; return true; } } else { - if (mIsDraggingImageButton) { + if (isDraggingImageButton) { requestLayoutUpdate(); } - mIsDraggingSidebar = false; - mIsDraggingImage = false; - mIsDraggingImageButton = false; - mIsCroppingImage = false; + mDragType = EMouseDragType::None; } return false; @@ -557,7 +553,7 @@ bool ImageViewer::mouse_motion_event( redraw(); } - if (mIsDraggingSidebar || canDragSidebarFrom(p)) { + if (mDragType == EMouseDragType::SidebarDrag || canDragSidebarFrom(p)) { mSidebarLayout->set_cursor(Cursor::HResize); mImageCanvas->set_cursor(Cursor::HResize); } else { @@ -565,73 +561,90 @@ bool ImageViewer::mouse_motion_event( mImageCanvas->set_cursor(Cursor::Arrow); } - if (mIsDraggingSidebar) { - mSidebar->set_fixed_width(clamp(p.x(), SIDEBAR_MIN_WIDTH, m_size.x() - 10)); - requestLayoutUpdate(); - } else if (mIsDraggingImage) { - nanogui::Vector2f relativeMovement = {rel}; - auto* glfwWindow = screen()->glfw_window(); - // There is no explicit access to the currently pressed modifier keys here, so we - // need to directly ask GLFW. - if (glfwGetKey(glfwWindow, GLFW_KEY_LEFT_SHIFT) || glfwGetKey(glfwWindow, GLFW_KEY_RIGHT_SHIFT)) { - relativeMovement /= 10; - } else if (glfwGetKey(glfwWindow, SYSTEM_COMMAND_LEFT) || glfwGetKey(glfwWindow, SYSTEM_COMMAND_RIGHT)) { - relativeMovement /= std::log2(1.1f); - } + switch (mDragType) { + case EMouseDragType::SidebarDrag: + mSidebar->set_fixed_width(clamp(p.x(), SIDEBAR_MIN_WIDTH, m_size.x() - 10)); + requestLayoutUpdate(); + break; - // If left mouse button is held, move the image with mouse movement - if ((button & 1) != 0) { - mImageCanvas->translate(relativeMovement); - } + case EMouseDragType::ImageDrag: { + nanogui::Vector2f relativeMovement = {rel}; + auto* glfwWindow = screen()->glfw_window(); + // There is no explicit access to the currently pressed modifier keys here, so we + // need to directly ask GLFW. + if (glfwGetKey(glfwWindow, GLFW_KEY_LEFT_SHIFT) || glfwGetKey(glfwWindow, GLFW_KEY_RIGHT_SHIFT)) { + relativeMovement /= 10; + } else if (glfwGetKey(glfwWindow, SYSTEM_COMMAND_LEFT) || glfwGetKey(glfwWindow, SYSTEM_COMMAND_RIGHT)) { + relativeMovement /= std::log2(1.1f); + } - // If middle mouse button is held, zoom in-out with up-down mouse movement - if ((button & 4) != 0) { - mImageCanvas->scale(relativeMovement.y() / 10.0f, Vector2f{mDraggingStartPosition}); - } - } else if (mIsDraggingImageButton) { - auto& buttons = mImageButtonContainer->children(); - nanogui::Vector2i relMousePos = (absolute_position() + p) - mImageButtonContainer->absolute_position(); - for (size_t i = 0; i < buttons.size(); ++i) { - if (i == mDraggedImageButtonId) { - continue; + // If left mouse button is held, move the image with mouse movement + if ((button & 1) != 0) { + mImageCanvas->translate(relativeMovement); } - auto* imgButton = dynamic_cast(buttons[i]); - if (imgButton->contains(relMousePos)) { - nanogui::Vector2i pos = imgButton->position(); - pos.y() += ((int)mDraggedImageButtonId - (int)i) * imgButton->size().y(); - imgButton->set_position(pos); - imgButton->mouse_enter_event(relMousePos, false); - - moveImageInList(mDraggedImageButtonId, i); - mDraggedImageButtonId = i; - break; + + // If middle mouse button is held, zoom in-out with up-down mouse movement + if ((button & 4) != 0) { + mImageCanvas->scale(relativeMovement.y() / 10.0f, Vector2f{mDraggingStartPosition}); } + + break; } - dynamic_cast(buttons[mDraggedImageButtonId])->set_position( - relMousePos - mDraggingStartPosition - ); - } else if (mIsCroppingImage) { - Vector2i relStartMousePos = (absolute_position() + mDraggingStartPosition) - mImageCanvas->absolute_position(); - Vector2i relMousePos = (absolute_position() + p) - mImageCanvas->absolute_position(); - - // Require a minimum movement of 3 (nanogui space) pixels to start cropping. - // Since this is measured in nanogui / screen space and not image space, this - // does not prevent the cropping of smaller image regions. Just zoom in before - // cropping smaller regions than 3 image pixels. - if (norm(relStartMousePos - relMousePos) < 3) { - return false; + case EMouseDragType::ImageCrop: { + Vector2i relStartMousePos = (absolute_position() + mDraggingStartPosition) - mImageCanvas->absolute_position(); + Vector2i relMousePos = (absolute_position() + p) - mImageCanvas->absolute_position(); + + // Require a minimum movement of 3 (nanogui space) pixels to start cropping. + // Since this is measured in nanogui / screen space and not image space, this + // does not prevent the cropping of smaller image regions. Just zoom in before + // cropping smaller regions than 3 image pixels. + if (norm(relStartMousePos - relMousePos) < 3) { + return false; + } + + auto startImageCoords = mImageCanvas->getDisplayWindowCoords(*mCurrentImage, relStartMousePos); + auto imageCoords = mImageCanvas->getDisplayWindowCoords(*mCurrentImage, relMousePos); + + // sanitize the input crop + Box2i crop = {{startImageCoords, imageCoords}}; + crop.max += Vector2i{1}; + + // we do not need to worry about min/max ordering here, as setCrop sanitizes the input for us + mImageCanvas->setCrop(crop); + + break; } - auto startImageCoords = mImageCanvas->getDisplayWindowCoords(*mCurrentImage, relStartMousePos); - auto imageCoords = mImageCanvas->getDisplayWindowCoords(*mCurrentImage, relMousePos); + case EMouseDragType::ImageButtonDrag: { + auto& buttons = mImageButtonContainer->children(); + nanogui::Vector2i relMousePos = (absolute_position() + p) - mImageButtonContainer->absolute_position(); + for (size_t i = 0; i < buttons.size(); ++i) { + if (i == mDraggedImageButtonId) { + continue; + } + auto* imgButton = dynamic_cast(buttons[i]); + if (imgButton->contains(relMousePos)) { + nanogui::Vector2i pos = imgButton->position(); + pos.y() += ((int)mDraggedImageButtonId - (int)i) * imgButton->size().y(); + imgButton->set_position(pos); + imgButton->mouse_enter_event(relMousePos, false); + + moveImageInList(mDraggedImageButtonId, i); + mDraggedImageButtonId = i; + break; + } + } + + dynamic_cast(buttons[mDraggedImageButtonId])->set_position( + relMousePos - mDraggingStartPosition + ); - // sanitize the input crop - Box2i crop = {{startImageCoords, imageCoords}}; - crop.max += Vector2i{1}; + break; + } - // we do not need to worry about min/max ordering here, as setCrop sanitizes the input for us - mImageCanvas->setCrop(crop); + case EMouseDragType::None: + break; } return false; @@ -1085,14 +1098,14 @@ void ImageViewer::draw_contents() { if (mRequiresLayoutUpdate) { nanogui::Vector2i oldDraggedImageButtonPos{0, 0}; auto& buttons = mImageButtonContainer->children(); - if (mIsDraggingImageButton) { + if (mDragType == EMouseDragType::ImageButtonDrag) { oldDraggedImageButtonPos = dynamic_cast(buttons[mDraggedImageButtonId])->position(); } updateLayout(); mRequiresLayoutUpdate = false; - if (mIsDraggingImageButton) { + if (mDragType == EMouseDragType::ImageButtonDrag) { dynamic_cast(buttons[mDraggedImageButtonId])->set_position(oldDraggedImageButtonPos); } } @@ -1140,7 +1153,7 @@ void ImageViewer::insertImage(shared_ptr image, size_t index, bool shallS throw invalid_argument{"Image may not be null."}; } - if (mIsDraggingImageButton && index <= mDraggedImageButtonId) { + if (mDragType == EMouseDragType::ImageButtonDrag && index <= mDraggedImageButtonId) { ++mDraggedImageButtonId; } @@ -1221,11 +1234,11 @@ void ImageViewer::removeImage(shared_ptr image) { return; } - if (mIsDraggingImageButton) { + if (mDragType == EMouseDragType::ImageButtonDrag) { // If we're currently dragging the to-be-removed image, stop. if ((size_t)id == mDraggedImageButtonId) { requestLayoutUpdate(); - mIsDraggingImageButton = false; + mDragType = EMouseDragType::None; } else if ((size_t)id < mDraggedImageButtonId) { --mDraggedImageButtonId; } @@ -1748,8 +1761,8 @@ void ImageViewer::toggleMaximized() { } void ImageViewer::setUiVisible(bool shouldBeVisible) { - if (!shouldBeVisible) { - mIsDraggingSidebar = false; + if (!shouldBeVisible && mDragType == EMouseDragType::SidebarDrag) { + mDragType = EMouseDragType::None; } mSidebar->set_visible(shouldBeVisible); From 28042b3502f64f977e3663cdace2edf65fff7ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Wed, 3 Jan 2024 10:49:21 +0100 Subject: [PATCH 13/21] ImageCanvas: assert crop region is contained in image --- include/tev/Box.h | 12 ++++++++++++ include/tev/UberShader.h | 3 ++- src/ImageCanvas.cpp | 6 ++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/include/tev/Box.h b/include/tev/Box.h index dd77759..542daad 100644 --- a/include/tev/Box.h +++ b/include/tev/Box.h @@ -50,6 +50,18 @@ struct Box { return result; } + bool contains_inclusive(const Vector& pos) const { + bool result = true; + for (uint32_t i = 0; i < N_DIMS; ++i) { + result &= pos[i] >= min[i] && pos[i] <= max[i]; + } + return result; + } + + bool contains(const Box& other) const { + return contains_inclusive(other.min) && contains_inclusive(other.max); + } + Box intersect(const Box& other) const { return {nanogui::max(min, other.min), nanogui::min(max, other.max)}; } diff --git a/include/tev/UberShader.h b/include/tev/UberShader.h index cbe7512..d7ce396 100644 --- a/include/tev/UberShader.h +++ b/include/tev/UberShader.h @@ -3,11 +3,12 @@ #pragma once +#include + #include #include #include -#include #include namespace tev { diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index 2bb111c..c676b5e 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -851,6 +851,8 @@ Task> ImageCanvas::computeCanvasStatistics( const Box2i& region, int priority ) { + TEV_ASSERT(Box2i{image->size()}.contains(region), "Region must be contained in image."); + auto flattened = channelsFromImages(image, reference, requestedChannelGroup, metric, priority); double mean = 0; @@ -892,7 +894,7 @@ Task> ImageCanvas::computeCanvasStatistics( } } - result->mean = pixelCount > 0 ? float(mean / pixelCount) : 0; + result->mean = pixelCount > 0 ? (float)(mean / pixelCount) : 0; result->maximum = maximum; result->minimum = minimum; @@ -929,7 +931,7 @@ Task> ImageCanvas::computeCanvasStatistics( } auto regionSize = region.size(); - auto numPixels = size_t(regionSize.x()) * size_t(regionSize.y()); + auto numPixels = (size_t)regionSize.x() * regionSize.y(); std::vector indices(numPixels * nChannels); vector> tasks; From 96e66fc7e0beefe470325a6809fd103de65b6e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Wed, 3 Jan 2024 11:05:06 +0100 Subject: [PATCH 14/21] Mention stats crop in help window --- src/HelpWindow.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/HelpWindow.cpp b/src/HelpWindow.cpp index 9ec31c5..62932d2 100644 --- a/src/HelpWindow.cpp +++ b/src/HelpWindow.cpp @@ -82,7 +82,8 @@ HelpWindow::HelpWindow(Widget* parent, bool supportsHdr, function closeC addRow(imageSelection, "Home / End", "Select first / last image"); addRow(imageSelection, "Space", "Toggle playback of images as video"); - addRow(imageSelection, "Click & Drag (+Shift/" + COMMAND + ")", "Translate image"); + addRow(imageSelection, "Click & Drag (+Shift/" + COMMAND + ")", "Translate image"); + addRow(imageSelection, "Click & Drag+" + ALT, "Select region of histogram"); addRow(imageSelection, "+ / - / Scroll (+Shift/" + COMMAND + ")", "Zoom in / out of image"); addRow(imageSelection, COMMAND + "+0", "Zoom to actual size"); From 3ef1af9af438605621058ae5c1b97252f6687593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Wed, 3 Jan 2024 11:05:35 +0100 Subject: [PATCH 15/21] Simplify shader code (no need for isCropping) --- src/UberShader.cpp | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/src/UberShader.cpp b/src/UberShader.cpp index c7f3e37..f699ba2 100644 --- a/src/UberShader.cpp +++ b/src/UberShader.cpp @@ -73,7 +73,6 @@ UberShader::UberShader(RenderPass* renderPass) { uniform int tonemap; uniform int metric; - uniform bool isCropped; uniform vec2 cropMin; uniform vec2 cropMax; @@ -168,17 +167,11 @@ UberShader::UberShader(RenderPass* renderPass) { return; } - float cropAlpha = 1.0; - if (isCropped) { - if (imageUv.x < cropMin.x - || imageUv.x > cropMax.x - || imageUv.y < cropMin.y - || imageUv.y > cropMax.y) - cropAlpha = 0.3; - } + float cropAlpha = + imageUv.x < cropMin.x || imageUv.x > cropMax.x || imageUv.y < cropMin.y || imageUv.y > cropMax.y ? 0.3 : 1.0; vec4 imageVal = sample(image, imageUv); - imageVal.a = imageVal.a * cropAlpha; + imageVal.a *= cropAlpha; if (!hasReference) { gl_FragColor = vec4( applyTonemap(applyExposureAndOffset(imageVal.rgb), vec4(checker, 1.0 - imageVal.a)), @@ -189,7 +182,7 @@ UberShader::UberShader(RenderPass* renderPass) { } vec4 referenceVal = sample(reference, referenceUv); - referenceVal.a = referenceVal.a * cropAlpha; + referenceVal.a *= cropAlpha; vec3 difference = imageVal.rgb - referenceVal.rgb; float alpha = (imageVal.a + referenceVal.a) * 0.5; @@ -339,7 +332,6 @@ UberShader::UberShader(RenderPass* renderPass) { const constant bool& clipToLdr, const constant int& tonemap, const constant int& metric, - const constant bool& isCropped, const constant float2& cropMin, const constant float2& cropMax, const constant float4& bgColor @@ -353,17 +345,10 @@ UberShader::UberShader(RenderPass* renderPass) { return float4(checker, 1.0f); } - float cropAlpha = 1.0f; - if (isCropped) { - if (vert.imageUv.x < cropMin.x - || vert.imageUv.x > cropMax.x - || vert.imageUv.y < cropMin.y - || vert.imageUv.y > cropMax.y) - cropAlpha = 0.3f; - } + float cropAlpha = vert.imageUv.x < cropMin.x || vert.imageUv.x > cropMax.x || vert.imageUv.y < cropMin.y || vert.imageUv.y > cropMax.y ? 0.3f : 1.0f; float4 imageVal = sample(image, image_sampler, vert.imageUv); - imageVal.a = imageVal.a * cropAlpha; + imageVal.a *= cropAlpha; if (!hasReference) { float4 color = float4( applyTonemap( @@ -382,7 +367,7 @@ UberShader::UberShader(RenderPass* renderPass) { } float4 referenceVal = sample(reference, reference_sampler, vert.referenceUv); - referenceVal.a = referenceVal.a * cropAlpha; + referenceVal.a *= cropAlpha; float3 difference = imageVal.rgb - referenceVal.rgb; float alpha = (imageVal.a + referenceVal.a) * 0.5f; @@ -500,13 +485,12 @@ void UberShader::draw( mShader->set_uniform("hasImage", hasImage); mShader->set_uniform("hasReference", hasReference); mShader->set_uniform("clipToLdr", clipToLdr); - mShader->set_uniform("isCropped", crop.has_value()); if (crop.has_value()) { mShader->set_uniform("cropMin", Vector2f{crop->min} / Vector2f{textureImage->size()}); mShader->set_uniform("cropMax", Vector2f{crop->max} / Vector2f{textureImage->size()}); } else { - mShader->set_uniform("cropMin", Vector2f{0}); - mShader->set_uniform("cropMax", Vector2f{1}); + mShader->set_uniform("cropMin", Vector2f{-std::numeric_limits::infinity()}); + mShader->set_uniform("cropMax", Vector2f{std::numeric_limits::infinity()}); } mShader->begin(); From b3b4eb531db8cf1a22e88df9eac68263691b2545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Wed, 3 Jan 2024 11:13:17 +0100 Subject: [PATCH 16/21] Fix reference sometimes failing to display when holding alt --- src/ImageCanvas.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index c676b5e..a095041 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -69,7 +69,7 @@ void ImageCanvas::draw_contents() { mShader->draw( 2.0f * inverse(Vector2f{m_size}) / mPixelRatio, Vector2f{20.0f}, - image->texture(mRequestedChannelGroup), + image->texture(mImage->channelsInGroup(mRequestedChannelGroup)), // The uber shader operates in [-1, 1] coordinates and requires the _inserve_ // image transform to obtain texture coordinates in [0, 1]-space. inverse(transform(image)), From 4a684612fa0b650f39e4d21d011b4cd6f316c157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Thu, 4 Jan 2024 14:56:42 +0100 Subject: [PATCH 17/21] Make Box compatible with ostream --- include/tev/Box.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/tev/Box.h b/include/tev/Box.h index 542daad..371644c 100644 --- a/include/tev/Box.h +++ b/include/tev/Box.h @@ -81,6 +81,12 @@ struct Box { Vector min, max; }; +template , int> = 0> +Stream& operator<<(Stream& os, const Box& v) { + os << '[' << v.min << ", " << v.max << ']'; + return os; +} + using Box2f = Box; using Box3f = Box; using Box4f = Box; From 643dfedc5679d48ea182f72be1d1328b91fe112e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Thu, 4 Jan 2024 14:57:10 +0100 Subject: [PATCH 18/21] Fix incorrect canvas statistics crop with non-zero-origin display windows --- src/ImageCanvas.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index a095041..91c96a6 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -758,10 +758,10 @@ shared_ptr>> ImageCanvas::canvasStatistics() { // the crop window to the image's data window for canvas statistics computation. Box2i region = image->dataWindow(); if (mCrop.has_value()) { - region = region.intersect(mCrop.value()); + region = region.intersect(mCrop.value().translate(image->displayWindow().min)); } - region = region.translate(image->displayWindow().min - image->dataWindow().min); + region = region.translate(-image->dataWindow().min); invokeTaskDetached([ image, reference, requestedChannelGroup, metric, @@ -769,8 +769,7 @@ shared_ptr>> ImageCanvas::canvasStatistics() { ]() mutable -> Task { co_await ThreadPool::global().enqueueCoroutine(priority); p.set_value(co_await computeCanvasStatistics( - image, reference, requestedChannelGroup, metric, - region, priority + image, reference, requestedChannelGroup, metric, region, priority )); }); From 848a74eb715940f7ce9547ecbf786ab00c4ff288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Thu, 4 Jan 2024 18:53:57 +0100 Subject: [PATCH 19/21] Better keybindings: hold C while dragging to select the crop region The Alt key (in combination with clicking and dragging) is used by various window managers to move windows around and to resize them. Even third party tools (like AltDrag on Windows) tend to use this sort of keybinding. Ergo: it's a bad idea to bind Alt+Drag. Instead, we're now holding C (for crop) while dragging to select the crop region. Alongside this change, I've also moved the Alt-Hold shortcut for showing just the reference image in comparison mode to Shift-Hold (which is more consistent with other reference-related shortcuts, too). --- src/HelpWindow.cpp | 4 ++-- src/ImageCanvas.cpp | 14 ++++++++++---- src/ImageViewer.cpp | 4 +++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/HelpWindow.cpp b/src/HelpWindow.cpp index 62932d2..77c5554 100644 --- a/src/HelpWindow.cpp +++ b/src/HelpWindow.cpp @@ -83,7 +83,7 @@ HelpWindow::HelpWindow(Widget* parent, bool supportsHdr, function closeC addRow(imageSelection, "Space", "Toggle playback of images as video"); addRow(imageSelection, "Click & Drag (+Shift/" + COMMAND + ")", "Translate image"); - addRow(imageSelection, "Click & Drag+" + ALT, "Select region of histogram"); + addRow(imageSelection, "Click & Drag+C (hold)", "Select region of histogram"); addRow(imageSelection, "+ / - / Scroll (+Shift/" + COMMAND + ")", "Zoom in / out of image"); addRow(imageSelection, COMMAND + "+0", "Zoom to actual size"); @@ -111,7 +111,7 @@ HelpWindow::HelpWindow(Widget* parent, bool supportsHdr, function closeC auto referenceSelection = new Widget{shortcuts}; referenceSelection->set_layout(new BoxLayout{Orientation::Vertical, Alignment::Fill, 0, 0}); - addRow(referenceSelection, ALT + " (hold)", "View currently selected reference"); + addRow(referenceSelection, "Shift (hold)", "View currently selected reference"); addRow(referenceSelection, "Shift+Left Click or Right Click", "Select hovered image as reference"); addRow(referenceSelection, "Shift+1…9", "Select N-th image as reference"); addRow(referenceSelection, "Shift+Down or Shift+S / Shift+Up or Shift+W", "Select next / previous image as reference"); diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index 91c96a6..b686110 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -48,9 +48,15 @@ bool ImageCanvas::scroll_event(const Vector2i& p, const Vector2f& rel) { void ImageCanvas::draw_contents() { auto* glfwWindow = screen()->glfw_window(); - bool altHeld = glfwGetKey(glfwWindow, GLFW_KEY_LEFT_ALT) || glfwGetKey(glfwWindow, GLFW_KEY_RIGHT_ALT); - bool ctrlHeld = glfwGetKey(glfwWindow, GLFW_KEY_LEFT_CONTROL) || glfwGetKey(glfwWindow, GLFW_KEY_RIGHT_CONTROL); - Image* image = (mReference && altHeld) ? mReference.get() : mImage.get(); + bool viewReferenceOnly = glfwGetKey(glfwWindow, GLFW_KEY_LEFT_SHIFT) || glfwGetKey(glfwWindow, GLFW_KEY_RIGHT_SHIFT); + bool viewImageOnly = glfwGetKey(glfwWindow, GLFW_KEY_LEFT_CONTROL) || glfwGetKey(glfwWindow, GLFW_KEY_RIGHT_CONTROL); + if (viewReferenceOnly && viewImageOnly) { + // If both modifiers are pressed at the same time, we want entirely different behavior from + // modifying which image is shown. Do nothing here. + viewReferenceOnly = viewImageOnly = false; + } + + Image* image = (mReference && viewReferenceOnly) ? mReference.get() : mImage.get(); if (!image) { mShader->draw( @@ -65,7 +71,7 @@ void ImageCanvas::draw_contents() { imageSpaceCrop = mCrop.value().translate(image->displayWindow().min - image->dataWindow().min); } - if (!mReference || ctrlHeld || image == mReference.get()) { + if (!mReference || viewImageOnly || image == mReference.get()) { mShader->draw( 2.0f * inverse(Vector2f{m_size}) / mPixelRatio, Vector2f{20.0f}, diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 9ec62cd..be09ed8 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -513,6 +513,8 @@ bool ImageViewer::mouse_button_event(const nanogui::Vector2i& p, int button, boo } } + auto* glfwWindow = screen()->glfw_window(); + bool isDraggingImageButton = mDragType == EMouseDragType::ImageButtonDrag; if (down && !isDraggingImageButton) { mDraggingStartPosition = p; @@ -520,7 +522,7 @@ bool ImageViewer::mouse_button_event(const nanogui::Vector2i& p, int button, boo mDragType = EMouseDragType::SidebarDrag; return true; } else if (mImageCanvas->contains(p)) { - mDragType = (modifiers & 4) ? EMouseDragType::ImageCrop : EMouseDragType::ImageDrag; + mDragType = glfwGetKey(glfwWindow, GLFW_KEY_C) ? EMouseDragType::ImageCrop : EMouseDragType::ImageDrag; if (mDragType == EMouseDragType::ImageCrop) { mImageCanvas->setCrop(std::nullopt); // alt + single click disables crop } From 1e00621b478e2e116473da0717ab9584d510ee76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Thu, 4 Jan 2024 19:01:28 +0100 Subject: [PATCH 20/21] Avoid annoying flicker when redefining crop regions Resets the crop upon a mouse *release* with next-to-no movement rather than a press. --- src/ImageViewer.cpp | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index be09ed8..6467c17 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -27,6 +27,7 @@ using namespace std; namespace tev { static const int SIDEBAR_MIN_WIDTH = 230; +static const float CROP_MIN_SIZE = 3; ImageViewer::ImageViewer( const shared_ptr& imagesLoader, @@ -516,22 +517,26 @@ bool ImageViewer::mouse_button_event(const nanogui::Vector2i& p, int button, boo auto* glfwWindow = screen()->glfw_window(); bool isDraggingImageButton = mDragType == EMouseDragType::ImageButtonDrag; - if (down && !isDraggingImageButton) { - mDraggingStartPosition = p; - if (canDragSidebarFrom(p)) { - mDragType = EMouseDragType::SidebarDrag; - return true; - } else if (mImageCanvas->contains(p)) { - mDragType = glfwGetKey(glfwWindow, GLFW_KEY_C) ? EMouseDragType::ImageCrop : EMouseDragType::ImageDrag; - if (mDragType == EMouseDragType::ImageCrop) { - mImageCanvas->setCrop(std::nullopt); // alt + single click disables crop + if (down) { + if (mDragType != EMouseDragType::ImageButtonDrag) { + mDraggingStartPosition = p; + if (canDragSidebarFrom(p)) { + mDragType = EMouseDragType::SidebarDrag; + return true; + } else if (mImageCanvas->contains(p)) { + mDragType = glfwGetKey(glfwWindow, GLFW_KEY_C) ? EMouseDragType::ImageCrop : EMouseDragType::ImageDrag; + return true; } - - return true; } } else { - if (isDraggingImageButton) { + if (mDragType == EMouseDragType::ImageButtonDrag) { requestLayoutUpdate(); + } else if (mDragType == EMouseDragType::ImageCrop) { + if (norm(mDraggingStartPosition - p) < CROP_MIN_SIZE) { + // If the user did not drag the mouse far enough, we assume that they + // wanted to reset the crop rather than create a new one. + mImageCanvas->setCrop(std::nullopt); + } } mDragType = EMouseDragType::None; @@ -597,11 +602,10 @@ bool ImageViewer::mouse_motion_event( Vector2i relStartMousePos = (absolute_position() + mDraggingStartPosition) - mImageCanvas->absolute_position(); Vector2i relMousePos = (absolute_position() + p) - mImageCanvas->absolute_position(); - // Require a minimum movement of 3 (nanogui space) pixels to start cropping. - // Since this is measured in nanogui / screen space and not image space, this - // does not prevent the cropping of smaller image regions. Just zoom in before - // cropping smaller regions than 3 image pixels. - if (norm(relStartMousePos - relMousePos) < 3) { + // Require a minimum movement to start cropping. Since this is measured in nanogui / screen space and not + // image space, this does not prevent the cropping of smaller image regions. Just zoom in before cropping + // smaller regions. + if (norm(relStartMousePos - relMousePos) < CROP_MIN_SIZE) { return false; } From 1c25e99f9a693877a3900a7e87d36d4f68929164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Thu, 4 Jan 2024 19:09:39 +0100 Subject: [PATCH 21/21] Treesitter syntax highlighting for inline glsl strings --- src/UberShader.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/UberShader.cpp b/src/UberShader.cpp index f699ba2..1e714d8 100644 --- a/src/UberShader.cpp +++ b/src/UberShader.cpp @@ -21,7 +21,7 @@ UberShader::UberShader(RenderPass* renderPass) { precision highp float;)"; # endif auto vertexShader = preamble + - R"( + R"glsl( uniform vec2 pixelSize; uniform vec2 checkerSize; @@ -43,10 +43,10 @@ UberShader::UberShader(RenderPass* renderPass) { referenceUv = position * referenceScale + referenceOffset; gl_Position = vec4(position, 1.0, 1.0); - })"; + })glsl"; auto fragmentShader = preamble + - R"( + R"glsl( #define SRGB 0 #define GAMMA 1 #define FALSE_COLOR 2 @@ -192,7 +192,7 @@ UberShader::UberShader(RenderPass* renderPass) { ); gl_FragColor.rgb = clamp(gl_FragColor.rgb, clipToLdr ? 0.0 : -64.0, clipToLdr ? 1.0 : 64.0); - })"; + })glsl"; #elif defined(NANOGUI_USE_METAL) auto vertexShader = R"(using namespace metal;