diff --git a/a4_prepare.py b/a4_prepare.py new file mode 100644 index 0000000..be2c61b --- /dev/null +++ b/a4_prepare.py @@ -0,0 +1,100 @@ +import cv2 +import numpy as np +import random +import json + +# Загрузка изображения +image = cv2.imread("a4keyboard_scan.jpg") +gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + +# Применяем адаптивный порог +thresh = cv2.adaptiveThreshold( + gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2 +) + +# Находим контуры и собираем размеры прямоугольников +rectangles = [ + cv2.boundingRect(contour) + for contour in cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[ + 0 + ] + if (cv2.boundingRect(contour)[2] > 20 and cv2.boundingRect(contour)[3] > 20) +] + +# Вычисляем медианные размеры +widths, heights = zip(*[(w, h) for _, _, w, h in rectangles]) +median_width, median_height = np.median(widths), np.median(heights) + +# Фильтрация прямоугольников по размеру +filtered_rectangles = [ + rect + for rect in rectangles + if 0.5 * median_width <= rect[2] <= 10 * median_width + and 0.5 * median_height <= rect[3] <= 10 * median_height +] + +# Порог для тёмного фона +darkness_threshold = 80 +filtered_rectangles_dark = [ + rect + for rect in filtered_rectangles + if cv2.mean(gray[rect[1] : rect[1] + rect[3], rect[0] : rect[0] + rect[2]])[0] + < darkness_threshold +] + +# Уменьшаем размеры и закрашиваем области +padding = 8 +for x, y, w, h in filtered_rectangles_dark: + x_padded, y_padded = x + padding, y + padding + w_padded, h_padded = max(w - 2 * padding, 1), max(h - 2 * padding, 1) + roi = image[y_padded : y_padded + h_padded, x_padded : x_padded + w_padded] + mean_color = cv2.mean(roi)[:3] + + # Генерация области с шумом + noise = np.random.uniform(-10, 10, roi.shape).astype(np.int16) + noisy_color = np.clip(np.array(mean_color, dtype=np.int16) + noise, 0, 255).astype( + np.uint8 + ) + image[y_padded : y_padded + h_padded, x_padded : x_padded + w_padded] = noisy_color + +# Масштабирование изображения и прямоугольников +scale_factor = 0.65 +# image = cv2.resize(image, (image.shape[1] // 2, image.shape[0] // 2)) +image = cv2.resize( + image, None, fx=scale_factor, fy=scale_factor, interpolation=cv2.INTER_LINEAR +) +scaled_rectangles = [ + ( + int(x * scale_factor), + int(y * scale_factor), + int(w * scale_factor), + int(h * scale_factor), + ) + for x, y, w, h in filtered_rectangles_dark +] + +# Сортируем прямоугольники построчно +row_tolerance = 10 +scaled_rectangles.sort(key=lambda rect: rect[1]) + +rows = [] +current_row = [scaled_rectangles[0]] + +for rect in scaled_rectangles[1:]: + if abs(rect[1] - current_row[-1][1]) <= row_tolerance: + current_row.append(rect) + else: + rows.append(current_row) + current_row = [rect] + +rows.append(current_row) + +sorted_rectangles = [ + rect for row in rows for rect in sorted(row, key=lambda rect: rect[0]) +] + +# Сохраняем изображение и данные прямоугольников +cv2.imwrite("background_image.png", image) + +with open("rectangles.json", "w") as f: + json.dump(sorted_rectangles, f) diff --git a/a4keyboard_scan.jpg b/a4keyboard_scan.jpg new file mode 100644 index 0000000..d743a64 Binary files /dev/null and b/a4keyboard_scan.jpg differ diff --git a/background_image.png b/background_image.png new file mode 100644 index 0000000..fe2c72f Binary files /dev/null and b/background_image.png differ diff --git a/rectangles.json b/rectangles.json new file mode 100644 index 0000000..5ae1f08 --- /dev/null +++ b/rectangles.json @@ -0,0 +1 @@ +[[21, 24, 66, 50], [168, 24, 65, 50], [240, 24, 66, 50], [313, 24, 66, 50], [386, 24, 66, 50], [495, 24, 66, 50], [570, 24, 65, 50], [642, 25, 65, 50], [715, 26, 65, 49], [826, 26, 65, 49], [898, 26, 65, 50], [971, 26, 65, 50], [1045, 26, 65, 50], [1136, 26, 65, 50], [1209, 25, 65, 50], [1282, 25, 66, 51], [22, 96, 65, 65], [95, 96, 65, 65], [168, 96, 65, 65], [241, 96, 65, 65], [313, 96, 65, 65], [386, 96, 66, 65], [459, 96, 66, 65], [532, 96, 66, 65], [606, 96, 65, 65], [679, 97, 65, 64], [752, 97, 65, 65], [825, 97, 65, 65], [898, 98, 65, 64], [971, 98, 138, 65], [1136, 97, 65, 65], [1209, 97, 66, 65], [1283, 97, 65, 65], [1376, 97, 65, 65], [1449, 97, 65, 65], [1522, 97, 65, 65], [1595, 96, 65, 65], [20, 169, 102, 65], [131, 169, 65, 65], [204, 169, 65, 64], [277, 169, 65, 64], [350, 169, 65, 65], [423, 169, 65, 65], [495, 169, 66, 65], [569, 169, 65, 65], [642, 170, 66, 64], [715, 170, 65, 65], [787, 170, 66, 65], [861, 170, 66, 64], [934, 170, 66, 64], [1008, 171, 102, 64], [1136, 170, 65, 65], [1209, 170, 65, 65], [1283, 170, 65, 65], [1376, 170, 65, 65], [1449, 170, 65, 65], [1522, 170, 65, 65], [1595, 170, 66, 137], [21, 241, 120, 65], [149, 241, 65, 65], [222, 242, 65, 65], [295, 242, 65, 65], [368, 242, 65, 65], [440, 241, 66, 66], [514, 242, 66, 65], [587, 242, 65, 65], [660, 243, 66, 65], [733, 243, 65, 65], [806, 243, 65, 64], [880, 243, 65, 64], [952, 243, 157, 65], [1376, 243, 65, 65], [1449, 243, 65, 65], [1522, 243, 66, 65], [20, 314, 157, 65], [185, 314, 66, 66], [258, 315, 65, 65], [331, 315, 65, 65], [403, 315, 66, 65], [477, 315, 66, 65], [549, 315, 66, 65], [622, 315, 66, 65], [696, 316, 66, 64], [769, 316, 65, 64], [843, 316, 65, 64], [915, 316, 194, 65], [1209, 316, 65, 65], [1376, 316, 65, 65], [1449, 316, 65, 65], [1522, 315, 65, 65], [1595, 315, 65, 137], [20, 387, 102, 65], [130, 388, 65, 65], [202, 388, 102, 65], [312, 388, 432, 66], [750, 389, 85, 65], [842, 390, 83, 64], [934, 390, 83, 64], [1025, 390, 84, 64], [1135, 389, 66, 65], [1209, 388, 66, 65], [1282, 389, 65, 65], [1376, 389, 138, 65], [1522, 389, 65, 65]] \ No newline at end of file diff --git a/speckb.py b/speckb.py new file mode 100644 index 0000000..6a4b415 --- /dev/null +++ b/speckb.py @@ -0,0 +1,341 @@ +from tkinter import Tk, Canvas, font +from PIL import Image, ImageTk +import json +import subprocess +import pyperclip + +# Загрузка данных +with open("rectangles.json", "r") as f: + rectangles = json.load(f) + +# Загрузка изображения +pil_image = Image.open("background_image.png") + +# Создаём интерфейс Tkinter +root = Tk() +root.title("SFG Special Symbols Keyboard") + +# Запретить изменение размера окна +root.resizable(False, False) + +# Создаём Canvas и устанавливаем фон +canvas = Canvas(root, width=pil_image.width, height=pil_image.height) +canvas.pack() +tk_image = ImageTk.PhotoImage(pil_image) +canvas.create_image(0, 0, anchor="nw", image=tk_image) + +key_map = { + 0: "Esc", + 1: "F1", + 2: "F2", + 3: "F3", + 4: "F4", + 5: "F5", + 6: "F6", + 7: "F7", + 8: "F8", + 9: "F9", + 10: "F10", + 11: "F11", + 12: "F12", + 13: "PrtSc", + 14: "ScrL", + 15: "Pause", + 16: "`", + 17: "1", + 18: "2", + 19: "3", + 20: "4", + 21: "5", + 22: "6", + 23: "7", + 24: "8", + 25: "9", + 26: "0", + 27: "-", + 28: "=", + 29: "Backspace", + 30: "Ins", + 31: "Home", + 32: "PgUp", + 33: "NumL", + 34: "/.", + 35: "*.", + 36: "-.", + 37: "Tab", + 38: "q", + 39: "w", + 40: "e", + 41: "r", + 42: "t", + 43: "y", + 44: "u", + 45: "i", + 46: "o", + 47: "p", + 48: "[", + 49: "]", + 50: "\\", + 51: "Del", + 52: "End", + 53: "PgDn", + 54: "7.", + 55: "8.", + 56: "9.", + 57: "+.", + 58: "Caps", + 59: "a", + 60: "s", + 61: "d", + 62: "f", + 63: "g", + 64: "h", + 65: "j", + 66: "k", + 67: "l", + 68: ";", + 69: "'", + 70: "Enter", + 71: "4.", + 72: "5.", + 73: "6.", + 74: "ShiftL", + 75: "z", + 76: "x", + 77: "c", + 78: "v", + 79: "b", + 80: "n", + 81: "m", + 82: ",", + 83: ".", + 84: "/", + 85: "ShiftR", + 86: "↑", + 87: "1.", + 88: "2.", + 89: "3.", + 90: "Ret.", + 91: "CtrlL", + 92: "Win", + 93: "Alt", + 94: "Space", + 95: "AltGr", + 96: "Fn", + 97: "Menu", + 98: "CtrlR", + 99: "←", + 100: "↓", + 101: "→", + 102: "Ins.", + 103: "Del.", +} + +keycode_map = { + 9: "Esc", + 67: "F1", + 68: "F2", + 69: "F3", + 70: "F4", + 71: "F5", + 72: "F6", + 73: "F7", + 74: "F8", + 75: "F9", + 76: "F10", + 95: "F11", + 96: "F12", + 107: "PrtSc", + 78: "ScrL", + 127: "Pause", + 49: "`", + 10: "1", + 11: "2", + 12: "3", + 13: "4", + 14: "5", + 15: "6", + 16: "7", + 17: "8", + 18: "9", + 19: "0", + 20: "-", + 21: "=", + 22: "Backspace", + 118: "Ins", + 110: "Home", + 112: "PgUp", + 77: "NumL", + 106: "/.", + 63: "*.", + 82: "-.", + 23: "Tab", + 24: "q", + 25: "w", + 26: "e", + 27: "r", + 28: "t", + 29: "y", + 30: "u", + 31: "i", + 32: "o", + 33: "p", + 34: "[", + 35: "]", + 51: "\\", + 119: "Del", + 115: "End", + 117: "PgDn", + 79: "7.", + 80: "8.", + 81: "9.", + 86: "+.", + 66: "Caps", + 38: "a", + 39: "s", + 40: "d", + 41: "f", + 42: "g", + 43: "h", + 44: "j", + 45: "k", + 46: "l", + 47: ";", + 48: "'", + 36: "Enter", + 83: "4.", + 84: "5.", + 85: "6.", + 50: "ShiftL", + 52: "z", + 53: "x", + 54: "c", + 55: "v", + 56: "b", + 57: "n", + 58: "m", + 59: ",", + 60: ".", + 61: "/", + 62: "ShiftR", + 111: "↑", + 87: "1.", + 88: "2.", + 89: "3.", + 104: "Ret.", + 37: "CtrlL", + 133: "Win", + 64: "Alt", + 65: "Space", + 108: "AltGr", + 105: "CtrlR", + 113: "←", + 116: "↓", + 114: "→", + 118: "Ins", + 119: "Del", + 90: "Ins.", + 91: "Del.", + 135: "Menu", +} + +# Словарь специальных символов для физики и математики +special_symbols = { + "q": "∞", # Бесконечность + "w": "∑", # Сумма + "e": "∫", # Интеграл + "r": "√", # Корень + "t": "∂", # Частная производная + "y": "≈", # Приблизительно равно + "u": "≥", # Больше или равно + "i": "≤", # Меньше или равно + "o": "⊕", # Прямое суммирование + "p": "±", # Плюс-минус + "[": "→", # Стрелка вправо + "]": "←", # Стрелка влево + "a": "⊗", # Тензорное произведение + "s": "∝", # Пропорционально + "d": "≠", # Не равно + "f": "∃", # Существует + "g": "∀", # Для всех + "h": "∈", # Принадлежит + "j": "∉", # Не принадлежит + "k": "⊂", # Подмножество + "l": "⊃", # Надмножество + ";": "∩", # Пересечение + "'": "∪", # Объединение + "z": "∧", # Логическое И + "x": "∨", # Логическое ИЛИ + "c": "¬", # Логическое НЕ + "v": "∴", # Следовательно + "b": "∠", # Угол + "n": "°", # Градусы + "m": "∥", # Параллельность + ",": "≅", # Конгруэнтность + ".": "∂", # Частная производная (дублируем для удобства) + "/": "÷", # Деление +} + + +# Словарь для отслеживания подсветки +highlighted_rects = {} + +# Создаём текстовые подписи для клавиш +padding = 8 +sym_font = font.Font(family="Arial", size=12, weight="bold", slant="roman") +spec_font = font.Font(family="Arial", size=18, weight="bold", slant="roman") + +for i, (x, y, w, h) in enumerate(rectangles): + key_label = key_map.get(i, f"Key {i}") + special_symbol = special_symbols.get(key_label, "") + + rect_id = canvas.create_rectangle(x, y, x + w, y + h, outline="black", width=0) + + spec_id = canvas.create_text( + x + padding + (w - 2 * padding) // 2, + y + padding + (h - 2 * padding) // 2, + text=special_symbol, + font=spec_font, + fill="white", + ) + + text_id = canvas.create_text( + x + padding, # Смещение на 5 пикселей от левого края + y + padding, # Смещение на 5 пикселей от верхнего края + text=key_label, + font=sym_font, + fill="#777777", + anchor="nw", # Выравнивание текста по верхнему левому углу + ) + highlighted_rects[key_label] = rect_id + + +# Обработчик нажатия клавиши +def on_key_press(event): + key = keycode_map.get(event.keycode) + if key in highlighted_rects: + # Подсветка клавиши + canvas.itemconfig(highlighted_rects[key], outline="red", width=4) + + +# Обработчик отпускания клавиши +def on_key_release(event): + print(event.keycode) + key = keycode_map.get(event.keycode) + special_symbol = special_symbols.get(key) + if key in highlighted_rects: + # Убираем подсветку клавиши + canvas.itemconfig(highlighted_rects[key], outline="white", width=2) + + if special_symbol: + pyperclip.copy(special_symbol) # Копируем символ в буфер обмена + root.after(100, root.destroy) # Закрываем программу через 100 мс + subprocess.Popen(["bash", "-c", "sleep 0.12 ; xdotool key Ctrl+v"]) + + +# Привязываем обработчики к окну +root.bind("", on_key_press) +root.bind("", on_key_release) + +# Запускаем интерфейс +root.mainloop()