Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image canvas implementation #7501

Open
thewoz opened this issue Apr 16, 2024 · 15 comments
Open

Image canvas implementation #7501

thewoz opened this issue Apr 16, 2024 · 15 comments

Comments

@thewoz
Copy link

thewoz commented Apr 16, 2024

Version/Branch of Dear ImGui:

Version 1.90.5, Branch: docking

Back-ends:

ImGui_ImplGlfw ImGui_ImplOpenGL3

Compiler, OS:

macOS

Full config/build information:

No response

Details:

My Issue/Question:

Hi all,
I am trying to implement an image navigator/canvas.
Something similar to (#6051 (comment))
I tried to see what he does but I can't extrapolate it.
So I'm starting from scratch. I'm having some problems handling zoom and scroll.
What happens is that I have strange jumps that I don't understand.
I tried to see if it was a problem related to #2915 but I'm not sure.
Specifically what I need is an image viewer where the image is centered (now it is not) where I can move around.
Namely zoom and drag.

Screenshots/Video:

aaa.mov

Minimal, Complete and Verifiable Example code:

clang++ -std=c++17 pkg-config --cflags opencv4 main.cpp pkg-config --libs opencv4 pkg-config --libs --static glfw3 -lglad -framework OpenGL -limgui

#include <cstdio>
#include <cstdlib>

#include <opencv2/opencv.hpp>

#include <glad/glad.h>

#include <GLFW/glfw3.h>

#define IMGUI_DEFINE_MATH_OPERATORS
#include <imgui/imgui.hpp>

static void glfwErrorCallback(int error, const char * description) {
  fprintf(stderr, "GLFW error (%d): %s\n", error, description);
}

//****************************************************************************/
// main
//****************************************************************************/
int main(int argc, const char * argv[]) {

  glfwSetErrorCallback(glfwErrorCallback);

  if(!glfwInit()) {
    fprintf(stderr, "GLFW init error\n");
    exit(EXIT_FAILURE);
  }
  
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
  glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
  glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
  glfwWindowHint(GLFW_SAMPLES, 4);
  
  // Create a GLFWwindow object that we can use for GLFW's functions
  GLFWwindow * window = glfwCreateWindow(800, 600, "Image Test", NULL, NULL);
  
  if(window == NULL) {
    fprintf(stderr, "Failed to create GLFW window\n");
    glfwTerminate();
    exit(EXIT_FAILURE);
  }
  
  glfwMakeContextCurrent(window);
  
  if(!gladLoadGLLoader((GLADloadproc) glfwGetProcAddress)) {
    fprintf(stderr, "Failed to initialize GLAD\n");
    abort();
  }
  
  glfwSwapInterval(1);

  IMGUI_CHECKVERSION();

  ImGui::CreateContext();
  ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable;
  ImGui_ImplGlfw_InitForOpenGL(window, true);
  ImGui_ImplOpenGL3_Init("#version 150");
  ImGui::StyleColorsDark();

  cv::Mat image = cv::imread( "/Users/thewoz/Desktop/Lena.png", cv::IMREAD_COLOR);

  cv::cvtColor(image, image, cv::COLOR_BGR2RGBA);

    while(!glfwWindowShouldClose(window)) {
    
    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplGlfw_NewFrame();
    ImGui::NewFrame();
    
    ImGuiViewport * viewport = ImGui::GetMainViewport();
    
    // Set viewport explicitly so GetFrameHeight reacts to DPI changes
    ImGui::SetCurrentViewport(nullptr, (ImGuiViewportP*)viewport);
    
    float height = ImGui::GetFrameHeight();
    
    if(ImGui::BeginMainMenuBar()) {
      if(ImGui::BeginMenu("Menu")) {
        ImGui::MenuItem("Hello!", NULL, false, true);
        ImGui::EndMenu();
      }
      ImGui::EndMainMenuBar();
    }
    
    if(ImGui::BeginViewportSideBar("StatusBar", viewport, ImGuiDir_Down, height, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar)) {
        if(ImGui::BeginMenuBar()) {
          ImGui::Text("Zoom: 100");
          ImGui::SameLine();
          ImGui::Text("Frame: 1/1");
          ImGui::EndMenuBar();
        }
        ImGui::End();
    }
    
    ImGuiID mainDockSpaceId = ImGui::DockSpaceOverViewport();
    
    ImGuiWindowClass window_class;
    window_class.DockNodeFlagsOverrideSet = ImGuiDockNodeFlags_NoTabBar;
    
    ImGui::SetNextWindowClass(&window_class);
    ImGui::Begin("Down", NULL, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar);
    ImGui::End();
    
    ImGui::SetNextWindowClass(&window_class);
    ImGui::Begin("Left", NULL, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
    ImGui::GetStyle().WindowPadding.x = 0;
    ImGui::GetStyle().WindowPadding.y = 0;
    
    static float imageScale = 1;

    GLuint texture;
    glGenTextures(1, &texture);
    
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
    
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.cols, image.rows, 0, GL_RGBA, GL_UNSIGNED_BYTE, image.data);
    
    ImVec2 imageSize = ImVec2(imageScale * image.size().width, imageScale * image.size().height);
        
    ImGui::Image(reinterpret_cast<void*>(static_cast<intptr_t>(texture)), imageSize);
    
    ImGui::SetItemKeyOwner(ImGuiKey_MouseWheelY);
    
    // If the mouse is over the image
    if(ImGui::IsItemHovered()) {
      
      ImVec2 mousePos = ImGui::GetIO().MousePos;
      
      // calculate the position in the mouse in the image reference frame
      mousePos.x = (mousePos.x + ImGui::GetScrollX()) / imageScale;
      mousePos.y = (mousePos.y + ImGui::GetScrollY() - ImGui::GetWindowPos().y) / imageScale;
      
      // whether the wheel has been moved
      if(ImGui::GetIO().MouseWheel != 0 && ImGui::GetIO().KeyCtrl) {
        
        imageScale = imageScale * pow(1.50, ImGui::GetIO().MouseWheel);
        
        if(imageScale < 0.1) imageScale = 0.1;
        if(imageScale > 30)  imageScale = 30;

        // I update the scroll position
        ImGui::SetScrollX(mousePos.x - (ImGui::GetWindowPos().x * 0.5));
        ImGui::SetScrollY(mousePos.y - (ImGui::GetWindowPos().y * 0.5));
        
      }
      
      // if I am dragging the image
      if(ImGui::IsMouseDragging(ImGuiMouseButton_Left, 0.0f)) {
              
        ImGui::SetScrollX(ImGui::GetScrollX() - ImGui::GetIO().MouseDelta.x);
        ImGui::SetScrollY(ImGui::GetScrollY() - ImGui::GetIO().MouseDelta.y);

      }
      
    }
  
    ImGui::End();
    
    ImGui::SetNextWindowClass(&window_class);
    ImGui::Begin("Right", NULL, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar);
    ImGui::End();
    
    static bool sFirstFrame = true;
    if(sFirstFrame) {
      
      sFirstFrame = false;
      
      ImGui::DockBuilderRemoveNode(mainDockSpaceId);
      ImGui::DockBuilderAddNode(mainDockSpaceId, ImGuiDockNodeFlags_None);
      
      ImGuiID dock_id_up;
      ImGuiID dock_id_down;
      
      ImGuiID dock_id_left;
      ImGuiID dock_id_right;
      
      ImGui::DockBuilderSplitNode(mainDockSpaceId, ImGuiDir_Up, 0.5f, &dock_id_up, &dock_id_down);
      
      ImGui::DockBuilderSplitNode(dock_id_up, ImGuiDir_Right, 0.5f, &dock_id_right, &dock_id_left);
      
      ImGui::DockBuilderDockWindow("Down", dock_id_down);
      ImGui::DockBuilderDockWindow("Left", dock_id_left);
      ImGui::DockBuilderDockWindow("Right", dock_id_right);
      
      ImGui::DockBuilderFinish(mainDockSpaceId);
      
    }
   
    ImGui::Render();
    
    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
   
    glfwPollEvents();

    glfwSwapBuffers(window);
    
  }
  
  glfwTerminate();

  return 0;
  
}

@GamingMinds-DanielC
Copy link
Contributor

A few problems I notice in your code:

  1. When you modify mousePos.x, you don't take the window position into account.
  2. You don't take the unmodified mouse position into account when calculating the new scroll values. Better make a new variable for the modified one...
    mousePosInImage = (mousePos - windowPos + scroll) / scale
    That can be resolved to this (after adjusting the scale)...
    scroll = mousePosInImage * scale + windowPos - mousePos
  3. When you change your scale, the content size of your window will change. Since you don't communicate the scroll value in advance, the last one will be taken into account when the configured scroll values will be clamped, so you will have undesired behavior when zooming in the bottom right corner.

@thewoz
Copy link
Author

thewoz commented Apr 16, 2024

Hi @GamingMinds-DanielC
thank you very much for your reply.
I understood what you say in (1) and (2) I made the changes and things are much better.

The code now looks like this:

      ImVec2 mousePosInImage;
      mousePosInImage.x = (ImGui::GetIO().MousePos.x - ImGui::GetWindowPos().x + ImGui::GetScrollX()) / imageScale;
      mousePosInImage.y = (ImGui::GetIO().MousePos.y - ImGui::GetWindowPos().y + ImGui::GetScrollY()) / imageScale;

      // whether the wheel has been moved
      if(ImGui::GetIO().MouseWheel != 0 && ImGui::GetIO().KeyCtrl) {
        
        imageScale = imageScale * pow(1.50, ImGui::GetIO().MouseWheel);
        
        if(imageScale < 0.1) imageScale = 0.1;
        if(imageScale > 30)  imageScale = 30;
     
        // I update the scroll position
        ImGui::SetScrollX(mousePosInImage.x * imageScale + ImGui::GetWindowPos().x - ImGui::GetIO().MousePos.x);
        ImGui::SetScrollY(mousePosInImage.y * imageScale + ImGui::GetWindowPos().y - ImGui::GetIO().MousePos.y);
        
      }

In general I have difficulty understanding the general structure of imgui coordinate system.

For point (3) I don't quite understand what you are saying.
To whom do I not communicate the sliding value in advance?

@GamingMinds-DanielC
Copy link
Contributor

Some more infos for point 3:
When you call ImGui::Begin("Left", ...), the scroll position will be clamped to the content size of the window, to avoid scrolling outside of the available content. That is intended, but the content size is known to the window as the content size of the last frame. If your content size is f.e. 1000 and your window is 600 high, the scroll position will be clamped to 1000-600=400.

If you zoom in on the bottom so that your new content height will be 1200 and calculate your scroll position to be 600, that will be the correct value. But the window will not know of the new content height, think it will probably be 1000 like in the last frame and clamp your scroll position to 400 again. Only when you submit your image will the new size be known, but then it is too late and the bottom 200 pixels will be scrolled out instead of being visible.

To avoid this, you can tell your window in advance. You can call ImGui::SetNextWindowContentSize() followed by ImGui::SetNextWindowScroll(), both before beginning the window, then the correct values will be known and zooming at the border should feel better.

@thewoz
Copy link
Author

thewoz commented Apr 17, 2024

Hi @GamingMinds-DanielC,
thank you very much for the additional information!!!
Everything works now!!

This is the updated code:

#include <cstdio>
#include <cstdlib>

#include <opencv2/opencv.hpp>

#include <glad/glad.h>

#include <GLFW/glfw3.h>

#define IMGUI_DEFINE_MATH_OPERATORS
#include <imgui/imgui.hpp>

static void glfwErrorCallback(int error, const char * description) {
  fprintf(stderr, "GLFW error (%d): %s\n", error, description);
}

//****************************************************************************/
// main
//****************************************************************************/
int main(int argc, const char * argv[]) {

  glfwSetErrorCallback(glfwErrorCallback);

  if(!glfwInit()) {
    fprintf(stderr, "GLFW init error\n");
    exit(EXIT_FAILURE);
  }
  
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
  glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
  glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
  glfwWindowHint(GLFW_SAMPLES, 4);
  
  // Create a GLFWwindow object that we can use for GLFW's functions
  GLFWwindow * window = glfwCreateWindow(800, 600, "Image Test", NULL, NULL);
  
  if(window == NULL) {
    fprintf(stderr, "Failed to create GLFW window\n");
    glfwTerminate();
    exit(EXIT_FAILURE);
  }
  
  glfwMakeContextCurrent(window);
  
  if(!gladLoadGLLoader((GLADloadproc) glfwGetProcAddress)) {
    fprintf(stderr, "Failed to initialize GLAD\n");
    abort();
  }
  
  glfwSwapInterval(1);

  IMGUI_CHECKVERSION();

  ImGui::CreateContext();
  ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable;
  ImGui_ImplGlfw_InitForOpenGL(window, true);
  ImGui_ImplOpenGL3_Init("#version 150");
  ImGui::StyleColorsDark();

  cv::Mat image = cv::imread( "/Users/thewoz/Dropbox/Research/COBBS/test/Lena.png", cv::IMREAD_COLOR);

  cv::cvtColor(image, image, cv::COLOR_BGR2RGBA);

  ImVec2 scroll;
  ImVec2 imageSize;
  
  while(!glfwWindowShouldClose(window)) {
    
    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplGlfw_NewFrame();
    ImGui::NewFrame();
    
    ImGuiViewport * viewport = ImGui::GetMainViewport();
    
    // Set viewport explicitly so GetFrameHeight reacts to DPI changes
    ImGui::SetCurrentViewport(nullptr, (ImGuiViewportP*)viewport);
    
    float height = ImGui::GetFrameHeight();
    
    if(ImGui::BeginMainMenuBar()) {
      if(ImGui::BeginMenu("Menu")) {
        ImGui::MenuItem("Hello!", NULL, false, true);
        ImGui::EndMenu();
      }
      ImGui::EndMainMenuBar();
    }
    
    if(ImGui::BeginViewportSideBar("StatusBar", viewport, ImGuiDir_Down, height, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar)) {
        if(ImGui::BeginMenuBar()) {
          ImGui::Text("Zoom: 100");
          ImGui::SameLine();
          ImGui::Text("Frame: 1/1");
          ImGui::EndMenuBar();
        }
        ImGui::End();
    }
    
    ImGuiID mainDockSpaceId = ImGui::DockSpaceOverViewport();
    
    ImGuiWindowClass window_class;
    window_class.DockNodeFlagsOverrideSet = ImGuiDockNodeFlags_NoTabBar;
    
    ImGui::SetNextWindowClass(&window_class);
    ImGui::Begin("Down", NULL, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar);
    ImGui::End();
    
    ImGui::SetNextWindowClass(&window_class);
    ImGui::SetNextWindowContentSize(imageSize);
    ImGui::SetNextWindowScroll(scroll);

    ImGui::Begin("Left", NULL, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);

    ImGui::GetStyle().WindowPadding.x = 0;
    ImGui::GetStyle().WindowPadding.y = 0;

    static float imageScale = 1;

    GLuint texture;
    glGenTextures(1, &texture);
    
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
    
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.cols, image.rows, 0, GL_RGBA, GL_UNSIGNED_BYTE, image.data);
    
    imageSize = ImVec2(imageScale * image.size().width, imageScale * image.size().height);
    
    //ImGui::SetCursorPos((ImGui::GetWindowSize() - imageSize) * 0.5f);
    
    ImGui::Image(reinterpret_cast<void*>(static_cast<intptr_t>(texture)), imageSize);
    
    ImGui::SetItemKeyOwner(ImGuiKey_MouseWheelY);
    
    // If the mouse is over the image
    if(ImGui::IsItemHovered()) {
            
      // calculate the position in the mouse in the image reference frame
      ImVec2 mousePosInImage;
      mousePosInImage.x = (ImGui::GetIO().MousePos.x - ImGui::GetWindowPos().x + ImGui::GetScrollX()) / imageScale;
      mousePosInImage.y = (ImGui::GetIO().MousePos.y - ImGui::GetWindowPos().y + ImGui::GetScrollY()) / imageScale;

      // whether the wheel has been moved
      if(ImGui::GetIO().MouseWheel != 0 && ImGui::GetIO().KeyCtrl) {
        
        imageScale = imageScale * pow(1.50, ImGui::GetIO().MouseWheel);
        
        if(imageScale < 0.1) imageScale = 0.1;
        if(imageScale > 30)  imageScale = 30;
     
        imageSize = ImVec2(imageScale * image.size().width, imageScale * image.size().height);

        scroll.x = mousePosInImage.x * imageScale + ImGui::GetWindowPos().x - ImGui::GetIO().MousePos.x;
        scroll.y = mousePosInImage.y * imageScale + ImGui::GetWindowPos().y - ImGui::GetIO().MousePos.y;

        // I update the scroll position
        ImGui::SetScrollX(scroll.x);
        ImGui::SetScrollY(scroll.y);
        
      }
      
      // if I am dragging the image
      if(ImGui::IsMouseDragging(ImGuiMouseButton_Left, 0.0f)) {
      
        scroll.x = ImGui::GetScrollX() - ImGui::GetIO().MouseDelta.x;
        scroll.y = ImGui::GetScrollY() - ImGui::GetIO().MouseDelta.y;
        
        // I update the scroll position
        ImGui::SetScrollX(scroll.x);
        ImGui::SetScrollY(scroll.y);

      }
      
    }
  
    ImGui::End();
    
    ImGui::SetNextWindowClass(&window_class);
    ImGui::Begin("Right", NULL, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar);
    ImGui::End();
    
    static bool sFirstFrame = true;
    if(sFirstFrame) {
      
      sFirstFrame = false;
      
      ImGui::DockBuilderRemoveNode(mainDockSpaceId);
      ImGui::DockBuilderAddNode(mainDockSpaceId, ImGuiDockNodeFlags_None);
      
      ImGuiID dock_id_up;
      ImGuiID dock_id_down;
      
      ImGuiID dock_id_left;
      ImGuiID dock_id_right;
      
      ImGui::DockBuilderSplitNode(mainDockSpaceId, ImGuiDir_Up, 0.5f, &dock_id_up, &dock_id_down);
      
      ImGui::DockBuilderSplitNode(dock_id_up, ImGuiDir_Right, 0.5f, &dock_id_right, &dock_id_left);
      
      ImGui::DockBuilderDockWindow("Down", dock_id_down);
      ImGui::DockBuilderDockWindow("Left", dock_id_left);
      ImGui::DockBuilderDockWindow("Right", dock_id_right);
      
      ImGui::DockBuilderFinish(mainDockSpaceId);
      
    }
   
    ImGui::Render();
    
    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
   
    glfwPollEvents();

    glfwSwapBuffers(window);
    
  }
  
  glfwTerminate();

  return 0;
  
}

Now I'm trying to center the image and I added this line of code (#2212) before the image definition:
ImGui::SetCursorPos((ImGui::GetWindowSize() - imageSize) * 0.5f);

The problem is that now the image is centered but it errs in taking I think the mouse coordinates and thus the scroll position when zooming. I think the problem is because I moved the cursor zero.

@GamingMinds-DanielC
Copy link
Contributor

The problem is that now the image is centered but it errs in taking I think the mouse coordinates and thus the scroll position when zooming. I think the problem is because I moved the cursor zero.

Yes, the transformation of the mouse position into image space and the calculation of the scroll position assume an unmodified cursor position. If you want to modify that, you need to take that into account as well. But the more you add, the more unwieldy it gets, I would strongly suggest to put your canvas into an object with its own state and handle the reusable stuff in there. Something that could be used f.e. like this:

	const ImVec2 imageMin = ImVec2( 0, 0 );
	const ImVec2 imageMax = imageSize;

	m_Canvas.setContentRect( imageMin, imageMax );

	if ( m_Canvas.begin( "canvas", ImVec2( 0, 0 ) ) )
	{
		ImDrawList* drawList = m_Canvas.getDrawList();

		drawList->AddImage( textureId, m_Canvas.contentToScreen( imageMin ), m_Canvas.contentToScreen( imageMax ) );

		m_Canvas.end();
	}

@thewoz
Copy link
Author

thewoz commented Apr 17, 2024

Hi thank you for your reply.
Yes my idea was, after having a working example, to "engineer" it and create an object.

I tried to consider the position of the cursor but with poor success.

I modified the code like this:

mousePosInImage.x = (ImGui::GetIO().MousePos.x - ImGui::GetWindowPos().x - ImGui::GetCursorPos().x + ImGui::GetScrollX()) / imageScale;
mousePosInImage.y = (ImGui::GetIO().MousePos.y - ImGui::GetWindowPos().y - ImGui::GetCursorPos().y + ImGui::GetScrollY()) / imageScale;

and

scroll.x = mousePosInImage.x * imageScale + ImGui::GetWindowPos().x - ImGui::GetCursorPos().x - ImGui::GetIO().MousePos.x;
scroll.y = mousePosInImage.y * imageScale + ImGui::GetWindowPos().y - ImGui::GetCursorPos().y - ImGui::GetIO().MousePos.y;

But it doesn't work...
I tried to consider padding as well but it doesn't work.
There is something I don't understand about how the reference systems are.

@GamingMinds-DanielC
Copy link
Contributor

I modified the code like this:
...
But it doesn't work...

Looks like a simple sign error. You subtract the cursor position just like the window position when transforming the mouse position into image space, that should be correct. But to calculate the scroll position, you should then ADD the cursor position, just like the window position, not subtract it again.

@thewoz
Copy link
Author

thewoz commented Apr 17, 2024

I am so sorry I put the wrong code.
If above I subtract the cursor position below I have to put it back. However this way doesn't work.
I was able to calculate the right mouse position on the image by doing this:

ImVec2 cursorPos = (ImGui::GetWindowSize() - imageSize) * 0.5f;
ImGui::SetCursorPos(cursorPos);

...

mousePosInImage.x = (ImGui::GetIO().MousePos.x - ImGui::GetWindowPos().x - cursorPos.x + ImGui::GetScrollX()) / imageScale;
mousePosInImage.y = (ImGui::GetIO().MousePos.y - ImGui::GetWindowPos().y - cursorPos.y + ImGui::GetScrollY()) / imageScale;

While even if I calculate the scroll position in this way.
The result is baggy. The zoom no longer works well.

scroll.x = mousePosInImage.x * imageScale + ImGui::GetWindowPos().x + cursorPos.x - ImGui::GetIO().MousePos.x;
scroll.y = mousePosInImage.y * imageScale + ImGui::GetWindowPos().y + cursorPos.y - ImGui::GetIO().MousePos.y;
test1.mov

I noticed that even if I don't change ImGui::SetCursorPos and go back to this version of the code.
I had a problem with the scroll.
That is, as long as I zoom or drag the image the scroll position seems correct. But I can't touch the scroll itself it makes strange jumps and goes back.

test2.mov

@GamingMinds-DanielC
Copy link
Contributor

The jumping back after letting go of the scroll bars is easy to fix. You set the scroll position before beginning the window, but you never update it unless you are dragging or scrolling the image. You need to update your scroll with the current scroll values right after beginning the window, that's where the scroll bars take effect.

As for the zooming, try this: adjust the cursor position for the new image size before calculating the scroll position.

@thewoz
Copy link
Author

thewoz commented Apr 18, 2024

Thank you very much for all the suggestions you gave me.
I solved the jumping problem in this way. I don't know if that was what you meant, but it works.
Here what I did:

    ImGui::Begin("Left", NULL, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);

    ImGui::GetStyle().WindowPadding.x = 0;
    ImGui::GetStyle().WindowPadding.y = 0;

    [OpenGL Texture Stuff]
    
    imageSize = ImVec2(imageScale * image.size().width, imageScale * image.size().height);
            
    ImGui::SetCursorPos(cursorPos);
     
    ImGui::Image(reinterpret_cast<void*>(static_cast<intptr_t>(texture)), imageSize);
    
    ImGui::SetItemKeyOwner(ImGuiKey_MouseWheelY);
    
    scroll.x = ImGui::GetScrollX();
    scroll.y = ImGui::GetScrollY();
        
    // If the mouse is over the image
    if(ImGui::IsItemHovered()) {

      ImVec2 mousePosInImage;
      mousePosInImage.x = (ImGui::GetIO().MousePos.x - ImGui::GetWindowPos().x - cursorPos.x + ImGui::GetScrollX()) / imageScale;
      mousePosInImage.y = (ImGui::GetIO().MousePos.y - ImGui::GetWindowPos().y - cursorPos.y + ImGui::GetScrollY()) / imageScale;
      
      // whether the wheel has been moved
      if(ImGui::GetIO().MouseWheel != 0 && ImGui::GetIO().KeyCtrl) {
        
        imageScale = imageScale * pow(1.50, ImGui::GetIO().MouseWheel);
        
        if(imageScale < 0.1) imageScale = 0.1;
        if(imageScale > 30)  imageScale = 30;
     
        imageSize = ImVec2(imageScale * image.size().width, imageScale * image.size().height);
        
        //cursorPos = (ImGui::GetWindowSize() - imageSize) * 0.5f;

        scroll.x = mousePosInImage.x * imageScale + ImGui::GetWindowPos().x + cursorPos.x - ImGui::GetIO().MousePos.x;
        scroll.y = mousePosInImage.y * imageScale + ImGui::GetWindowPos().y + cursorPos.y - ImGui::GetIO().MousePos.y;
        
      }
      
      // if I am dragging the image
      if(ImGui::IsMouseDragging(ImGuiMouseButton_Left, 0.0f)) {
        scroll.x = ImGui::GetScrollX() - ImGui::GetIO().MouseDelta.x;
        scroll.y = ImGui::GetScrollY() - ImGui::GetIO().MouseDelta.y;
      }
      
    }
    
    ImGui::End();

However I am really sorry, and I feel so bad, to bother you again.
I'm still struggling to fix the zoom with the centered image. I can't quite figure it out.
If I uncomment this line zoom no longer works well

cursorPos = (ImGui::GetWindowSize() - imageSize) * 0.5f;

I noticed is that, among other things, it no longer lets me scroll to the top left and right once I zoom in on the image.

b.mov

@GamingMinds-DanielC
Copy link
Contributor

Thank you very much for all the suggestions you gave me. I solved the jumping problem in this way. I don't know if that was what you meant, but it works. Here what I did:

Yes, that's basically what I meant with updating the scroll values after ImGui::Begin(). There are a few lines in between in your version, but it is functionally equivalent.

I noticed is that, among other things, it no longer lets me scroll to the top left and right once I zoom in on the image.

To be safe, you should calculate cursorPos both times you calculate imageSize, not only when zooming. Window sizes can change after all.

Not being able to scroll to the top left is because your cursor position gets negative values when the image is bigger than the window. When calculating the cursor position (both times) clamp the coordinates to 0 on the low end, then this shouldn't happen anymore.

Another thing that comes to mind: instead of ImGui::GetWindowPos(), try ImGui::GetContentRegionAvail() (must be retrieved once before setting the cursor position). That way the size lost to window decorations (in this case scroll bars) gets taken into account as well.

@thewoz
Copy link
Author

thewoz commented Apr 23, 2024

Hello,
Thank you very much again for the replies.
I have done various tests and finally this seems to be the best solution.
I also tried using ImGui::GetContentRegionAvail() but it doesn't seem to improve the situation.

    ImGui::SetNextWindowClass(&window_class);
    ImGui::SetNextWindowContentSize(imageSize);
    ImGui::SetNextWindowScroll(scroll);

    ImGui::Begin("Left", NULL, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);

    ImGui::GetStyle().WindowPadding.x = 0;
    ImGui::GetStyle().WindowPadding.y = 0;

    [OpenGL Image Stuff]
    
    imageSize = ImVec2(imageScale * image.size().width, imageScale * image.size().height);
            
    cursorPos = (ImGui::GetWindowSize() - imageSize) * 0.5f;
    if(cursorPos.x < 0) cursorPos.x = 0;
    if(cursorPos.y < 0) cursorPos.y = 0;

    ImGui::SetCursorPos(cursorPos);
     
    ImGui::Image(reinterpret_cast<void*>(static_cast<intptr_t>(texture)), imageSize);
    
    ImGui::SetItemKeyOwner(ImGuiKey_MouseWheelY);
    
    scroll.x = ImGui::GetScrollX();
    scroll.y = ImGui::GetScrollY();
        
    // If the mouse is over the image
    if(ImGui::IsItemHovered()) {

      ImVec2 mousePosInImage;
      mousePosInImage.x = (ImGui::GetIO().MousePos.x - ImGui::GetWindowPos().x - cursorPos.x + ImGui::GetScrollX()) / imageScale;
      mousePosInImage.y = (ImGui::GetIO().MousePos.y - ImGui::GetWindowPos().y - cursorPos.y + ImGui::GetScrollY()) / imageScale;
     
      // whether the wheel has been moved
      if(ImGui::GetIO().MouseWheel != 0 && ImGui::GetIO().KeyCtrl) {
        
        imageScale = imageScale * pow(1.50, ImGui::GetIO().MouseWheel);
        
        if(imageScale < 0.1) imageScale = 0.1;
        if(imageScale > 30)  imageScale = 30;
     
        imageSize = ImVec2(imageScale * image.size().width, imageScale * image.size().height);
        
        cursorPos = (ImGui::GetWindowSize() - imageSize) * 0.5f;

        if(cursorPos.x < 0) cursorPos.x = 0;
        if(cursorPos.y < 0) cursorPos.y = 0;

        scroll.x = mousePosInImage.x * imageScale + ImGui::GetWindowPos().x + cursorPos.x - ImGui::GetIO().MousePos.x;
        scroll.y = mousePosInImage.y * imageScale + ImGui::GetWindowPos().y + cursorPos.y - ImGui::GetIO().MousePos.y;
        
      }
      
      // if I am dragging the image
      if(ImGui::IsMouseDragging(ImGuiMouseButton_Left, 0.0f)) {
        scroll.x = ImGui::GetScrollX() - ImGui::GetIO().MouseDelta.x;
        scroll.y = ImGui::GetScrollY() - ImGui::GetIO().MouseDelta.y;
      }
      
    }
    
    ImGui::End();

However, two imperfections remain that I cannot understand.

The first is that the zoom does not follow the mouse until the "scroll bars" in some have moved.
In the video there is me trying to zoom in on the yellow hat in the lower right corner. Zooming in the magnification doesn't follow the mouse. So I move the mouse and repeat the operation. What you can see is that when both scroll bars have moved the zoom actually follows the mouse.

test1.mov

The second is that zooming in and out of the image there is a strange shift or anyway something doesn't fit.
In the video there is me zooming in on the pupil of the eye.
The mouse is not moved. I.e. the coordinates of ImGui::GetIO().MousePos always remain the same.
As you can see the zoom does not stay on the pupil but goes around.

cc.mov

In this sped-up video you can see that I don't move the mouse but the image below changes.

ccc.mov

It must have to do with either cursor position or scroll.
GetWindowPos() and GetWindowSize() always remain the same

@GamingMinds-DanielC
Copy link
Contributor

GamingMinds-DanielC commented Apr 23, 2024

The first is that the zoom does not follow the mouse until the "scroll bars" in some have moved. In the video there is me trying to zoom in on the yellow hat in the lower right corner. Zooming in the magnification doesn't follow the mouse. So I move the mouse and repeat the operation. What you can see is that when both scroll bars have moved the zoom actually follows the mouse.

ImGui doesn't let you scroll past the content, not even when setting the scroll position programmatically. So as long as the image is smaller than the window, it will stay centered, ignoring the mouse position when zooming. That is normal. If you want a different behavior, you could add a border around your image. That border can be empty space, but it should be independent of your image scale. If you make it f.e. 90% of the window size, you can scroll a tiny image to any edge of the window without it leaving the display area entirely. But you will have more things to keep track of and need to compensate.

The second is that zooming in and out of the image there is a strange shift or anyway something doesn't fit. In the video there is me zooming in on the pupil of the eye. The mouse is not moved. I.e. the coordinates of ImGui::GetIO().MousePos always remain the same. As you can see the zoom does not stay on the pupil but goes around.

ImGui often snaps things to pixel positions for better visuals (like f.e. sharp text). This looks like rounding errors accumulating over time. To get rid of this effect, you most likely have to reimplement your canvas with everything you learned, but with a different approach. Don't use the cursor at all, draw manually, round only for display purposes but keep exact values, ... But even without rounding to integers, the floating point format will accumulate errors over time. At some point you will have to stop with "good enough".

Small update:

I also tried using ImGui::GetContentRegionAvail() but it doesn't seem to improve the situation.

In the first video of your last post, look at the 10 second timestamp, the top border is bigger than the bottom one because the scroll bars don't get taken into account. Same with the left and right borders at 16 seconds. The effect is there the entire time, just easier to see at those timestamps. Your image is not correctly centered because you use the window size (includes scroll bars) instead of the content region.

@thewoz
Copy link
Author

thewoz commented Apr 23, 2024

Hi @GamingMinds-DanielC
Thank you so much for all the help.
I understand perfectly what you mean.
I think for now the "good enough" for me has come.
Maybe in a future, when the rest of the project is finished, I will come back to this part of the code.
I attach the latest version of the complete code hoping it will be useful to someone in the future.

#include <cstdio>
#include <cstdlib>

#include <opencv2/opencv.hpp>

#include <glad/glad.h>

#include <GLFW/glfw3.h>

#define IMGUI_DEFINE_MATH_OPERATORS
#include <imgui/imgui.hpp>

static void glfwErrorCallback(int error, const char * description) {
  fprintf(stderr, "GLFW error (%d): %s\n", error, description);
}

//****************************************************************************/
// main
//****************************************************************************/
int main(int argc, const char * argv[]) {

  glfwSetErrorCallback(glfwErrorCallback);

  if(!glfwInit()) {
    fprintf(stderr, "GLFW init error\n");
    exit(EXIT_FAILURE);
  }
  
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
  glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
  glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
  glfwWindowHint(GLFW_SAMPLES, 4);
  
  // Create a GLFWwindow object that we can use for GLFW's functions
  GLFWwindow * window = glfwCreateWindow(800, 600, "Image Test", NULL, NULL);
  
  if(window == NULL) {
    fprintf(stderr, "Failed to create GLFW window\n");
    glfwTerminate();
    exit(EXIT_FAILURE);
  }
  
  glfwMakeContextCurrent(window);
  
  if(!gladLoadGLLoader((GLADloadproc) glfwGetProcAddress)) {
    fprintf(stderr, "Failed to initialize GLAD\n");
    abort();
  }
  
  glfwSwapInterval(1);

  IMGUI_CHECKVERSION();

  ImGui::CreateContext();
  ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable;
  ImGui_ImplGlfw_InitForOpenGL(window, true);
  ImGui_ImplOpenGL3_Init("#version 150");
  ImGui::StyleColorsDark();

  cv::Mat image = cv::imread("/Users/thewoz/Dropbox/Research/COBBS/test/Lena.png", cv::IMREAD_COLOR);
  
  cv::cvtColor(image, image, cv::COLOR_BGR2RGBA);

  float imageScale = 1;

  ImVec2 scroll = ImVec2(0,0);
  ImVec2 imageSize = ImVec2(imageScale * image.size().width, imageScale * image.size().height);
  ImVec2 cursorPos = ImVec2(0,0);

  while(!glfwWindowShouldClose(window)) {
    
    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplGlfw_NewFrame();
    ImGui::NewFrame();
    
    ImGuiViewport * viewport = ImGui::GetMainViewport();
    
    // Set viewport explicitly so GetFrameHeight reacts to DPI changes
    ImGui::SetCurrentViewport(nullptr, (ImGuiViewportP*)viewport);
    
    float height = ImGui::GetFrameHeight();
    
    if(ImGui::BeginMainMenuBar()) {
      if(ImGui::BeginMenu("Menu")) {
        ImGui::MenuItem("Hello!", NULL, false, true);
        ImGui::EndMenu();
      }
      ImGui::EndMainMenuBar();
    }
    
    if(ImGui::BeginViewportSideBar("StatusBar", viewport, ImGuiDir_Down, height, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar)) {
        if(ImGui::BeginMenuBar()) {
          ImGui::Text("Zoom: ");
          ImGui::SameLine();
          ImGui::Text("Frame: 1/1");
          ImGui::EndMenuBar();
        }
        ImGui::End();
    }
    
    ImGuiID mainDockSpaceId = ImGui::DockSpaceOverViewport();
    
    ImGuiWindowClass window_class;
    window_class.DockNodeFlagsOverrideSet = ImGuiDockNodeFlags_NoTabBar;
    
    ImGui::SetNextWindowClass(&window_class);
    ImGui::Begin("Down", NULL, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar);
    ImGui::End();
    
    ImGui::SetNextWindowClass(&window_class);
    ImGui::SetNextWindowContentSize(imageSize);
    ImGui::SetNextWindowScroll(scroll);

    ImGui::Begin("Left", NULL, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);

    ImGui::GetStyle().WindowPadding.x = 0;
    ImGui::GetStyle().WindowPadding.y = 0;

    GLuint texture;
    glGenTextures(1, &texture); // FIXME: There is a memory leak here. It should be done only the first time.
    
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
        
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.cols, image.rows, 0, GL_RGBA, GL_UNSIGNED_BYTE, image.data);
    
    imageSize = ImVec2(imageScale * image.size().width, imageScale * image.size().height);
            
    cursorPos = (ImGui::GetWindowSize() - imageSize) * 0.5f;
    if(cursorPos.x < 0) cursorPos.x = 0;
    if(cursorPos.y < 0) cursorPos.y = 0;

    ImGui::SetCursorPos(cursorPos);
     
    ImGui::Image(reinterpret_cast<void*>(static_cast<intptr_t>(texture)), imageSize);
    
    ImGui::SetItemKeyOwner(ImGuiKey_MouseWheelY);
    
    scroll.x = ImGui::GetScrollX();
    scroll.y = ImGui::GetScrollY();
      
       // If the mouse is over the image
    if(ImGui::IsItemHovered()) {

      ImVec2 mousePosInImage;
      mousePosInImage.x = (ImGui::GetIO().MousePos.x - ImGui::GetWindowPos().x - cursorPos.x + ImGui::GetScrollX()) / imageScale;
      mousePosInImage.y = (ImGui::GetIO().MousePos.y - ImGui::GetWindowPos().y - cursorPos.y + ImGui::GetScrollY()) / imageScale;
      
      // whether the wheel has been moved
      if(ImGui::GetIO().MouseWheel != 0 && ImGui::GetIO().KeyCtrl) {
        
        imageScale = imageScale * pow(1.50, ImGui::GetIO().MouseWheel);
        
        if(imageScale < 0.1) imageScale = 0.1;
        if(imageScale < 0.9) imageScale = 0.9;
        if(imageScale > 30)  imageScale = 30;
     
        imageSize = ImVec2(imageScale * image.size().width, imageScale * image.size().height);
        
        cursorPos = (ImGui::GetWindowSize() - imageSize) * 0.5f;

        if(cursorPos.x < 0) cursorPos.x = 0;
        if(cursorPos.y < 0) cursorPos.y = 0;

        scroll.x = mousePosInImage.x * imageScale + ImGui::GetWindowPos().x + cursorPos.x - ImGui::GetIO().MousePos.x;
        scroll.y = mousePosInImage.y * imageScale + ImGui::GetWindowPos().y + cursorPos.y - ImGui::GetIO().MousePos.y;
        
      }
      
      // if I am dragging the image
      if(ImGui::IsMouseDragging(ImGuiMouseButton_Left, 0.0f)) {
        scroll.x = ImGui::GetScrollX() - ImGui::GetIO().MouseDelta.x;
        scroll.y = ImGui::GetScrollY() - ImGui::GetIO().MouseDelta.y;
      }
      
    }
    
    ImGui::End();
    
    ImGui::SetNextWindowClass(&window_class);
    ImGui::Begin("Right", NULL, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar);
    ImGui::End();
    
    static bool sFirstFrame = true;
    if(sFirstFrame) {
      
      sFirstFrame = false;
      
      ImGui::DockBuilderRemoveNode(mainDockSpaceId);
      ImGui::DockBuilderAddNode(mainDockSpaceId, ImGuiDockNodeFlags_None);
      
      ImGuiID dock_id_up;
      ImGuiID dock_id_down;
      
      ImGuiID dock_id_left;
      ImGuiID dock_id_right;
      
      ImGui::DockBuilderSplitNode(mainDockSpaceId, ImGuiDir_Up, 0.5f, &dock_id_up, &dock_id_down);
      
      ImGui::DockBuilderSplitNode(dock_id_up, ImGuiDir_Right, 0.5f, &dock_id_right, &dock_id_left);
      
      ImGui::DockBuilderDockWindow("Down", dock_id_down);
      ImGui::DockBuilderDockWindow("Left", dock_id_left);
      ImGui::DockBuilderDockWindow("Right", dock_id_right);
      
      ImGui::DockBuilderFinish(mainDockSpaceId);
      
    }
   
    ImGui::Render();
    
    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
   
    glfwPollEvents();

    glfwSwapBuffers(window);
    
  }
  
  glfwTerminate();

  return 0;
  
}

@ocornut
Copy link
Owner

ocornut commented Apr 23, 2024

ImGui doesn't let you scroll past the content, not even when setting the scroll position programmatically. So as long as the image is smaller than the window, it will stay centered, ignoring the mouse position when zooming. That is normal. If you want a different behavior, you could add a border around your image.

That's also what you can override with SetNextWindowContentSize().

I suppose we could aim to provide an demo applet for this, with zooming and navigation.
(While we don't have access to textures in the demo we could manually display a shape of any kind)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants