From 3afa31e90ffa33e22a3f238337e24b70ca868238 Mon Sep 17 00:00:00 2001 From: beaver Date: Sun, 6 Sep 2026 01:06:15 +0200 Subject: [PATCH] yey --- .../image_preparing.cpython-313.pyc | Bin 646 -> 609 bytes python/__pycache__/interface.cpython-313.pyc | Bin 15928 -> 15851 bytes python/__pycache__/main.cpython-313.pyc | Bin 272 -> 233 bytes python/__pycache__/transition.cpython-313.pyc | Bin 11706 -> 11402 bytes python/check_transition.py | 5 - python/image_preparing.py | 4 +- python/interface.py | 100 +++++++-------- python/transition.py | 117 ++++++++---------- 8 files changed, 107 insertions(+), 119 deletions(-) delete mode 100644 python/check_transition.py diff --git a/python/__pycache__/image_preparing.cpython-313.pyc b/python/__pycache__/image_preparing.cpython-313.pyc index e9d14cbeba143fb5d1d005ba2a823e498aef6d9d..3c17f34e739951fa5d6ecb0db8465b8ed714ad9e 100644 GIT binary patch delta 137 zcmZo;eaOQ5nU|M~0SFe%o0Ii)B5y3`4j^YLLnuSi#!61cAjyKtl8pSkn9SV7^wju* zqSS)KqRhN>y@JYHQbno7nN_Lr5UG;2{7 eW~eMso2@tbGovNfLIp=|=7Zc^j*^qjm;wMOK`Va% delta 153 zcmaFJ(#FdBnU|M~0SL?$8?)|C+{NZDA-$*5wJVil8{Sdy6> zQ&5zjm6}`!=o*R diff --git a/python/__pycache__/interface.cpython-313.pyc b/python/__pycache__/interface.cpython-313.pyc index 03d7381ddc1f5cb86d0a6151916a75def1031d92..acb12c06b91920958bb9e60b0fff344a569b340e 100644 GIT binary patch delta 1565 zcmZWoZ%h+s9Om9T`j^t$mI}R=(z8&8l!6xMpn_HBqJkp-oVs`(<$!LbW%TgxR7f;t zF!@a*6vMs9gB}EpS(*&g}>^0v@F`s zO0MHDqW`(NESBK8X*R)~PR$5;T20?(aKZ2C^6a?bOW-z6a&PnO$c!MQlKFK*Y&U&P zxD2T&KFP+5^X02l#j-B*6ksf))7;5520O(i`9$8M<}GeuDX)2zmx!l@7qn_MZHuFk zBuwyl)fP*G>I{BT|GwiokQ@1 z^CDq^e>=m*zCpQoMyB(1Z)^oz#r+_ z_P|_Vo|;c(PT`iL7`&MSn#*g@WM&u#hAEY%!$MfHhOzrz#Mmh2 zCvZF=PNt=}Af7lW@u$Sm(_;7x24`2VA1aA=0%%R>N=GKCqT6J33oZ+??QCaNfHD>@d}SMFlg(O*r79v#*4& z606ETel?`6L{ufFoKVvd)vRR$_I5ndG|e?(V9h!^BLu<0LlK|GQP5dH7|uoPK6^n2 jfU}?}1KxsDqrA!~;3*ggP*I==;3!ZAV9$OLxj_5_Jo_|d delta 1741 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 Z3>BpL(RTP&qt~=&g#0JrBdxNx{{RvH)WHA% delta 3238 zcmbVOe{37o9l!IP?X%B6V<*lJC$Zh6juX-}Z9_xI50d_J!mqX=rK26*)wwus?K}6~ zd5r^G3@o9x77ltmUUH1>LOk38#Dfh4!4d)3TiXZ|1}9~_#{m0k8!&A|^nNFN!(O{- zo1uw_m>vRn+Fy}QB7tY)c8mCO;5AY+j?1|WA7S9@hNe1GQjmBcYeo(LE~n9PF_Vkf zG%Ax!!M_-SGnzRr0)YpTM#d!@(iFI(Y1_wp4yQRe89$r>;!!>>AJ**?lO}nA6Osu| zkPc^Z@}wvnmH`?h<)kQV%H*^fE-Q;Mq#E#2Mij<*AzMy7V~$OP*L| z9Shxa-S7x?Wadi~;r1?$&Q2?X1?r_eg;AxyIJiW;wr6SIMb8_LF1PhvrY`Th;(7nk zi$7d;Je1$_iQPL(uClft_paKU3nOzQi^rAV$2R{zjD(}|rirj2-SgdxJ6_3_*ygj~ z+|-$=CF^DPD>gKpo~#h$G>f3Isp^Giu{vI+K<8*f^i! zvI3Wji9#$RCIwjnEl2>*lDZJ&i8ZTjp>@7>arF6){EjuNPoYi?D#w+bFIfs(Yt9@m z1`G1(dsJ(Ceh0Q*-(24^T@6APKBfOohAZ$|Mmd{F@tR2%Q%OmVST$w>i0K&G66WDA z{Ew-pHh^UvF>YcaDe&mJ_=yP#^dM#-S^8gvT;ACv12`>P8 zV9?SsW0pWXhFnS039Q{bF3I{bjF^>UQgRw?LPT15Su>1Jp`Oz$SR^IJxs+xqFDY4& z%CG#cY%NFZ1P3x$m2Orrj6@=+OQQ&o*VsKz?VijJt+^^*WKJ@QPjv;)4xNjhi58C( z52{Ufmt1$}hu7?N%CKth$nRcdE3UHs66;qEp3R+=eDym+H%$irrpEZ}~ zr#oITyvm$n3cmC1;$YExy8dc#ODVWT4Q^AbyHxMp7fIFCGizKIHJ>sU%;(8sXOTW_ zzZ&Q)1v=HhR<(MY>g`%OuDb5IE&6R*C;or6tE}t!er4?C$2FUG)m8a|eb#vEv*vCr zn3uwryDs)$*>$B~bw38H-5VVPmmI%#taL|Lyu%-LN8v+mpD8+PDS3zCG51#62r1!Q z@luv*X7$RhyqCLqJ#R6a~ID4?OcDm0@R^-Ql(yfCgAxG6A& zwgOHdkZ@Rh6|DD_qt*4zO@Rd@wFirTh~Qs1r8Ov1E1UL|tWlKG*v5ioW%F=}9YL;P zb1e+d4?ll@e%G39V?kQkzPDts?SpSu&AW=}6}tBtZGJBLboAK~5Q6)CZ;?&VQr+(h zAiWx^*OVAPp5P_$GsHOr6V=@)Qx&R54TqjVj4x0}?N0*{Mi(e2ZxgZw&&e8%4Wk() znTsC-Cy|_HMghgiTeoOt6kf@6HXX6w5gL_91XLk_2kRa%2k?aY7(8EBKT|~XcTt!A zi~t?iQKJM^MTD@FTZ-JW zvt`-28M+#3(TZ&iD+hjwS3HN|G=^VbID_FV0*#ikX-Q%bx$ZS(pQ(xIUYX{RE5`Kl zw7w~D5ecW^-o}&YCO>QZf+Syp9|n73`>;CWWG*AhnrX6pX?ivKIsRehKtd9M9Iqde%!iX2tr5O16Y+mhF(4KNqvJ->_{#)ft)1^ zPzZ6tG4OjNGL5?QE68jJ_PJkn6lvAlo`3L~)4h7H6n|M-01 zO$^vF`UdoB03OD}HVk^2r{h4(0NqT;!}aQG-Oo#0I+IEY2{3>KA4L#pF6Vznw*o5Q zAc23)VVFnI^ldJ$S+FgSh*K!kE+HYvw;Z&B_wby{c=%CElzbW1Mf$q}nD>Kq_z@zc zOXc!lbuV$*5w-o(LIo0_s;<5H;cNcx3-zs) zqzM;wFbTWcMo1Yx)7FuE5;LB_5X7JtR{Y{A*G*i`^wd?(6*#EBX?OixZ{Z)h?f{}R zrZ+8)XH$GX_!}CcqY~02th=iwviUQDMUUG0$TCrLleUq=yLthvru$j0klnmj;W Zw^ozR>s#RAyMp%aedIrhX40)|{vRN?$>ab4 diff --git a/python/check_transition.py b/python/check_transition.py deleted file mode 100644 index b027a84..0000000 --- a/python/check_transition.py +++ /dev/null @@ -1,5 +0,0 @@ -import importlib -importlib.invalidate_caches() -import transition -print('has transform_images:', hasattr(transition, 'transform_images')) -print('transform_images object:', getattr(transition, 'transform_images', None)) diff --git a/python/image_preparing.py b/python/image_preparing.py index 81fbe42..f1a7821 100644 --- a/python/image_preparing.py +++ b/python/image_preparing.py @@ -1,8 +1,8 @@ from PIL import Image -def fix_image_resolution(image_path, size=(0,0)): - if image_path == None or size == (0,0): +def resize_image_to_resolution(image_path, size=(0, 0)): + if image_path is None or size == (0, 0): return None with Image.open(image_path) as image: diff --git a/python/interface.py b/python/interface.py index 203ec10..2a19dfd 100644 --- a/python/interface.py +++ b/python/interface.py @@ -12,9 +12,9 @@ class ImageInterface: def __init__(self, base_dir=None): self.base_dir = Path(base_dir or Path(__file__).resolve().parent.parent) self.image_sets = { - "source": self._load_images(str(self.base_dir / "images" / "source" / "*.png")), - "goal": self._load_images(str(self.base_dir / "images" / "goal" / "*.png")), - "shenanigans": self._load_images(str(self.base_dir / "images" / "shenanigans" / "*.png")), + "source": self.load_images(str(self.base_dir / "images" / "source" / "*.png")), + "goal": self.load_images(str(self.base_dir / "images" / "goal" / "*.png")), + "shenanigans": self.load_images(str(self.base_dir / "images" / "shenanigans" / "*.png")), } self.current_category = None self.current_index = 0 @@ -31,19 +31,19 @@ class ImageInterface: "button": "#2b2b2b", "button_hover": "#3a3a3a", } - def _load_images(self, pattern): + def load_images(self, pattern): return sorted(glob(pattern)) - def _ensure_figure(self): + def ensure_figure(self): if self.figure is None: self.figure = plt.figure(figsize=(10, 6)) - self.key_connection_id = self.figure.canvas.mpl_connect("key_press_event", self._on_key_press) - def _clear_layout(self): - self._ensure_figure() + self.key_connection_id = self.figure.canvas.mpl_connect("key_press_event", self.on_key_press) + def clear_layout(self): + self.ensure_figure() self.figure.clf() self.image_axis = None self.list_axis = None self.buttons = [] - def _make_button(self, rect, label, callback): + def make_button(self, rect, label, callback): button_axis = self.figure.add_axes(rect) button_axis.set_facecolor(self.colors["button"]) button_axis.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False) @@ -52,12 +52,12 @@ class ImageInterface: button.on_clicked(callback) self.buttons.append(button) return button - def _open_image(self, image_paths, index): + def open_image(self, image_paths, index): if not image_paths: return None safe_index = max(0, min(index, len(image_paths) - 1)) return Image.open(image_paths[safe_index]) - def _draw_image_panel(self, axis, image, title): + def draw_image_panel(self, axis, image, title): axis.clear() axis.set_title(title) axis.set_facecolor(self.colors["panel"]) @@ -67,7 +67,7 @@ class ImageInterface: axis.imshow(image) else: axis.text(0.5, 0.5, "No images found", ha="center", va="center", color=self.colors["text"]) - def _draw_list_panel(self, axis, image_paths, current_index): + def draw_list_panel(self, axis, image_paths, current_index): axis.clear() axis.set_facecolor(self.colors["panel"]) axis.axis("off") @@ -83,19 +83,19 @@ class ImageInterface: lines.append(f"{prefix}{name}") axis.text(0.02, 0.98, "\n".join(lines), ha="left", va="top", family="monospace", color=self.colors["text"]) axis.title.set_color(self.colors["text"]) - def _show_menu(self): + def show_menu(self): self.current_category = None - self._clear_layout() + self.clear_layout() self.figure.patch.set_facecolor(self.colors["figure"]) - self._menu_row(0.66, "Source Images", "source", lambda event: self._open_category("source")) - self._menu_row(0.50, "Goal Images", "goal", lambda event: self._open_category("goal")) - self._make_button([0.08, 0.34, 0.32, 0.12], "Shenanigans Image", lambda event: self._open_shenanigans()) + self.menu_row(0.66, "Source Images", "source", lambda event: self.open_category("source")) + self.menu_row(0.50, "Goal Images", "goal", lambda event: self.open_category("goal")) + self.make_button([0.08, 0.34, 0.32, 0.12], "Shenanigans Image", lambda event: self.open_shenanigans()) self.figure.canvas.draw_idle() - def _open_category(self, category): + def open_category(self, category): self.current_category = category self.current_index = 0 - self._show_browser() - def _open_shenanigans(self): + self.show_browser() + def open_shenanigans(self): src_paths = self.image_sets.get("source", []) goal_paths = self.image_sets.get("goal", []) if not src_paths or not goal_paths: @@ -109,77 +109,77 @@ class ImageInterface: importlib.reload(transition) except Exception: pass - if not hasattr(transition, "transform_images"): + if not hasattr(transition, "show_image_transition"): return - transition.transform_images(src_path, goal_path) - def _selected_image_name(self, category): + transition.show_image_transition(src_path, goal_path) + def selected_image_name(self, category): selected_index = self.selected_indices.get(category) image_paths = self.image_sets.get(category, []) if selected_index is None or not image_paths: return None safe_index = max(0, min(selected_index, len(image_paths) - 1)) return Path(image_paths[safe_index]).name - def _menu_label(self, base_label, category=None): + def menu_label(self, base_label, category=None): if category is None: return base_label - selected_name = self._selected_image_name(category) + selected_name = self.selected_image_name(category) if selected_name is None: return base_label return f"{base_label} - {selected_name}" - def _menu_row(self, y, label, category, callback): - self._make_button([0.08, y, 0.32, 0.12], label, callback) - selected_name = self._selected_image_name(category) + def menu_row(self, y, label, category, callback): + self.make_button([0.08, y, 0.32, 0.12], label, callback) + selected_name = self.selected_image_name(category) if selected_name is not None: self.figure.text(0.44, y + 0.055, f"- {selected_name}", color=self.colors["text"], va="center", ha="left", fontsize=11) - def _select_current_image(self): + def select_current_image(self): if self.current_category in ("source", "goal"): self.selected_indices[self.current_category] = self.current_index - self._show_menu() - def _close_interface(self): + self.show_menu() + def close_interface(self): if self.figure is not None: plt.close(self.figure) - def _show_browser(self): - self._clear_layout() + def show_browser(self): + self.clear_layout() self.figure.patch.set_facecolor(self.colors["figure"]) self.figure.suptitle(self.current_category.replace("_", " ").title(), fontsize=16, color=self.colors["text"]) self.image_axis, self.list_axis = self.figure.subplots(1, 2, gridspec_kw={"width_ratios": [3, 1]}) - self._refresh_browser_view() - self._make_button([0.02, 0.02, 0.12, 0.06], "Back", lambda event: self._show_menu()) + self.refresh_browser_view() + self.make_button([0.02, 0.02, 0.12, 0.06], "Back", lambda event: self.show_menu()) if self.current_category in ("source", "goal"): - self._make_button([0.76, 0.10, 0.18, 0.06], "Select", lambda event: self._select_current_image()) - self._make_button([0.76, 0.02, 0.08, 0.06], "<", lambda event: self.previous_image()) - self._make_button([0.86, 0.02, 0.08, 0.06], ">", lambda event: self.next_image()) + self.make_button([0.76, 0.10, 0.18, 0.06], "Select", lambda event: self.select_current_image()) + self.make_button([0.76, 0.02, 0.08, 0.06], "<", lambda event: self.previous_image()) + self.make_button([0.86, 0.02, 0.08, 0.06], ">", lambda event: self.next_image()) self.figure.subplots_adjust(left=0.05, right=0.95, bottom=0.12, top=0.90, wspace=0.22) self.figure.canvas.draw_idle() - def _refresh_browser_view(self): + def refresh_browser_view(self): images = self.image_sets.get(self.current_category, []) - current_image = self._open_image(images, self.current_index) + current_image = self.open_image(images, self.current_index) category_title = self.current_category.replace("_", " ").title() if self.current_category else "Images" - self._draw_image_panel(self.image_axis, current_image, f"{category_title} {self.current_index + 1}/{max(len(images), 1)}") - self._draw_list_panel(self.list_axis, images, self.current_index) + self.draw_image_panel(self.image_axis, current_image, f"{category_title} {self.current_index + 1}/{max(len(images), 1)}") + self.draw_list_panel(self.list_axis, images, self.current_index) self.figure.canvas.draw_idle() - def _step_image(self, delta): + def step_image(self, delta): images = self.image_sets.get(self.current_category, []) if images: self.current_index = (self.current_index + delta) % len(images) - self._refresh_browser_view() + self.refresh_browser_view() def next_image(self): - self._step_image(1) + self.step_image(1) def previous_image(self): - self._step_image(-1) - def _on_key_press(self, event): + self.step_image(-1) + def on_key_press(self, event): if self.current_category is None: if event.key in ("escape", "q"): - self._close_interface() + self.close_interface() return if event.key in ("right", "d"): self.next_image() elif event.key in ("left", "a"): self.previous_image() elif event.key == "escape": - self._show_menu() + self.show_menu() def render(self): - self._show_menu() + self.show_menu() plt.show() def launch_interface(): diff --git a/python/transition.py b/python/transition.py index df674fd..ba296c6 100644 --- a/python/transition.py +++ b/python/transition.py @@ -4,75 +4,68 @@ from PIL import Image, ImageDraw import matplotlib.pyplot as plt from matplotlib.tri import Triangulation -from image_preparing import fix_image_resolution +from image_preparing import resize_image_to_resolution import time transformation_resolution = (1600, 1200) -def auto_correspondences_grid(pil, nx=8, ny=6): - w, h = pil.size +def create_grid_correspondences(image, nx=8, ny=6): + w, h = image.size xs = np.linspace(0, w - 1, nx) ys = np.linspace(0, h - 1, ny) pts = [] for y in ys: for x in xs: pts.append([float(x), float(y)]) - corners = [[0.0, 0.0], [w - 1.0, 0.0], [w - 1.0, h - 1.0], [0.0, h - 1.0]] - for c in corners: - if c not in pts: - pts.insert(0, c) - return np.array(pts, dtype=float), np.array(pts, dtype=float).copy() -def compute_delaunay_on_points(pts): - pts = np.asarray(pts) - tri = Triangulation(pts[:, 0], pts[:, 1]) + return np.array(pts, dtype=float).copy() +def calculate_delaunay_triangles(points): + tri = Triangulation(points[:, 0], points[:, 1]) return tri.triangles -def _affine_coeffs(from_tri, to_tri): +def calculate_affine_coefficients(from_triangle, to_triangle): A = [] B = [] - for (x_dst, y_dst), (x_src, y_src) in zip(from_tri, to_tri): + for (x_dst, y_dst), (x_src, y_src) in zip(from_triangle, to_triangle): A.append([x_dst, y_dst, 1, 0, 0, 0]) A.append([0, 0, 0, x_dst, y_dst, 1]) B.append(x_src) B.append(y_src) sol, *_ = np.linalg.lstsq(np.array(A), np.array(B), rcond=None) return tuple(sol.tolist()) -def _warp_triangle(src_pil, src_tri, dst_tri, out_size, offset): +def warp_triangle(source_image, source_triangle, destination_triangle, output_size, offset): dx, dy = offset - dst_local = np.array(dst_tri) - np.array([dx, dy]) - coeffs = _affine_coeffs(dst_local, src_tri) - return src_pil.transform(out_size, Image.AFFINE, coeffs, resample=Image.BILINEAR) -def reconstruct_goal_from_source(src_pil, pts1, pts2): - w, h = src_pil.size - pts1 = np.asarray(pts1) - pts2 = np.asarray(pts2) - triangles = compute_delaunay_on_points(pts2) + destination_local = np.array(destination_triangle) - np.array([dx, dy]) + coefficients = calculate_affine_coefficients(destination_local, source_triangle) + return source_image.transform(output_size, Image.AFFINE, coefficients, resample=Image.BILINEAR) +def reconstruct_goal_from_source(source_image, source_points, destination_points): + w, h = source_image.size + triangles = calculate_delaunay_triangles(destination_points) out = Image.new("RGB", (w, h)) for tri in triangles: tri = np.asarray(tri, dtype=int) - src_tri = pts1[tri] - dst_tri = pts2[tri] - min_x = max(int(np.floor(dst_tri[:, 0].min())), 0) - max_x = min(int(np.ceil(dst_tri[:, 0].max())), w) - min_y = max(int(np.floor(dst_tri[:, 1].min())), 0) - max_y = min(int(np.ceil(dst_tri[:, 1].max())), h) + source_triangle = source_points[tri] + destination_triangle = destination_points[tri] + min_x = max(int(np.floor(destination_triangle[:, 0].min())), 0) + max_x = min(int(np.ceil(destination_triangle[:, 0].max())), w) + min_y = max(int(np.floor(destination_triangle[:, 1].min())), 0) + max_y = min(int(np.ceil(destination_triangle[:, 1].max())), h) if max_x <= min_x or max_y <= min_y: continue out_w = max_x - min_x out_h = max_y - min_y - warped = _warp_triangle(src_pil, src_tri, dst_tri, (out_w, out_h), (min_x, min_y)) + warped = warp_triangle(source_image, source_triangle, destination_triangle, (out_w, out_h), (min_x, min_y)) mask = Image.new("L", (out_w, out_h), 0) draw = ImageDraw.Draw(mask) - tri_local = [(x - min_x, y - min_y) for x, y in dst_tri.tolist()] + tri_local = [(x - min_x, y - min_y) for x, y in destination_triangle.tolist()] draw.polygon(tri_local, fill=255) out.paste(warped, (min_x, min_y), mask) return out -def transform_images(src_path, goal_path): - src = fix_image_resolution(src_path, size=transformation_resolution) - goal = fix_image_resolution(goal_path, size=transformation_resolution) +def show_image_transition(source_path, goal_path): + src = resize_image_to_resolution(source_path, size=transformation_resolution) + goal = resize_image_to_resolution(goal_path, size=transformation_resolution) start = time.time() - image = tile_reposition_transition(src, goal, grid=transformation_resolution) + image = reposition_image_tiles(src, goal, grid=transformation_resolution) end = time.time() print(f"Transition took {end - start:.2f} seconds") fig, ax = plt.subplots(figsize=(8, 6), facecolor="black") @@ -81,32 +74,32 @@ def transform_images(src_path, goal_path): ax.imshow(image, cmap=None) ax.axis("off") - def _close_on_key(event): + def close_transition_on_key(event): if event.key in ("q", "escape"): plt.close(fig) - fig.canvas.mpl_connect("key_press_event", _close_on_key) + fig.canvas.mpl_connect("key_press_event", close_transition_on_key) plt.show() -def _grid_edges(size, divisions): +def calculate_grid_edges(size, divisions): return np.rint(np.linspace(0, size, divisions + 1)).astype(np.int32) -def _build_tile_boxes_and_centers(w, h, nx, ny): - x_edges = _grid_edges(w, nx) - y_edges = _grid_edges(h, ny) +def build_tile_boxes_and_centers(width, height, columns, rows): + x_edges = calculate_grid_edges(width, columns) + y_edges = calculate_grid_edges(height, rows) - x0 = np.repeat(x_edges[:-1], ny) - x1 = np.repeat(x_edges[1:], ny) - y0 = np.tile(y_edges[:-1], nx) - y1 = np.tile(y_edges[1:], nx) + x0 = np.repeat(x_edges[:-1], rows) + x1 = np.repeat(x_edges[1:], rows) + y0 = np.tile(y_edges[:-1], columns) + y1 = np.tile(y_edges[1:], columns) boxes = np.stack([x0, y0, x1, y1], axis=1) centers = np.stack([(x0 + x1) * 0.5, (y0 + y1) * 0.5], axis=1).astype(np.float32) areas = np.maximum((x1 - x0) * (y1 - y0), 1).astype(np.float32) return boxes, centers, areas -def _integral_image_rgb(arr): - integral = arr.cumsum(axis=0).cumsum(axis=1) +def calculate_integral_image_rgb(array): + integral = array.cumsum(axis=0).cumsum(axis=1) return np.pad(integral, ((1, 0), (1, 0), (0, 0)), mode="constant", constant_values=0) -def _tile_means_from_integral(integral, boxes, areas): +def calculate_tile_means(integral, boxes, areas): x0 = boxes[:, 0] y0 = boxes[:, 1] x1 = boxes[:, 2] @@ -119,16 +112,16 @@ def _tile_means_from_integral(integral, boxes, areas): + integral[y0, x0] ) return sums / areas[:, None] -def _match_tiles_by_feature_rank(src_avgs, src_centers, goal_avgs, goal_centers, w, h): +def match_tiles_by_feature_rank(source_averages, source_centers, goal_averages, goal_centers, width, height): color_scale = 255.0 * np.sqrt(3.0) - spatial_scale = max(np.hypot(w, h), 1e-6) + spatial_scale = max(np.hypot(width, height), 1e-6) src_feat = np.hstack([ - src_avgs / color_scale, - (src_centers / spatial_scale) * 0.35, + source_averages / color_scale, + (source_centers / spatial_scale) * 0.35, ]) goal_feat = np.hstack([ - goal_avgs / color_scale, + goal_averages / color_scale, (goal_centers / spatial_scale) * 0.35, ]) @@ -137,24 +130,24 @@ def _match_tiles_by_feature_rank(src_avgs, src_centers, goal_avgs, goal_centers, src_order = np.argsort(src_feat @ projection) goal_order = np.argsort(goal_feat @ projection) return src_order, goal_order -def tile_reposition_transition(src_pil, goal_pil, grid=transformation_resolution): - src = src_pil.convert("RGB") - goal = goal_pil.convert("RGB") +def reposition_image_tiles(source_image, goal_image, grid=transformation_resolution): + src = source_image.convert("RGB") + goal = goal_image.convert("RGB") w, h = src.size nx, ny = grid - src_boxes, src_centers, src_areas = _build_tile_boxes_and_centers(w, h, nx, ny) - goal_boxes, goal_centers, goal_areas = _build_tile_boxes_and_centers(w, h, nx, ny) + src_boxes, src_centers, src_areas = build_tile_boxes_and_centers(w, h, nx, ny) + goal_boxes, goal_centers, goal_areas = build_tile_boxes_and_centers(w, h, nx, ny) src_arr = np.asarray(src, dtype=np.float32) goal_arr = np.asarray(goal, dtype=np.float32) - src_integral = _integral_image_rgb(src_arr) - goal_integral = _integral_image_rgb(goal_arr) + src_integral = calculate_integral_image_rgb(src_arr) + goal_integral = calculate_integral_image_rgb(goal_arr) - src_avgs = _tile_means_from_integral(src_integral, src_boxes, src_areas) - goal_avgs = _tile_means_from_integral(goal_integral, goal_boxes, goal_areas) + src_avgs = calculate_tile_means(src_integral, src_boxes, src_areas) + goal_avgs = calculate_tile_means(goal_integral, goal_boxes, goal_areas) - src_order, goal_order = _match_tiles_by_feature_rank( + src_order, goal_order = match_tiles_by_feature_rank( src_avgs, src_centers, goal_avgs,