OpenCV:Example:Tkinter
OpenCV를 Python:Tkinter에 그리는 방법
Example
# -*- coding: utf-8 -*-
from argparse import Namespace
from math import floor
from typing import Final
from PIL.Image import fromarray
from PIL.ImageTk import PhotoImage
from numpy import full, uint8, ndarray
from tkinter import NW, Canvas, Tk
from answer_mediaplayer.logging.logging import logger
DEFAULT_TITLE: Final[str] = "Answer MediaPlayer"
class DefaultApp:
def __init__(self, width=800, height=600, x=0, y=0, title=DEFAULT_TITLE, fps=60):
self._milliseconds = floor(1000 / fps)
self._tk = Tk()
self._tk.title(title)
self._tk.geometry(f"{width}x{height}+{x}+{y}")
self._tk.resizable(True, True)
self._canvas = Canvas(self._tk, width=width, height=height, bg="white")
self._canvas.pack(fill="both", expand=True)
self._image = self.make_bgr888(width, height)[:, :, ::-1]
self._photo = PhotoImage(image=fromarray(self._image, mode="RGB"))
self._canvas.create_image(0, 0, image=self._photo, anchor=NW)
self._tk.after(self._milliseconds, self.update)
@staticmethod
def make_bgr888(width: int, height: int, blue=0, green=0, red=0) -> ndarray:
return full(
shape=(height, width, 3),
fill_value=(blue, green, red),
dtype=uint8,
)
def update(self) -> None:
try:
width = self._tk.winfo_width()
height = self._tk.winfo_height()
logger.info(f"DefaultApp.update() ... {width}x{height}")
self._image = self.make_bgr888(width, height, blue=255)[:, :, ::-1]
self._photo = PhotoImage(image=fromarray(self._image, mode="RGB"))
self._canvas.create_image(0, 0, image=self._photo, anchor=NW)
except BaseException as e:
logger.exception(e)
finally:
self._tk.after(self._milliseconds, self.update)
def run(self) -> None:
self._tk.mainloop()
def default_main(args: Namespace) -> None:
assert args is not None
debug = args.debug
verbose = args.verbose
assert isinstance(debug, bool)
assert isinstance(verbose, int)
app = DefaultApp()
app.run()
중요한 점은 canvas에 그려야 할 데이터를 가지고 있는 Numpy Array 와 PhotoImage 객체의 영속성을 보장해야 한다. (한마디로 임시변수로 만들면 안된다)
Refactoring class ver
# -*- coding: utf-8 -*-
from functools import partial
from threading import Lock
from tkinter import NW, Canvas, Event, Tk
from typing import Final, Optional, Tuple
import cv2
from numpy import uint8, zeros
from numpy.typing import NDArray
from PIL.Image import fromarray
from PIL.ImageTk import PhotoImage
INFINITY_AFTER: Final[int] = -1
DEFAULT_WIDTH: Final[int] = 640
DEFAULT_HEIGHT: Final[int] = 360
DEFAULT_X: Final[int] = 0
DEFAULT_Y: Final[int] = 0
DEFAULT_TITLE: Final[str] = "TkWindow"
DEFAULT_FPS: Final[int] = 30
USE_INFINITY_AFTER: Final[bool] = True
class TkWindow:
_exception: Optional[BaseException]
def __init__(
self,
width=DEFAULT_WIDTH,
height=DEFAULT_HEIGHT,
x=DEFAULT_X,
y=DEFAULT_Y,
title=DEFAULT_TITLE,
fps=DEFAULT_FPS,
use_infinity_after=USE_INFINITY_AFTER,
):
self._exception = None
self._milliseconds = 1000 // fps
self._tk = Tk()
self._tk.title(title)
self._tk.geometry(f"{width}x{height}+{x}+{y}")
self._tk.resizable(True, True)
self._canvas = Canvas(self._tk, width=width, height=height, bg="white")
self._canvas.pack(fill="both", expand=True)
self._image = zeros((width, height, 3), dtype=uint8)
self._photo = PhotoImage(image=fromarray(self._image, mode="RGB"))
self._canvas.create_image(0, 0, image=self._photo, anchor=NW)
self._tk.bind("<Configure>", self._configure)
self._tk.bind("<Escape>", self._escape)
self._tk.bind("<Key>", self._key)
if use_infinity_after:
self._tk.after(0, partial(self._update, INFINITY_AFTER))
else:
self._tk.after(0, partial(self._update, 0))
self._tk_lock = Lock()
@property
def tk(self) -> Tk:
return self._tk
@property
def width(self) -> int:
return self._tk.winfo_width()
@property
def height(self) -> int:
return self._tk.winfo_height()
@property
def size(self) -> Tuple[int, int]:
return self.width, self.height
def mainloop(self, shield_exception=False) -> None:
self._tk.mainloop() # Holding
if not shield_exception and self._exception is not None:
raise RuntimeError from self._exception
def quit_threadsafe(self) -> None:
with self._tk_lock:
self._tk.quit()
def _configure(self, event: Event) -> None:
pass
def _escape(self, event: Event) -> None:
assert event is not None
self.on_escape()
def _key(self, event: Event) -> None:
assert self._exception is None
try:
self.on_keydown(event.char)
except BaseException as e:
self._exception = e
self.quit_threadsafe()
def cvt_preview(self, image: NDArray) -> NDArray:
resized = cv2.resize(image, self.size)
if len(resized.shape) == 2:
return cv2.cvtColor(resized, cv2.COLOR_GRAY2RGB)
else:
return resized[:, :, ::-1]
def _update(self, remain=INFINITY_AFTER, image: Optional[NDArray] = None) -> None:
assert self._exception is None
try:
grab = image if image else self.on_grab()
self._image = self.cvt_preview(grab)
self._photo = PhotoImage(image=fromarray(self._image, mode="RGB"))
self._canvas.create_image(0, 0, image=self._photo, anchor=NW)
except BaseException as e:
self._exception = e
self.quit_threadsafe()
finally:
if remain == INFINITY_AFTER:
self.after_threadsafe(INFINITY_AFTER)
elif remain >= 1:
self.after_threadsafe(remain - 1)
def after_threadsafe(
self,
remain=INFINITY_AFTER,
image: Optional[NDArray] = None,
*,
milliseconds: Optional[int] = None,
) -> None:
with self._tk_lock:
ms = milliseconds if milliseconds is not None else self._milliseconds
self._tk.after(ms, partial(self._update, remain, image))
def on_grab(self) -> NDArray:
return zeros((self.width, self.height, 3), dtype=uint8)
def on_escape(self) -> None:
self.quit_threadsafe()
def on_keydown(self, code: str) -> None:
pass