Displaying Images and Windows
OpenCV's HighGUI module provides the minimal display loop every interactive vision app needs: imshow to render, namedWindow to control the window, waitKey to process events, and destroyWindow to clean up. The loop pattern - show, wait, check the key, break on quit - is the skeleton of every demo, and the window property flags are what turn a fixed popup into a resizable, titled workspace.
The Display Loop
import cv2
img = cv2.imread("photo.jpg")
if img is None:
raise SystemExit("image not found")
cv2.imshow("Photo", img)
# waitKey processes GUI events and returns the pressed key (-1 if none)
# pass 0 to wait indefinitely, or a timeout in milliseconds
while True:
key = cv2.waitKey(1) & 0xFF
if key == ord("q"): # press q to quit
break
cv2.destroyAllWindows() # close every window when done
Three rules for the loop. First, waitKey is not optional - without it the window freezes, because GUI events are only processed while waiting. Second, mask the return with & 0xFF - on some platforms the high byte carries modifier keys and breaks the comparison. Third, always end with destroyAllWindows, especially in a long-running process that shows many images.
Window Properties
import cv2
img = cv2.imread("photo.jpg")
# a resizable window with a title
cv2.namedWindow("Inspector", cv2.WINDOW_NORMAL)
cv2.resizeWindow("Inspector", 800, 600) # explicit size
cv2.imshow("Inspector", img)
# move the window to a screen position
cv2.moveWindow("Inspector", 100, 100)
# set the size limit for a resizable window
cv2.resizeWindow("Inspector", 640, 480)
cv2.waitKey(0)
cv2.destroyWindow("Inspector") # or destroyAllWindows()
namedWindow with WINDOW_NORMAL gives a resizable window; WINDOW_AUTOSIZE (the imshow default) locks the size to the image. The companion functions - resizeWindow, moveWindow, getWindowProperty - let you script the layout, which is how multi-panel vision tools (original left, processed right, mask bottom) are arranged without a framework.
Multi-Panel Layouts
import cv2
import numpy as np
img = cv2.imread("photo.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 100, 200)
cv2.imshow("Original", img)
cv2.imshow("Grayscale", gray)
cv2.imshow("Edges", edges)
cv2.waitKey(0)
cv2.destroyAllWindows()
Showing the pipeline stages side by side - source, intermediate, result - is the fastest way to debug a vision chain: the exact step where the information disappears is visible at a glance. Each imshow call opens (or reuses) its own window, and grayscale images (2-D arrays) display correctly without conversion. This three-panel pattern is the default debugging view for the rest of the tutorial.
Gotchas
- Headless failures -
imshowneeds a display; on a server use the headless build and write to files. - Window reuse -
imshowwith the same name updates that window; a stale name from a crashed run can cause surprises in a notebook. - Notebook rendering - in Jupyter,
cv2.imshowmay not show; the idiom ismatplotlibwith the BGR-to-RGB flip, orIPython.displaywith an encoded image. - Blocking -
waitKey(0)blocks forever;waitKey(1)is the non-blocking tick that keeps a loop responsive.
Summary
Display with the minimal loop: imshow, then a while that calls waitKey(1), masks the key with & 0xFF, and breaks on quit, ending in destroyAllWindows. Use namedWindow(WINDOW_NORMAL) plus resizeWindow/moveWindow for a controllable workspace, and show pipeline stages in parallel panels to debug by inspection. Remember the environment caveats - headless builds for servers, matplotlib for notebooks - and the display layer is just a transport: the vision work happens on the arrays, and the window is where you watch it happen.