diff --git a/images/goal/president.png b/images/goal/president.png new file mode 100644 index 0000000..b2ed8a1 Binary files /dev/null and b/images/goal/president.png differ diff --git a/python/__pycache__/image_preparing.cpython-313.pyc b/python/__pycache__/image_preparing.cpython-313.pyc index 82550fe..b085ade 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 3af6629..38df233 100644 Binary files a/python/__pycache__/interface.cpython-313.pyc and b/python/__pycache__/interface.cpython-313.pyc differ diff --git a/python/__pycache__/transition.cpython-313.pyc b/python/__pycache__/transition.cpython-313.pyc new file mode 100644 index 0000000..4a9693d Binary files /dev/null and b/python/__pycache__/transition.cpython-313.pyc differ diff --git a/python/check_transition.py b/python/check_transition.py new file mode 100644 index 0000000..b027a84 --- /dev/null +++ b/python/check_transition.py @@ -0,0 +1,5 @@ +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/interface.py b/python/interface.py index 44d7d2b..2b95e2f 100644 --- a/python/interface.py +++ b/python/interface.py @@ -6,6 +6,7 @@ import matplotlib.pylab as plt from matplotlib.widgets import Button from image_preparing import fix_image_resolution +import transition class ImageInterface: @@ -96,15 +97,22 @@ class ImageInterface: self.current_index = 0 self._show_browser() def _open_shenanigans(self): - image_paths = self.image_sets.get("source", []) - selected_index = self.selected_indices.get("source") - if selected_index is None: - selected_index = self.current_index if self.current_category == "source" else 0 - selected_path = None - if image_paths: - safe_index = max(0, min(selected_index, len(image_paths) - 1)) - selected_path = image_paths[safe_index] - fix_image_resolution(selected_path) + src_paths = self.image_sets.get("source", []) + goal_paths = self.image_sets.get("goal", []) + if not src_paths or not goal_paths: + return + sel_src = self.selected_indices.get("source") + sel_goal = self.selected_indices.get("goal") + src_path = src_paths[sel_src] if sel_src is not None else src_paths[0] + goal_path = goal_paths[sel_goal] if sel_goal is not None else goal_paths[0] + try: + import importlib + importlib.reload(transition) + except Exception: + pass + if not hasattr(transition, "transform_images"): + return + transition.transform_images(src_path, goal_path) def _selected_image_name(self, category): selected_index = self.selected_indices.get(category) image_paths = self.image_sets.get(category, []) diff --git a/python/transition.py b/python/transition.py new file mode 100644 index 0000000..7222da9 --- /dev/null +++ b/python/transition.py @@ -0,0 +1,156 @@ +from pathlib import Path +import numpy as np +from PIL import Image, ImageDraw +import matplotlib.pyplot as plt +from matplotlib.tri import Triangulation + +from image_preparing import fix_image_resolution + + +def prepare_image(path): + pil = Image.open(path).convert("RGB").resize((800, 600)) + return pil +def auto_correspondences_grid(pil, nx=8, ny=6): + w, h = pil.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 tri.triangles +def _affine_coeffs(from_tri, to_tri): + A = [] + B = [] + for (x_dst, y_dst), (x_src, y_src) in zip(from_tri, to_tri): + 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): + 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) + 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) + 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)) + 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()] + draw.polygon(tri_local, fill=255) + out.paste(warped, (min_x, min_y), mask) + return out +def transform_images(src_path, goal_path): + fix_image_resolution(src_path) + fix_image_resolution(goal_path) + src = prepare_image(src_path) + goal = prepare_image(goal_path) + frames = tile_reposition_transition(src, goal, grid=(40, 30), n_frames=24) + fig, ax = plt.subplots(figsize=(8, 6)) + im = ax.imshow(frames[0]) + ax.axis("off") + for frame in frames: + im.set_data(frame) + fig.canvas.draw_idle() + plt.pause(0.05) + plt.show() +def tile_reposition_transition(src_pil, goal_pil, grid=(40, 30), n_frames=24): + + src = src_pil.convert("RGBA") + goal = goal_pil.convert("RGBA") + w, h = src.size + nx, ny = grid + + def make_tiles(pil): + tiles = [] + for iy in range(ny): + y0 = int(round(iy * h / ny)) + y1 = int(round((iy + 1) * h / ny)) + for ix in range(nx): + x0 = int(round(ix * w / nx)) + x1 = int(round((ix + 1) * w / nx)) + box = (x0, y0, x1, y1) + crop = pil.crop(box).convert("RGB") + arr = np.array(crop, dtype=float) + avg = arr.reshape(-1, 3).mean(axis=0) + center = ((x0 + x1) / 2.0, (y0 + y1) / 2.0) + tiles.append({"box": box, "img": crop, "avg": avg, "center": center}) + return tiles + + src_tiles = make_tiles(src) + goal_tiles = make_tiles(goal) + + max_spatial = np.hypot(w, h) + src_available = set(range(len(src_tiles))) + mapping = {} + for g_idx, g in enumerate(goal_tiles): + best = None + best_cost = None + for s_idx in list(src_available): + s = src_tiles[s_idx] + color_dist = np.linalg.norm(g["avg"] - s["avg"]) / (255.0 * np.sqrt(3)) + spatial_dist = np.linalg.norm(np.array(g["center"]) - np.array(s["center"])) / max_spatial + cost = color_dist + 0.35 * spatial_dist + if best_cost is None or cost < best_cost: + best_cost = cost + best = s_idx + if best is None: + best = src_available.pop() + else: + src_available.remove(best) + mapping[best] = g_idx + + targets = {} + for s_idx, g_idx in mapping.items(): + s = src_tiles[s_idx] + g = goal_tiles[g_idx] + targets[s_idx] = {"start": (s["box"][0], s["box"][1]), "end": (g["box"][0], g["box"][1]), "img": s["img"], "size": (s["box"][2]-s["box"][0], s["box"][3]-s["box"][1])} + + frames = [] + for k in range(n_frames + 1): + t = k / float(n_frames) + frame = Image.new("RGBA", (w, h), (0, 0, 0, 255)) + for s_idx, info in targets.items(): + sx, sy = info["start"] + ex, ey = info["end"] + cx = int(round(sx + (ex - sx) * t)) + cy = int(round(sy + (ey - sy) * t)) + frame.paste(info["img"], (cx, cy)) + frames.append(frame.convert("RGB")) + + final = Image.new("RGB", (w, h)) + for s_idx, g_idx in mapping.items(): + src_img = src_tiles[s_idx]["img"].convert("RGB") + x0, y0, x1, y1 = goal_tiles[g_idx]["box"] + final.paste(src_img, (x0, y0)) + frames[-1] = final + return frames + +