pax_global_header00006660000000000000000000000064150025670330014513gustar00rootroot0000000000000052 comment=eb152b40e27305bf4d9ffc08cd6bfe4f46b77166 GL_image_display-0.23/000077500000000000000000000000001500256703300146305ustar00rootroot00000000000000GL_image_display-0.23/.gitignore000066400000000000000000000002701500256703300166170ustar00rootroot00000000000000*~ *.glsl.h *.docstring.h *.so* *.d *.o sfmviz-test cscope.* __pycache__ *.rej GL_image_display-test-glut GL_image_display-test-fltk Fl_Gl_Image_Widget_pywrap.* Fl_Gl_Image_Widget.py GL_image_display-0.23/Fl_Gl_Image_Widget.cc000066400000000000000000000501511500256703300205310ustar00rootroot00000000000000#include "Fl_Gl_Image_Widget.hh" #include "util.h" #include #include #include Fl_Gl_Image_Widget::DeferredInitCache::DeferredInitCache() : image_filename(NULL), image_data(NULL), line_segment_sets(NULL), Nline_segment_sets(0) { } Fl_Gl_Image_Widget::DeferredInitCache::~DeferredInitCache() { dealloc_update_image(); dealloc_set_lines(); } void Fl_Gl_Image_Widget::DeferredInitCache::dealloc_update_image(void) { free((void*)image_filename); image_filename = NULL; free((void*)image_data); image_data = NULL; } void Fl_Gl_Image_Widget::DeferredInitCache::dealloc_set_lines(void) { for(int i=0; ipoints); } free(line_segment_sets); line_segment_sets = NULL; Nline_segment_sets = 0; } bool Fl_Gl_Image_Widget::DeferredInitCache::save_update_image ( int _decimation_level, bool _flip_x, bool _flip_y, const char* _image_filename, const char* _image_data, int _image_width, int _image_height, int _image_bpp, int _image_pitch) { dealloc_update_image(); if(_image_filename != NULL) { image_filename = strdup(_image_filename); if(image_filename == NULL) { MSG("strdup(_image_filename) failed! Giving up"); dealloc_update_image(); return false; } } else if(_image_data != NULL) { if(!(_image_bpp == 8 || _image_bpp == 24)) { MSG("I support only 8 bits-per-pixel images and 24 bits-per-pixel images. Got %d", _image_bpp); return false; } if(_image_pitch <= 0) { _image_pitch = _image_width * _image_bpp/8; } const int size = _image_pitch*_image_height; image_data = (char*)malloc(size); if(image_data == NULL) { MSG("malloc(image_size) failed! Giving up"); dealloc_update_image(); return false; } memcpy(image_data, _image_data, size); } decimation_level = _decimation_level; flip_x = _flip_x; flip_y = _flip_y; image_width = _image_width; image_height = _image_height; image_bpp = _image_bpp; image_pitch = _image_pitch; return true; } bool Fl_Gl_Image_Widget::DeferredInitCache::save_set_lines ( const GL_image_display_line_segments_t* _line_segment_sets, int _Nline_segment_sets) { dealloc_set_lines(); line_segment_sets = (GL_image_display_line_segments_t*) malloc(Nline_segment_sets * sizeof(line_segment_sets[0])); if(line_segment_sets == NULL) { MSG("malloc() failed"); dealloc_set_lines(); return false; } memset((void*)line_segment_sets,0,Nline_segment_sets * sizeof(line_segment_sets[0])); Nline_segment_sets = _Nline_segment_sets; for(int i=0; isegments.Nsegments*2*2; set->points = (float*)malloc(Npoints*sizeof(set->points[0])); if(set->points == NULL) { MSG("malloc() failed"); dealloc_set_lines(); return false; } memcpy((void*)set->points, (const void*)_set->points, Npoints*sizeof(set->points[0])); set->segments = _set->segments; } return true; } bool Fl_Gl_Image_Widget::DeferredInitCache::apply(Fl_Gl_Image_Widget* w) { // image_filename and image_data may be NULL. In that case update_image2() // will paint the viewport black bool result1 = w->update_image2(decimation_level, flip_x, flip_y, image_filename, image_data, image_width, image_height, image_bpp, image_pitch); dealloc_update_image(); bool result2 = w->set_lines(line_segment_sets, Nline_segment_sets); dealloc_set_lines(); return result1 && result2; } Fl_Gl_Image_Widget::Fl_Gl_Image_Widget(int x, int y, int w, int h, // On some hardware (i915 for instance) // double-buffering causes redrawing bugs // (the window sometimes is never // updated), so disabling // double-buffering is a good workaround. // In general, single-buffering causes // redraw flicker, so double-buffering is // recommended where possible bool double_buffered) : Fl_Gl_Window(x, y, w, h), m_deferred_init_cache() { /* Here I don't ask for FL_OPENGL3. This is due a a bug in my graphics driver or fltk or something like that. If I have Intel integrated graphics (i915 or uhd620), then the (FL_OPENGL3 | FL_DOUBLE) combination doesn't work right: lots of redraws are missed for whatever reason, and the user gets either nothing or an out-of-date frame. Turning FL_DOUBLE off fixes THAT, but then the horizonator point picking doesn't work: glReadPixels(..., GL_DEPTH_COMPONENT, ...) returns an error. For some reason, omitting FL_OPENGL3 fixes the issues. That is despite the horizonator using a geometry shader, which requires at LEAST opengl 3.2. There's a related-looking bug report: https://github.com/fltk/fltk/issues/1005 but the conclusion isn't clear to me. For the time being I simply disable FL_OPENGL3, and move on. More investigation and maybe a good bug report would be a good thing to do later */ const int m = // FL_OPENGL3 | FL_RGB | (double_buffered ? FL_DOUBLE : 0); mode(m); Fl::gl_visual(m); memset(&m_ctx, 0, sizeof(m_ctx)); } Fl_Gl_Image_Widget::~Fl_Gl_Image_Widget() { if(m_ctx.did_init) { make_current(); GL_image_display_deinit(&m_ctx); } } void Fl_Gl_Image_Widget::draw(void) { make_current(); if(!m_ctx.did_init) { if(!GL_image_display_init( &m_ctx, false)) { MSG("GL_image_display_init() failed. Giving up"); return; } if(!m_deferred_init_cache.apply(this)) return; } if(!valid()) GL_image_display_resize_viewport(&m_ctx, pixel_w(), pixel_h()); GL_image_display_redraw(&m_ctx); } bool Fl_Gl_Image_Widget::process_mousewheel_zoom(double dy, double event_x, double event_y, double viewport_width, double viewport_height) { make_current(); double z = 1. + 0.2*dy; z = fmin(fmax(z, 0.4), 2.); // logic follows vertex.glsl // // I affect the zoom by scaling visible_width_pixels. I need to // compute the new center coords that keep the pixel under the // mouse in the same spot. Let's say I'm at viewport pixel qv, // and image pixel qi. I then have (as in // GL_image_display_map_pixel_image_from_viewport) // // qix = ((((qvx+0.5)/viewport_width)*2-1)/(2*aspect_x)*visible_width01+center01_x)*image_width - 0.5 // // I want qvx,qix to be invariant, so I choose center01_x to // compensate for the changes in visible_width01: // // ((((qvx+0.5)/viewport_width)*2-1)/(2*aspect_x)* visible_width01 +center01_x )*image_width - 0.5 = // ((((qvx+0.5)/viewport_width)*2-1)/(2*aspect_x)*(visible_width01*z)+center01_x_new)*image_width - 0.5 // // (((qvx+0.5)/viewport_width)*2-1)/(2*aspect_x)* visible_width01 +center01_x = // (((qvx+0.5)/viewport_width)*2-1)/(2*aspect_x)*(visible_width01*z)+center01_x_new // // center01_x_new = center01_x + // (((qvx+0.5)/viewport_width)*2-1)/(2*aspect_x)* visible_width01*(1-z) // x_centerpixel is center01_x/image_width double qvx = event_x; double qvy = event_y; return set_panzoom( m_ctx.x_centerpixel + (((qvx+0.5)/viewport_width)*2.-1.)/(2.*m_ctx.aspect_x) * m_ctx.visible_width01*(1.-z) * (double)m_ctx.image_width, m_ctx.y_centerpixel - (((1. - (qvy+0.5)/viewport_height))*2.-1.)/(2.*m_ctx.aspect_y) * m_ctx.visible_width01*(1.-z) * (double)m_ctx.image_height, m_ctx.visible_width_pixels * z ); } bool Fl_Gl_Image_Widget::process_mousewheel_pan(double dx, double dy, double viewport_width, double viewport_height) { make_current(); return set_panzoom(m_ctx.x_centerpixel + 50. * dx * m_ctx.visible_width_pixels / viewport_width, m_ctx.y_centerpixel + 50. * dy * m_ctx.visible_width_pixels / viewport_height, m_ctx.visible_width_pixels); } bool Fl_Gl_Image_Widget::process_mousedrag_pan(double dx, double dy, double viewport_width, double viewport_height) { make_current(); // logic follows vertex.glsl // // I need to compute the new center coords that keep the pixel under // the mouse in the same spot. Let's say I'm at viewport pixel qv, // and image pixel qi. I then have (looking at scaling only, // ignoring ALL translations) // // qvx/viewport_width ~ // qix/image_width / visible_width01*aspectx // // -> qix ~ qvx*visible_width01/aspectx/viewport_width*image_width // // I want to always point at the same pixel: qix is constant. // Changes in qvx should be compensated by moving centerx. Since I'm // looking at relative changes only, I don't care about the // translations, and they could be ignored in the above expression return set_panzoom( m_ctx.x_centerpixel - dx * m_ctx.visible_width01 / (m_ctx.aspect_x * viewport_width) * (double)m_ctx.image_width, m_ctx.y_centerpixel - dy * m_ctx.visible_width01 / (m_ctx.aspect_y * viewport_height) * (double)m_ctx.image_height, m_ctx.visible_width_pixels); } bool Fl_Gl_Image_Widget::process_keyboard_panzoom_orig(void) { return process_keyboard_panzoom(); } bool Fl_Gl_Image_Widget::process_keyboard_panzoom(void) { make_current(); return set_panzoom( ((double)m_ctx.image_width - 1.0f)/2., ((double)m_ctx.image_height - 1.0f)/2., m_ctx.image_width); } int Fl_Gl_Image_Widget::handle(int event) { switch(event) { case FL_SHOW: if(shown()) make_current(); break; case FL_FOCUS: return 1; case FL_MOUSEWHEEL: if(m_ctx.did_init && m_ctx.did_init_texture && m_ctx.did_set_panzoom) { if( (Fl::event_state() & FL_CTRL) && Fl::event_dy() != 0) { // control + wheelup/down: zoom if(!process_mousewheel_zoom((double)Fl::event_dy(), (double)Fl::event_x(), (double)Fl::event_y(), (double)pixel_w(), (double)pixel_h())) { MSG("process_mousewheel_zoom() failed. Trying to continue..."); return 1; } return 1; } else { // no control: the wheel pans // I encourage straight motions int dx = Fl::event_dx(); int dy = Fl::event_dy(); if( abs(dy) > abs(dx)) dx = 0; else dy = 0; if(!process_mousewheel_pan((double)dx, (double)dy, (double)pixel_w(), (double)pixel_h())) { MSG("process_mousewheel_pan() failed. Trying to continue..."); return 1; } return 1; } } break; case FL_PUSH: if(Fl::event_button() == FL_LEFT_MOUSE) { // I pan and zoom with left-click-and-drag m_last_drag_update_xy[0] = Fl::event_x(); m_last_drag_update_xy[1] = Fl::event_y(); return 1; } break; case FL_DRAG: // I pan and zoom with left-click-and-drag if(m_ctx.did_init && m_ctx.did_init_texture && m_ctx.did_set_panzoom && (Fl::event_state() & FL_BUTTON1)) { if(!process_mousedrag_pan((double)Fl::event_x() - m_last_drag_update_xy[0], (double)Fl::event_y() - m_last_drag_update_xy[1], (double)pixel_w(), (double)pixel_h())) { MSG("process_mousedrag_pan() failed. Trying to continue..."); return 1; } m_last_drag_update_xy[0] = Fl::event_x(); m_last_drag_update_xy[1] = Fl::event_y(); return 1; } break; case FL_ENTER: // Focus follows mouse. I want to be able to receive the 'u' button take_focus(); return 1; case FL_KEYUP: if(m_ctx.did_init && m_ctx.did_init_texture && Fl::event_key() == 'u') { if(!process_keyboard_panzoom()) { MSG("process_keyboard_panzoom() failed. Trying to continue..."); return 1; } return 1; } break; } return Fl_Gl_Window::handle(event); } bool Fl_Gl_Image_Widget::update_image2(int decimation_level, bool flip_x, bool flip_y, // Either this should be given const char* image_filename, // Or these should be given const char* image_data, int image_width, int image_height, int image_bpp, int image_pitch) { make_current(); if(!m_ctx.did_init) { // If the GL context wasn't inited yet, I must init it first. BUT in // order to init it, some things about the X window must be set up. // I cannot rely on them being set up here, so I init stuff only in // the draw() call below. If I try to init here, I see this: // // python3: ../src/dispatch_common.c:868: epoxy_get_proc_address: Assertion `0 && "Couldn't find current GLX or EGL context.\n"' failed. // // So I save the data in this call, and apply it later, when I'm // ready if(!GL_image_display_update_image__validate_input(image_filename, image_data, image_width, image_height, image_bpp, true)) { MSG("Deferred update_image call failed validation"); return false; } if(!m_deferred_init_cache.save_update_image(decimation_level, flip_x, flip_y, image_filename, image_data, image_width, image_height, image_bpp, image_pitch)) { MSG("m_deferred_init_cache.save_update_image() failed"); return false; } return true; } // have new image to ingest if( !GL_image_display_update_image2(&m_ctx, decimation_level, flip_x, flip_y, image_filename, image_data,image_width,image_height,image_bpp,image_pitch) ) { MSG("GL_image_display_update_image() failed"); return false; } redraw(); return true; } // For legacy compatibility. Calls update_image2() with flip_x, flip_y = false bool Fl_Gl_Image_Widget::update_image( int decimation_level, // Either this should be given const char* image_filename, // Or these should be given const char* image_data, int image_width, int image_height, int image_bpp, int image_pitch) { return update_image2(decimation_level, false, false, image_filename, image_data, image_width, image_height, image_bpp, image_pitch); } bool Fl_Gl_Image_Widget::set_panzoom(double x_centerpixel, double y_centerpixel, double visible_width_pixels) { make_current(); bool result = GL_image_display_set_panzoom(&m_ctx, x_centerpixel, y_centerpixel, visible_width_pixels); redraw(); return result; } bool Fl_Gl_Image_Widget::map_pixel_viewport_from_image(double* xout, double* yout, double x, double y) { return GL_image_display_map_pixel_viewport_from_image(&m_ctx, xout, yout, x, y); } bool Fl_Gl_Image_Widget::map_pixel_image_from_viewport(double* xout, double* yout, double x, double y) { return GL_image_display_map_pixel_image_from_viewport(&m_ctx, xout, yout, x, y); } bool Fl_Gl_Image_Widget::set_lines(const GL_image_display_line_segments_t* line_segment_sets, int Nline_segment_sets) { make_current(); if(!m_ctx.did_init) { // Need to save the inputs, and do this later. See docs for // Fl_Gl_Image_Widget::update_image2() if(!m_deferred_init_cache.save_set_lines(line_segment_sets, Nline_segment_sets)) { MSG("m_deferred_init_cache.save_set_lines() failed"); return false; } return true; } bool result = GL_image_display_set_lines(&m_ctx, line_segment_sets, Nline_segment_sets); redraw(); return result; } GL_image_display-0.23/Fl_Gl_Image_Widget.hh000066400000000000000000000121611500256703300205420ustar00rootroot00000000000000#pragma once extern "C" { #include "GL_image_display.h" } #include class Fl_Gl_Image_Widget : public Fl_Gl_Window { protected: GL_image_display_context_t m_ctx; int m_last_drag_update_xy[2]; struct DeferredInitCache { DeferredInitCache(); ~DeferredInitCache(); void dealloc_update_image(void); void dealloc_set_lines (void); bool save_update_image( int _decimation_level, bool _flip_x, bool _flip_y, const char* _image_filename, const char* _image_data, int _image_width, int _image_height, int _image_bpp, int _image_pitch); bool save_set_lines(const GL_image_display_line_segments_t* _line_segment_sets, int _Nline_segment_sets); bool apply(Fl_Gl_Image_Widget* w); // update_image int decimation_level; bool flip_x; bool flip_y; char* image_filename; char* image_data; int image_width; int image_height; int image_bpp; int image_pitch; // set_lines GL_image_display_line_segments_t* line_segment_sets; int Nline_segment_sets; } m_deferred_init_cache; public: Fl_Gl_Image_Widget(int x, int y, int w, int h, // On some hardware (i915 for instance) double-buffering // causes redrawing bugs (the window sometimes is never // updated), so disabling double-buffering is a good // workaround. In general, single-buffering causes redraw // flicker, so double-buffering is recommended where // possible bool double_buffered = true); virtual ~Fl_Gl_Image_Widget(); void draw(void); virtual int handle(int event); /////// C API wrappers /////// /////// these directly wrap the GL_image_display.h C API. The arguments and /////// function names are the same, except for the leading context: we pass /////// &m_ctx bool update_image2(int decimation_level = 0, bool flip_x = false, bool flip_y = false, // Either this should be given const char* image_filename = NULL, // Or these should be given const char* image_data = NULL, int image_width = 0, int image_height = 0, int image_bpp = 0, int image_pitch = 0); // For legacy compatibility. Calls update_image2() with flip_x, flip_y = false bool update_image( int decimation_level = 0, // Either this should be given const char* image_filename = NULL, // Or these should be given const char* image_data = NULL, int image_width = 0, int image_height = 0, int image_bpp = 0, int image_pitch = 0); // This is virtual, so a subclass could override the implementation virtual bool set_panzoom(double x_centerpixel, double y_centerpixel, double visible_width_pixels); bool map_pixel_viewport_from_image(double* xout, double* yout, double x, double y); bool map_pixel_image_from_viewport(double* xout, double* yout, double x, double y); bool set_lines(const GL_image_display_line_segments_t* line_segment_sets, int Nline_segment_sets); // internals of the interactive pan/zoom operations. Used primarily to // connect multiple Fl_Gl_Image_Widget together in interactive operations virtual bool process_mousewheel_zoom(double dy, double event_x, double event_y, double viewport_width, double viewport_height); virtual bool process_mousewheel_pan(double dx, double dy, double viewport_width, double viewport_height); virtual bool process_mousedrag_pan(double dx, double dy, double viewport_width, double viewport_height); // Old name for process_keyboard_panzoom(). For backwards compatibility only virtual bool process_keyboard_panzoom_orig(void); virtual bool process_keyboard_panzoom(void); }; GL_image_display-0.23/Fl_Gl_Image_Widget.i000066400000000000000000000603661500256703300204050ustar00rootroot00000000000000%define DOCSTRING """Wrapper module containing the Gl_Image_Widget class This is the FLTK widget in the GL_image_display project: https://github.com/dkogan/GL_image_display It displays an image in an FLTK widget, using OpenGL internally, which gives us efficient draws and redraws. The widget is intended to work with the FLTK GUI tookit: https://www.fltk.org/ with Python bindings provided by the pyfltk project: https://pyfltk.sourceforge.io/""" %enddef %module(docstring=DOCSTRING, directors="1", package="_Fl_Gl_Image_Widget") Fl_Gl_Image_Widget %feature("compactdefaultargs"); // Enable directors globally, except for some functions that are troublesome, //and don't obviously benefit from directors %feature("director"); %feature("nodirector") Fl_Gl_Image_Widget::show; %feature("nodirector") Fl_Gl_Image_Widget::draw; %feature("nodirector") Fl_Gl_Image_Widget::resize; %feature("nodirector") Fl_Gl_Image_Widget::hide; %feature("nodirector") Fl_Gl_Image_Widget::as_group; %feature("nodirector") Fl_Gl_Image_Widget::as_window; %feature("nodirector") Fl_Gl_Image_Widget::as_gl_window; %feature("nodirector") Fl_Gl_Image_Widget::flush; // Comes directly from pyfltk/swig/macros.i // Connect C++ exceptions to Python exceptions %feature("director:except") { if ($error != NULL) { throw Swig::DirectorMethodException(); } } %exception { try { $action } catch (Swig::DirectorException &e) { SWIG_fail; } } // ignore all variables -> no getters and setters %rename("$ignore",%$isvariable) ""; %feature("autodoc", "1"); %feature("docstring") ::Fl_Gl_Image_Widget """Gl_Image_Widget class: efficient image display in FLTK SYNOPSIS from fltk import * import Fl_Gl_Image_Widget w = Fl_Window(800, 600, 'Image display with Fl_Gl_Image_Widget') g = Fl_Gl_Image_Widget.Fl_Gl_Image_Widget(0,0, 800,600) w.resizable(w) w.end() w.show() g.update_image(image_filename = 'image.jpg') Fl.run() This is the FLTK widget in the GL_image_display project: https://github.com/dkogan/GL_image_display It displays an image in an FLTK widget, using OpenGL internally, which gives us efficient draws and redraws. The widget is intended to work with the FLTK GUI tookit: https://www.fltk.org/ with Python bindings provided by the pyfltk project: https://pyfltk.sourceforge.io/ """; %feature("docstring") Fl_Gl_Image_Widget::Fl_Gl_Image_Widget """Fl_Gl_Image_Widget constructor SYNOPSIS from fltk import * import Fl_Gl_Image_Widget w = Fl_Window(800, 600, 'Image display with Fl_Gl_Image_Widget') g = Fl_Gl_Image_Widget.Fl_Gl_Image_Widget(0,0, 800,600) w.resizable(w) w.end() w.show() g.update_image(image_filename = 'image.jpg') Fl.run() The Fl_Gl_Image_Widget is initialized like any other FLTK widget, using the sequential arguments: x, y, width, height. The data being displayed is NOT given to this method: Fl_Gl_Image_Widget.update_image() needs to be called to provide this data ARGUMENTS: - x: required integer that specifies the x pixel coordinate of the top-left corner of the widget - y: required integer that specifies the y pixel coordinate of the top-left corner of the widget - w: required integer that specifies the width of the widget - h: required integer that specifies the height of the widget - double_buffered: optional boolean, defaulting to True. On some hardware (i915 for instance) double-buffering causes redrawing bugs (the window sometimes is never updated), so disabling double-buffering is a good workaround. In general, single-buffering causes redraw flicker, so double-buffering is recommended where possible """; %feature("docstring") Fl_Gl_Image_Widget::draw """Fl_Gl_Image_Widget draw() routine This is a draw() method that all FLTK widgets have, and works the same way. Usually the end user does not need to call this method """; %feature("docstring") Fl_Gl_Image_Widget::handle """Event handling() routine This is the usual handle() method that all FLTK widgets use to process events. Usually the end user does not need to call this method """; %import(module="fltk") "FL/Fl_Group.H" %import(module="fltk") "FL/Fl_Widget.H" %import(module="fltk") "FL/Fl_Window.H" %import(module="fltk") "FL/Fl_Gl_Window.H" %{ #define NPY_NO_DEPRECATED_API NPY_API_VERSION #include "Fl_Gl_Image_Widget.hh" #include %} // Comes directly from pyfltk/swig/macros.i %define CHANGE_OWNERSHIP(name) %pythonappend name##::##name %{ if len(args) == 5: # retain reference to label self.my_label = args[-1] if self.parent() != None: # delegate ownership to C++ self.this.disown() %} %enddef CHANGE_OWNERSHIP(Fl_Gl_Image_Widget) %init %{ import_array(); %} // Errors should throw instead of returning false %typemap(out) bool { if(!$1) { PyErr_SetString(PyExc_RuntimeError, "Wrapped function failed!"); SWIG_fail; } Py_INCREF(Py_True); $result = Py_True; } %typemap(directorout) bool { $result = PyObject_IsTrue($1); } %typemap(in) (const char* image_data, int image_width, int image_height, int image_bpp, int image_pitch) { if($input == NULL || $input == Py_None) { $1 = NULL; $2 = 0; $3 = 0; $4 = 0; $5 = 0; } else { if(!PyArray_Check((PyArrayObject*)$input)) { PyErr_SetString(PyExc_RuntimeError, "update_image(): 'image_data' argument must be None or a numpy array"); SWIG_fail; } { const npy_intp* dims = PyArray_DIMS((PyArrayObject*)$input); int ndim = PyArray_NDIM((PyArrayObject*)$input); int type = PyArray_TYPE((PyArrayObject*)$input); const npy_intp* strides = PyArray_STRIDES((PyArrayObject*)$input); if (type != NPY_UINT8) { PyErr_Format(PyExc_RuntimeError, "update_image(): 'image_data' argument must be a numpy array with type=uint8. Got dtype=%d", type); SWIG_fail; } if(!(ndim == 2 || ndim == 3)) { PyErr_Format(PyExc_RuntimeError, "update_image(): 'image_data' argument must be None or a 2-dimensional or a 3-dimensional numpy array. Got %d-dimensional array", ndim); SWIG_fail; } if (ndim == 3) { if(dims[2] != 3) { PyErr_Format(PyExc_RuntimeError, "update_image(): 'image_data' argument is a 3-dimensional array. I expected the last dim to have length 3 (BGR), but it has length %d", dims[2]); SWIG_fail; } if(strides[2] != 1) { PyErr_Format(PyExc_RuntimeError, "update_image(): 'image_data' argument is a 3-dimensional array. The last dim (BGR) must be stored densely", dims[2]); SWIG_fail; } $4 = 24; } else $4 = 8; if (strides[1] != (ndim == 3 ? 3 : 1)) { PyErr_Format(PyExc_RuntimeError, "update_image(): 'image_data' argument must be a numpy array with each row stored densely"); SWIG_fail; } $1 = (char*)PyArray_DATA((PyArrayObject*)$input); $2 = dims[1]; $3 = dims[0]; $5 = strides[0]; } } } %feature("docstring") Fl_Gl_Image_Widget::update_image2 """Update the image being displayed in the widget SYNOPSIS from fltk import * import Fl_Gl_Image_Widget w = Fl_Window(800, 600, 'Image display with Fl_Gl_Image_Widget') g = Fl_Gl_Image_Widget.Fl_Gl_Image_Widget(0,0, 800,600) w.resizable(w) w.end() w.show() g.update_image(image_filename = 'image.jpg') Fl.run() The data displayed by the widget is providing using this update_image() method. This method is given the full-resolution data, subject to any decimation specified in decimation_level argument: - if decimation_level==0: the given image is displayed at full resolution - if decimation_level==1: the given image is displayed at half-resolution - if decimation_level==2: the given image is displayed at quarter-resolution and so on. This method may be called as many times as necessary. The decimation level and image dimensions MUST match those given in the first call to this function. The data may be passed-in to this method in one of three ways: - image_filename: the image is read from a file on disk, with the given filename. image_data must be None - image_data: the image data is read from the given numpy array. image_filename must be None - both image_filename and image_data are None: we draw a black image An exception is raised on error ARGUMENTS - image_filename: optional string, specifying the filename containing the image to display. Exclusive with image_data - image_data: optional numpy array containing the data being displayed. Must have dtype=np.uint8. Exclusive with image_filename. The array shape is either - (height,width) to pass a grayscale image - (height,width,3) to pass a BGR color image - decimation_level: optional integer, defaulting to 0. Specifies the resolution of the displayed image. - flip_x: optional boolean, defaulting to False. Display a left/right mirror of the image - flip_y: optional boolean, defaulting to False. Display an up/down mirror of the image """; %typemap(in, numinputs=0) (double* xout, double* yout) (double xout_temp, double yout_temp) { $1 = &xout_temp; $2 = &yout_temp; } %typemap(argout) (double* xout, double* yout) { $result = Py_BuildValue("(dd)", *$1, *$2); } %typemap(in) (double x, double y) { if(!PySequence_Check($input)) { PyErr_SetString(PyExc_RuntimeError, "Expected one argument: an iterable of length 2. This isn't an iterable"); SWIG_fail; } if(2 != PySequence_Length($input)) { PyErr_SetString(PyExc_RuntimeError, "Expected one argument: an iterable of length 2. This doesn't have length 2"); SWIG_fail; } { PyObject* o = PySequence_ITEM($input, 0); if(o == NULL) { PyErr_SetString(PyExc_RuntimeError, "Couldn't retrieve first element in the argument"); SWIG_fail; } $1 = PyFloat_AsDouble(o); Py_DECREF(o); if(PyErr_Occurred()) { PyErr_SetString(PyExc_RuntimeError, "Couldn't interpret first element as 'double'"); SWIG_fail; } } { PyObject* o = PySequence_ITEM($input, 1); if(o == NULL) { PyErr_SetString(PyExc_RuntimeError, "Couldn't retrieve second element in the argument"); SWIG_fail; } $2 = PyFloat_AsDouble(o); Py_DECREF(o); if(PyErr_Occurred()) { PyErr_SetString(PyExc_RuntimeError, "Couldn't interpret second element as 'double'"); SWIG_fail; } } } %feature("docstring") Fl_Gl_Image_Widget::map_pixel_image_from_viewport """Compute image pixel coords from viewport pixel coords SYNOPSIS from fltk import * from Fl_Gl_Image_Widget import Fl_Gl_Image_Widget class Fl_Gl_Image_Widget_Derived(Fl_Gl_Image_Widget): def handle(self, event): if event == FL_ENTER: return 1 if event == FL_MOVE: try: qi = self.map_pixel_image_from_viewport( (Fl.event_x(),Fl.event_y()), ) status.value(f'{qi[0]:.2f},{qi[1]:.2f}') except: status.value('') return super().handle(event) window = Fl_Window(800, 600, 'Image display with Fl_Gl_Image_Widget') image = Fl_Gl_Image_Widget_Derived(0,0, 800,580) status = Fl_Output(0,580,800,20) window.resizable(image) window.end() window.show() image.update_image(image_filename = 'image.png') Fl.run() The map_pixel_image_from_viewport() and map_pixel_viewport_from_image() functions map between the coordinates of the viewport and the image being displayed (original image; prior to any decimation). This is useful to implement user interaction methods that respond to user clicks or draw overlays. The inputs and outputs both contain floating-point pixels. The map is linear, so no out-of-bounds checking is done on the input or on the output: negative values can be ingested or output. An exception is thrown in case of error (usually, if something hasn't been initialized yet or if the input is invalid). ARGUMENTS - qin: the input pixel coordinate. This is an iterable of length two that contains two floating-point values. This may be a numpy array of a tuple or a list, for instance. RETURNED VALUE A length-2 tuple containing the mapped pixel coordinate """; %feature("docstring") Fl_Gl_Image_Widget::map_pixel_viewport_from_image """Compute viewport pixel coords from image pixel coords SYNOPSIS from fltk import * from Fl_Gl_Image_Widget import Fl_Gl_Image_Widget class Fl_Gl_Image_Widget_Derived(Fl_Gl_Image_Widget): def handle(self, event): if event == FL_ENTER: return 1 if event == FL_MOVE: try: qi = self.map_pixel_image_from_viewport( (Fl.event_x(),Fl.event_y()), ) status.value(f'{qi[0]:.2f},{qi[1]:.2f}') except: status.value('') return super().handle(event) window = Fl_Window(800, 600, 'Image display with Fl_Gl_Image_Widget') image = Fl_Gl_Image_Widget_Derived(0,0, 800,580) status = Fl_Output(0,580,800,20) window.resizable(image) window.end() window.show() image.update_image(image_filename = 'image.png') Fl.run() The map_pixel_image_from_viewport() and map_pixel_viewport_from_image() functions map between the coordinates of the viewport and the image being displayed (original image; prior to any decimation). This is useful to implement user interaction methods that respond to user clicks or draw overlays. The inputs and outputs both contain floating-point pixels. The map is linear, so no out-of-bounds checking is done on the input or on the output: negative values can be ingested or output. An exception is thrown in case of error (usually, if something hasn't been initialized yet or if the input is invalid). ARGUMENTS - qin: the input pixel coordinate. This is an iterable of length two that contains two floating-point values. This may be a numpy array of a tuple or a list, for instance. RETURNED VALUE A length-2 tuple containing the mapped pixel coordinate """; %typemap(in) (const GL_image_display_line_segments_t* line_segment_sets, int Nline_segment_sets) (PyObject* set = NULL) { PyObject* points = NULL; PyObject* color_rgb = NULL; if(!PySequence_Check($input)) { PyErr_SetString(PyExc_RuntimeError, "$symname() argument isn't a list"); SWIG_fail; } $2 = PySequence_Length($input); $1 = (GL_image_display_line_segments_t*)malloc($2*sizeof(GL_image_display_line_segments_t)); if($1 == NULL) { PyErr_SetString(PyExc_RuntimeError, "$symname() Couldn't allocate $1"); SWIG_fail; } int ndim = 0; const npy_intp* dims = NULL; for(int i=0; i<$2; i++) { set = PySequence_ITEM($input, i); if(!PyDict_Check(set)) { PyErr_SetString(PyExc_RuntimeError, "$symname(): each argument should be a dict"); SWIG_fail; break; } color_rgb = PyDict_GetItemString(set, "color_rgb"); if(color_rgb == NULL) { PyErr_SetString(PyExc_RuntimeError, "$symname(): each argument should be a dict with keys 'points' and 'color_rgb'. Missing: 'color_rgb'"); SWIG_fail; break; } if(!PyArray_Check((PyArrayObject*)color_rgb)) { PyErr_SetString(PyExc_RuntimeError, "$symname(): each argument should be a dict with keys 'points' and 'color_rgb', both pointing to numpy arrays. 'color_rgb' element is not a numpy array"); SWIG_fail; break; } ndim = PyArray_NDIM((PyArrayObject*)color_rgb); dims = PyArray_DIMS((PyArrayObject*)color_rgb); if(! (ndim == 1 && dims[0] == 3 && PyArray_TYPE((PyArrayObject*)color_rgb) == NPY_FLOAT32 && PyArray_CHKFLAGS((PyArrayObject*)color_rgb, NPY_ARRAY_C_CONTIGUOUS)) ) { PyErr_SetString(PyExc_RuntimeError, "$symname(): color_rgb needs to have shape (3,), contain float32 and be contiguous"); SWIG_fail; break; } points = PyDict_GetItemString(set, "points"); if(points == NULL) { PyErr_SetString(PyExc_RuntimeError, "$symname(): each argument should be a dict with keys 'points' and 'color_rgb'. Missing: 'points'"); SWIG_fail; break; } if(!PyArray_Check((PyArrayObject*)points)) { PyErr_SetString(PyExc_RuntimeError, "$symname(): each argument should be a dict with keys 'points' and 'color_rgb', both pointing to numpy arrays. 'points' element is not a numpy array"); SWIG_fail; break; } ndim = PyArray_NDIM((PyArrayObject*)points); dims = PyArray_DIMS((PyArrayObject*)points); if(! (ndim == 3 && dims[1] == 2 && dims[2] == 2 && PyArray_TYPE((PyArrayObject*)points) == NPY_FLOAT32 && PyArray_CHKFLAGS((PyArrayObject*)points, NPY_ARRAY_C_CONTIGUOUS)) ) { PyErr_SetString(PyExc_RuntimeError, "$symname(): points need to have shape (N,2,2), contain float32 and be contiguous"); SWIG_fail; break; } $1[i].segments.Nsegments = dims[0]; memcpy($1[i].segments.color_rgb, PyArray_DATA((PyArrayObject*)color_rgb), 3*sizeof(float)); $1[i].points = (const float*)PyArray_DATA((PyArrayObject*)points); Py_XDECREF(set); set = NULL; } } %typemap(freearg) (const GL_image_display_line_segments_t* line_segment_sets, int Nline_segment_sets) { free($1); Py_XDECREF(set$argnum); } %feature("docstring") Fl_Gl_Image_Widget::set_panzoom """Updates the pan, zoom settings of an image view SYNOPSIS from fltk import * from Fl_Gl_Image_Widget import Fl_Gl_Image_Widget class Fl_Gl_Image_Widget_Derived(Fl_Gl_Image_Widget): def set_panzoom(self, x_centerpixel, y_centerpixel, visible_width_pixels, panzoom_siblings = True): r'''Pan/zoom the image This is an override of the function to do this: any request to pan/zoom the widget will come here first. panzoom_siblings dispatches any pan/zoom commands to all the widgets, so that they all work in unison. ''' if not panzoom_siblings: return super().set_panzoom(x_centerpixel, y_centerpixel, visible_width_pixels) # All the widgets should pan/zoom together return \ all( w.set_panzoom(x_centerpixel, y_centerpixel, visible_width_pixels, panzoom_siblings = False) \ for w in (widget_image0, widget_image1) ) window = Fl_Window(800, 600, 'Image display with Fl_Gl_Image_Widget') image0 = Fl_Gl_Image_Widget_Derived(0, 0, 400,600) image1 = Fl_Gl_Image_Widget_Derived(400,0, 400,600) window.resizable(image) window.end() window.show() image0.update_image(image_filename = 'image0.png') image1.update_image(image_filename = 'image1.png') Fl.run() This is a thin wrapper around the C API function GL_image_display_set_panzoom(). USUALLY there's no reason to the user to call this: the default Fl_Gl_Image_Widget behavior already includes interactive navigation that calls this function as needed. The primary reason this is available in Python is to allow the user to hook these calls in their derived classes to enhance or modify the pan/zoom behaviors. The example in the SYNOPSIS above displays two images side by size, and pans/zooms them in unison: when the user changes the view in one widget, the change is applied to BOTH widgets. If any of the given values are Inf or NaN or abs() >= 1e20, we use the previously-set value. ARGUMENTS - x_centerpixel, y_centerpixel: the pixel coordinates of the image to place in the center of the viewport. This is the 'pan' setting - visible_width_pixels: how many horizontal image pixels should span the viewport. This is the 'zoom' setting RETURNED VALUE None on success. An exception is thrown in case of error (usually, if something hasn't been initialized yet or if the input is invalid). """; %rename(_set_lines) set_lines; // In C++ update_image() is the legacy function and update_image2() is the "real" function. // Here I use the "update_image2" wrapping for both names %ignore Fl_Gl_Image_Widget::update_image; %include "Fl_Gl_Image_Widget.hh" %pythoncode %{ def set_lines(self, *args): """Compute image pixel coords from viewport pixel coords SYNOPSIS from fltk import * from Fl_Gl_Image_Widget import Fl_Gl_Image_Widget class Fl_Gl_Image_Widget_Derived(Fl_Gl_Image_Widget): def handle(self, event): if event == FL_PUSH: try: qv = (Fl.event_x(),Fl.event_y()) qi = \ np.array( \ self.map_pixel_image_from_viewport(qv), dtype=float ).round() x,y = q self.set_lines( dict(points = np.array( (((x - 50, y), (x + 50, y)), ((x, y - 50), (x, y + 50))), dtype=np.float32), color_rgb = np.array((1,0,0), dtype=np.float32) )) except: self.set_lines() return 1 return super().handle(event) window = Fl_Window(800, 600, 'Image display with Fl_Gl_Image_Widget') image = Fl_Gl_Image_Widget_Derived(0,0, 800,600) window.resizable(image) window.end() window.show() image.update_image(image_filename = 'image.png') Fl.run() Updates the set of lines we draw as an overlay on top of the image. The hierarchy: - Each SET of lines is drawn with the same color, and consists of separate line SEGMENTS - Each line SEGMENT connects two independent points (x0,y0) and (x1,y1) in image pixel coordinates. Each call looks like set_lines( SET, SET, SET, ... ) Where each SET is dict(points = POINTS, color_rgb = COLOR) - POINTS is a numpy array of shape (Nsegments,2,2) where each innermost row is (x,y). This array must have dtype=np.float32 and must be stored contiguously - COLOR is the (red,green,blue) tuple to use for the line. This is a numpy array of shape (3,). This array must have dtype=np.float32 and must be stored contiguously. The color is passed directly to OpenGL, and uses values in [0,1] RETURNED VALUE None on success. An exception is thrown in case of error (usually, if something hasn't been initialized yet or if the input is invalid). """ return self._set_lines(args) Fl_Gl_Image_Widget.set_lines = set_lines # In C++ update_image() is the legacy function and update_image2() is the "real" function. # Here I use the "update_image2" wrapping for both names Fl_Gl_Image_Widget.update_image = Fl_Gl_Image_Widget.update_image2 %} GL_image_display-0.23/GL_image_display-test-fltk.cc000066400000000000000000000274551500256703300222600ustar00rootroot00000000000000// This is the C++ Fl_Gl_Image_Widget FLTK widget sample program. The Python // test program is separate, and lives in GL_image_display-test-fltk.py #include #include #include #include extern "C" { #include "util.h" } #include "Fl_Gl_Image_Widget.hh" #define WINDOW_W 800 #define WINDOW_H 600 #define STATUS_H 20 #define DECIMATION 2 class Fl_Gl_Image_Widget_Derived; static Fl_Double_Window* g_window; static Fl_Gl_Image_Widget_Derived* g_gl_widgets[4]; static Fl_Output* g_status_text; static const char*const* g_images; static void update_status(double image_pixel_x, double image_pixel_y); class Fl_Gl_Image_Widget_Derived : public Fl_Gl_Image_Widget { public: Fl_Gl_Image_Widget_Derived(int x, int y, int w, int h, bool double_buffered) : Fl_Gl_Image_Widget(x,y,w,h, double_buffered) {} /* These override the pan/zoom commands to pan/zoom all the widgets together if SHIFT is depressed */ bool process_mousewheel_zoom(double dy, double x, double y, double viewport_width, double viewport_height) { if(!(Fl::event_state() & FL_SHIFT)) { // We do the normal thing because shift is not pressed: the user // didn't ask us to do anything special with this pan/zoom call return Fl_Gl_Image_Widget:: process_mousewheel_zoom(dy, x,y,viewport_width,viewport_height); } // All the widgets should pan/zoom together int Nwidgets = (int)(sizeof(g_gl_widgets)/sizeof(g_gl_widgets[0])); bool result = true; for(int i=0; imake_current(); // I need to fake a mousewheel-zoom event in another widget, and // I need to determine a fake center point. I select the same // point, relatively, in the window double viewport_width_new = (double)g_gl_widgets[i]->pixel_w(); double viewport_height_new = (double)g_gl_widgets[i]->pixel_h(); result = result && g_gl_widgets[i]-> Fl_Gl_Image_Widget:: process_mousewheel_zoom(dy, (x + 0.5)/viewport_width * viewport_width_new - 0.5, (y + 0.5)/viewport_height * viewport_height_new - 0.5, viewport_width_new, viewport_height_new); } make_current(); return result; } bool process_mousewheel_pan(double dx, double dy, double viewport_width, double viewport_height) { if(!(Fl::event_state() & FL_SHIFT)) { // We do the normal thing because shift is not pressed: the user // didn't ask us to do anything special with this pan/zoom call return Fl_Gl_Image_Widget:: process_mousewheel_pan(dx,dy,viewport_width,viewport_height); } // All the widgets should pan/zoom together int Nwidgets = (int)(sizeof(g_gl_widgets)/sizeof(g_gl_widgets[0])); bool result = true; for(int i=0; imake_current(); double viewport_width_new = (double)g_gl_widgets[i]->pixel_w(); double viewport_height_new = (double)g_gl_widgets[i]->pixel_h(); result = result && g_gl_widgets[i]-> Fl_Gl_Image_Widget:: process_mousewheel_pan(dx, dy, viewport_width_new, viewport_height_new); } make_current(); return result; } bool process_mousedrag_pan(double dx, double dy, double viewport_width, double viewport_height) { if(!(Fl::event_state() & FL_SHIFT)) { // We do the normal thing because shift is not pressed: the user // didn't ask us to do anything special with this pan/zoom call return Fl_Gl_Image_Widget:: process_mousedrag_pan(dx,dy,viewport_width,viewport_height); } // All the widgets should pan/zoom together int Nwidgets = (int)(sizeof(g_gl_widgets)/sizeof(g_gl_widgets[0])); bool result = true; for(int i=0; imake_current(); double viewport_width_new = (double)g_gl_widgets[i]->pixel_w(); double viewport_height_new = (double)g_gl_widgets[i]->pixel_h(); result = result && g_gl_widgets[i]-> Fl_Gl_Image_Widget:: process_mousedrag_pan(dx, dy, viewport_width_new, viewport_height_new); } make_current(); return result; } bool process_keyboard_panzoom(void) { if(!(Fl::event_state() & FL_SHIFT)) { // We do the normal thing because shift is not pressed: the user // didn't ask us to do anything special with this pan/zoom call return Fl_Gl_Image_Widget:: process_keyboard_panzoom(); } // All the widgets should pan/zoom together int Nwidgets = (int)(sizeof(g_gl_widgets)/sizeof(g_gl_widgets[0])); bool result = true; for(int i=0; imake_current(); result = result && g_gl_widgets[i]-> Fl_Gl_Image_Widget:: process_keyboard_panzoom(); } make_current(); return result; } int handle(int event) { switch(event) { case FL_ENTER: // I want FL_MOVE events and I want to make sure the parent widget // does its procesing. This is required for the focus-follows-mouse // logic for the keyboard-based navigation to work Fl_Gl_Image_Widget::handle(event); return 1; case FL_MOVE: { double image_pixel_x, image_pixel_y; GL_image_display_map_pixel_image_from_viewport(&m_ctx, &image_pixel_x, &image_pixel_y, (double)Fl::event_x(), (double)Fl::event_y()); update_status(image_pixel_x, image_pixel_y); // Let the other handlers run break; } case FL_PUSH: { double image_pixel_x, image_pixel_y; GL_image_display_map_pixel_image_from_viewport(&m_ctx, &image_pixel_x, &image_pixel_y, (double)Fl::event_x(), (double)Fl::event_y()); const float pool[] = {(float)image_pixel_x - 50, (float)image_pixel_y, (float)image_pixel_x + 50, (float)image_pixel_y, (float)image_pixel_x, (float)image_pixel_y - 50, (float)image_pixel_x, (float)image_pixel_y + 50}; const GL_image_display_line_segments_t cross = { .segments = {.Nsegments = 2, .color_rgb = {1.f,0.f,0.f}}, .points = pool }; if( !set_lines(&cross, 1)) { fprintf(stderr, "GL_image_display_set_lines() failed\n"); } redraw(); break; } default: ; } return Fl_Gl_Image_Widget::handle(event); } }; static void timer_callback(void* cookie __attribute__((unused))) { static int c = 0; for(int i=0; i<2; i++) for(int j=0; j<2; j++) { if(!g_gl_widgets[2*i+j]->update_image2(DECIMATION, false, false, g_images[(2*i+j + c)%4])) { MSG("Couldn't update the image. Giving up."); g_window->hide(); return; } } Fl::repeat_timeout(1.0, timer_callback); c++; } static void update_status(double image_pixel_x, double image_pixel_y) { char str[1024]; int str_written = 0; #define append(fmt, ...) \ { \ int Navailable = sizeof(str) - str_written; \ int Nwritten = snprintf(&str[str_written], Navailable, \ fmt, ##__VA_ARGS__); \ if(Navailable <= Nwritten) \ { \ MSG("Static buffer overflow. Increase buffer size. update_status() setting empty string"); \ g_status_text->static_value(""); \ return; \ } \ str_written += Nwritten; \ } if(image_pixel_x > 0) { append("Pixel (%.2f,%.2f) ", image_pixel_x, image_pixel_y); } g_status_text->value(str); #undef append } int main(int argc, char** argv) { if(argc != 5) { MSG("ERROR: Need 4 images on the commandline"); return 1; } g_images = (const char*const*)&argv[1]; Fl::lock(); g_window = new Fl_Double_Window( WINDOW_W, WINDOW_H, "OpenGL image visualizer" ); Fl_Group* images = new Fl_Group(0,0,WINDOW_W,WINDOW_H-STATUS_H); { int w = WINDOW_W/2; int h = (WINDOW_H - STATUS_H)/2; int y = 0; bool double_buffered = true; g_gl_widgets[0] = new Fl_Gl_Image_Widget_Derived(0, y, w, h, double_buffered); g_gl_widgets[1] = new Fl_Gl_Image_Widget_Derived(w, y, WINDOW_W-w, h, double_buffered); y = h; h = WINDOW_H - STATUS_H - y; g_gl_widgets[2] = new Fl_Gl_Image_Widget_Derived(0, y, w, h, double_buffered); g_gl_widgets[3] = new Fl_Gl_Image_Widget_Derived(w, y, WINDOW_W-w, h, double_buffered); } images->end(); { g_status_text = new Fl_Output(0, WINDOW_H-STATUS_H, WINDOW_W, STATUS_H); } g_window->resizable(images); g_window->end(); g_window->show(); Fl::add_timeout(1.0, timer_callback); Fl::run(); return 0; } GL_image_display-0.23/GL_image_display-test-fltk.py000077500000000000000000000036301500256703300223130ustar00rootroot00000000000000#!/usr/bin/python3 # This is the Python Fl_Gl_Image_Widget FLTK widget sample program. The C++ test # program is separate, and lives in GL_image_display-test-fltk.cc import sys from fltk import * from Fl_Gl_Image_Widget import Fl_Gl_Image_Widget import numpy as np try: image_filename = sys.argv[1] except: print("Need image to display on the commandline", file=sys.stderr) sys.exit(1) class Fl_Gl_Image_Widget_Derived(Fl_Gl_Image_Widget): def handle(self, event): if event == FL_ENTER: return 1 if event == FL_MOVE: try: qi = self.map_pixel_image_from_viewport( (Fl.event_x(),Fl.event_y()), ) status.value(f"{qi[0]:.2f},{qi[1]:.2f}") except: status.value("") if event == FL_PUSH: try: x,y = self.map_pixel_image_from_viewport( (Fl.event_x(),Fl.event_y()), ) except: return super().handle(event) self.set_lines( dict(points = np.array( (((x - 50, y), (x + 50, y)), ((x, y - 50), (x, y + 50))), dtype=np.float32), color_rgb = np.array((1,0,0), dtype=np.float32) )) self.redraw() return super().handle(event) window = Fl_Window(800, 600, "Image display with Fl_Gl_Image_Widget") image = Fl_Gl_Image_Widget_Derived(0,0, 800,580) status = Fl_Output(0,580,800,20) window.resizable(image) window.end() window.show() if 1: image.update_image(image_filename = image_filename, decimation_level = 0) else: import cv2 image.update_image(image_data = cv2.imread(image_filename), decimation_level = 0) Fl.run() GL_image_display-0.23/GL_image_display-test-glut.c000066400000000000000000000067061500256703300221240ustar00rootroot00000000000000// Tests the one-time GLUT-based opengl render. #include #include #include #include #include #include #include #include #include #include "GL_image_display.h" #include "util.h" GL_image_display_context_t ctx; int main(int argc, char* argv[]) { if(argc != 2 && argc != 3) { fprintf(stderr, "Need one or two images on the commandline. If two, I flip between them at 1Hz\n"); return 1; } if( !GL_image_display_init( &ctx, true) ) return 1; char** images = &argv[1]; int i_image = 0; if( !GL_image_display_update_image(&ctx,0, images[i_image], NULL,0,0,0,0) ) { fprintf(stderr, "GL_image_display_update_image() failed\n"); return 1; } if( !GL_image_display_set_panzoom(&ctx, 1580, 900, 3500) ) { fprintf(stderr, "GL_image_display_set_panzoom() failed\n"); return 1; } if( !GL_image_display_set_lines(&ctx, ((const GL_image_display_line_segments_t[]) { { .segments = {.Nsegments = 1, .color_rgb = {1.f,0.f,0.f}}, .points = ((float[]){63, 113, 937,557})}, { .segments = {.Nsegments = 2, .color_rgb = {0.f,1.f,0.f}}, .points = ((float[]){1749,645, 1597,100, 1597,100, 1247,224})} }), 2 )) { fprintf(stderr, "GL_image_display_set_lines() failed\n"); return 1; } void timerfunc(int cookie __attribute__((unused))) { i_image = 1 - i_image; if( !GL_image_display_update_image(&ctx,0, images[i_image], NULL,0,0,0,0) ) { fprintf(stderr, "GL_image_display_update_image() failed\n"); return; } glutPostRedisplay(); glutTimerFunc(1000, timerfunc, 0); } void window_display(void) { GL_image_display_redraw(&ctx); glutSwapBuffers(); } void window_keyPressed(unsigned char key, int x __attribute__((unused)) , int y __attribute__((unused)) ) { switch (key) { case 'q': case 27: // Need both to avoid a segfault. This works differently with // different opengl drivers glutExit(); exit(0); } glutPostRedisplay(); } void _GL_image_display_resized(int width, int height) { GL_image_display_resize_viewport(&ctx, width, height); } glutDisplayFunc (window_display); glutKeyboardFunc(window_keyPressed); glutReshapeFunc (_GL_image_display_resized); if(argc == 3) glutTimerFunc(1000, timerfunc, 0); glutMainLoop(); return 0; } GL_image_display-0.23/GL_image_display.c000066400000000000000000001061251500256703300201720ustar00rootroot00000000000000#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include "GL_image_display.h" #include "util.h" static_assert(GL_image_display_num_uniforms <= GL_image_display_max_num_uniforms, "must have GL_image_display_num_uniforms <= GL_image_display_max_num_uniforms"); static_assert(GL_image_display_num_programs <= GL_image_display_max_num_programs, "must have GL_image_display_num_programs <= GL_image_display_max_num_programs"); #define MAX_NUMBER_LINE_VERTICES 1024 #define assert_opengl() \ do { \ int error = glGetError(); \ if( error != GL_NO_ERROR ) \ { \ MSG("Error: %#x! Giving up", error); \ assert(0); \ } \ } while(0) static bool select_program_indexed(GL_image_display_context_t* ctx, int i) { glUseProgram(ctx->programs[i].program); assert_opengl(); return true; } // Set a uniform in all my programs #define set_uniform_1f(...) _set_uniform(1f, ##__VA_ARGS__) #define set_uniform_2f(...) _set_uniform(2f, ##__VA_ARGS__) #define set_uniform_3f(...) _set_uniform(3f, ##__VA_ARGS__) #define set_uniform_1fv(...) _set_uniform(1fv, ##__VA_ARGS__) #define set_uniform_1i(...) _set_uniform(1i, ##__VA_ARGS__) #define _set_uniform(kind, ctx, uniform, ...) \ do { \ for(int _i=0; _iprograms[_i].uniforms[GL_image_display_uniform_index_##uniform], \ ## __VA_ARGS__); \ assert_opengl(); \ } \ } while(0) // The main init routine. We support 2 modes: // // - GLUT: static window (use_glut = true) // - no GLUT: higher-level application (use_glut = false) bool GL_image_display_init( // output GL_image_display_context_t* ctx, // input bool use_glut) { bool result = false; *ctx = (GL_image_display_context_t){.use_glut = use_glut}; if(use_glut) { const bool double_buffered = true; static bool global_inited = false; if(!global_inited) { glutInitContextFlags(GLUT_FORWARD_COMPATIBLE); glutInitContextVersion(4,2); glutInitContextProfile(GLUT_CORE_PROFILE); glutInit(&(int){1}, &(char*){"exec"}); global_inited = true; } glutInitDisplayMode( GLUT_RGB | GLUT_DEPTH | (double_buffered ? GLUT_DOUBLE : 0) ); glutInitWindowSize(1024,1024); ctx->glut_window = glutCreateWindow("GL_image_display"); const char* version = (const char*)glGetString(GL_VERSION); // MSG("glGetString(GL_VERSION) says we're using GL %s", version); // MSG("Epoxy says we're using GL %d", epoxy_gl_version()); if (version[0] == '1') { if (!glutExtensionSupported("GL_ARB_vertex_shader")) { MSG("Sorry, GL_ARB_vertex_shader is required."); goto done; } if (!glutExtensionSupported("GL_ARB_fragment_shader")) { MSG("Sorry, GL_ARB_fragment_shader is required."); goto done; } if (!glutExtensionSupported("GL_ARB_vertex_buffer_object")) { MSG("Sorry, GL_ARB_vertex_buffer_object is required."); goto done; } if (!glutExtensionSupported("GL_EXT_framebuffer_object")) { MSG("GL_EXT_framebuffer_object not found!"); goto done; } } } static_assert(sizeof(GLint) == sizeof(ctx->programs[0].uniforms[0]), "GL_image_display_context_t.program.uniform_... must be a GLint"); glClearColor(0, 0, 0, 0); // Needed to make non-multiple-of-4-width images work. Otherwise // glTexSubImage2D() fails glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // vertices { // image { // The location specified in the layout in image.vertex.glsl const int VBO_location_image = 0; glGenVertexArrays(1, &ctx->programs[GL_image_display_program_index_image].VBO_array); glBindVertexArray(ctx->programs[GL_image_display_program_index_image].VBO_array); glGenBuffers(1, &ctx->programs[GL_image_display_program_index_image].VBO_buffer); glBindBuffer(GL_ARRAY_BUFFER, ctx->programs[GL_image_display_program_index_image].VBO_buffer); glEnableVertexAttribArray(VBO_location_image); GLbyte xy[2*2*2]; for(int i=0; i<2; i++) for(int j=0; j<2; j++) { xy[(i*2+j)*2 + 0] = j; xy[(i*2+j)*2 + 1] = i; } glBufferData(GL_ARRAY_BUFFER, 2*2*2*sizeof(xy[0]), xy, GL_STATIC_DRAW); glVertexAttribPointer(VBO_location_image, 2, // 2 values per vertex. z = 0 for all GL_BYTE, GL_FALSE, 0, NULL); } // line { // The location specified in the layout in line.vertex.glsl const int VBO_location_line = 1; glGenVertexArrays(1, &ctx->programs[GL_image_display_program_index_line].VBO_array); glBindVertexArray(ctx->programs[GL_image_display_program_index_line].VBO_array); glGenBuffers(1, &ctx->programs[GL_image_display_program_index_line].VBO_buffer); glBindBuffer(GL_ARRAY_BUFFER, ctx->programs[GL_image_display_program_index_line].VBO_buffer); glEnableVertexAttribArray(VBO_location_line); glBufferData(GL_ARRAY_BUFFER, MAX_NUMBER_LINE_VERTICES*2*sizeof(float), NULL, GL_DYNAMIC_DRAW); glVertexAttribPointer(VBO_location_line, 2, // 2 values per vertex. z = 0 for all GL_FLOAT, GL_FALSE, 0, NULL); } } // shaders { const GLchar* image_vertex_glsl = #include "image.vertex.glsl.h" ; const GLchar* image_geometry_glsl = #include "image.geometry.glsl.h" ; const GLchar* image_fragment_glsl = #include "image.fragment.glsl.h" ; const GLchar* line_vertex_glsl = #include "line.vertex.glsl.h" ; const GLchar* line_geometry_glsl = #include "line.geometry.glsl.h" ; const GLchar* line_fragment_glsl = #include "line.fragment.glsl.h" ; char msg[1024]; int len; for(int i=0; iprograms[i].program = glCreateProgram(); assert_opengl(); } #define build_shader(programtype, shadertype,SHADERTYPE) \ GLuint shadertype##Shader = glCreateShader(GL_##SHADERTYPE##_SHADER); \ assert_opengl(); \ \ glShaderSource(shadertype##Shader, 1, \ (const GLchar**)&programtype##_##shadertype##_glsl, NULL); \ assert_opengl(); \ \ glCompileShader(shadertype##Shader); \ assert_opengl(); \ glGetShaderInfoLog( shadertype##Shader, sizeof(msg), &len, msg ); \ if( strlen(msg) ) \ printf(#programtype " " #shadertype " shader info: %s\n", msg); \ \ glAttachShader(ctx->programs[GL_image_display_program_index_##programtype].program, shadertype ##Shader); \ assert_opengl(); #define build_program(programtype) \ { \ build_shader(programtype, vertex, VERTEX); \ build_shader(programtype, fragment, FRAGMENT); \ build_shader(programtype, geometry, GEOMETRY); \ glLinkProgram(ctx->programs[GL_image_display_program_index_##programtype].program); assert_opengl(); \ glGetProgramInfoLog( ctx->programs[GL_image_display_program_index_##programtype].program, sizeof(msg), &len, msg ); \ if( strlen(msg) ) \ printf(#programtype" program info after glLinkProgram(): %s\n", msg); \ } build_program(image); build_program(line); // I use the same uniforms for all the programs #define make_uniform(name) \ for(int _i=0; _iprograms[_i].uniforms[GL_image_display_uniform_index_##name] = \ glGetUniformLocation(ctx->programs[_i].program, \ #name); \ assert_opengl(); \ } make_uniform(image_width_full); make_uniform(image_height_full); make_uniform(aspect); make_uniform(center01); make_uniform(visible_width01); make_uniform(flip_x); make_uniform(flip_y); make_uniform(flip_y_data_is_upside_down); make_uniform(line_color_rgb); make_uniform(black_image); #undef make_uniform } result = true; ctx->did_init = true; done: return result; } // This exists because the FLTK widget often defers the first update_image() // call, so I don't do error checking until it's too late. Here I try to // validate the input as much as I can immediately, so that common errors are // caught early bool GL_image_display_update_image__validate_input ( // Either this should be given const char* image_filename, // Or these should be given const char* image_data, int image_width, int image_height, // Supported: // - 8 for "grayscale" // - 24 for "bgr" int image_bpp, bool check_image_file) { FIBITMAP* fib = NULL; bool result = false; if(image_filename == NULL && image_data != NULL && !(image_width > 0 && image_height > 0)) { MSG("image_filename is NULL && image_data != NULL, so all of (image_width, image_height) must have valid values"); goto done; } if(image_filename != NULL && !(image_data == NULL && image_width <= 0 && image_height <= 0)) { MSG("image_filename is not NULL, so all of (image_data, image_width, image_height) must have null values"); goto done; } if(image_data != NULL && image_width > 0) { if(!(image_bpp == 8 || image_bpp == 24)) { MSG("I support 8 bits-per-pixel and 24 bits-per-pixel images. Got %d", image_bpp); goto done; } } if(!check_image_file) { result = true; goto done; } if( image_filename != NULL ) { FREE_IMAGE_FORMAT format = FreeImage_GetFileType(image_filename,0); if(format == FIF_UNKNOWN) { MSG("Couldn't load '%s'", image_filename); goto done; } fib = FreeImage_Load(format, image_filename, 0); if(fib == NULL) { MSG("Couldn't load '%s'", image_filename); goto done; } // grayscale if(FreeImage_GetColorType(fib) == FIC_MINISBLACK && FreeImage_GetBPP(fib) == 8) { result = true; goto done; } else { // normalize images if( // palettized FreeImage_GetColorType(fib) == FIC_PALETTE || // 32-bit RGBA (FreeImage_GetColorType(fib) == FIC_RGBALPHA && FreeImage_GetBPP(fib) == 32) ) { result = true; goto done; } if(FreeImage_GetColorType(fib) == FIC_RGB && FreeImage_GetBPP(fib) == 24) { result = true; goto done; } MSG("Only 8-bit grayscale and 24-bit RGB images and 32-bit RGBA images are supported. Conversion to 24-bit didn't work. Giving up."); goto done; } } else result = true; done: if(fib != NULL) FreeImage_Unload(fib); return result; } #define CONFIRM_SET( what) if(!ctx->what) { MSG("CONFIRM_SET("#what ") failed!"); return false; } #define CONFIRM_SET_QUIET(what) if(!ctx->what) { return false; } static bool set_aspect(GL_image_display_context_t* ctx, int viewport_width, int viewport_height) { CONFIRM_SET(did_init); // I scale the dimensions to keep the displayed aspect ratio square and to // not cut off any part of the image if( viewport_width*ctx->image_height < ctx->image_width*viewport_height ) { ctx->aspect_x = 1.0; ctx->aspect_y = (double)(viewport_width*ctx->image_height) / (double)(viewport_height*ctx->image_width); } else { ctx->aspect_x = (double)(viewport_height*ctx->image_width) / (double)(viewport_width*ctx->image_height); ctx->aspect_y = 1.0; } set_uniform_2f(ctx, aspect, (float)ctx->aspect_x, (float)ctx->aspect_y); ctx->did_set_aspect = true; return true; } bool GL_image_display_update_image2( GL_image_display_context_t* ctx, int decimation_level, bool flip_x, bool flip_y, // At most this ... const char* image_filename, // ... or these should be non-NULL. If // neither is, we reset to an all-black // image const char* image_data, int image_width, int image_height, // Supported: // - 8 for "grayscale" // - 24 for "bgr" int image_bpp, int image_pitch) { if(!GL_image_display_update_image__validate_input ( image_filename, image_data, image_width, image_height, image_bpp, false)) { return false; } if(image_width > 0) { if(image_pitch <= 0) { // Pitch isn't given, so I assume the image data is stored densely image_pitch = image_width*image_bpp/8; } } bool result = false; FIBITMAP* fib = NULL; char* buf = NULL; if(!ctx->did_init) { MSG("Cannot init textures if GL_image_display overall has not been initted. Call GL_image_display_init() first"); goto done; } ctx->flip_x = flip_x; ctx->flip_y = flip_y; if( image_filename != NULL ) { FREE_IMAGE_FORMAT format = FreeImage_GetFileType(image_filename,0); if(format == FIF_UNKNOWN) { MSG("Couldn't load '%s'", image_filename); goto done; } fib = FreeImage_Load(format, image_filename, 0); if(fib == NULL) { MSG("Couldn't load '%s'", image_filename); goto done; } // grayscale if(FreeImage_GetColorType(fib) == FIC_MINISBLACK && FreeImage_GetBPP(fib) == 8) { image_bpp = 8; } else { // normalize images if( // palettized FreeImage_GetColorType(fib) == FIC_PALETTE || // 32-bit RGBA (FreeImage_GetColorType(fib) == FIC_RGBALPHA && FreeImage_GetBPP(fib) == 32) ) { // I explicitly un-palettize images, if that's what I was given FIBITMAP* fib24 = FreeImage_ConvertTo24Bits(fib); FreeImage_Unload(fib); fib = fib24; if(fib == NULL) { MSG("Couldn't unpalettize '%s'", image_filename); goto done; } } if(!(FreeImage_GetColorType(fib) == FIC_RGB && FreeImage_GetBPP(fib) == 24)) { MSG("Only 8-bit grayscale and 24-bit RGB images and 32-bit RGBA images are supported. Conversion to 24-bit didn't work. Giving up."); goto done; } image_bpp = 24; } image_width = (int)FreeImage_GetWidth(fib); image_height = (int)FreeImage_GetHeight(fib); image_pitch = (int)FreeImage_GetPitch(fib); image_data = (char*)FreeImage_GetBits(fib); // FreeImage_Load() loads images upside down set_uniform_1i(ctx, flip_y_data_is_upside_down, 1); } else set_uniform_1i(ctx, flip_y_data_is_upside_down, 0); set_uniform_1i(ctx, flip_x, ctx->flip_x); set_uniform_1i(ctx, flip_y, ctx->flip_y); if(image_filename == NULL && image_data == NULL) { set_uniform_1i(ctx, black_image, 1); if(!ctx->did_init_texture) { // we haven't initialized the texture, so the fragment shader won't // work to paint everything black. I do that manually here. glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); } result = true; goto done; } set_uniform_1i(ctx, black_image, 0); if(!ctx->did_init_texture) { ctx->image_width = image_width >> decimation_level; ctx->image_height = image_height >> decimation_level; set_uniform_1i(ctx, image_width_full, image_width); set_uniform_1i(ctx, image_height_full, image_height); ctx->decimation_level = decimation_level; glGenTextures(1, &ctx->texture_ID); assert_opengl(); glActiveTexture( GL_TEXTURE0 ); assert_opengl(); glBindTexture( GL_TEXTURE_2D, ctx->texture_ID ); assert_opengl(); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, ctx->image_width, ctx->image_height, 0, GL_BGR, GL_UNSIGNED_BYTE, (const GLvoid *)NULL); assert_opengl(); // I'm going to be updating the texture data later, so I set up a PBO to do // that glGenBuffers(1, &ctx->texture_PBO_ID); assert_opengl(); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, ctx->texture_PBO_ID); assert_opengl(); glBufferData(GL_PIXEL_UNPACK_BUFFER, ctx->image_width*ctx->image_height*3, NULL, GL_STREAM_DRAW); assert_opengl(); ctx->did_init_texture = true; if(!GL_image_display_set_panzoom(ctx, (double)(ctx->image_width - 1) / 2., (double)(ctx->image_height - 1) / 2., ctx->image_width)) goto done; // Render image dimensions changed. I need to update the aspect-ratio // uniform, which depends on these and the viewport dimensions. The // container UI library must call GL_image_display_resize_viewport() if the // viewport size changes. The image dimensions will never change after // this GLint viewport_xywh[4]; glGetIntegerv(GL_VIEWPORT, viewport_xywh); if(!set_aspect(ctx, viewport_xywh[2], viewport_xywh[3])) goto done; } else { if(! (ctx->image_width == image_width >> decimation_level && ctx->image_height == image_height >> decimation_level) ) { MSG("Inconsistent image sizes. Initialized with (%d,%d), but new image '%s' has (%d,%d). Ignoring the new image", ctx->image_width, ctx->image_height, image_filename == NULL ? "(explicitly given data)" : image_filename, image_width >> decimation_level, image_height >> decimation_level); goto done; } if(ctx->decimation_level != decimation_level) { MSG("Inconsistent decimation level. Initialized with %d, but new image '%s' has %d. Ignoring the new image", ctx->decimation_level, image_filename == NULL ? "(explicitly given data)" : image_filename, decimation_level); goto done; } glBindTexture( GL_TEXTURE_2D, ctx->texture_ID ); assert_opengl(); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, ctx->texture_PBO_ID); assert_opengl(); } buf = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY); if(buf == NULL) { MSG("Couldn't map the texture buffer"); goto done; } { // Copy the buffer const int step_input = 1 << decimation_level; for(int i=0; iimage_height; i++) { const char* row_input = &image_data[i*step_input*image_pitch]; char* row_output = &buf[ i* ctx->image_width*3]; if(image_bpp == 24) { // 24-bit input images. Same as the texture if(step_input == 1) { // easy no-decimation case memcpy(row_output, row_input, 3*ctx->image_width); } else { for(int j=0; jimage_width; j++) { for(int k=0; k<3; k++) row_output[k] = row_input[k]; row_input += 3*step_input; row_output += 3; } } } else { // 8-bit input images, but 24-bit texture for(int j=0; jimage_width; j++) { for(int k=0; k<3; k++) row_output[k] = row_input[0]; row_input += 1*step_input; row_output += 3; } } } } glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); buf = NULL; assert_opengl(); glTexSubImage2D(GL_TEXTURE_2D, 0, 0,0, ctx->image_width, ctx->image_height, GL_BGR, GL_UNSIGNED_BYTE, 0); assert_opengl(); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); assert_opengl(); result = true; done: if(fib != NULL) FreeImage_Unload(fib); if(buf != NULL) glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); return result; } bool GL_image_display_update_image( GL_image_display_context_t* ctx, int decimation_level, const char* image_filename, const char* image_data, int image_width, int image_height, int image_bpp, int image_pitch) { return GL_image_display_update_image2( ctx, decimation_level, false,false, image_filename, image_data, image_width, image_height, image_bpp, image_pitch); } void GL_image_display_deinit( GL_image_display_context_t* ctx ) { if(ctx->use_glut && ctx->glut_window != 0) { glutDestroyWindow(ctx->glut_window); ctx->glut_window = 0; } } bool GL_image_display_resize_viewport(GL_image_display_context_t* ctx, int viewport_width, int viewport_height) { CONFIRM_SET(did_init); if(ctx->use_glut) { if(ctx->glut_window == 0) return false; glutSetWindow(ctx->glut_window); } ctx->viewport_width = viewport_width; ctx->viewport_height = viewport_height; glViewport(0, 0, viewport_width, viewport_height); if(ctx->did_init_texture) return set_aspect(ctx, viewport_width, viewport_height); return true; } bool GL_image_display_set_panzoom(GL_image_display_context_t* ctx, double x_centerpixel, double y_centerpixel, double visible_width_pixels) { CONFIRM_SET(did_init_texture); #define TRY_EXISTING_OR_SET(what) \ /* check for isfinite() AND big values because -ffast-math breaks isfinite()*/ \ if( !isfinite(what) || what >= 1e20 || what <= -1e20 ) \ { \ /* Invalid input. I keep existing value IF there is an existing value */ \ if(!ctx->did_set_panzoom) \ { \ MSG("set_panzoom() was asked to use previous value for " #what "but it hasn't been initialized yet. Giving up."); \ return false; \ } \ } \ else \ ctx->what = what TRY_EXISTING_OR_SET(x_centerpixel); TRY_EXISTING_OR_SET(y_centerpixel); TRY_EXISTING_OR_SET(visible_width_pixels); #undef TRY_EXISTING_OR_SET ctx->visible_width01 = ctx->visible_width_pixels / (double)ctx->image_width; set_uniform_1f(ctx, visible_width01, (float)ctx->visible_width01); // Adjustments: // // OpenGL treats images upside-down, so I flip the y // // OpenGL [0,1] extents look at the left edge of the leftmost pixel and // the right edge of the rightmost pixel respectively, so an offset of 0.5 // pixels is required ctx->center01_x = ( ctx->x_centerpixel + 0.5) / (double)ctx->image_width; ctx->center01_y = ((double)(ctx->image_height-1) - ctx->y_centerpixel + 0.5) / (double)ctx->image_height; set_uniform_2f(ctx, center01, (float)ctx->center01_x, (float)ctx->center01_y); ctx->did_set_panzoom = true; return true; } bool GL_image_display_set_lines(GL_image_display_context_t* ctx, const GL_image_display_line_segments_t* line_segment_sets, int Nline_segment_sets) { CONFIRM_SET(did_init); ctx->Nline_segment_sets = Nline_segment_sets; if(Nline_segment_sets <= 0) return true; glBindVertexArray(ctx->programs[GL_image_display_program_index_line].VBO_array); glBindBuffer(GL_ARRAY_BUFFER, ctx->programs[GL_image_display_program_index_line].VBO_buffer); float* buffer = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY); assert(buffer); ctx->line_segment_sets = realloc(ctx->line_segment_sets, Nline_segment_sets * sizeof(line_segment_sets[0])); if(ctx->line_segment_sets == NULL) { MSG("realloc(line segment sets failed"); return false; } int Nvertices_stored = 0; for(int iset=0; isetsegments.Nsegments; if(Nvertices_stored + 2*Nsegments > MAX_NUMBER_LINE_VERTICES) { MSG("Too many line segment vertices. Increase MAX_NUMBER_LINE_VERTICES. Giving up on all the lines"); ctx->Nline_segment_sets = 0; free(ctx->line_segment_sets); ctx->line_segment_sets = NULL; return false; } memcpy(buffer, set->points, 4*Nsegments*sizeof(float)); ctx->line_segment_sets[iset] = set->segments; Nvertices_stored += 2*Nsegments; buffer = &buffer[4*Nsegments]; set++; } int res = glUnmapBuffer(GL_ARRAY_BUFFER); assert( res == GL_TRUE ); return true; } bool GL_image_display_redraw(GL_image_display_context_t* ctx) { CONFIRM_SET( did_init); // Some of these can trigger during normal operation. I don't spam the // console in that case CONFIRM_SET_QUIET(did_init_texture); CONFIRM_SET_QUIET(did_set_aspect); CONFIRM_SET_QUIET(did_set_panzoom); if(ctx->use_glut) { if(ctx->glut_window == 0) return false; glutSetWindow(ctx->glut_window); } glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // // Wireframe rendering. For testing // glPolygonMode(GL_FRONT_AND_BACK, GL_LINE ); void bind_program(int program_index) { glUseProgram(ctx->programs[program_index].program); assert_opengl(); glBindVertexArray(ctx->programs[program_index].VBO_array); glBindBuffer(GL_ARRAY_BUFFER, ctx->programs[program_index].VBO_buffer); } ///////////// Render the image { bind_program(GL_image_display_program_index_image); assert_opengl(); glBindTexture( GL_TEXTURE_2D, ctx->texture_ID); assert_opengl(); glDrawElements(GL_TRIANGLES, 2*3, GL_UNSIGNED_BYTE, ((uint8_t[]){0,1,2, 2,1,3})); } ///////////// Render the overlaid lines { bind_program(GL_image_display_program_index_line); assert_opengl(); int ipoint0 = 0; for(int iset=0; isetNline_segment_sets; iset++) { const GL_image_display_line_segments_nopoints_t* set = &ctx->line_segment_sets[iset]; uint16_t indices[set->Nsegments*2]; for(int i=0; iNsegments*2; i++) indices[i] = i+ipoint0; set_uniform_3f(ctx, line_color_rgb, set->color_rgb[0], set->color_rgb[1], set->color_rgb[2]); glDrawElements(GL_LINES, set->Nsegments*2, GL_UNSIGNED_SHORT, indices); ipoint0 += set->Nsegments*2; } } return true; } bool GL_image_display_map_pixel_viewport_from_image(GL_image_display_context_t* ctx, double* xout, double* yout, double x, double y) { // This is analogous to what the vertex shader (vertex.glsl) does CONFIRM_SET(did_set_panzoom); CONFIRM_SET(did_init_texture); CONFIRM_SET(did_set_aspect); double vertex_x = (x+0.5) / ((double)(1 << ctx->decimation_level)*(double)ctx->image_width); double vertex_y = (y+0.5) / ((double)(1 << ctx->decimation_level)*(double)ctx->image_height); // GL does things upside down so the logic on vertex_y has the opposite polarity if(ctx->flip_x) vertex_x = 1.0 - vertex_x; if(!ctx->flip_y) vertex_y = 1.0 - vertex_y; // gl_Position. In [-1,1] double glpos_x = (vertex_x - ctx->center01_x) / ctx->visible_width01 * 2. * ctx->aspect_x; double glpos_y = (vertex_y - ctx->center01_y) / ctx->visible_width01 * 2. * ctx->aspect_y; // gl_Position in [0,1] double glpos01_x = glpos_x / 2. + 0.5; double glpos01_y = glpos_y / 2. + 0.5; glpos01_y = 1. - glpos01_y; // GL does things upside down *xout = glpos01_x * (double)ctx->viewport_width - 0.5; *yout = glpos01_y * (double)ctx->viewport_height - 0.5; return true; } bool GL_image_display_map_pixel_image_from_viewport(GL_image_display_context_t* ctx, double* xout, double* yout, double x, double y) { // This is analogous to what the vertex shader (vertex.glsl) does, in // reverse // Some of these can trigger during normal operation. I don't spam the // console in that case CONFIRM_SET_QUIET(did_set_panzoom); CONFIRM_SET_QUIET(did_init_texture); CONFIRM_SET_QUIET(did_set_aspect); // gl_Position in [0,1] double glpos01_x = ((x+0.5) / (double)ctx->viewport_width); double glpos01_y = ((y+0.5) / (double)ctx->viewport_height); glpos01_y = 1. - glpos01_y; // GL does things upside down // gl_Position. In [-1,1] double glpos_x = glpos01_x*2. - 1.; double glpos_y = glpos01_y*2. - 1.; double vertex_x = glpos_x / (2. * ctx->aspect_x) * ctx->visible_width01 + ctx->center01_x; double vertex_y = glpos_y / (2. * ctx->aspect_y) * ctx->visible_width01 + ctx->center01_y; // GL does things upside down so the logic on vertex_y has the opposite polarity if(ctx->flip_x) vertex_x = 1.0 - vertex_x; if(!ctx->flip_y) vertex_y = 1.0 - vertex_y; *xout = vertex_x*(double)(1 << ctx->decimation_level)*(double)ctx->image_width - 0.5; *yout = vertex_y*(double)(1 << ctx->decimation_level)*(double)ctx->image_height - 0.5; return true; } GL_image_display-0.23/GL_image_display.h000066400000000000000000000217051500256703300201770ustar00rootroot00000000000000#pragma once #include #include enum { GL_image_display_program_index_image, GL_image_display_program_index_line, GL_image_display_num_programs, GL_image_display_max_num_programs = 4 // I static_assert(GL_image_display_num_programs <= // GL_image_display_max_num_programs) in GL_image_display.c }; enum { GL_image_display_uniform_index_image_width_full, GL_image_display_uniform_index_image_height_full, GL_image_display_uniform_index_aspect, GL_image_display_uniform_index_center01, GL_image_display_uniform_index_visible_width01, GL_image_display_uniform_index_flip_x, GL_image_display_uniform_index_flip_y, GL_image_display_uniform_index_flip_y_data_is_upside_down, GL_image_display_uniform_index_line_color_rgb, GL_image_display_uniform_index_black_image, GL_image_display_num_uniforms, GL_image_display_max_num_uniforms = 32 // I static_assert(GL_image_display_num_uniforms <= // GL_image_display_max_num_uniforms) in GL_image_display.c }; typedef struct { uint32_t VBO_array, VBO_buffer; uint32_t program; // These should be GLint, but I don't want to #include . // I will static_assert() this in the .c to make sure they are compatible // // I place GL_image_display_max_num_uniforms here so that adding new // uniforms doesn't break the abi int32_t uniforms[GL_image_display_max_num_uniforms]; } GL_image_display_opengl_program_t; // The rendered line segments are defined as a number of segment sets. Each // segment set contains Nsegments line segments, with each line segment being // defined with 4 floats: {x0,y0,x1,y1}. The coordinates live contiguously in // the vertex_pool passed to GL_image_display_set_lines typedef struct { int Nsegments; float color_rgb[3]; } GL_image_display_line_segments_nopoints_t; typedef struct { GL_image_display_line_segments_nopoints_t segments; // Nsegments*2*2 values. Each segment has two points. Each point has (x,y) const float* points; } GL_image_display_line_segments_t; // By default, everything in this structure is set to 0 at init time typedef struct { bool use_glut; // meaningful only if use_glut. 0 means "invalid" or "closed" int glut_window; uint32_t texture_ID; uint32_t texture_PBO_ID; // valid if did_init_texture int image_width, image_height; int decimation_level; // valid if did_set_aspect int viewport_width, viewport_height; int Nline_segment_sets; GL_image_display_line_segments_nopoints_t* line_segment_sets; double x_centerpixel; double y_centerpixel; double visible_width_pixels; double visible_width01; double center01_x, center01_y; double aspect_x, aspect_y; union { struct { bool flip_x : 1; bool flip_y : 1; bool did_init : 1; bool did_init_texture : 1; bool did_set_aspect : 1; bool did_set_panzoom : 1; }; uint32_t flags; }; GL_image_display_opengl_program_t programs[GL_image_display_num_programs]; } GL_image_display_context_t; // All API functions return true on sucesss, false on error // The main init routine. We support 2 modes: // // - GLUT: static window (use_glut = true) // - no GLUT: higher-level application (use_glut = false) // // The GL_image_display_context_t structure should be zeroed out before calling. // The usual call looks like this: // // GL_image_display_context_t ctx = {}; // if( !GL_image_display_init(&ctx, false) ) // { // ERROR; // } bool GL_image_display_init( // output stored here GL_image_display_context_t* ctx, // input bool use_glut); // Update the image data being displayed. If image_filename==NULL && // image_data==NULL, we reset to an all-black image bool GL_image_display_update_image2(GL_image_display_context_t* ctx, // 0 == display full-resolution, original image // // 1 == decimate by a factor of 2: the // rendered image contains one pixel from // each 2x2 block of input // // 2 == decimate by a factor of 4: the // rendered image contains one pixel from // each 4x4 block of input // // and so on int decimation_level, bool flip_x, bool flip_y, // At most this ... const char* image_filename, // ... or these should be non-NULL. If // neither is, we reset to an all-black // image const char* image_data, int image_width, int image_height, // Supported: // - 8 for "grayscale" // - 24 for "bgr" int image_bpp, // how many bytes are used to represent each // row in image_data. Useful to display // non-contiguous data. As a shorthand, // image_pitch <= 0 can be passed-in to // indicate contiguous data int image_pitch); // Legacy compatibility function. Simple wrapper around // GL_image_display_update_image2(). Arguments are the same, except flip_x and // flip_y don't exits, and default to false bool GL_image_display_update_image( GL_image_display_context_t* ctx, int decimation_level, const char* image_filename, const char* image_data, int image_width, int image_height, int image_bpp, int image_pitch); // This exists because the FLTK widget often defers the first update_image() // call, so I don't do error checking until it's too late. Here I try to // validate the input as much as I can immediately, so that common errors are // caught early bool GL_image_display_update_image__validate_input ( // Either this should be given const char* image_filename, // Or these should be given const char* image_data, int image_width, int image_height, // Supported: // - 8 for "grayscale" // - 24 for "bgr" int image_bpp, bool check_image_file); void GL_image_display_deinit( GL_image_display_context_t* ctx ); // Usually this is called in response to the higher-level viewport being // resized, due to the window displaying the image being resized, for instance. bool GL_image_display_resize_viewport(GL_image_display_context_t* ctx, int viewport_width, int viewport_height); // Called to pan/zoom the image. Usually called in response to some interactive // user action, such as clicking/dragging with the mouse // If any of the given values are inf or nan or abs() >= 1e20, I use the // previously-set value bool GL_image_display_set_panzoom(GL_image_display_context_t* ctx, double x_centerpixel, double y_centerpixel, double visible_width_pixels); // Set the line overlay that we draw on top of the image. The full set of lines // being plotted are given in each call to this function bool GL_image_display_set_lines(GL_image_display_context_t* ctx, const GL_image_display_line_segments_t* line_segment_sets, int Nline_segment_sets); // Render bool GL_image_display_redraw(GL_image_display_context_t* ctx); // Convert a pixel from/to image coordinates to/from viewport coordinates bool GL_image_display_map_pixel_viewport_from_image(GL_image_display_context_t* ctx, double* xout, double* yout, double x, double y); bool GL_image_display_map_pixel_image_from_viewport(GL_image_display_context_t* ctx, double* xout, double* yout, double x, double y); GL_image_display-0.23/Makefile000066400000000000000000000076141500256703300163000ustar00rootroot00000000000000include choose_mrbuild.mk include $(MRBUILD_MK)/Makefile.common.header PROJECT_NAME := GL_image_display ABI_VERSION := 0 TAIL_VERSION := 1 LDLIBS += \ -lGLU -lGL -lepoxy -lglut \ -lfreeimage \ -lm \ -pthread CFLAGS += --std=gnu99 CCXXFLAGS += -Wno-missing-field-initializers -Wno-unused-parameter ################# library ############### LIB_SOURCES := GL_image_display.c GL_image_display.o: $(foreach w,image line,$(foreach t,vertex geometry fragment,$w.$t.glsl.h)) %.glsl.h: %.glsl < $< sed 's/.*/"&\\n"/g' > $@.tmp && mv $@.tmp $@ EXTRA_CLEAN += *.glsl.h BIN_SOURCES += \ GL_image_display-test-glut.c ################ FLTK widget library ############# # This is mostly a copy of the library logic in mrbuild. mrbuild currently # doesn't support more than one DSO per project, so I duplicate that logic here # for the second library LIB_SOURCES_FLTK := Fl_Gl_Image_Widget.cc LIB_OBJECTS_FLTK := $(addsuffix .o,$(basename $(LIB_SOURCES_FLTK))) LIB_NAME_FLTK = libGL_image_display_fltk LIB_TARGET_SO_BARE_FLTK = $(LIB_NAME_FLTK).so LIB_TARGET_SO_ABI_FLTK = $(LIB_TARGET_SO_BARE_FLTK).$(ABI_VERSION) LIB_TARGET_SO_FULL_FLTK = $(LIB_TARGET_SO_ABI_FLTK).$(TAIL_VERSION) LIB_TARGET_SO_ALL_FLTK = $(LIB_TARGET_SO_BARE_FLTK) $(LIB_TARGET_SO_ABI_FLTK) $(LIB_TARGET_SO_FULL_FLTK) $(LIB_OBJECTS_FLTK): CCXXFLAGS += -fPIC $(LIB_TARGET_SO_FULL_FLTK): LDFLAGS += -shared $(LD_DEFAULT_SYMVER) -fPIC -Wl,-soname,$(notdir $(LIB_TARGET_SO_BARE_FLTK)).$(ABI_VERSION) -Wl,-rpath='$$ORIGIN'$(call get_parentdir_relative_to_childdir,$(abspath .),$(dir $(abspath $@))) $(LIB_TARGET_SO_BARE_FLTK) $(LIB_TARGET_SO_ABI_FLTK): $(LIB_TARGET_SO_FULL_FLTK) ln -fs $(notdir $(LIB_TARGET_SO_FULL_FLTK)) $@ $(LIB_TARGET_SO_FULL_FLTK): $(LIB_OBJECTS_FLTK) $(CC_LINKER) $(LDFLAGS) $(filter %.o, $^) $(filter-out %.o, $^) $(LDLIBS) -o $@ all: $(LIB_TARGET_SO_ALL_FLTK) install: install_lib_fltk .PHONY: install_lib_fltk install_lib_fltk: $(LIB_TARGET_SO_ALL_FLTK) mkdir -p $(DESTDIR)/$(USRLIB) cp -P $(LIB_TARGET_SO_FULL_FLTK) $(DESTDIR)/$(USRLIB) chrpath -d $(DESTDIR)/$(USRLIB)/$(LIB_TARGET_SO_FULL_FLTK) ln -fs $(notdir $(LIB_TARGET_SO_FULL_FLTK)) $(DESTDIR)/$(USRLIB)/$(notdir $(LIB_TARGET_SO_ABI_FLTK)) ln -fs $(notdir $(LIB_TARGET_SO_FULL_FLTK)) $(DESTDIR)/$(USRLIB)/$(notdir $(LIB_TARGET_SO_BARE_FLTK)) $(LIB_TARGET_SO_FULL_FLTK): lib$(PROJECT_NAME).so $(LIB_TARGET_SO_FULL_FLTK): LDLIBS += -lfltk_gl -lfltk -lX11 ############### FLTK test application ############ BIN_SOURCES += \ GL_image_display-test-fltk.cc CXXFLAGS_FLTK := $(shell fltk-config --use-images --cxxflags) CXXFLAGS += $(CXXFLAGS_FLTK) GL_image_display-test-fltk: $(LIB_TARGET_SO_FULL_FLTK) GL_image_display-test-fltk: LDLIBS += -lfltk_gl -lfltk -lX11 ############### FLTK widget Python wrapper ############ install all: Fl_Gl_Image_Widget.py _Fl_Gl_Image_Widget$(PY_EXT_SUFFIX) %.py %_pywrap.h %_pywrap.cc: %.i swig \ -w302 -w312 -w325 -w362 -w389 -w401 -w473 -w509 \ -I/usr/include/ \ -DFL_EXPORT="" \ -DFL_OVERRIDE="" \ -DPYTHON \ -DPYTHON3 \ -python \ -c++ \ -keyword \ -shadow \ -fastdispatch \ -outdir . \ -o $*_pywrap.cc \ $< EXTRA_CLEAN += Fl_Gl_Image_Widget.py *_pywrap.h *_pywrap.cc Fl_Gl_Image_Widget.py Fl_Gl_Image_Widget_pywrap.cc: Fl_Gl_Image_Widget.hh Fl_Gl_Image_Widget_pywrap.o: CXXFLAGS += $(PY_MRBUILD_CFLAGS) _Fl_Gl_Image_Widget$(PY_EXT_SUFFIX): Fl_Gl_Image_Widget_pywrap.o $(LIB_TARGET_SO_FULL_FLTK) $(PY_MRBUILD_LINKER) $(LDFLAGS) $(PY_MRBUILD_LDFLAGS) -Wl,-rpath=$$ORIGIN/ $^ -o $@ # The python libraries (compiled ones and ones written in python) all live in # mrcal/ DIST_PY3_MODULES := Fl_Gl_Image_Widget.py _Fl_Gl_Image_Widget$(PY_EXT_SUFFIX) DIST_INCLUDE := \ Fl_Gl_Image_Widget.hh \ GL_image_display.h # I don't ship any binaries DIST_BIN := "" DIST_DOC := \ GL_image_display-test-fltk.cc \ GL_image_display-test-fltk.py \ GL_image_display-test-glut.c include $(MRBUILD_MK)/Makefile.common.footer GL_image_display-0.23/README.org000066400000000000000000000117011500256703300162760ustar00rootroot00000000000000This is an image-display library that uses OpenGL internally for efficient (re)drawing. * Overview Image-display widgets are common in GUI toolkits (FLTK has [[https://www.fltk.org/doc-1.3/classFl__RGB__Image.html][=Fl_RGB_Image=]] for instance), but they generally use the CPU to draw, which becomes slow when given large images. By contrast, /this/ toolkit uses OpenGL hardware, so after the initial cost to load an image, the drawing, redrawing, panning and zooming are effectively free. This makes it possible to quickly build responsive applications that display images. Three separate interfaces are available: - C library defined in [[https://github.com/dkogan/GL_image_display/blob/master/GL_image_display.h][=GL_image_display.h=]] - C++ library providing an FLTK widget, defined in [[https://github.com/dkogan/GL_image_display/blob/master/Fl_Gl_Image_Widget.hh][=Fl_Gl_Image_Widget.hh=]] - Python library wrapping the FLTK widget to make it usable in Python. Defined in the SWIG interface in [[https://github.com/dkogan/GL_image_display/blob/master/Fl_Gl_Image_Widget.i][=Fl_Gl_Image_Widget.i=]] * Build Run =make=. A few dependencies are required to build. See the =Build-Depends= section in the [[https://salsa.debian.org/debian/gl-image-display/-/blob/master/debian/control][=debian/control= file]]. * C library The core functionality in this library is made available in the C library. This can be used directly to build a GLUT application (sample in [[https://github.com/dkogan/GL_image_display/blob/master/GL_image_display-test-glut.c][=GL_image_display-test-glut.c=]]), or it can be used to implement higher-level components, such as the [[https://github.com/dkogan/GL_image_display/blob/master/Fl_Gl_Image_Widget.hh][=Fl_Gl_Image_Widget=]] FLTK widget. Please see [[https://github.com/dkogan/GL_image_display/blob/master/GL_image_display.h][=GL_image_display.h=]] and the GLUT sample for the API. * FLTK widget An =Fl_Gl_Image_Widget= is provided in [[https://github.com/dkogan/GL_image_display/blob/master/Fl_Gl_Image_Widget.hh][=Fl_Gl_Image_Widget.hh=]], with a sample application in [[https://github.com/dkogan/GL_image_display/blob/master/GL_image_display-test-fltk.cc][=GL_image_display-test-fltk.cc=]]. This is a "normal" FLTK widget, that wraps the necessary functions from the C API, and that defines the expected UI behaviors to make it immediately usable. The UI supported by the default =Fl_Gl_Image_Widget::handle()= function: - Mouse click/drag pans - Mousewheel pans. Vertical and horizontal mousewheels are supported to pan in the two directions - Ctrl-vertical-mousewheel zooms - =u= key on the keyboard reverts to the original full-size view (this is the key used by [[http://gnuplot.info][gnuplot]].) To define new/different interactions, subclass =Fl_Gl_Image_Widget=, and override the =handle()= method. The widget automatically handles resizing operations. The normal usage sequence is very simple: - Construct the =Fl_Gl_Image_Widget= - Call the =update_image2()= method to give it data Note that the =set_panzoom()= method is =virtual=, so a derived class can override it to get a "notification" of any pan/zoom operation. One application of this is to sync the pan/zoom setting between multiple =Fl_Gl_Image_Widget= objects in a single application. * Python FLTK widget For FLTK applications written in Python using [[https://pyfltk.sourceforge.io/][pyfltk]], a Python flavor of the =Fl_Gl_Image_Widget= is available. The wrapper code is generated with [[http://www.swig.org][SWIG]], so the Python API largely mirrors the C++ API. Some Python-specific notes: - Instead of returning =False= to indicate failure, these functions raise an =Exception= - SWIG directors are used, so the widget can be subclassed in Python, and works as one would expect - =update_image2()= can take an image filename (as in C++) /or/ a numpy array of data. The dimensions, depth and pitch are all read from this array - The pixel mapping functions =map_pixel_viewport_from_image()= and =map_pixel_image_from_viewport()= ingest a length-2 iterable (or a numpy array) and return a length-2 tuple. - =set_lines()= takes in =dict= objects each with keys - =points=: a numpy array of shape (=Nsegments=,2,2). These are the line segments, each represented as =(x0,y0)= - =(x1,y1)= - =color_rgb=: a numpy array of shape (3,) which contains the RGB color for this set of line segments. Each channel ranges from 0 to 1. A sample application is available in [[https://github.com/dkogan/GL_image_display/blob/master/GL_image_display-test-fltk.py][=GL_image_display-test-fltk.py=]]. * Repository https://www.github.com/dkogan/GL_image_display * Author Dima Kogan (=dima@secretsauce.net=) * License and Copyright Copyright (c) 2021 California Institute of Technology ("Caltech"). U.S. Government sponsorship acknowledged. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 GL_image_display-0.23/choose_mrbuild.mk000066400000000000000000000014651500256703300201650ustar00rootroot00000000000000# Use the local mrbuild or the system mrbuild or tell the user how to download # it ifneq (,$(wildcard mrbuild/)) MRBUILD_MK=mrbuild MRBUILD_BIN=mrbuild/bin else ifneq (,$(wildcard /usr/include/mrbuild/Makefile.common.header)) MRBUILD_MK=/usr/include/mrbuild MRBUILD_BIN=/usr/bin else V := 1.13 SHA512 := 7a1422026cdbe12cea6882c3b76087dcc4c1d258369ec2abb941a779a539253a37850563f801049d570e8ad342722030fd918114388b5155a89644491a221f16 URL := https://github.com/dkogan/mrbuild/archive/refs/tags/v$V.tar.gz TARGZ := mrbuild-$V.tar.gz cmd := wget -O $(TARGZ) ${URL} && sha512sum --quiet --strict -c <(echo $(SHA512) $(TARGZ)) && tar xvfz $(TARGZ) && ln -fs mrbuild-$V mrbuild $(error mrbuild not found. Either 'apt install mrbuild', or if not possible, get it locally like this: '${cmd}') endif GL_image_display-0.23/image.fragment.glsl000066400000000000000000000007231500256703300204010ustar00rootroot00000000000000/* -*- c -*- */ #version 330 layout(location = 0) out vec3 frag_color; in vec2 tex_xy_fragment; uniform int black_image; uniform sampler2D tex; void main(void) { if(black_image != 0) { frag_color = vec3(0.,0.,0.); } else if(tex_xy_fragment.x < 0. || tex_xy_fragment.x > 1. || tex_xy_fragment.y < 0. || tex_xy_fragment.y > 1.) { discard; } else { frag_color = texture(tex, tex_xy_fragment).xyz; } } GL_image_display-0.23/image.geometry.glsl000066400000000000000000000005461500256703300204340ustar00rootroot00000000000000/* -*- c -*- */ #version 330 layout (triangles) in; layout (triangle_strip, max_vertices=3) out; in vec2 tex_xy_geometry[]; out vec2 tex_xy_fragment; void main() { for(int i=0; i #define MSG(fmt, ...) fprintf(stderr, "%s(%d) at %s(): " fmt "\n", __FILE__, __LINE__, __func__, ##__VA_ARGS__)