diff --git a/include/tev/Box.h b/include/tev/Box.h index 371644c..4628823 100644 --- a/include/tev/Box.h +++ b/include/tev/Box.h @@ -30,6 +30,16 @@ struct Box { return nanogui::max(max - min, Vector{(T)0}); } + T area() const { + auto size = this->size(); + T result = (T)1; + for (uint32_t i = 0; i < N_DIMS; ++i) { + result *= size[i]; + } + + return result; + } + Vector middle() const { return (min + max) / (T)2; } @@ -39,6 +49,7 @@ struct Box { for (uint32_t i = 0; i < N_DIMS; ++i) { result &= max[i] >= min[i]; } + return result; } @@ -47,6 +58,7 @@ struct Box { for (uint32_t i = 0; i < N_DIMS; ++i) { result &= pos[i] >= min[i] && pos[i] < max[i]; } + return result; } @@ -55,6 +67,7 @@ struct Box { for (uint32_t i = 0; i < N_DIMS; ++i) { result &= pos[i] >= min[i] && pos[i] <= max[i]; } + return result; } @@ -81,12 +94,6 @@ 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; @@ -95,3 +102,18 @@ using Box3i = Box; using Box4i = Box; } + +template +struct fmt::formatter> : fmt::formatter { + template + auto format(const tev::Box& box, FormatContext& ctx) { + return formatter::format(fmt::format("[{}, {}]", box.min, box.max), ctx); + } +}; + +template , int> = 0> +Stream& operator<<(Stream& os, const tev::Box& v) { + os << '[' << v.min << ", " << v.max << ']'; + return os; +} + diff --git a/include/tev/Common.h b/include/tev/Common.h index 08c9c78..4cb9465 100644 --- a/include/tev/Common.h +++ b/include/tev/Common.h @@ -61,6 +61,16 @@ struct fmt::formatter : fmt::formatter } }; +template +struct fmt::formatter> : fmt::formatter { + template + auto format(const nanogui::Array& v, FormatContext& ctx) { + std::ostringstream s; + s << v; + return formatter::format(s.str(), ctx); + } +}; + struct NVGcontext; namespace nanogui { diff --git a/include/tev/ImageCanvas.h b/include/tev/ImageCanvas.h index 7171b1e..78366a1 100644 --- a/include/tev/ImageCanvas.h +++ b/include/tev/ImageCanvas.h @@ -36,38 +36,20 @@ class ImageCanvas : public nanogui::Canvas { void translate(const nanogui::Vector2f& amount); void scale(float amount, const nanogui::Vector2f& origin); - float scale() const { - return nanogui::extractScale(mTransform); - } - - void setExposure(float exposure) { - mExposure = exposure; - } + float scale() const { return nanogui::extractScale(mTransform); } - void setOffset(float offset) { - mOffset = offset; - } - - void setGamma(float gamma) { - mGamma = gamma; - } + void setExposure(float exposure) { mExposure = exposure; } + void setOffset(float offset) { mOffset = offset; } + void setGamma(float gamma) { mGamma = gamma; } float applyExposureAndOffset(float value) const; - void setImage(std::shared_ptr image) { - mImage = image; - } + void setImage(std::shared_ptr image) { mImage = image; } + void setReference(std::shared_ptr reference) { mReference = reference; } + void setRequestedChannelGroup(const std::string& groupName) { mRequestedChannelGroup = groupName; } - void setReference(std::shared_ptr reference) { - mReference = reference; - } - - void setRequestedChannelGroup(const std::string& groupName) { - mRequestedChannelGroup = groupName; - } - - nanogui::Vector2i getImageCoords(const Image& image, nanogui::Vector2i mousePos); - nanogui::Vector2i getDisplayWindowCoords(const Image& image, nanogui::Vector2i mousePos); + 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) { @@ -76,59 +58,39 @@ class ImageCanvas : public nanogui::Canvas { return result; } - ETonemap tonemap() const { - return mTonemap; - } - - void setTonemap(ETonemap tonemap) { - mTonemap = tonemap; - } + ETonemap tonemap() const { return mTonemap; } + void setTonemap(ETonemap tonemap) { mTonemap = tonemap; } static nanogui::Vector3f applyTonemap(const nanogui::Vector3f& value, float gamma, ETonemap tonemap); nanogui::Vector3f applyTonemap(const nanogui::Vector3f& value) const { return applyTonemap(value, mGamma, mTonemap); } - EMetric metric() const { - return mMetric; - } - - void setMetric(EMetric metric) { - mMetric = metric; - } - - void setCrop(const std::optional& crop) { - mCrop = crop; - } - - std::optional getCrop() { - return mCrop; - } + EMetric metric() const { return mMetric; } + void setMetric(EMetric metric) { mMetric = metric; } static float applyMetric(float value, float reference, EMetric metric); float applyMetric(float value, float reference) const { return applyMetric(value, reference, mMetric); } - auto backgroundColor() { - return mShader->backgroundColor(); - } + std::optional crop() { return mCrop; } + void setCrop(const std::optional& crop) { mCrop = crop; } + Box2i cropInImageCoords() const; - void setBackgroundColor(const nanogui::Color& color) { - mShader->setBackgroundColor(color); - } + auto backgroundColor() { return mShader->backgroundColor(); } + void setBackgroundColor(const nanogui::Color& color) { mShader->setBackgroundColor(color); } void fitImageToScreen(const Image& image); void resetTransform(); - void setClipToLdr(bool value) { - mClipToLdr = value; - } - - bool clipToLdr() const { - return mClipToLdr; - } + bool clipToLdr() const { return mClipToLdr; } + void setClipToLdr(bool value) { mClipToLdr = value; } + // The following functions return four values per pixel in RGBA order. The number of pixels is given by + // `imageDataSize()`. If the canvas does not currently hold an image, or no channels are displayed, then zero pixels + // are returned. + nanogui::Vector2i imageDataSize() const { return cropInImageCoords().size(); } std::vector getHdrImageData(bool divideAlpha, int priority) const; std::vector getLdrImageData(bool divideAlpha, int priority) const; diff --git a/src/HelpWindow.cpp b/src/HelpWindow.cpp index 77c5554..c18755a 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+C (hold)", "Select region of histogram"); + addRow(imageSelection, "Click & Drag+C (hold)", "Crop image"); addRow(imageSelection, "+ / - / Scroll (+Shift/" + COMMAND + ")", "Zoom in / out of image"); addRow(imageSelection, COMMAND + "+0", "Zoom to actual size"); diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index b686110..f885bba 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -285,7 +285,7 @@ void ImageCanvas::drawCoordinateSystem(NVGcontext* ctx) { } if (mCrop.has_value()) { - drawWindow(mCrop.value(), CROP_COLOR, false, false, "Stats crop", flags); + drawWindow(mCrop.value(), CROP_COLOR, false, false, "Crop", flags); } }; @@ -484,17 +484,21 @@ float ImageCanvas::applyExposureAndOffset(float value) const { return pow(2.0f, mExposure) * value + mOffset; } -Vector2i ImageCanvas::getImageCoords(const Image& image, Vector2i nanoPos) { - Vector2f imagePos = inverse(textureToNanogui(&image)) * Vector2f{nanoPos}; +Vector2i ImageCanvas::getImageCoords(const Image* image, Vector2i nanoPos) { + Vector2f imagePos = inverse(textureToNanogui(image)) * Vector2f{nanoPos}; return { static_cast(floor(imagePos.x())), static_cast(floor(imagePos.y())), }; } -Vector2i ImageCanvas::getDisplayWindowCoords(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); + if (image) { + imageCoords += Vector2f(image->dataWindow().min - image->displayWindow().min); + } + + return imageCoords; } void ImageCanvas::getValuesAtNanoPos(Vector2i nanoPos, vector& result, const vector& channels) { @@ -503,7 +507,7 @@ void ImageCanvas::getValuesAtNanoPos(Vector2i nanoPos, vector& result, co return; } - auto imageCoords = getImageCoords(*mImage, nanoPos); + auto imageCoords = getImageCoords(mImage.get(), nanoPos); for (const auto& channel : channels) { const Channel* c = mImage->channel(channel); TEV_ASSERT(c, "Requested channel must exist."); @@ -512,7 +516,7 @@ void ImageCanvas::getValuesAtNanoPos(Vector2i nanoPos, vector& result, co // Subtract reference if it exists. if (mReference) { - auto referenceCoords = getImageCoords(*mReference, nanoPos); + auto referenceCoords = getImageCoords(mReference.get(), nanoPos); for (size_t i = 0; i < result.size(); ++i) { bool isAlpha = Channel::isAlpha(channels[i]); float defaultVal = isAlpha && mReference->contains(referenceCoords) ? 1.0f : 0.0f; @@ -574,6 +578,28 @@ float ImageCanvas::applyMetric(float image, float reference, EMetric metric) { } } +Box2i ImageCanvas::cropInImageCoords() const { + if (!mImage) { + return Box2i{0}; + } + + // 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 = mImage->dataWindow(); + if (mCrop.has_value()) { + region = region.intersect(mCrop.value().translate(mImage->displayWindow().min)); + + // If there is no intersection between the crop and the image, return the empty region about the image origin. + if (!region.isValid()) { + return Box2i{0}; + } + } + + region = region.translate(-mImage->dataWindow().min); + TEV_ASSERT(region.isValid(), "Crop region must be valid."); + return region; +} + void ImageCanvas::fitImageToScreen(const Image& image) { Vector2f nanoguiImageSize = Vector2f{image.displayWindow().size()} / mPixelRatio; mTransform = Matrix3f::scale(Vector2f{min(m_size.x() / nanoguiImageSize.x(), m_size.y() / nanoguiImageSize.y())}); @@ -584,28 +610,30 @@ void ImageCanvas::resetTransform() { } std::vector ImageCanvas::getHdrImageData(bool divideAlpha, int priority) const { - std::vector result; - if (!mImage) { - return result; + return {}; } const auto& channels = channelsFromImages(mImage, mReference, mRequestedChannelGroup, mMetric, priority); - auto numPixels = mImage->numPixels(); - if (channels.empty()) { - return result; + return {}; } + auto imageRegion = cropInImageCoords(); + size_t numPixels = (size_t)imageRegion.area(); int nChannelsToSave = std::min((int)channels.size(), 4); // Flatten image into vector - result.resize(4 * numPixels, 0); - - ThreadPool::global().parallelFor(0, nChannelsToSave, [&channels, &result](int i) { - const auto& channelData = channels[i].data(); - for (size_t j = 0; j < channelData.size(); ++j) { - result[j * 4 + i] = channelData[j]; + std::vector result(4 * numPixels, 0); + + ThreadPool::global().parallelFor(0, nChannelsToSave, [&channels, &result, &imageRegion](int i) { + const auto& channel = channels[i]; + for (int y = imageRegion.min.y(); y < imageRegion.max.y(); ++y) { + int yresult = y - imageRegion.min.y(); + for (int x = imageRegion.min.x(); x < imageRegion.max.x(); ++x) { + int xresult = x - imageRegion.min.x(); + result[(yresult * imageRegion.size().x() + xresult) * 4 + i] = channel.at({x, y}); + } } }, priority); @@ -618,7 +646,7 @@ std::vector ImageCanvas::getHdrImageData(bool divideAlpha, int priority) // Divide alpha out if needed (for storing in non-premultiplied formats) if (divideAlpha) { - ThreadPool::global().parallelFor(0, min(nChannelsToSave, 3), [&result,numPixels](int i) { + ThreadPool::global().parallelFor(0, min(nChannelsToSave, 3), [&result, numPixels](int i) { for (size_t j = 0; j < numPixels; ++j) { float alpha = result[j * 4 + 3]; if (alpha == 0) { @@ -634,19 +662,11 @@ std::vector ImageCanvas::getHdrImageData(bool divideAlpha, int priority) } std::vector ImageCanvas::getLdrImageData(bool divideAlpha, int priority) const { - std::vector result; - - if (!mImage) { - return result; - } - - auto numPixels = mImage->numPixels(); + // getHdrImageData always returns four floats per pixel (RGBA). auto floatData = getHdrImageData(divideAlpha, priority); + std::vector result(floatData.size()); - // Store as LDR image. - result.resize(floatData.size()); - - ThreadPool::global().parallelFor(0, numPixels, [&](size_t i) { + ThreadPool::global().parallelFor(0, floatData.size() / 4, [&](size_t i) { size_t start = 4 * i; Vector3f value = applyTonemap({ applyExposureAndOffset(floatData[start]), @@ -665,12 +685,11 @@ std::vector ImageCanvas::getLdrImageData(bool divideAlpha, int priority) c } void ImageCanvas::saveImage(const fs::path& path) const { - if (!mImage) { - return; + Vector2i imageSize = imageDataSize(); + if (imageSize.x() == 0 || imageSize.y() == 0) { + throw runtime_error{"Can not save image with zero pixels."}; } - Vector2i imageSize = mImage->size(); - tlog::info() << "Saving currently displayed image as " << path << "."; auto start = chrono::system_clock::now(); @@ -737,20 +756,11 @@ shared_ptr>> ImageCanvas::canvasStatistics() { return iter->second; } - static std::atomic sId{0}; - // Later requests must have higher priority than previous ones. - int priority = ++sId; - - auto image = mImage, reference = mReference; - auto requestedChannelGroup = mRequestedChannelGroup; - auto metric = mMetric; - promise> promise; mCanvasStatistics.insert(make_pair(key, make_shared>>(promise.get_future()))); - // Remember the keys associateed with the participating images. Such that their - // canvas statistics can be retrieved and deleted when either of the images - // is closed or mutated. + // Remember the keys associateed with the participating images. Such that their canvas statistics can be retrieved + // and deleted when either of the images is closed or mutated. mImageIdToCanvasStatisticsKey[mImage->id()].emplace_back(key); mImage->setStaleIdCallback([this](int id) { purgeCanvasStatistics(id); }); @@ -759,19 +769,16 @@ shared_ptr>> ImageCanvas::canvasStatistics() { mReference->setStaleIdCallback([this](int id) { purgeCanvasStatistics(id); }); } - // 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); - + // Later requests must have higher priority than previous ones. + static std::atomic sId{0}; invokeTaskDetached([ - image, reference, requestedChannelGroup, metric, - region, priority, p=std::move(promise) + image=mImage, + reference=mReference, + requestedChannelGroup=mRequestedChannelGroup, + metric=mMetric, + region=cropInImageCoords(), + priority=++sId, + p=std::move(promise) ]() mutable -> Task { co_await ThreadPool::global().enqueueCoroutine(priority); p.set_value(co_await computeCanvasStatistics( @@ -856,6 +863,7 @@ Task> ImageCanvas::computeCanvasStatistics( const Box2i& region, int priority ) { + TEV_ASSERT(region.isValid(), "Region must be valid."); TEV_ASSERT(Box2i{image->size()}.contains(region), "Region must be contained in image."); auto flattened = channelsFromImages(image, reference, requestedChannelGroup, metric, priority); @@ -936,14 +944,14 @@ Task> ImageCanvas::computeCanvasStatistics( } auto regionSize = region.size(); - auto numPixels = (size_t)regionSize.x() * regionSize.y(); + auto numPixels = region.area(); std::vector indices(numPixels * nChannels); vector> tasks; for (int i = 0; i < nChannels; ++i) { const auto& channel = flattened[i]; tasks.emplace_back( - ThreadPool::global().parallelForAsync(0, numPixels, [&, i](size_t j) { + ThreadPool::global().parallelForAsync(0, numPixels, [&, i](int 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})); @@ -956,7 +964,7 @@ Task> ImageCanvas::computeCanvasStatistics( } co_await ThreadPool::global().parallelForAsync(0, nChannels, [&](int i) { - for (size_t j = 0; j < numPixels; ++j) { + for (int j = 0; j < numPixels; ++j) { result->histogram[indices[j + i * numPixels] + i * NUM_BINS] += alphaChannel ? alphaChannel->eval(j) : 1; } }, priority); diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index c25c37d..af28cab 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 if (canDragSidebarFrom(p)) { mDragType = EMouseDragType::SidebarDrag; return true; - } else if (mImageCanvas->contains(p)) { + } else if (mImageCanvas->contains(p) && mCurrentImage) { mDragType = glfwGetKey(glfwWindow, GLFW_KEY_C) ? EMouseDragType::ImageCrop : EMouseDragType::ImageDrag; return true; } @@ -607,8 +607,8 @@ bool ImageViewer::mouse_motion_event( return false; } - auto startImageCoords = mImageCanvas->getDisplayWindowCoords(*mCurrentImage, relStartMousePos); - auto imageCoords = mImageCanvas->getDisplayWindowCoords(*mCurrentImage, relMousePos); + auto startImageCoords = mImageCanvas->getDisplayWindowCoords(mCurrentImage.get(), relStartMousePos); + auto imageCoords = mImageCanvas->getDisplayWindowCoords(mCurrentImage.get(), relMousePos); // sanitize the input crop Box2i crop = {{startImageCoords, imageCoords}}; @@ -817,9 +817,7 @@ bool ImageViewer::keyboard_event(int key, int scancode, int action, int modifier } else { tlog::error() << "Failed to copy image path to clipboard."; } - } else { - auto imageSize = mCurrentImage->size(); - + } else if (auto imageSize = mImageCanvas->imageDataSize(); imageSize.x() > 0 && imageSize.y() > 0) { clip::image_spec imageMetadata; imageMetadata.width = imageSize.x(); imageMetadata.height = imageSize.y(); @@ -2053,7 +2051,7 @@ void ImageViewer::updateTitle() { auto rel = mouse_pos() - mImageCanvas->position(); vector values = mImageCanvas->getValuesAtNanoPos({rel.x(), rel.y()}, channels); - nanogui::Vector2i imageCoords = mImageCanvas->getImageCoords(*mCurrentImage, {rel.x(), rel.y()}); + nanogui::Vector2i imageCoords = mImageCanvas->getImageCoords(mCurrentImage.get(), {rel.x(), rel.y()}); TEV_ASSERT(values.size() >= channelTails.size(), "Should obtain a value for every existing channel."); string valuesString;