commit
e34c373686
12 changed files with 955 additions and 0 deletions
@ -0,0 +1,3 @@ |
|||
venv/ |
|||
src/__pycache__/ |
|||
images/ |
|||
@ -0,0 +1,6 @@ |
|||
Pouzite knihovny: |
|||
1. Flask |
|||
2. kociemba |
|||
3. opencv-python (cv2) |
|||
4. numpy |
|||
5. rpi_ws281x |
|||
@ -0,0 +1,17 @@ |
|||
Hlavni soubory: |
|||
1. Main.py - vstupni bod aplikace (entry point) |
|||
-Inicializuje system a spousti samotny webovy server. |
|||
2. Web_ui.py - sprava weboveho rozhrani |
|||
- Ridi stav skenovani (pamatuje si stav kostky a stranu) |
|||
- Spravuje uzivatelske rezimy |
|||
3. scan_one_face_3.py |
|||
- hlavni system pro digitalizaci kostky |
|||
- propojuje hardware (kameru, led kruh a dalsi.) |
|||
- funkce pro porieni snimku |
|||
|
|||
Pomocne a kalibracni skripty (pouzite pri vyvoji) |
|||
calibrate_roi.py |
|||
detect_roi.py |
|||
capture.py |
|||
kociemba_test.py |
|||
|
|||
@ -0,0 +1,159 @@ |
|||
import os |
|||
import cv2 |
|||
import numpy as np |
|||
|
|||
IMG_PATH = "/home/kluci/rubik_images/face_1.jpg" |
|||
|
|||
DEBUG_FULL = "/home/kluci/rubik_images/face_1_debug_full.jpg" |
|||
DEBUG_CROP = "/home/kluci/rubik_images/face_1_debug_crop.jpg" |
|||
|
|||
|
|||
ROI_X = 750 |
|||
ROI_Y = 215 |
|||
ROI_W = 580 |
|||
ROI_H = 580 |
|||
|
|||
|
|||
ROI_MARGIN = 20 |
|||
|
|||
|
|||
SAMPLE_SIZE = 30 |
|||
|
|||
def clamp(v, lo, hi): |
|||
if v < lo: |
|||
return lo |
|||
if v > hi: |
|||
return hi |
|||
return v |
|||
def classify_hsv(h, s, v): |
|||
|
|||
if s < 45 and v > 120: |
|||
return "W" |
|||
if v < 50: |
|||
return "?" |
|||
|
|||
if 18 <= h <= 35 and s > 70 and v > 80: |
|||
return "Y" |
|||
if 36 <= h <= 85 and s > 60 and v > 60: |
|||
return "G" |
|||
if 86 <= h <= 130 and s > 60 and v > 50: |
|||
return "B" |
|||
|
|||
if (h <= 10 or h >= 170) and s > 70 and v > 60: |
|||
return "R" |
|||
if 11 <= h <= 17 and s > 70 and v > 60: |
|||
return "O" |
|||
|
|||
return "?" |
|||
|
|||
def main(): |
|||
if not os.path.exists(IMG_PATH): |
|||
print("ERROR: image not found:", IMG_PATH) |
|||
return |
|||
|
|||
img = cv2.imread(IMG_PATH) |
|||
if img is None: |
|||
print("ERROR: failed to read:", IMG_PATH) |
|||
return |
|||
|
|||
H_img, W_img = img.shape[:2] |
|||
|
|||
|
|||
x = clamp(ROI_X, 0, W_img - 2) |
|||
y = clamp(ROI_Y, 0, H_img - 2) |
|||
w = clamp(ROI_W, 2, W_img - x) |
|||
h = clamp(ROI_H, 2, H_img - y) |
|||
|
|||
|
|||
x2 = clamp(x + ROI_MARGIN, 0, W_img - 2) |
|||
y2 = clamp(y + ROI_MARGIN, 0, H_img - 2) |
|||
w2 = clamp(w - 2 * ROI_MARGIN, 2, W_img - x2) |
|||
h2 = clamp(h - 2 * ROI_MARGIN, 2, H_img - y2) |
|||
|
|||
crop = img[y2:y2+h2, x2:x2+w2].copy() |
|||
hsv = cv2.cvtColor(crop, cv2.COLOR_BGR2HSV) |
|||
|
|||
ch, cw = crop.shape[:2] |
|||
cell_w = cw // 3 |
|||
cell_h = ch // 3 |
|||
|
|||
debug_full = img.copy() |
|||
debug_crop = crop.copy() |
|||
|
|||
|
|||
cv2.rectangle(debug_full, (x, y), (x + w, y + h), (0, 0, 255), 3) |
|||
cv2.rectangle(debug_full, (x2, y2), (x2 + w2, y2 + h2), (0, 255, 255), 2) |
|||
|
|||
grid = [] |
|||
idx = 1 |
|||
|
|||
print("ROI used: x=%d y=%d w=%d h=%d (inner x=%d y=%d w=%d h=%d)" % (x, y, w, h, x2, y2, w2, h2)) |
|||
print("Detected colors (3x3):") |
|||
|
|||
for r in range(3): |
|||
row = [] |
|||
for c in range(3): |
|||
rx0 = c * cell_w |
|||
ry0 = r * cell_h |
|||
rx1 = (c + 1) * cell_w |
|||
ry1 = (r + 1) * cell_h |
|||
|
|||
cx = (rx0 + rx1) // 2 |
|||
cy = (ry0 + ry1) // 2 |
|||
|
|||
half = SAMPLE_SIZE // 2 |
|||
sx0 = clamp(cx - half, 0, cw - 1) |
|||
sy0 = clamp(cy - half, 0, ch - 1) |
|||
sx1 = clamp(cx + half, 0, cw) |
|||
sy1 = clamp(cy + half, 0, ch) |
|||
|
|||
patch = hsv[sy0:sy1, sx0:sx1] |
|||
mean = patch.reshape(-1, 3).mean(axis=0) |
|||
Hm, Sm, Vm = float(mean[0]), float(mean[1]), float(mean[2]) |
|||
|
|||
color = classify_hsv(Hm, Sm, Vm) |
|||
row.append(color) |
|||
|
|||
|
|||
cv2.rectangle(debug_crop, (rx0, ry0), (rx1, ry1), (0, 255, 0), 2) |
|||
cv2.rectangle(debug_crop, (sx0, sy0), (sx1, sy1), (255, 0, 0), 2) |
|||
cv2.putText(debug_crop, color, (rx0 + 10, ry0 + 55), |
|||
cv2.FONT_HERSHEY_SIMPLEX, 2.0, (0, 255, 0), 3) |
|||
cv2.putText(debug_crop, "H%d S%d V%d" % (int(Hm), int(Sm), int(Vm)), |
|||
(rx0 + 10, ry1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, |
|||
(0, 255, 255), 2) |
|||
|
|||
|
|||
fx0 = x2 + rx0 |
|||
fy0 = y2 + ry0 |
|||
fx1 = x2 + rx1 |
|||
fy1 = y2 + ry1 |
|||
fsx0 = x2 + sx0 |
|||
fsy0 = y2 + sy0 |
|||
fsx1 = x2 + sx1 |
|||
fsy1 = y2 + sy1 |
|||
|
|||
cv2.rectangle(debug_full, (fx0, fy0), (fx1, fy1), (0, 255, 0), 2) |
|||
cv2.rectangle(debug_full, (fsx0, fsy0), (fsx1, fsy1), (255, 0, 0), 2) |
|||
cv2.putText(debug_full, color, (fx0 + 5, fy0 + 35), |
|||
cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 255, 0), 2) |
|||
|
|||
print("%d: cell[%d,%d] H=%.1f S=%.1f V=%.1f => %s" % (idx, r, c, Hm, Sm, Vm, color)) |
|||
idx += 1 |
|||
|
|||
grid.append(row) |
|||
|
|||
cv2.imwrite(DEBUG_FULL, debug_full) |
|||
cv2.imwrite(DEBUG_CROP, debug_crop) |
|||
|
|||
print("") |
|||
print("Grid:") |
|||
for r in grid: |
|||
print(" ".join(r)) |
|||
|
|||
print("") |
|||
print("Saved:", DEBUG_FULL) |
|||
print("Saved:", DEBUG_CROP) |
|||
|
|||
if __name__ == "__main__": |
|||
main() |
|||
@ -0,0 +1,87 @@ |
|||
#!/usr/bin/env python3 |
|||
import os |
|||
import json |
|||
import cv2 |
|||
|
|||
IMG_PATH = "/home/kluci/rubik_images/face_1.jpg" |
|||
ROI_PATH = "/home/kluci/rubik_images/roi.json" |
|||
|
|||
points = [] |
|||
|
|||
def on_mouse(event, x, y, flags, param): |
|||
global points |
|||
if event == cv2.EVENT_LBUTTONDOWN: |
|||
points.append((x, y)) |
|||
print("Clicked:", x, y) |
|||
|
|||
def main(): |
|||
if not os.path.exists(IMG_PATH): |
|||
print("ERROR: image not found:", IMG_PATH) |
|||
print("Run capture script first to create face_1.jpg") |
|||
return |
|||
|
|||
img = cv2.imread(IMG_PATH) |
|||
if img is None: |
|||
print("ERROR: failed to read image:", IMG_PATH) |
|||
return |
|||
|
|||
cv2.namedWindow("calibrate_roi", cv2.WINDOW_NORMAL) |
|||
cv2.setMouseCallback("calibrate_roi", on_mouse) |
|||
|
|||
while True: |
|||
vis = img.copy() |
|||
|
|||
# draw clicked points |
|||
for p in points: |
|||
cv2.circle(vis, p, 8, (0, 0, 255), -1) |
|||
|
|||
# if 2 points, draw rectangle |
|||
if len(points) >= 2: |
|||
x1, y1 = points[0] |
|||
x2, y2 = points[1] |
|||
x_min, x_max = (min(x1, x2), max(x1, x2)) |
|||
y_min, y_max = (min(y1, y2), max(y1, y2)) |
|||
cv2.rectangle(vis, (x_min, y_min), (x_max, y_max), (0, 255, 0), 3) |
|||
|
|||
cv2.imshow("calibrate_roi", vis) |
|||
key = cv2.waitKey(20) & 0xFF |
|||
|
|||
# ESC = exit without save |
|||
if key == 27: |
|||
print("Exit without saving.") |
|||
break |
|||
|
|||
# S = save if we have 2 points |
|||
if key in (ord('s'), ord('S')): |
|||
if len(points) < 2: |
|||
print("Need 2 clicks: top-left and bottom-right.") |
|||
continue |
|||
|
|||
x1, y1 = points[0] |
|||
x2, y2 = points[1] |
|||
x_min, x_max = (min(x1, x2), max(x1, x2)) |
|||
y_min, y_max = (min(y1, y2), max(y1, y2)) |
|||
|
|||
roi = { |
|||
"x": int(x_min), |
|||
"y": int(y_min), |
|||
"w": int(x_max - x_min), |
|||
"h": int(y_max - y_min) |
|||
} |
|||
|
|||
with open(ROI_PATH, "w") as f: |
|||
json.dump(roi, f) |
|||
|
|||
print("Saved ROI to:", ROI_PATH) |
|||
print(roi) |
|||
break |
|||
|
|||
# R = reset points |
|||
if key in (ord('r'), ord('R')): |
|||
points = [] |
|||
print("Reset points.") |
|||
|
|||
cv2.destroyAllWindows() |
|||
|
|||
if __name__ == "__main__": |
|||
main() |
|||
@ -0,0 +1,24 @@ |
|||
|
|||
import os |
|||
import time |
|||
from picamera2 import Picamera2 |
|||
|
|||
OUTDIR = "/home/kluci/rubik_images" |
|||
OUTFILE = os.path.join(OUTDIR, "face_1.jpg") |
|||
|
|||
def main(): |
|||
os.makedirs(OUTDIR, exist_ok=True) |
|||
|
|||
picam = Picamera2() |
|||
config = picam.create_still_configuration(main={"size": (1920, 1080)}) |
|||
picam.configure(config) |
|||
|
|||
picam.start() |
|||
time.sleep(1.5) |
|||
picam.capture_file(OUTFILE) |
|||
picam.stop() |
|||
|
|||
print("Saved:", OUTFILE) |
|||
|
|||
if __name__ == "__main__": |
|||
main() |
|||
@ -0,0 +1,119 @@ |
|||
|
|||
import os |
|||
import cv2 |
|||
import numpy as np |
|||
|
|||
IMG_PATH = "/home/kluci/rubik_images/face_1.jpg" |
|||
DEBUG_PATH = "/home/kluci/rubik_images/face_1_debug.jpg" |
|||
|
|||
SAMPLE_SIZE = 50 |
|||
|
|||
def clamp(v, lo, hi): |
|||
if v < lo: |
|||
return lo |
|||
if v > hi: |
|||
return hi |
|||
return v |
|||
|
|||
def classify_hsv(h, s, v): |
|||
""" |
|||
Velmi jednoducha klasifikace barvy z HSV. |
|||
HSV v OpenCV: |
|||
H: 0-179 |
|||
S: 0-255 |
|||
V: 0-255 |
|||
|
|||
Tohle je START nastaveni. Budeme to ladit podle tvych fotek. |
|||
Vraci jedno pismeno: W Y R O G B |
|||
""" |
|||
|
|||
if s < 40 and v > 120: |
|||
return "W" |
|||
|
|||
if v < 50: |
|||
return "?" |
|||
|
|||
if 18 <= h <= 35 and s > 70 and v > 80: |
|||
return "Y" |
|||
|
|||
if 36 <= h <= 85 and s > 60 and v > 60: |
|||
return "G" |
|||
|
|||
if 86 <= h <= 130 and s > 60 and v > 50: |
|||
return "B" |
|||
|
|||
if (h <= 10 or h >= 170) and s > 70 and v > 60: |
|||
return "R" |
|||
if 11 <= h <= 17 and s > 70 and v > 60: |
|||
return "O" |
|||
|
|||
return "?" |
|||
|
|||
def main(): |
|||
if not os.path.exists(IMG_PATH): |
|||
print("ERROR: image not found:", IMG_PATH) |
|||
print("Run capture_face.py first.") |
|||
return |
|||
|
|||
img = cv2.imread(IMG_PATH) |
|||
if img is None: |
|||
print("ERROR: failed to read image:", IMG_PATH) |
|||
return |
|||
|
|||
h_img, w_img = img.shape[:2] |
|||
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) |
|||
|
|||
cell_w = w_img // 3 |
|||
cell_h = h_img // 3 |
|||
|
|||
debug = img.copy() |
|||
|
|||
grid = [] |
|||
idx = 1 |
|||
|
|||
print("Detected 3x3 colors:") |
|||
for r in range(3): |
|||
row = [] |
|||
for c in range(3): |
|||
x0 = c * cell_w |
|||
y0 = r * cell_h |
|||
x1 = (c + 1) * cell_w |
|||
y1 = (r + 1) * cell_h |
|||
|
|||
cx = (x0 + x1) // 2 |
|||
cy = (y0 + y1) // 2 |
|||
|
|||
half = SAMPLE_SIZE // 2 |
|||
sx0 = clamp(cx - half, 0, w_img - 1) |
|||
sy0 = clamp(cy - half, 0, h_img - 1) |
|||
sx1 = clamp(cx + half, 0, w_img) |
|||
sy1 = clamp(cy + half, 0, h_img) |
|||
|
|||
patch = hsv[sy0:sy1, sx0:sx1] |
|||
mean = patch.reshape(-1, 3).mean(axis=0) |
|||
H, S, V = float(mean[0]), float(mean[1]), float(mean[2]) |
|||
|
|||
color = classify_hsv(H, S, V) |
|||
row.append(color) |
|||
|
|||
cv2.rectangle(debug, (x0, y0), (x1, y1), (0, 255, 0), 2) # mrizka |
|||
cv2.rectangle(debug, (sx0, sy0), (sx1, sy1), (255, 0, 0), 2) # vzorek |
|||
cv2.putText(debug, f"{color}", (x0 + 20, y0 + 60), |
|||
cv2.FONT_HERSHEY_SIMPLEX, 2.0, (0, 255, 0), 3) |
|||
cv2.putText(debug, f"H{int(H)} S{int(S)} V{int(V)}", (x0 + 10, y1 - 15), |
|||
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) |
|||
|
|||
print(f"{idx}: cell[{r},{c}] -> H={H:.1f} S={S:.1f} V={V:.1f} => {color}") |
|||
idx += 1 |
|||
|
|||
grid.append(row) |
|||
|
|||
cv2.imwrite(DEBUG_PATH, debug) |
|||
print("Saved debug image:", DEBUG_PATH) |
|||
print("") |
|||
print("Grid (top row first):") |
|||
for r in grid: |
|||
print(" ".join(r)) |
|||
|
|||
if __name__ == "__main__": |
|||
main() |
|||
@ -0,0 +1,45 @@ |
|||
import kociemba |
|||
|
|||
faces = [ |
|||
"Horni (U - stredova Bila)", |
|||
"Prava (R - stredova Cervena)", |
|||
"Predni (F - stredova Zelena)", |
|||
"Spodni (D - stredova Zluta)", |
|||
"Leva (L - stredova Oranzova)", |
|||
"Zadni (B - stredova Modra)" |
|||
] |
|||
|
|||
color_to_face = { |
|||
'W': 'U', |
|||
'Y': 'D', |
|||
'R': 'R', |
|||
'O': 'L', |
|||
'G': 'F', |
|||
'B': 'B' |
|||
} |
|||
|
|||
kociemba_string = "" |
|||
|
|||
for face in faces: |
|||
while True: |
|||
user_input = input(f"Zadej 9 barev pro stenu {face} zleva doprava (W, Y, R, O, G, B): ").strip().upper() |
|||
|
|||
if len(user_input) == 9 and all(char in color_to_face for char in user_input): |
|||
translated_face = "".join(color_to_face[char] for char in user_input) |
|||
kociemba_string += translated_face |
|||
break |
|||
else: |
|||
print("Neplatny vstup. Zadej presne 9 pismen z povolenych barev.") |
|||
|
|||
print("\n--- KONTROLA ---") |
|||
print(f"Vygenerovany string: {kociemba_string}") |
|||
|
|||
confirmation = input("Chces vygenerovat reseni? (ano/ne): ").strip().lower() |
|||
|
|||
if confirmation == "ano": |
|||
try: |
|||
solution = kociemba.solve(kociemba_string) |
|||
print("\n--- RESENI ---") |
|||
print(solution) |
|||
except Exception as e: |
|||
print(f"\nNelze najit reseni, zkontroluj zadane barvy. Detail chyby: {e}") |
|||
@ -0,0 +1,11 @@ |
|||
from web_ui import start_server |
|||
|
|||
def main(): |
|||
print("=====================================") |
|||
print(" Rubik's Cube Solver - Startuje... ") |
|||
print("=====================================") |
|||
|
|||
start_server() |
|||
|
|||
if __name__ == "__main__": |
|||
main() |
|||
@ -0,0 +1,209 @@ |
|||
import os |
|||
import re |
|||
import time |
|||
import cv2 |
|||
import numpy as np |
|||
|
|||
from picamera2 import Picamera2 |
|||
from rpi_ws281x import PixelStrip, Color |
|||
|
|||
OUTDIR = "/home/kluci/rubiks_solver/images" |
|||
|
|||
ROI_X = 600 |
|||
ROI_Y = 190 |
|||
ROI_W = 600 |
|||
ROI_H = 600 |
|||
|
|||
LED_COUNT = 24 |
|||
LED_PIN = 18 |
|||
LED_BRIGHTNESS = 3 |
|||
|
|||
ROI_MARGIN = 20 |
|||
SAMPLE_SIZE = 55 |
|||
|
|||
RES_W = 1920 |
|||
RES_H = 1080 |
|||
|
|||
REF_COLORS = { |
|||
"W": (148, 30, 230), |
|||
"R": (172, 130, 250), |
|||
"O": (6, 130, 250), |
|||
"Y": (27, 130, 250), |
|||
"G": (68, 200, 185), |
|||
"B": (110, 200, 185) |
|||
} |
|||
|
|||
_global_strip = None |
|||
|
|||
def get_strip(): |
|||
global _global_strip |
|||
if _global_strip is None: |
|||
_global_strip = PixelStrip(LED_COUNT, LED_PIN, brightness=LED_BRIGHTNESS) |
|||
_global_strip.begin() |
|||
return _global_strip |
|||
|
|||
def led_on(strip): |
|||
strip.setBrightness(LED_BRIGHTNESS) |
|||
for i in range(strip.numPixels()): |
|||
strip.setPixelColor(i, Color(255, 220, 120)) |
|||
strip.show() |
|||
|
|||
def led_off(strip): |
|||
for i in range(strip.numPixels()): |
|||
strip.setPixelColor(i, Color(0, 0, 0)) |
|||
strip.show() |
|||
|
|||
def clamp(v, lo, hi): |
|||
if v < lo: return lo |
|||
if v > hi: return hi |
|||
return v |
|||
|
|||
def next_face_index(folder): |
|||
pat = re.compile(r"^face_(\d+)\.jpg$") |
|||
max_i = 0 |
|||
for name in os.listdir(folder): |
|||
m = pat.match(name) |
|||
if m: |
|||
i = int(m.group(1)) |
|||
if i > max_i: |
|||
max_i = i |
|||
return max_i + 1 |
|||
|
|||
def hue_dist(a, b): |
|||
d = abs(a - b) |
|||
return min(d, 180 - d) |
|||
|
|||
def classify_hsv(h, s, v): |
|||
if s < 60 and v > 150: |
|||
return "W" |
|||
best_name = "?" |
|||
best_score = 999999 |
|||
for name, ref in REF_COLORS.items(): |
|||
rh, rs, rv = ref |
|||
dh = hue_dist(h, rh) |
|||
ds = abs(s - rs) |
|||
dv = abs(v - rv) |
|||
if name == "W": |
|||
score = dh * 0.5 + ds * 3.0 + dv * 1.0 |
|||
elif name in ("R", "O"): |
|||
score = dh * 4.0 + ds * 1.0 + dv * 0.6 |
|||
else: |
|||
score = dh * 3.0 + ds * 0.8 + dv * 0.5 |
|||
if score < best_score: |
|||
best_score = score |
|||
best_name = name |
|||
return best_name |
|||
|
|||
def capture_image(filepath): |
|||
picam = Picamera2() |
|||
try: |
|||
config = picam.create_still_configuration(main={"size": (RES_W, RES_H)}) |
|||
picam.configure(config) |
|||
picam.start() |
|||
time.sleep(1.5) |
|||
picam.capture_file(filepath) |
|||
finally: |
|||
picam.stop() |
|||
picam.close() |
|||
|
|||
def analyze_image(img): |
|||
H_img, W_img = img.shape[:2] |
|||
x = clamp(ROI_X, 0, W_img - 2) |
|||
y = clamp(ROI_Y, 0, H_img - 2) |
|||
w = clamp(ROI_W, 2, W_img - x) |
|||
h = clamp(ROI_H, 2, H_img - y) |
|||
x2 = clamp(x + ROI_MARGIN, 0, W_img - 2) |
|||
y2 = clamp(y + ROI_MARGIN, 0, H_img - 2) |
|||
w2 = clamp(w - 2 * ROI_MARGIN, 2, W_img - x2) |
|||
h2 = clamp(h - 2 * ROI_MARGIN, 2, H_img - y2) |
|||
|
|||
crop = img[y2:y2+h2, x2:x2+w2].copy() |
|||
hsv = cv2.cvtColor(crop, cv2.COLOR_BGR2HSV) |
|||
ch, cw = crop.shape[:2] |
|||
cell_w = cw // 3 |
|||
cell_h = ch // 3 |
|||
|
|||
grid = [] |
|||
debug_full = img.copy() |
|||
debug_crop = crop.copy() |
|||
|
|||
cv2.rectangle(debug_full, (x, y), (x + w, y + h), (0, 0, 255), 3) |
|||
cv2.rectangle(debug_full, (x2, y2), (x2 + w2, y2 + h2), (0, 255, 255), 2) |
|||
|
|||
for r in range(3): |
|||
row = [] |
|||
for c in range(3): |
|||
rx0 = c * cell_w |
|||
ry0 = r * cell_h |
|||
rx1 = (c + 1) * cell_w |
|||
ry1 = (r + 1) * cell_h |
|||
cx = (rx0 + rx1) // 2 |
|||
cy = (ry0 + ry1) // 2 |
|||
half = SAMPLE_SIZE // 2 |
|||
sx0 = clamp(cx - half, 0, cw - 1) |
|||
sy0 = clamp(cy - half, 0, ch - 1) |
|||
sx1 = clamp(cx + half, 0, cw) |
|||
sy1 = clamp(cy + half, 0, ch) |
|||
|
|||
patch = hsv[sy0:sy1, sx0:sx1] |
|||
flat = patch.reshape(-1, 3) |
|||
good = flat[(flat[:, 2] < 245)] |
|||
if len(good) > 20: flat = good |
|||
med = np.median(flat, axis=0) |
|||
Hm, Sm, Vm = float(med[0]), float(med[1]), float(med[2]) |
|||
|
|||
color = classify_hsv(Hm, Sm, Vm) |
|||
row.append(color) |
|||
|
|||
cv2.rectangle(debug_crop, (rx0, ry0), (rx1, ry1), (0, 255, 0), 2) |
|||
cv2.rectangle(debug_crop, (sx0, sy0), (sx1, sy1), (255, 0, 0), 2) |
|||
cv2.putText(debug_crop, color, (rx0 + 10, ry0 + 55), cv2.FONT_HERSHEY_SIMPLEX, 2.0, (0, 255, 0), 3) |
|||
cv2.putText(debug_crop, "H%d S%d V%d" % (int(Hm), int(Sm), int(Vm)), (rx0 + 10, ry1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) |
|||
|
|||
fx0, fy0 = x2 + rx0, y2 + ry0 |
|||
fx1, fy1 = x2 + rx1, y2 + ry1 |
|||
fsx0, fsy0 = x2 + sx0, y2 + sy0 |
|||
fsx1, fsy1 = x2 + sx1, y2 + sy1 |
|||
|
|||
cv2.rectangle(debug_full, (fx0, fy0), (fx1, fy1), (0, 255, 0), 2) |
|||
cv2.rectangle(debug_full, (fsx0, fsy0), (fsx1, fsy1), (255, 0, 0), 2) |
|||
cv2.putText(debug_full, color, (fx0 + 5, fy0 + 35), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 255, 0), 2) |
|||
|
|||
grid.append(row) |
|||
|
|||
return grid, debug_full, debug_crop |
|||
|
|||
def scan_single_face(): |
|||
os.makedirs(OUTDIR, exist_ok=True) |
|||
|
|||
strip = get_strip() |
|||
led_off(strip) |
|||
|
|||
idx = next_face_index(OUTDIR) |
|||
img_path = os.path.join(OUTDIR, "face_%d.jpg" % idx) |
|||
debug_full_path = os.path.join(OUTDIR, "debug_full_%d.jpg" % idx) |
|||
debug_crop_path = os.path.join(OUTDIR, "debug_crop_%d.jpg" % idx) |
|||
|
|||
led_on(strip) |
|||
time.sleep(0.3) |
|||
capture_image(img_path) |
|||
led_off(strip) |
|||
|
|||
img = cv2.imread(img_path) |
|||
if img is None: |
|||
return "" |
|||
|
|||
img = cv2.rotate(img, cv2.ROTATE_180) |
|||
grid, debug_full, debug_crop = analyze_image(img) |
|||
|
|||
cv2.imwrite(debug_full_path, debug_full) |
|||
cv2.imwrite(debug_crop_path, debug_crop) |
|||
|
|||
result_string = "".join(color for row in grid for color in row) |
|||
|
|||
led_off(strip) |
|||
return result_string |
|||
|
|||
if __name__ == "__main__": |
|||
test_result = scan_single_face() |
|||
print("Test skenu:", test_result) |
|||
@ -0,0 +1,176 @@ |
|||
from flask import Flask, request, redirect, url_for |
|||
import kociemba |
|||
import os |
|||
from scan_one_face_3 import scan_single_face |
|||
|
|||
app = Flask(__name__) |
|||
|
|||
app_state = { |
|||
"kociemba_string": "", |
|||
"current_face_idx": 0, |
|||
"last_scan": "", |
|||
"mode": "advanced", # Pamatuje si rezim |
|||
"faces": [ |
|||
"(Bila strana) Polozte kostku zlutou nahoru modrou k sobe", |
|||
"(Cervena strana) Polozte kostku oranzovou nahoru bilou k sobe", |
|||
"(Zelena strana) Polozte kostku modrou nahoru bilou k sobe", |
|||
"(Zluta strana) Polozte kostku bilou nahoru zelenou k sobe", |
|||
"(Oranzova strana) Polozte kostku cervenou nahoru bilou k sobe", |
|||
"(Modra strana) Polozte kostku zelenou nahoru bilou k sobe" |
|||
], |
|||
"color_to_face": {'W': 'U', 'Y': 'D', 'R': 'R', 'O': 'L', 'G': 'F', 'B': 'B'} |
|||
} |
|||
|
|||
# 1. HLAVNI MENU |
|||
|
|||
@app.route('/') |
|||
def index(): |
|||
return """ |
|||
<body style="font-family: Arial; text-align: center; margin-top: 30px;"> |
|||
<h1>Rubik's Cube Solver</h1> |
|||
<p style="color: gray;">Zvolte rezim skenovani</p> |
|||
|
|||
<div style="margin-top: 30px;"> |
|||
<form action="/start/normal" method="post"> |
|||
<button style="padding: 15px 30px; margin: 10px; background-color: #3498db; color: white; font-size: 18px; width: 85%; border-radius: 8px; border: none;">1. Normal Mode<br><small>(Automaticke potvrzovani)</small></button> |
|||
</form> |
|||
<form action="/start/advanced" method="post"> |
|||
<button style="padding: 15px 30px; margin: 10px; background-color: #9b59b6; color: white; font-size: 18px; width: 85%; border-radius: 8px; border: none;">2. Advanced Mode<br><small>(Rucni oprava barev)</small></button> |
|||
</form> |
|||
<form action="/tutorial" method="get"> |
|||
<button style="padding: 15px 30px; margin: 10px; background-color: #2ecc71; color: white; font-size: 18px; width: 85%; border-radius: 8px; border: none;">3. Tutorial / Navod</button> |
|||
</form> |
|||
</div> |
|||
|
|||
<hr style="margin-top: 60px;"> |
|||
|
|||
<details> |
|||
<summary style="color: gray; cursor: pointer;">Nastaveni pro vyvojare</summary> |
|||
<form action="/dev_mode" method="post" style="margin-top: 15px;"> |
|||
Heslo: <input type="password" name="password" style="width: 100px;"> |
|||
<button type="submit" style="padding: 5px; background-color: #e74c3c; color: white; border: none; border-radius: 4px;">Prepnout na VNC (Domaci Wi-Fi)</button> |
|||
</form> |
|||
</details> |
|||
</body> |
|||
""" |
|||
|
|||
# TLACITKA Z MENU |
|||
|
|||
@app.route('/start/<mode>', methods=['POST']) |
|||
def start_mode(mode): |
|||
app_state["kociemba_string"] = "" |
|||
app_state["current_face_idx"] = 0 |
|||
app_state["last_scan"] = "" |
|||
app_state["mode"] = mode |
|||
return redirect(url_for('step')) |
|||
|
|||
@app.route('/tutorial') |
|||
def tutorial(): |
|||
return """ |
|||
<body style="font-family: Arial; padding: 20px;"> |
|||
<h1 style="text-align: center;">Jak skenovat kostku</h1> |
|||
<p>1. Kostku skladame postupne v tomto poradi barev stran: <b>Bila, Cervena, Zelena, Zluta, Oranzova, Modra</b>.</p> |
|||
<p>2. Vzdy dodrzujte spravnou orientaci kostky (ktera barva je nahore a ktera k vam).</p> |
|||
<p>3. V <b>Normal modu</b> program barvy rovnou prelozi. V <b>Advanced modu</b> si je muzete zkontrolovat a prepsat pismena (W, R, G, Y, O, B).</p> |
|||
<br> |
|||
<div style="text-align: center;"> |
|||
<a href="/" style="padding: 15px 30px; background-color: #34495e; color: white; text-decoration: none; border-radius: 8px; font-size: 18px;">Zpet do menu</a> |
|||
</div> |
|||
</body> |
|||
""" |
|||
|
|||
@app.route('/dev_mode', methods=['POST']) |
|||
def dev_mode(): |
|||
password = request.form.get('password') |
|||
if password == "12345": |
|||
print("Prepinam do vyvojarskeho modu...") |
|||
os.system('sudo nmcli con up "Dobi"') |
|||
return "<body style='text-align:center; margin-top:50px;'><h2>Prepinam site...</h2><p>Raspberry nyni vysila signal do domaci Wi-Fi. Pripojte se z PC pres VNC.</p></body>" |
|||
else: |
|||
return "<h2 style='color:red; text-align:center;'>Spatne heslo!</h2><a href='/'>Zpet</a>" |
|||
|
|||
# 2. SAMOTNE SKENOVANI |
|||
|
|||
@app.route('/step') |
|||
def step(): |
|||
if app_state["current_face_idx"] >= 6: |
|||
return f""" |
|||
<body style="font-family: Arial; text-align: center; margin-top: 50px;"> |
|||
<h2>Vse naskenovano!</h2> |
|||
<p>String: <b>{app_state['kociemba_string']}</b></p> |
|||
<form action="/solve" method="post"><button style="padding: 20px; font-size: 20px; background-color: #3498db; color: white; border: none; border-radius: 8px;">Vygenerovat reseni</button></form> |
|||
<br><br><a href="/" style="color: red;">Zpet do hlavniho menu</a> |
|||
</body> |
|||
""" |
|||
|
|||
face_name = app_state["faces"][app_state["current_face_idx"]] |
|||
|
|||
html = f""" |
|||
<body style="font-family: Arial; text-align: center; margin-top: 50px;"> |
|||
<h3>Krok {app_state["current_face_idx"] + 1} ze 6</h3> |
|||
<h2>{face_name}</h2> |
|||
<form action="/scan" method="post"><button style="padding: 20px 40px; background-color: #f1c40f; font-size: 20px; border: none; border-radius: 8px;">Skenovat stranu</button></form> |
|||
""" |
|||
|
|||
if app_state["last_scan"]: |
|||
if "CHYBA" in app_state["last_scan"]: |
|||
html += f"<p style='color: red;'><b>{app_state['last_scan']}</b> Zkuste to znovu.</p>" |
|||
elif app_state["mode"] == "advanced": |
|||
html += f""" |
|||
<hr style="margin-top: 30px;"> |
|||
<h3 style="color: green;">Naskenovano: {app_state["last_scan"]}</h3> |
|||
<p>Zkontroluj pismena a potvrd:</p> |
|||
<form action="/confirm" method="post"> |
|||
<input type="text" name="colors" value="{app_state['last_scan']}" style="font-size: 24px; text-align: center;" required><br><br> |
|||
<button style="padding: 15px; background-color: #2ecc71; color: white; border: none; border-radius: 8px; font-size: 18px;">Potvrdit</button> |
|||
</form> |
|||
""" |
|||
|
|||
html += "<br><br><a href='/'>Zrusit a zpet do menu</a></body>" |
|||
return html |
|||
|
|||
@app.route('/scan', methods=['POST']) |
|||
def scan(): |
|||
scanned = scan_single_face() |
|||
|
|||
if scanned and len(scanned) == 9: |
|||
if app_state["mode"] == "normal": |
|||
if all(c in app_state['color_to_face'] for c in scanned): |
|||
translated = "".join(app_state['color_to_face'][c] for c in scanned) |
|||
app_state["kociemba_string"] += translated |
|||
app_state["current_face_idx"] += 1 |
|||
app_state["last_scan"] = "" |
|||
else: |
|||
app_state["last_scan"] = "CHYBA: Neznama barva ve skenu." |
|||
else: |
|||
app_state["last_scan"] = scanned |
|||
else: |
|||
app_state["last_scan"] = "CHYBA_KAMERY" |
|||
|
|||
return redirect(url_for('step')) |
|||
|
|||
@app.route('/confirm', methods=['POST']) |
|||
def confirm(): |
|||
colors = request.form.get('colors').strip().upper() |
|||
if len(colors) == 9 and all(c in app_state['color_to_face'] for c in colors): |
|||
translated = "".join(app_state['color_to_face'][c] for c in colors) |
|||
app_state["kociemba_string"] += translated |
|||
app_state["current_face_idx"] += 1 |
|||
app_state["last_scan"] = "" |
|||
return redirect(url_for('step')) |
|||
|
|||
@app.route('/solve', methods=['POST']) |
|||
def solve(): |
|||
k_string = app_state["kociemba_string"] |
|||
if k_string == "UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB": |
|||
return "<h1 style='text-align:center; margin-top:50px;'>Kostka uz je slozena!</h1><br><div style='text-align:center;'><a href='/'>Zpet do menu</a></div>" |
|||
try: |
|||
solution = kociemba.solve(k_string) |
|||
return f"<body style='text-align:center; margin-top:50px;'><h1>Reseni:</h1><h2 style='color:blue;'>{solution}</h2><br><a href='/' style='padding: 10px; background: #2ecc71; color: white; text-decoration:none; border-radius: 5px;'>Skladat novou</a></body>" |
|||
except Exception as e: |
|||
return f"<h2 style='color:red; text-align:center; margin-top:50px;'>Nelze najit reseni: {e}</h2><div style='text-align:center;'><a href='/'>Zpet do menu</a></div>" |
|||
|
|||
def start_server(): |
|||
print("Spoustim webove rozhrani... Otevri prohlizec") |
|||
app.run(host='0.0.0.0', port=80) |
|||
|
|||
@ -0,0 +1,99 @@ |
|||
from flask import Flask, request, redirect, url_for |
|||
import kociemba |
|||
from scan_one_face_3 import scan_single_face |
|||
|
|||
app = Flask(__name__) |
|||
|
|||
app_state = { |
|||
"kociemba_string": "", |
|||
"current_face_idx": 0, |
|||
"last_scan": "", |
|||
"faces": [ |
|||
"(Bila strana) Polozte kostku zlutou nahoru modrou k sobe", |
|||
"(Cervena strana) Polozte kostku oranzovou nahoru bilou k sobe", |
|||
"(Zelena strana) Polozte kostku modrou nahoru bilou k sobe", |
|||
"(Zluta strana) Polozte kostku bilou nahoru zelenou k sobe", |
|||
"(Oranzova strana) Polozte kostku cervenou nahoru bilou k sobe", |
|||
"(Modra strana) Polozte kostku zelenou nahoru bilou k sobe" |
|||
], |
|||
"color_to_face": {'W': 'U', 'Y': 'D', 'R': 'R', 'O': 'L', 'G': 'F', 'B': 'B'} |
|||
} |
|||
|
|||
@app.route('/') |
|||
def index(): |
|||
if app_state["current_face_idx"] >= 6: |
|||
return f""" |
|||
<body style="font-family: Arial; text-align: center; margin-top: 50px;"> |
|||
<h2>Vse naskenovano!</h2> |
|||
<p>Vygenerovany string: <b>{app_state['kociemba_string']}</b></p> |
|||
<form action="/solve" method="post"><button style="padding: 20px; font-size: 20px; background-color: #3498db; color: white;">Vygenerovat reseni</button></form> |
|||
<br><a href="/reset">Zacit znovu</a> |
|||
</body> |
|||
""" |
|||
|
|||
face_name = app_state["faces"][app_state["current_face_idx"]] |
|||
|
|||
html = f""" |
|||
<body style="font-family: Arial; text-align: center; margin-top: 50px;"> |
|||
<h2>Krok {app_state["current_face_idx"] + 1} ze 6</h2> |
|||
<h1>{face_name}</h1> |
|||
<form action="/scan" method="post"><button style="padding: 20px 40px; background-color: #f1c40f; font-size: 20px;">Skenovat stranu</button></form> |
|||
""" |
|||
|
|||
if app_state["last_scan"]: |
|||
html += f""" |
|||
<hr> |
|||
<h3 style="color: green;">Naskenovano: {app_state["last_scan"]}</h3> |
|||
<p>Pokud je to spravne, potvrd. Pokud je chyba, prepis znaky:</p> |
|||
<form action="/confirm" method="post"> |
|||
<input type="text" name="colors" value="{app_state['last_scan']}" style="font-size: 24px; text-align: center;" required><br><br> |
|||
<button style="padding: 15px; background-color: #2ecc71; color: white;">Potvrdit</button> |
|||
</form> |
|||
""" |
|||
html += "</body>" |
|||
return html |
|||
|
|||
@app.route('/scan', methods=['POST']) |
|||
def scan(): |
|||
try: |
|||
scanned = scan_single_face() |
|||
if scanned and len(scanned) == 9: |
|||
app_state["last_scan"] = scanned |
|||
else: |
|||
app_state["last_scan"] = "CHYBA_FORMATU" |
|||
except Exception as e: |
|||
app_state["last_scan"] = f"CHYBA_KODU: {str(e)}" |
|||
|
|||
return redirect(url_for('index')) |
|||
|
|||
@app.route('/confirm', methods=['POST']) |
|||
def confirm(): |
|||
colors = request.form.get('colors').strip().upper() |
|||
if len(colors) == 9 and all(c in app_state['color_to_face'] for c in colors): |
|||
translated = "".join(app_state['color_to_face'][c] for c in colors) |
|||
app_state["kociemba_string"] += translated |
|||
app_state["current_face_idx"] += 1 |
|||
app_state["last_scan"] = "" |
|||
return redirect(url_for('index')) |
|||
|
|||
@app.route('/solve', methods=['POST']) |
|||
def solve(): |
|||
k_string = app_state["kociemba_string"] |
|||
if k_string == "UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB": |
|||
return "<h1 style='text-align:center;'>Kostka uz je slozena!</h1><a href='/reset'>Zpet</a>" |
|||
try: |
|||
solution = kociemba.solve(k_string) |
|||
return f"<body style='text-align:center;'><h1>Reseni:</h1><h2 style='color:blue;'>{solution}</h2><br><a href='/reset'>Skladat novou</a></body>" |
|||
except Exception as e: |
|||
return f"<h2 style='color:red;'>Nelze najit reseni: {e}</h2><a href='/reset'>Zpet</a>" |
|||
|
|||
@app.route('/reset') |
|||
def reset(): |
|||
app_state["kociemba_string"] = "" |
|||
app_state["current_face_idx"] = 0 |
|||
app_state["last_scan"] = "" |
|||
return redirect(url_for('index')) |
|||
|
|||
def start_server(): |
|||
print("Spoustim webove rozhrani... Otevri prohlizec na portu 5000") |
|||
app.run(host='0.0.0.0', port=5000) |
|||
Loading…
Reference in new issue