Já jsem zkusil vyhodit ten matplotlib. Jen jsem to na zkoušku namatlal a pořád to asi zbytečně pracuje s velkým obrázkem, který se jen pro zobrazení škáluje, ale litá to pěkně rychle.
To cv2 je jen binding 1:1 k té Céčkovské knihovně (cv) a v její dokumentaci je vždycky i signatura funkce v Pythonu a fungování je úplně stejné.
import cv2
import PIL.Image, PIL.ImageTk
import tkinter.filedialog
import tkinter as tk
def clamp(val, minval, maxval):
val = max(val, minval)
val = min(val, maxval)
return val
def remap_range(val, fromrange, torange):
frmin, frmax = fromrange
tomin, tomax = torange
frsize = frmax - frmin + 1
tosize = tomax - tomin + 1
delta = (val - frmin) / frsize
return tomin + tosize * delta
def fit_size(w1, h1, w2, h2):
scale = min(w2 / w1, h2 / h1)
return int(w1 * scale), int(h1 * scale)
class Box:
def __init__(self, x1, y1, x2, y2):
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
@property
def box(self):
return self.x1, self.y1, self.x2, self.y2
@property
def l(self):
return self.x1
@property
def r(self):
return self.x2
@property
def lr(self):
return self.x1, self.x2
@property
def t(self):
return self.y1
@property
def b(self):
return self.y2
@property
def tb(self):
return self.y1, self.y2
@property
def w(self):
return self.x2 - self.x1 + 1
@property
def h(self):
return self.y2 - self.y1 + 1
@property
def size(self):
return self.w, self.h
class RangeSlider(tk.Frame):
def __init__(self, master, valrange, valinit, *args, **kwargs):
tc = kwargs.pop("troughcolor", None)
super().__init__(master, *args, **kwargs)
self.valrange = valrange
self.lo = tk.Scale(
self,
from_=valrange[0],
to=valrange[1],
orient=tk.HORIZONTAL,
troughcolor=tc,
command=self._lo_changed,
)
self.hi = tk.Scale(
self,
from_=valrange[0],
to=valrange[1],
orient=tk.HORIZONTAL,
troughcolor=tc,
command=self._hi_changed,
)
self.vals = valinit
self.set(*self.vals, False)
self.lo.pack(fill=tk.X)
self.hi.pack(fill=tk.X)
self.on_change_callback = None
def on_change(self, callback):
self.on_change_callback = callback
def _lo_changed(self, value):
lo = int(value)
hi = max(lo, self.hi.get())
# change = (lo, hi) != self.vals
change = lo != self.vals[0]
self.set(lo, hi, False)
if change and self.on_change_callback:
self.on_change_callback(self, lo, hi)
def _hi_changed(self, value):
hi = int(value)
lo = min(hi, self.lo.get())
# change = (lo, hi) != self.vals
change = hi != self.vals[1]
self.set(lo, hi, False)
if change and self.on_change_callback:
self.on_change_callback(self, lo, hi)
def set(self, lo, hi, notify=True):
lo = clamp(lo, *self.valrange)
hi = clamp(hi, *self.valrange)
self.vals = (None, None) if notify else (lo, hi)
self.lo.set(lo)
self.hi.set(hi)
def get(self):
return self.lo.get(), self.hi.get()
class PixelPicker(tk.Canvas):
def __init__(self, master, image, *args, **kwargs):
super().__init__(
master, *args, **kwargs, bd=0, highlightthickness=0, cursor="tcross"
)
self.size = 0, 0
self.cimg = self.create_image(0, 0, anchor=tk.NW)
self.crect = self.create_rectangle(0, 0, 0, 0, dash=(10,), state="hidden")
self.image = image
self.sbox = Box(0, 0, *image.size)
self.vbox = None # prvni vykresleni bude v <Configure>
self.on_pick_callback = None
self.bind("<Configure>", self._on_resize)
self.bind("<Button-1>", self._on_pick)
self.bind("<Button-3>", self._on_zoom_selstart)
self.bind("<B3-Motion>", self._on_zoom_selupdate)
self.bind("<ButtonRelease-3>", self._on_zoom_selend)
self.bind("<Double-Button-3>", self._on_zoom_reset)
def set_image(self, image):
self.image = image
self.sbox = Box(0, 0, *image.size)
self.vbox = Box(0, 0, *fit_size(*self.sbox.size, *self.size))
self._show_image(self.image, self.sbox, self.vbox)
def on_pick(self, callback):
self.on_pick_callback = callback
def _show_image(self, image, sbox, vbox):
self.vimg = image.resize(vbox.size, PIL.Image.Resampling.NEAREST, sbox.box)
self.photo = PIL.ImageTk.PhotoImage(image=self.vimg)
self.itemconfigure(self.cimg, image=self.photo)
def _zoom_image(self, selbox):
if selbox.w < 5 or selbox.h < 5:
return
x1 = remap_range(selbox.l, self.vbox.lr, self.sbox.lr)
x2 = remap_range(selbox.r, self.vbox.lr, self.sbox.lr)
y1 = remap_range(selbox.t, self.vbox.tb, self.sbox.tb)
y2 = remap_range(selbox.b, self.vbox.tb, self.sbox.tb)
self.sbox = Box(x1, y1, x2, y2)
self.vbox = Box(0, 0, *fit_size(*selbox.size, *self.size))
self._show_image(self.image, self.sbox, self.vbox)
def _on_resize(self, event):
self.size = (event.width, event.height)
self.vbox = Box(0, 0, *fit_size(*self.sbox.size, *self.size))
self._show_image(self.image, self.sbox, self.vbox)
def _on_pick(self, event):
if self.on_pick_callback:
x = remap_range(event.x, self.vbox.lr, self.sbox.lr)
y = remap_range(event.y, self.vbox.tb, self.sbox.tb)
self.on_pick_callback(int(x), int(y))
def _on_zoom_selstart(self, event):
self.coords(self.crect, event.x, event.y, event.x, event.y)
self.itemconfigure(self.crect, state="normal")
def _on_zoom_selupdate(self, event):
x1, y1, _, _ = self.coords(self.crect)
self.coords(self.crect, x1, y1, event.x, event.y)
def _on_zoom_selend(self, event):
self.itemconfigure(self.crect, state="hidden")
x1, y1, x2, y2 = self.coords(self.crect)
x1 = clamp(x1, self.vbox.l, self.vbox.r)
x2 = clamp(x2, self.vbox.l, self.vbox.r)
y1 = clamp(y1, self.vbox.t, self.vbox.b)
y2 = clamp(y2, self.vbox.t, self.vbox.b)
self._zoom_image(Box(x1, y1, x2, y2))
def _on_zoom_reset(self, event):
self.set_image(self.image)
def main():
root = tk.Tk()
# filename = "pink-peony-148225487837A.jpg"
filename = tk.filedialog.askopenfilename(
filetypes=(
("Images", (".bmp", ".jpg", ".png")),
("All files", "*.*"),
)
)
if not filename:
return
image = cv2.cvtColor(cv2.imread(filename), cv2.COLOR_BGR2RGB)
image_r, image_g, image_b = cv2.split(image)
pil_image = PIL.Image.fromarray(image)
w, h = fit_size(*pil_image.size, 500, 500)
img_frame = tk.Frame(root)
im1 = PixelPicker(img_frame, pil_image, width=w, height=h)
im1.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
im2 = PixelPicker(img_frame, pil_image, width=w, height=h)
im2.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
img_frame.pack(expand=True, fill=tk.BOTH)
pixel_canvas = tk.Canvas(root, width=100, height=50, background="black")
pixel_canvas.pack()
rslider = RangeSlider(root, (0, 255), (10, 20), troughcolor="red")
rslider.pack(fill=tk.BOTH)
gslider = RangeSlider(root, (0, 255), (10, 20), troughcolor="green")
gslider.pack(fill=tk.BOTH)
bslider = RangeSlider(root, (0, 255), (10, 20), troughcolor="blue")
bslider.pack(fill=tk.BOTH)
mask_r = mask_g = mask_b = mask_full = masked = None
def update(slider, lo, hi):
nonlocal mask_r, mask_g, mask_b, mask_full, masked
# mask_r = mask_g = mask_b = mask_full = masked = None
if mask_r is None or slider is None or slider is rslider:
rl, rh = rslider.get()
mask_r = cv2.inRange(image_r, rl, rh, mask_r)
if mask_g is None or slider is None or slider is gslider:
gl, gh = gslider.get()
mask_g = cv2.inRange(image_g, gl, gh, mask_g)
if mask_b is None or slider is None or slider is bslider:
bl, bh = bslider.get()
mask_b = cv2.inRange(image_b, bl, bh, mask_b)
mask_full = cv2.bitwise_and(mask_r, mask_g, mask_full)
mask_full = cv2.bitwise_and(mask_full, mask_b, mask_full)
if masked is not None:
masked.fill(0)
masked = cv2.bitwise_and(image, image, masked, mask_full)
im2.set_image(PIL.Image.fromarray(masked))
def pixel_pick(x, y):
r, g, b = image[y, x]
rslider.set(r - 20, r + 20, False)
gslider.set(g - 20, g + 20, False)
bslider.set(b - 20, b + 20, False)
hex_rgb = "#%02X%02X%02X" % (r, g, b)
pixel_canvas.config(background=hex_rgb)
update(None, 0, 0)
im1.on_pick(pixel_pick)
for s in (rslider, gslider, bslider):
s.on_change(update)
root.mainloop()
if __name__ == "__main__":
main()