immvision.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. """# ImmVision: Download & Inspect Images
  2. [ImmVision](https://github.com/pthom/immvision) is an image debugger for Dear ImGui with zoom, pan, pixel inspection, and colormaps.
  3. This demo shows:
  4. * how to *download a file from a URL* in two ways: synchronous or asynchronous
  5. * how to use ImmVision to display "synced" images
  6. **Try it:**
  7. * Drag to pan, scroll to zoom (move one image, the other follows)
  8. * Resize the images by dragging the bottom-right corner of each
  9. * Adjust blur and derivative order.
  10. * Open the "Options" panel on the filtered image to try colormaps
  11. **Links:**
  12. - [ImmVision repository](https://github.com/pthom/immvision)
  13. """
  14. # ruff: noqa: E402
  15. import math
  16. import numpy as np
  17. from numpy.typing import NDArray
  18. from enum import Enum
  19. from imgui_bundle import imgui, immvision, immapp, hello_imgui
  20. import cv2
  21. # Each run downloads a different random image from picsum.photos
  22. _IMAGE_URL = "https://picsum.photos/640/480"
  23. def _decode_image(image_bytes: bytes) -> NDArray[np.uint8]:
  24. """Decode JPEG/PNG bytes to numpy array, with fallback test pattern."""
  25. if len(image_bytes) > 0:
  26. return cv2.imdecode( # type: ignore
  27. np.frombuffer(image_bytes, dtype=np.uint8),
  28. cv2.IMREAD_COLOR)
  29. # Fallback: colorful test pattern
  30. img = np.zeros((480, 640, 3), dtype=np.uint8)
  31. for i in range(480):
  32. for j in range(640):
  33. img[i, j] = (i % 256, j % 256, (i + j) % 256)
  34. return img
  35. def download_random_image_sync() -> NDArray[np.uint8]:
  36. return _decode_image(immapp.download_url_bytes(_IMAGE_URL))
  37. async def download_random_image_async() -> NDArray[np.uint8]:
  38. return _decode_image(await immapp.download_url_bytes_async(_IMAGE_URL))
  39. # Tell ImmVision we use RGB order (not BGR like OpenCV defaults)
  40. immvision.use_rgb_color_order()
  41. class SobelParams:
  42. """Parameters for the Sobel edge filter."""
  43. class Orientation(Enum):
  44. Horizontal = 0
  45. Vertical = 1
  46. blur_size: float = 1.25
  47. deriv_order: int = 1
  48. k_size: int = 7
  49. orientation: Orientation = Orientation.Vertical
  50. def compute_sobel(image, params: SobelParams):
  51. """Apply Sobel edge detection."""
  52. gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
  53. blurred = cv2.GaussianBlur(
  54. gray / 255.0, (0, 0),
  55. sigmaX=params.blur_size,
  56. sigmaY=params.blur_size)
  57. scale = 1.0 / math.pow(
  58. 2.0, params.k_size - 2 * params.deriv_order - 2)
  59. dx = params.deriv_order if params.orientation == SobelParams.Orientation.Vertical else 0
  60. dy = 0 if dx > 0 else params.deriv_order
  61. return cv2.Sobel(
  62. blurred, ddepth=cv2.CV_64F,
  63. dx=dx, dy=dy,
  64. ksize=params.k_size, scale=scale)
  65. class AppState:
  66. def __init__(self, image):
  67. self.image = image
  68. self.sobel_params = SobelParams()
  69. self.image_sobel = compute_sobel(
  70. image, self.sobel_params)
  71. # ImmVision display params
  72. # zoom_key links both images (synced pan/zoom)
  73. disp_w = 350
  74. self.params = immvision.ImageParams()
  75. self.params.image_display_size = (disp_w, 0)
  76. self.params.zoom_key = "z"
  77. self.params_sobel = immvision.ImageParams()
  78. self.params_sobel.image_display_size = (disp_w, 0)
  79. self.params_sobel.zoom_key = "z"
  80. self.params_sobel.show_options_panel = True
  81. def gui_sobel_params(p: SobelParams) -> bool:
  82. """GUI for Sobel filter parameters. Returns True if changed."""
  83. changed = False
  84. em = hello_imgui.em_size()
  85. # Blur
  86. imgui.set_next_item_width(em * 8)
  87. c, p.blur_size = imgui.slider_float(
  88. "Blur", p.blur_size, 0.5, 10)
  89. changed = changed or c
  90. imgui.same_line()
  91. # Deriv order
  92. imgui.text("Order:")
  93. imgui.same_line()
  94. for order in (1, 2, 3, 4):
  95. c, p.deriv_order = imgui.radio_button(str(order), p.deriv_order, order)
  96. changed = changed or c
  97. imgui.same_line()
  98. # Orientation
  99. imgui.text(" Dir:")
  100. imgui.same_line()
  101. _Orientation = SobelParams.Orientation # alias for brevity
  102. if imgui.radio_button("H", p.orientation == _Orientation.Horizontal):
  103. p.orientation = _Orientation.Horizontal
  104. changed = True
  105. imgui.same_line()
  106. if imgui.radio_button("V", p.orientation == _Orientation.Vertical):
  107. p.orientation = _Orientation.Vertical
  108. changed = True
  109. return changed
  110. def gui(state: AppState) -> None:
  111. s = state
  112. # Documentation panel
  113. immapp.render_markdown_doc_panel(__doc__, height_em=10)
  114. # Sobel parameters
  115. params_changed = gui_sobel_params(s.sobel_params)
  116. # Download another image
  117. imgui.same_line(spacing=hello_imgui.em_size(5))
  118. new_image = False
  119. if imgui.button("Download new image"):
  120. # Sync download: blocks briefly while fetching.
  121. # We can't use await here (GUI callbacks are synchronous).
  122. # For a non-blocking alternative, one could start an async
  123. # download in a background task, store the result in a
  124. # shared variable, and pick it up on the next frame.
  125. # See main_async() for async download at startup.
  126. s.image = download_random_image_sync()
  127. new_image = True
  128. # Recompute Sobel if params or image changed
  129. if params_changed or new_image:
  130. s.image_sobel = compute_sobel(s.image, s.sobel_params)
  131. # refresh_image tells ImmVision to re-upload the texture
  132. # (must be set each frame: True if changed, False otherwise)
  133. s.params.refresh_image = new_image
  134. s.params_sobel.refresh_image = params_changed or new_image
  135. # Display both images side by side
  136. immvision.image("Original", s.image, s.params)
  137. imgui.same_line()
  138. immvision.image("Sobel", s.image_sobel, s.params_sobel)
  139. async def main_async():
  140. """Async main: downloads image without blocking,
  141. then runs the app asynchronously."""
  142. # At startup, we download an image asynchronously
  143. image = await download_random_image_async()
  144. state = AppState(image)
  145. await immapp.run_async(
  146. lambda: gui(state),
  147. window_size=(1000, 700),
  148. window_title="ImmVision: Image Inspection",
  149. with_markdown=True,
  150. fps_idle=0,
  151. ini_disable=True)
  152. import asyncio
  153. from imgui_bundle import __bundle_pyodide__
  154. if __bundle_pyodide__:
  155. # We are already in an async context with pyodide
  156. asyncio.ensure_future(main_async())
  157. else:
  158. # On desktop, create one
  159. asyncio.run(main_async())