Source code for imgui_datascience.imgui_cv

import cv2
import copy
import xxhash
import numpy as np
import imgui
import OpenGL.GL as gl
from .static_vars import *
from timeit import default_timer as timer
from . import imgui_ext
import math

_start = timer()

USE_FAST_HASH = True

def _is_close(a, b):
    return math.fabs(a - b) < 1E-6


# noinspection PyShadowingNames
[docs]class ImageAdjustments: def __init__(self, factor=1., delta=0.): self.factor = factor self.delta = delta
[docs] def is_none(self): return _is_close(self.factor, 1.) and _is_close(self.delta, 0.)
[docs] def adjust(self, image): if self.is_none(): return image else: adjusted = ((image + self.delta) * self.factor).astype(image.dtype) return adjusted
def __hash__(self): return hash((self.factor, self.delta))
def _hash_image(image): """ Two hash variant are possible : - if imgui_cv.USE_FAST_HASH is True : select 100 random pixels and hash them - otherwise : compute the hash of the whole image (using xxhash for performance) :param image: :return:hash """ if USE_FAST_HASH: rng = np.random.RandomState(89) inds = rng.randint(low=0, high=image.size, size=100) b = image.flat[inds] result = hash(tuple(b.data)) return result else: # cf https://stackoverflow.com/questions/16589791/most-efficient-property-to-hash-for-numpy-array h = xxhash.xxh64() h.update(image) result = h.intdigest() h.reset() return result
[docs]class ImageAndAdjustments: def __init__(self, image, image_adjustments): self.image = image self.image_adjustments = image_adjustments
[docs] def adjusted_image(self): return self.image_adjustments.adjust(self.image)
def __hash__(self): hash_adjust = hash(self.image_adjustments) hash_image = _hash_image(self.image) result = hash((hash_adjust, hash_image)) return result def __eq__(self, other): """ For performance reasons, the __eq__ operator is made to take only the hash into account. @see _image_to_texture() """ hash1 = hash(self) hash2 = hash(other) return hash1 == hash2
[docs]class SizePixel: def __init__(self, width=0, height=0): self.width = int(width) self.height = int(height)
[docs] @staticmethod def from_image(image): self = SizePixel() self.width = image.shape[1] self.height = image.shape[0] return self
[docs] def as_tuple_width_height(self): return self.width, self.height
def _to_rgb_image(img): img_rgb = None if len(img.shape) >= 3: channels = img.shape[2] else: channels = 1 if channels == 1: if img.dtype == np.uint8: img_rgb = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) elif img.dtype in [np.float32, np.float64]: img_grey = np.uint8(img * 255.) img_rgb = cv2.cvtColor(img_grey, cv2.COLOR_GRAY2BGR) elif channels == 3: if not img.dtype == np.uint8: raise ValueError("imgui_cv does only support uint8 images with multiple channels") img_rgb = img elif channels == 4: if not img.dtype == np.uint8: raise ValueError("imgui_cv does only support uint8 images with multiple channels") # we do not handle alpha very well... img_rgb = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) return img_rgb def _image_rgb_to_texture_impl(img_rgb): """ Performs the actual transfer to the gpu and returns a texture_id """ # inspired from https://www.programcreek.com/python/example/95539/OpenGL.GL.glPixelStorei (example 3) width = img_rgb.shape[1] height = img_rgb.shape[0] texture_id = gl.glGenTextures(1) gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) gl.glBindTexture(gl.GL_TEXTURE_2D, texture_id) gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, width, height, 0, gl.GL_BGR, gl.GL_UNSIGNED_BYTE, img_rgb) gl.glBindTexture(gl.GL_TEXTURE_2D, 0) return texture_id # ALL_TEXTURES contains a dict of all the images that were transferred to the GPU # plus their last access time ALL_TEXTURES = {} def _image_to_texture(image_and_adjustments, always_refresh = False): """ _image_to_texture will transfer the image to the GPU and return a texture Id Some GPU might choke if too many textures are transferred. For this reason : - a cache is maintained (ALL_TEXTURES) - a quick comparison is made before the transfer: @see _hash_image() @see ImageAndAdjustments.__eq__() : for performance reasons, the __eq__ operator is made to take only the hash into account. :param image_and_adjustments: :return: texture_id """ if always_refresh or (image_and_adjustments not in ALL_TEXTURES): image_and_adjustments_copy = copy.deepcopy(image_and_adjustments) img_adjusted = image_and_adjustments_copy.adjusted_image() img_rgb = _to_rgb_image(img_adjusted) texture_id = _image_rgb_to_texture_impl(img_rgb) ALL_TEXTURES[image_and_adjustments_copy] = [texture_id, timer()] # print("Added one texture, len=" + str(len(ALL_TEXTURES))) texture_and_time = ALL_TEXTURES[image_and_adjustments] texture_and_time[1] = timer() # update this texture last usage time return texture_and_time[0] def _clear_all_cv_textures(): global ALL_TEXTURES all_textures_updated = {} textures_to_delete = [] now = timer() for id, texture_and_time in ALL_TEXTURES.items(): age_seconds = now - texture_and_time[1] if age_seconds < 0.3: all_textures_updated[id] = texture_and_time else: textures_to_delete.append(texture_and_time[0]) ALL_TEXTURES = all_textures_updated if len(textures_to_delete) > 0: gl.glDeleteTextures(textures_to_delete) # print("Delete {0} old texture(s), len={1}".format(len(textures_to_delete), len(ALL_TEXTURES))) def _image_viewport_size(image, width=None, height=None): image_width = image.shape[1] image_height = image.shape[0] if (width is not None) and (height is not None): viewport_size = SizePixel(width, height) elif width is not None: viewport_size = SizePixel(width, round(image_height / image_width * width)) elif height is not None: viewport_size = SizePixel(round(image_width / image_height * height), height) else: viewport_size = SizePixel.from_image(image) return viewport_size @static_vars( zoomed_status={}, zoom_click_times={}, last_shown_image=None) def _image_impl( image_and_ajustments, width=None, height=None, title="", always_refresh = False ): statics = _image_impl.statics statics.last_shown_image = image_and_ajustments zoom_key = imgui_ext.make_unique_label(title) if zoom_key not in statics.zoomed_status: statics.zoom_click_times[zoom_key] = 0 statics.zoomed_status[zoom_key] = False if statics.zoomed_status[zoom_key]: viewport_size = SizePixel.from_image(image_and_ajustments.image) else: viewport_size = _image_viewport_size(image_and_ajustments.image, width, height) if zoom_key not in statics.zoomed_status: statics.zoomed_status[zoom_key] = False statics.zoom_click_times[zoom_key] = timer() texture_id = _image_to_texture(image_and_ajustments, always_refresh = always_refresh) if title == "": imgui.image_button(texture_id, viewport_size.width, viewport_size.height, frame_padding=0) is_mouse_hovering = imgui.is_item_hovered() else: imgui.begin_group() imgui.image_button(texture_id, viewport_size.width, viewport_size.height, frame_padding=0) is_mouse_hovering = imgui.is_item_hovered() imgui.text(title) imgui.end_group() if is_mouse_hovering and imgui.get_io().mouse_down[0]: last_time = statics.zoom_click_times[zoom_key] now = timer() if now - last_time > 0.3: statics.zoomed_status[zoom_key] = not statics.zoomed_status[zoom_key] statics.zoom_click_times[zoom_key] = now return mouse_position_last_image()
[docs]def image( img, width=None, height=None, title="", image_adjustments=None, always_refresh = False ): if image_adjustments is None: image_adjustments = ImageAdjustments() image_and_ajustments = ImageAndAdjustments(img, image_adjustments) return _image_impl( image_and_ajustments, width=width, height=height, title=title, always_refresh = always_refresh )
def _is_in_image(pixel, image_shape): # type : (imgui.Vec2, shape) -> Bool w = image_shape[1] h = image_shape[0] x = pixel.x y = pixel.y return x >= 0 and x < w and y >= 0 and y < h def _is_in_last_image(pixel): last_image_shape = _image_impl.statics.last_shown_image.image.shape return _is_in_image(pixel, last_image_shape)
[docs]def mouse_position_last_image(): io = imgui.get_io() mouse = io.mouse_pos rect_min = imgui.get_item_rect_min() mouse_relative = imgui.Vec2(mouse.x - rect_min.x, mouse.y - rect_min.y) if not _is_in_last_image(mouse_relative): return None else: return mouse_relative
[docs]def is_mouse_hovering_last_image(): # only works if the image was presented in its original size if not imgui.is_item_hovered_rect(): return False mouse = mouse_position_last_image() if mouse is None: return False else: return True
[docs]def image_explorer(image, width=None, height=None, title="", zoom_key="", hide_buttons=False, image_adjustments=None): """ :param image_adjustments: :param hide_buttons: :param image: opencv / np image. :param width: :param height: :param title: an optional title :param zoom_key: Set the same zoom_key for two image if you want to link their zoom settings :return: mouse location in image coordinates (None if the mouse is outside of the image) """ if image_adjustments is None: image_adjustments = ImageAdjustments() from ._imgui_cv_zoom import image_explorer_autostore_zoominfo viewport_size = _image_viewport_size(image, width, height) imgui.begin_group() mouse_location_original_image = image_explorer_autostore_zoominfo( image, viewport_size, title, zoom_key, image_adjustments, hide_buttons=hide_buttons) imgui.end_group() return mouse_location_original_image