import cv2
import numpy as np
from PIL import Image
# ==========================
# CONFIG
# ==========================
ROOM_IMAGE = r"E:\xampp\htdocs\python\jan_jatra_improvement\Gemini_Generated_Image_rct6syrct6syrct6.png"
TILE_IMAGE = r"E:\xampp\htdocs\python\jan_jatra_improvement\img_002.jpg"
OUTPUT = "final_grid_engine.png"
GROUT_WIDTH = 3
GROUT_COLOR = (160, 160, 160)
# ==========================
# GLOBALS
# ==========================
original_img = None
room_img = None
tile_img = None
vertical_lines = []
horizontal_lines = []
obstacles = []
current_points = []
obstacle_points = []
mouse_pos = None
mode = "vertical"
show_grid = True
grout_on = True
tile_cols = 5 # user adjustable with +/-
tile_rows_offset = 0 # fine-tune rows with [ / ]
# ==========================
# IMAGE LOADING
# ==========================
def load_image(path):
pil = Image.open(path).convert("RGB")
return cv2.cvtColor(np.array(pil), cv2.COLOR_RGB2BGR)
# ==========================
# LINE MATH
# ==========================
def line_intersect(l1, l2):
x1, y1, x2, y2 = map(float, l1)
x3, y3, x4, y4 = map(float, l2)
denom = (x1-x2)*(y3-y4) - (y1-y2)*(x3-x4)
if abs(denom) < 1e-8:
return None
px = ((x1*y2 - y1*x2)*(x3-x4) - (x1-x2)*(x3*y4 - y3*x4)) / denom
py = ((x1*y2 - y1*x2)*(y3-y4) - (y1-y2)*(x3*y4 - y3*x4)) / denom
return (int(round(px)), int(round(py)))
def extend_line(line, scale=10000):
x1, y1, x2, y2 = line
dx, dy = x2 - x1, y2 - y1
L = max(np.hypot(dx, dy), 1e-6)
ux, uy = dx / L, dy / L
return (int(x1 - ux*scale), int(y1 - uy*scale),
int(x2 + ux*scale), int(y2 + uy*scale))
def sort_vertical(lines, img_h):
def x_at_mid(l):
x1, y1, x2, y2 = l
if abs(y2 - y1) < 1e-6: return (x1 + x2) / 2
t = (img_h / 2 - y1) / (y2 - y1)
return x1 + t * (x2 - x1)
return sorted(lines, key=x_at_mid)
def sort_horizontal(lines, img_w):
def y_at_mid(l):
x1, y1, x2, y2 = l
if abs(x2 - x1) < 1e-6: return (y1 + y2) / 2
t = (img_w / 2 - x1) / (x2 - x1)
return y1 + t * (y2 - y1)
return sorted(lines, key=y_at_mid)
# ==========================
# CORNER ORDERING (from reference code β prevents stretching)
# ==========================
def order_points_perspective(pts):
"""
Order 4 points as: TL, TR, BR, BL
Same method as reference code β guaranteed correct ordering.
"""
pts = np.array(pts, dtype=np.float32)
rect = np.zeros((4, 2), dtype=np.float32)
s = pts.sum(axis=1) # x + y
d = np.diff(pts, axis=1)[:, 0] # x - y
rect[0] = pts[np.argmin(s)] # top-left (smallest x+y)
rect[2] = pts[np.argmax(s)] # bottom-right (largest x+y)
rect[1] = pts[np.argmax(d)] # top-right (largest x-y)
rect[3] = pts[np.argmin(d)] # bottom-left (smallest x-y)
return rect
# ==========================
# GRID FROM LINES
# ==========================
def compute_line_grid():
"""Compute intersection grid from drawn lines (for visualization)."""
h, w = original_img.shape[:2]
v_sorted = sort_vertical(vertical_lines, h)
h_sorted = sort_horizontal(horizontal_lines, w)
grid = []
for hl in h_sorted:
row = []
for vl in v_sorted:
pt = line_intersect(vl, hl)
if pt is not None:
row.append(pt)
grid.append(row)
return grid
def get_outer_corners():
"""Extract and properly order the 4 outer corners from grid."""
grid = compute_line_grid()
if not grid or len(grid) < 2:
return None
n_cols = min(len(row) for row in grid)
if n_cols < 2:
return None
tl = grid[0][0]
tr = grid[0][n_cols - 1]
br = grid[-1][n_cols - 1]
bl = grid[-1][0]
# KEY FIX: use reference code's ordering method
return order_points_perspective(np.float32([tl, tr, br, bl]))
# ==========================
# TILE COUNT COMPUTATION (aspect-ratio preserving)
# ==========================
def compute_tile_counts(corners):
"""
Auto-compute num_rows so tiles maintain their aspect ratio.
num_cols = user-set tile_cols
num_rows = computed from floor proportions + tile aspect ratio
"""
h_t, w_t = tile_img.shape[:2]
# Measure floor quad dimensions in screen space
top_w = np.linalg.norm(corners[1] - corners[0])
bottom_w = np.linalg.norm(corners[2] - corners[3])
left_h = np.linalg.norm(corners[3] - corners[0])
right_h = np.linalg.norm(corners[2] - corners[1])
avg_w = (top_w + bottom_w) / 2
avg_h = (left_h + right_h) / 2
num_cols = max(1, tile_cols)
# KEY: auto-compute rows to preserve tile aspect ratio
# We want: (num_cols * w_t) / (num_rows * h_t) β avg_w / avg_h
# β num_rows = num_cols * w_t * avg_h / (h_t * avg_w)
num_rows = max(1, int(round(
num_cols * (avg_h / max(avg_w, 1)) * (w_t / max(h_t, 1))
)))
# Apply user fine-tune offset
num_rows = max(1, num_rows + tile_rows_offset)
return num_cols, num_rows
# ==========================
# TILE PLACEMENT (reference code's place_grid method)
# ==========================
def apply_tiles():
"""
Exact same approach as reference code's place_grid():
1. Build rectangular tiled texture at NATIVE tile resolution
2. Single perspective warp to floor quad
3. Mask + composite
"""
global room_img
corners = get_outer_corners()
if corners is None:
print(" β Need at least 2 vertical + 2 horizontal lines!")
return
h, w = original_img.shape[:2]
h_t, w_t = tile_img.shape[:2]
result = original_img.copy()
num_cols, num_rows = compute_tile_counts(corners)
print(f" Tiles: {num_cols} cols Γ {num_rows} rows = {num_cols * num_rows}")
print(f" Tile size: {w_t}Γ{h_t}px")
# ββ Step 1: Build tiled texture (reference code style) ββ
width_rect = num_cols * w_t
height_rect = num_rows * h_t
tiled = np.zeros((height_rect, width_rect, 3), dtype=np.uint8)
for row in range(num_rows):
for col in range(num_cols):
tiled[row * h_t : (row + 1) * h_t,
col * w_t : (col + 1) * w_t] = tile_img
# ββ Step 1b: Bake grout lines into texture ββ
if grout_on and GROUT_WIDTH > 0:
gw = max(1, GROUT_WIDTH)
for r in range(1, num_rows):
y = r * h_t
tiled[max(0, y - gw):min(height_rect, y + gw), :] = GROUT_COLOR
for c in range(1, num_cols):
x = c * w_t
tiled[:, max(0, x - gw):min(width_rect, x + gw)] = GROUT_COLOR
# ββ Step 2: Perspective warp (EXACTLY like reference code) ββ
src_pts = np.float32([
[0, 0 ], # TL
[width_rect, 0 ], # TR
[width_rect, height_rect], # BR
[0, height_rect] # BL
])
M = cv2.getPerspectiveTransform(src_pts, corners)
warped = cv2.warpPerspective(
tiled, M, (w, h),
flags=cv2.INTER_LANCZOS4,
borderMode=cv2.BORDER_CONSTANT,
borderValue=(0, 0, 0)
)
# ββ Step 3: Mask (floor quad minus obstacles) ββ
mask = np.zeros((h, w), dtype=np.uint8)
cv2.fillPoly(mask, [corners.astype(np.int32).reshape(-1, 1, 2)], 255)
for obs in obstacles:
if len(obs) >= 3:
cv2.fillPoly(mask, [np.array(obs, np.int32).reshape(-1, 1, 2)], 0)
# ββ Step 4: Composite (same as reference) ββ
mask3 = cv2.merge([mask, mask, mask])
room_img = np.where(mask3 == 255, warped, result).astype(np.uint8)
print(f" β Done! No stretch β tile aspect ratio preserved.")
# ==========================
# TILE PREVIEW GRID (computed from transform matrix)
# ==========================
def compute_tile_preview():
"""
Show where tiles will ACTUALLY go (not grid line intersections).
Uses the perspective transform matrix to project tile boundaries.
"""
corners = get_outer_corners()
if corners is None:
return None
h_t, w_t = tile_img.shape[:2]
num_cols, num_rows = compute_tile_counts(corners)
width_rect = num_cols * w_t
height_rect = num_rows * h_t
src_pts = np.float32([
[0, 0], [width_rect, 0],
[width_rect, height_rect], [0, height_rect]
])
M = cv2.getPerspectiveTransform(src_pts, corners)
# Project each tile corner through the transform
preview = []
for r in range(num_rows + 1):
row = []
for c in range(num_cols + 1):
pt = np.float64([c * w_t, r * h_t, 1.0])
t = M @ pt
if abs(t[2]) > 1e-8:
t /= t[2]
row.append((int(t[0]), int(t[1])))
else:
row.append(None)
preview.append(row)
return preview, num_cols, num_rows
# ==========================
# MOUSE
# ==========================
def mouse_cb(event, x, y, flags, param):
global current_points, mouse_pos, obstacle_points
if event == cv2.EVENT_MOUSEMOVE:
mouse_pos = (x, y)
elif event == cv2.EVENT_LBUTTONDOWN:
if mode in ("vertical", "horizontal"):
current_points.append((x, y))
if len(current_points) == 2:
line = (*current_points[0], *current_points[1])
if mode == "vertical":
vertical_lines.append(line)
print(f" + V-line #{len(vertical_lines)}")
else:
horizontal_lines.append(line)
print(f" + H-line #{len(horizontal_lines)}")
current_points = []
elif mode == "obstacle":
obstacle_points.append((x, y))
elif event == cv2.EVENT_RBUTTONDOWN:
current_points = []
obstacle_points = []
# ==========================
# OVERLAY
# ==========================
def draw_overlay():
img = room_img.copy()
h, w = img.shape[:2]
# ββ V lines (green) ββ
for l in vertical_lines:
el = extend_line(l)
cv2.line(img, (el[0],el[1]), (el[2],el[3]), (0,140,0), 1, cv2.LINE_AA)
cv2.line(img, (l[0],l[1]), (l[2],l[3]), (0,255,0), 2, cv2.LINE_AA)
cv2.circle(img, (l[0],l[1]), 5, (255,255,255), -1)
cv2.circle(img, (l[2],l[3]), 5, (255,255,255), -1)
# ββ H lines (orange) ββ
for l in horizontal_lines:
el = extend_line(l)
cv2.line(img, (el[0],el[1]), (el[2],el[3]), (140,60,0), 1, cv2.LINE_AA)
cv2.line(img, (l[0],l[1]), (l[2],l[3]), (255,120,0), 2, cv2.LINE_AA)
cv2.circle(img, (l[0],l[1]), 5, (255,255,255), -1)
cv2.circle(img, (l[2],l[3]), 5, (255,255,255), -1)
# ββ Tile preview grid (where tiles ACTUALLY go) ββ
if show_grid and len(vertical_lines) >= 2 and len(horizontal_lines) >= 2:
result = compute_tile_preview()
if result:
preview, nc, nr = result
for i in range(len(preview) - 1):
for j in range(len(preview[i]) - 1):
quad = [preview[i][j], preview[i][j+1],
preview[i+1][j+1], preview[i+1][j]]
if all(p is not None for p in quad):
pts = np.array(quad, np.int32)
cv2.polylines(img, [pts], True,
(0, 255, 255), 1, cv2.LINE_AA)
# Corner dots
for row in preview:
for pt in row:
if pt and -50 <= pt[0] < w+50 and -50 <= pt[1] < h+50:
cv2.circle(img, pt, 4, (0,255,255), -1)
cv2.circle(img, pt, 4, (0,0,0), 1)
# Highlight 4 outer corners
corners = get_outer_corners()
if corners is not None:
for i, c in enumerate(corners):
ci = (int(c[0]), int(c[1]))
cv2.circle(img, ci, 8, (0,0,255), 2)
labels = ["TL","TR","BR","BL"]
cv2.putText(img, labels[i], (ci[0]+12, ci[1]-5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,255), 2)
# ββ Live line preview ββ
if len(current_points) == 1 and mouse_pos:
c = ((0,255,0) if mode == "vertical"
else (255,120,0) if mode == "horizontal"
else (0,0,255))
cv2.line(img, current_points[0], mouse_pos, c, 2, cv2.LINE_AA)
cv2.circle(img, current_points[0], 5, (255,255,255), -1)
# ββ Finished obstacles ββ
for obs in obstacles:
ov = img.copy()
cv2.fillPoly(ov, [np.array(obs, np.int32)], (0,0,180))
img = cv2.addWeighted(ov, 0.25, img, 0.75, 0)
cv2.polylines(img, [np.array(obs, np.int32)], True,
(0,0,255), 2, cv2.LINE_AA)
# ββ Obstacle in progress ββ
if obstacle_points:
for p in obstacle_points:
cv2.circle(img, p, 4, (0,0,255), -1)
if len(obstacle_points) > 1:
cv2.polylines(img, [np.array(obstacle_points)],
False, (0,0,255), 2)
if mouse_pos:
cv2.line(img, obstacle_points[-1], mouse_pos,
(0,0,200), 1, cv2.LINE_AA)
# ββ Tile thumbnail ββ
thumb_h = 80
th_o, tw_o = tile_img.shape[:2]
thumb_w = int(thumb_h * tw_o / th_o)
thumb = cv2.resize(tile_img, (thumb_w, thumb_h), interpolation=cv2.INTER_AREA)
tx, ty = w - thumb_w - 10, 10
if tx > 0:
img[ty:ty+thumb_h, tx:tx+thumb_w] = thumb
cv2.rectangle(img, (tx-1, ty-1),
(tx+thumb_w, ty+thumb_h), (255,255,255), 1)
cv2.putText(img, f"{tw_o}x{th_o}", (tx, ty+thumb_h+16),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255,255,255), 1)
# ββ HUD ββ
ov = img.copy()
cv2.rectangle(ov, (0, 0), (540, 95), (30, 30, 30), -1)
img = cv2.addWeighted(ov, 0.7, img, 0.3, 0)
mc = {"vertical":(0,255,0), "horizontal":(255,120,0), "obstacle":(0,0,255)}
cv2.putText(img, f"Mode: {mode.upper()}", (10, 22),
cv2.FONT_HERSHEY_SIMPLEX, 0.65, mc[mode], 2)
# Tile count info
corners = get_outer_corners()
if corners is not None:
nc, nr = compute_tile_counts(corners)
info = (f"V:{len(vertical_lines)} H:{len(horizontal_lines)} "
f"Tiles:{nc}x{nr}={nc*nr} "
f"Cols(+/-):{tile_cols} RowAdj([/]):{tile_rows_offset:+d}")
else:
info = f"V:{len(vertical_lines)} H:{len(horizontal_lines)} (need 2V + 2H)"
cv2.putText(img, info, (10, 48),
cv2.FONT_HERSHEY_SIMPLEX, 0.38, (220, 220, 220), 1)
cv2.putText(img,
f"Grid:{'ON' if show_grid else 'OFF'} "
f"Grout:{'ON' if grout_on else 'OFF'} "
f"| ENTER=place +/-=cols [/]=rows",
(10, 72), cv2.FONT_HERSHEY_SIMPLEX, 0.38, (180, 180, 180), 1)
return img
# ==========================
# MAIN
# ==========================
def main():
global original_img, room_img, tile_img, mode
global current_points, obstacle_points, obstacles
global vertical_lines, horizontal_lines
global show_grid, grout_on, tile_cols, tile_rows_offset
original_img = load_image(ROOM_IMAGE)
room_img = original_img.copy()
tile_img = load_image(TILE_IMAGE)
cv2.namedWindow("GRID ENGINE", cv2.WINDOW_NORMAL)
cv2.resizeWindow("GRID ENGINE", 1200, 800)
cv2.setMouseCallback("GRID ENGINE", mouse_cb)
print("""
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TILE GRID ENGINE v4 (No-Stretch Edition) β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββ£
β V β Vertical-line mode β
β H β Horizontal-line mode β
β O β Obstacle mode β
β Click Γ2 β Add line (auto) β
β Right-click β Cancel current drawing β
β C β Close obstacle polygon β
β Z β Undo last action β
β X β Clear all β
β R β Reset image (keep lines) β
β G β Toggle grid preview β
β T β Toggle grout β
β + / - β Adjust tile COLUMNS β
β [ / ] β Fine-tune tile ROWS β
β ENTER β Place tiles (no stretch!) β
β S β Save result β
β ESC β Exit β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
""")
while True:
display = draw_overlay()
cv2.imshow("GRID ENGINE", display)
key = cv2.waitKey(20) & 0xFF
if key == 27:
break
elif key == ord('v'):
mode = "vertical"; current_points = []
print(" β Vertical-line mode")
elif key == ord('h'):
mode = "horizontal"; current_points = []
print(" β Horizontal-line mode")
elif key == ord('o'):
mode = "obstacle"; current_points = []
print(" β Obstacle mode (click pts, C to close)")
elif key == ord('c'):
if mode == "obstacle" and len(obstacle_points) >= 3:
obstacles.append(obstacle_points.copy())
obstacle_points = []
print(f" β Obstacle #{len(obstacles)} closed")
elif key == ord('z'):
if mode == "vertical" and vertical_lines:
vertical_lines.pop(); print(" β© Undid V-line")
elif mode == "horizontal" and horizontal_lines:
horizontal_lines.pop(); print(" β© Undid H-line")
elif mode == "obstacle":
if obstacle_points: obstacle_points.pop()
elif obstacles: obstacles.pop(); print(" β© Undid obstacle")
elif key == ord('x'):
vertical_lines.clear(); horizontal_lines.clear()
obstacles.clear(); obstacle_points.clear(); current_points.clear()
room_img = original_img.copy()
tile_rows_offset = 0
print(" β Cleared all")
elif key == ord('r'):
room_img = original_img.copy()
print(" β» Image reset (lines kept)")
elif key == ord('g'):
show_grid = not show_grid
print(f" Grid: {'ON' if show_grid else 'OFF'}")
elif key == ord('t'):
grout_on = not grout_on
print(f" Grout: {'ON' if grout_on else 'OFF'}")
elif key in (ord('+'), ord('=')):
tile_cols = min(tile_cols + 1, 50)
print(f" Cols: {tile_cols}")
elif key in (ord('-'), ord('_')):
tile_cols = max(tile_cols - 1, 1)
print(f" Cols: {tile_cols}")
elif key == ord(']'):
tile_rows_offset += 1
print(f" Row offset: {tile_rows_offset:+d}")
elif key == ord('['):
tile_rows_offset -= 1
print(f" Row offset: {tile_rows_offset:+d}")
elif key == 13: # ENTER
print(" Placing tiles β¦")
apply_tiles()
elif key == ord('s'):
cv2.imwrite(OUTPUT, room_img)
print(f" πΎ Saved β {OUTPUT}")
cv2.destroyAllWindows()
if __name__ == "__main__":
main()