diff --git a/include/tev/Box.h b/include/tev/Box.h index be01080..371644c 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,26 @@ 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)}; + } + + Box translate(const Vector& offset) const { + return {min + offset, max + offset}; + } + bool operator==(const Box& other) const { return min == other.min && max == other.max; } @@ -55,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; diff --git a/include/tev/Common.h b/include/tev/Common.h index f64a455..08c9c78 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/include/tev/ImageCanvas.h b/include/tev/ImageCanvas.h index 1606ca7..7171b1e 100644 --- a/include/tev/ImageCanvas.h +++ b/include/tev/ImageCanvas.h @@ -3,13 +3,15 @@ #pragma once -#include +#include #include #include +#include #include #include +#include namespace tev { @@ -65,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) { @@ -94,6 +97,14 @@ class ImageCanvas : public nanogui::Canvas { mMetric = metric; } + void setCrop(const std::optional& crop) { + mCrop = crop; + } + + std::optional getCrop() { + return mCrop; + } + static float applyMetric(float value, float reference, EMetric metric); float applyMetric(float value, float reference) const { return applyMetric(value, reference, mMetric); @@ -141,6 +152,7 @@ class ImageCanvas : public nanogui::Canvas { std::shared_ptr reference, const std::string& requestedChannelGroup, EMetric metric, + const Box2i& region, int priority ); @@ -174,6 +186,7 @@ class ImageCanvas : public nanogui::Canvas { ETonemap mTonemap = SRGB; EMetric mMetric = Error; + std::optional mCrop; std::map>>> mCanvasStatistics; std::map> mImageIdToCanvasStatisticsKey; diff --git a/include/tev/ImageViewer.h b/include/tev/ImageViewer.h index 7826db4..b19c462 100644 --- a/include/tev/ImageViewer.h +++ b/include/tev/ImageViewer.h @@ -252,13 +252,18 @@ class ImageViewer : public nanogui::Screen { HelpWindow* mHelpWindow = nullptr; - bool mIsDraggingSidebar = false; - bool mIsDraggingImage = false; - bool mIsDraggingImageButton = false; + enum class EMouseDragType { + None, + ImageDrag, + ImageCrop, + ImageButtonDrag, + SidebarDrag, + }; + + nanogui::Vector2i mDraggingStartPosition; + EMouseDragType mDragType = EMouseDragType::None; size_t mDraggedImageButtonId; - nanogui::Vector2f mDraggingStartPosition; - size_t mClipboardIndex = 0; bool mSupportsHdr = false; diff --git a/include/tev/UberShader.h b/include/tev/UberShader.h index d957dbe..d7ce396 100644 --- a/include/tev/UberShader.h +++ b/include/tev/UberShader.h @@ -3,10 +3,14 @@ #pragma once +#include + #include #include #include +#include + namespace tev { class UberShader { @@ -27,7 +31,8 @@ class UberShader { float offset, float gamma, bool clipToLdr, - ETonemap tonemap + ETonemap tonemap, + const std::optional& crop ); // Draws a difference between a reference and an image. @@ -43,7 +48,8 @@ class UberShader { float gamma, bool clipToLdr, ETonemap tonemap, - EMetric metric + EMetric metric, + const std::optional& crop ); const nanogui::Color& backgroundColor() { diff --git a/src/HelpWindow.cpp b/src/HelpWindow.cpp index 9ec31c5..77c5554 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+C (hold)", "Select region of histogram"); addRow(imageSelection, "+ / - / Scroll (+Shift/" + COMMAND + ")", "Zoom in / out of image"); addRow(imageSelection, COMMAND + "+0", "Zoom to actual size"); @@ -110,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 f35b7c7..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( @@ -60,11 +66,16 @@ void ImageCanvas::draw_contents() { return; } - if (!mReference || ctrlHeld || image == mReference.get()) { + optional imageSpaceCrop = nullopt; + if (mCrop.has_value()) { + imageSpaceCrop = mCrop.value().translate(image->displayWindow().min - image->dataWindow().min); + } + + if (!mReference || viewImageOnly || image == mReference.get()) { 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)), @@ -72,7 +83,8 @@ void ImageCanvas::draw_contents() { mOffset, mGamma, mClipToLdr, - mTonemap + mTonemap, + imageSpaceCrop ); return; } @@ -93,7 +105,8 @@ void ImageCanvas::draw_contents() { mGamma, mClipToLdr, mTonemap, - mMetric + mMetric, + imageSpaceCrop ); } @@ -208,8 +221,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; @@ -271,6 +284,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 @@ -432,6 +448,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())) @@ -475,6 +492,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) { @@ -701,6 +723,15 @@ shared_ptr>> ImageCanvas::canvasStatistics() { fmt::format("{}-{}-{}-{}", mImage->id(), channels, mReference->id(), (int)mMetric) : fmt::format("{}-{}", mImage->id(), channels); + if (mCrop.has_value()) { + key += std::string("-crop") + + "-" + std::to_string(mCrop->min.x()) + + "-" + std::to_string(mCrop->min.y()) + + "-" + std::to_string(mCrop->max.x()) + + "-" + std::to_string(mCrop->max.y()) + ; + } + auto iter = mCanvasStatistics.find(key); if (iter != end(mCanvasStatistics)) { return iter->second; @@ -728,9 +759,24 @@ shared_ptr>> ImageCanvas::canvasStatistics() { mReference->setStaleIdCallback([this](int id) { purgeCanvasStatistics(id); }); } - invokeTaskDetached([image, reference, requestedChannelGroup, metric, priority, p=std::move(promise)]() mutable -> Task { + // 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().translate(image->displayWindow().min)); + } + + region = region.translate(-image->dataWindow().min); + + invokeTaskDetached([ + image, reference, requestedChannelGroup, metric, + region, 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, region, priority + )); }); return mCanvasStatistics.at(key); @@ -807,11 +853,14 @@ Task> ImageCanvas::computeCanvasStatistics( std::shared_ptr reference, const string& requestedChannelGroup, EMetric metric, + 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); - float mean = 0; + double mean = 0; float maximum = -numeric_limits::infinity(); float minimum = numeric_limits::infinity(); @@ -834,15 +883,23 @@ Task> ImageCanvas::computeCanvasStatistics( int nChannels = result->nChannels = alphaChannel ? (int)flattened.size() - 1 : (int)flattened.size(); + 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 = 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; + maximum = max(maximum, v); + minimum = min(minimum, v); + pixelCount++; + } + } + } } - result->mean = nChannels > 0 ? (mean / nChannels) : 0; + result->mean = pixelCount > 0 ? (float)(mean / pixelCount) : 0; result->maximum = maximum; result->minimum = minimum; @@ -878,7 +935,8 @@ Task> ImageCanvas::computeCanvasStatistics( co_return result; } - auto numPixels = image->numPixels(); + auto regionSize = region.size(); + auto numPixels = (size_t)regionSize.x() * regionSize.y(); std::vector indices(numPixels * nChannels); vector> tasks; @@ -886,7 +944,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 % regionSize.x()) + region.min.x(); + int y = (j / regionSize.x()) + 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 48eb608..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, @@ -479,7 +480,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. @@ -493,9 +494,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()) { + mDraggingStartPosition = relMousePos - imgButton->position(); + mDragType = EMouseDragType::ImageButtonDrag; mDraggedImageButtonId = i; - mIsDraggingImageButton = true; - mDraggingStartPosition = nanogui::Vector2f(relMousePos - imgButton->position()); break; } } @@ -513,24 +514,32 @@ bool ImageViewer::mouse_button_event(const nanogui::Vector2i &p, int button, boo } } - if (down && !mIsDraggingImageButton) { - if (canDragSidebarFrom(p)) { - mIsDraggingSidebar = true; - mDraggingStartPosition = nanogui::Vector2f(p); - return true; - } else if (mImageCanvas->contains(p)) { - mIsDraggingImage = true; - mDraggingStartPosition = nanogui::Vector2f(p); - return true; + auto* glfwWindow = screen()->glfw_window(); + + bool isDraggingImageButton = mDragType == EMouseDragType::ImageButtonDrag; + 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; + } } } else { - if (mIsDraggingImageButton) { + 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); + } } - mIsDraggingSidebar = false; - mIsDraggingImage = false; - mIsDraggingImageButton = false; + mDragType = EMouseDragType::None; } return false; @@ -551,7 +560,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 { @@ -559,52 +568,89 @@ 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 left mouse button is held, move the image with mouse movement + if ((button & 1) != 0) { + mImageCanvas->translate(relativeMovement); + } + + // 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}); + } - // 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()}); + break; } - } 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; + + case EMouseDragType::ImageCrop: { + Vector2i relStartMousePos = (absolute_position() + mDraggingStartPosition) - mImageCanvas->absolute_position(); + Vector2i relMousePos = (absolute_position() + p) - mImageCanvas->absolute_position(); + + // 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; } - 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; + + 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; + } + + 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 + ); + + break; } - dynamic_cast(buttons[mDraggedImageButtonId])->set_position( - relMousePos - nanogui::Vector2i(mDraggingStartPosition) - ); + case EMouseDragType::None: + break; } return false; @@ -1058,14 +1104,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); } } @@ -1113,7 +1159,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; } @@ -1194,11 +1240,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; } @@ -1721,8 +1767,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); diff --git a/src/UberShader.cpp b/src/UberShader.cpp index 15960e3..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 @@ -73,6 +73,9 @@ UberShader::UberShader(RenderPass* renderPass) { uniform int tonemap; uniform int metric; + uniform vec2 cropMin; + uniform vec2 cropMax; + uniform vec4 bgColor; varying vec2 checkerUv; @@ -164,7 +167,11 @@ UberShader::UberShader(RenderPass* renderPass) { return; } + 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 *= cropAlpha; if (!hasReference) { gl_FragColor = vec4( applyTonemap(applyExposureAndOffset(imageVal.rgb), vec4(checker, 1.0 - imageVal.a)), @@ -175,6 +182,7 @@ UberShader::UberShader(RenderPass* renderPass) { } vec4 referenceVal = sample(reference, referenceUv); + referenceVal.a *= cropAlpha; vec3 difference = imageVal.rgb - referenceVal.rgb; float alpha = (imageVal.a + referenceVal.a) * 0.5; @@ -184,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; @@ -324,6 +332,8 @@ UberShader::UberShader(RenderPass* renderPass) { const constant bool& clipToLdr, const constant int& tonemap, const constant int& metric, + const constant float2& cropMin, + const constant float2& cropMax, const constant float4& bgColor ) { float3 darkGray = float3(0.5f, 0.5f, 0.5f); @@ -335,7 +345,10 @@ UberShader::UberShader(RenderPass* renderPass) { return float4(checker, 1.0f); } + 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 *= cropAlpha; if (!hasReference) { float4 color = float4( applyTonemap( @@ -354,6 +367,7 @@ UberShader::UberShader(RenderPass* renderPass) { } float4 referenceVal = sample(reference, reference_sampler, vert.referenceUv); + referenceVal.a *= cropAlpha; float3 difference = imageVal.rgb - referenceVal.rgb; float alpha = (imageVal.a + referenceVal.a) * 0.5f; @@ -411,7 +425,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, + std::nullopt ); } @@ -424,14 +439,16 @@ void UberShader::draw( float offset, float gamma, bool clipToLdr, - ETonemap tonemap + ETonemap tonemap, + const std::optional& crop ) { draw( pixelSize, checkerSize, textureImage, transformImage, nullptr, Matrix3f{0.0f}, exposure, offset, gamma, clipToLdr, - tonemap, EMetric::Error + tonemap, EMetric::Error, + crop ); } @@ -447,7 +464,8 @@ void UberShader::draw( float gamma, bool clipToLdr, ETonemap tonemap, - EMetric metric + EMetric metric, + const std::optional& crop ) { bool hasImage = textureImage; if (!hasImage) { @@ -467,6 +485,13 @@ void UberShader::draw( mShader->set_uniform("hasImage", hasImage); mShader->set_uniform("hasReference", hasReference); mShader->set_uniform("clipToLdr", clipToLdr); + 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{-std::numeric_limits::infinity()}); + mShader->set_uniform("cropMax", Vector2f{std::numeric_limits::infinity()}); + } mShader->begin(); mShader->draw_array(Shader::PrimitiveType::Triangle, 0, 6, true);