diff --git a/python/__pycache__/image_preparing.cpython-313.pyc b/python/__pycache__/image_preparing.cpython-313.pyc index e9d14cb..3c17f34 100644 Binary files a/python/__pycache__/image_preparing.cpython-313.pyc and b/python/__pycache__/image_preparing.cpython-313.pyc differ diff --git a/python/__pycache__/interface.cpython-313.pyc b/python/__pycache__/interface.cpython-313.pyc index 03d7381..acb12c0 100644 Binary files a/python/__pycache__/interface.cpython-313.pyc and b/python/__pycache__/interface.cpython-313.pyc differ diff --git a/python/__pycache__/main.cpython-313.pyc b/python/__pycache__/main.cpython-313.pyc index f6b90e4..4d83ffe 100644 Binary files a/python/__pycache__/main.cpython-313.pyc and b/python/__pycache__/main.cpython-313.pyc differ diff --git a/python/__pycache__/transition.cpython-313.pyc b/python/__pycache__/transition.cpython-313.pyc index 94097ea..184c8ed 100644 Binary files a/python/__pycache__/transition.cpython-313.pyc and b/python/__pycache__/transition.cpython-313.pyc differ 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,