# pip install pillow import tkinter as tk from tkinter import filedialog, messagebox from PIL import Image, ImageTk import sqlite3 import datetime import os # Подключение к бд conn = sqlite3.connect('sqlite.db') cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS annotations ( id INTEGER PRIMARY KEY AUTOINCREMENT, filename TEXT NOT NULL, image_width INTEGER, image_height INTEGER, x1 INTEGER, y1 INTEGER, x2 INTEGER, y2 INTEGER, rectangle_width INTEGER, rectangle_height INTEGER, image_left INTEGER, image_bottom INTEGER, image_top INTEGER, image_right INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') conn.commit() root = tk.Tk() root.title("Image Annotation Tool") root.state('zoomed') root.configure(bg='#2b2b2b') bg_color = '#2b2b2b' frame_color = '#3c3c3c' text_color = '#ffffff' accent_color = '#4a90d9' panel = tk.Frame(root, width=200, height=600, bg=frame_color) panel.pack(side='right', fill='y') panel.pack_propagate(False) zagolovok = tk.Label(panel, text="Координаты \nизображения:", font=("Arial", 12, "bold"), bg=frame_color, fg=accent_color) zagolovok.pack(pady=20) levo_metka = tk.Label(panel, text="Left: 0", font=("Arial", 10), bg=frame_color, fg=text_color) levo_metka.pack(pady=8) niz_metka = tk.Label(panel, text="Bottom: 0", font=("Arial", 10), bg=frame_color, fg=text_color) niz_metka.pack(pady=8) verh_metka = tk.Label(panel, text="Top: 0", font=("Arial", 10), bg=frame_color, fg=text_color) verh_metka.pack(pady=8) pravo_metka = tk.Label(panel, text="Right: 0", font=("Arial", 10), bg=frame_color, fg=text_color) pravo_metka.pack(pady=8) razdelitel = tk.Frame(panel, height=2, bg=accent_color) razdelitel.pack(fill='x', padx=20, pady=20) ramka_pryam = tk.Frame(panel, bg=frame_color) ramka_pryam.pack(pady=10) metka_pryam = tk.Label(ramka_pryam, text="Координаты прямоугольника:", font=("Arial", 10, "bold"), bg=frame_color, fg=accent_color) metka_pryam.pack(pady=5) vvod_koord = {} metki_koord = ["X1:", "Y1:", "X2:", "Y2:"] znach_po_umolch = ["100", "100", "300", "300"] for i, metka in enumerate(metki_koord): ramochka = tk.Frame(ramka_pryam, bg=frame_color) ramochka.pack(pady=2) tk.Label(ramochka, text=metka, font=("Arial", 9), bg=frame_color, fg=text_color, width=4).pack(side='left') pole = tk.Entry(ramochka, width=8, font=("Arial", 9)) pole.insert(0, znach_po_umolch[i]) pole.pack(side='left') vvod_koord[metka.replace(":", "")] = pole peremesh_aktivno = False ugol_dlya_peremesh = None nach_koord_peremesh = None tekush_pryam = None tekush_izobr = None poz_izobr = None tekush_put_k_file = None pil_izobr_obj = None def obnovit_poly_vvoda(): if tekush_pryam and poz_izobr: koord = holst.coords(tekush_pryam) if koord and len(koord) >= 4: img_x, img_y = poz_izobr vvod_koord["X1"].delete(0, tk.END) vvod_koord["X1"].insert(0, str(int(koord[0] - img_x))) vvod_koord["Y1"].delete(0, tk.END) vvod_koord["Y1"].insert(0, str(int(koord[1] - img_y))) vvod_koord["X2"].delete(0, tk.END) vvod_koord["X2"].insert(0, str(int(koord[2] - img_x))) vvod_koord["Y2"].delete(0, tk.END) vvod_koord["Y2"].insert(0, str(int(koord[3] - img_y))) def narisovat_pryam(): global tekush_pryam try: x1 = int(vvod_koord["X1"].get()) y1 = int(vvod_koord["Y1"].get()) x2 = int(vvod_koord["X2"].get()) y2 = int(vvod_koord["Y2"].get()) holst.delete("pryamougolnik") holst.delete("ugol") if poz_izobr: img_x, img_y = poz_izobr abs_x1 = img_x + x1 abs_y1 = img_y + y1 abs_x2 = img_x + x2 abs_y2 = img_y + y2 if abs_x1 > abs_x2: abs_x1, abs_x2 = abs_x2, abs_x1 if abs_y1 > abs_y2: abs_y1, abs_y2 = abs_y2, abs_y1 tekush_pryam = holst.create_rectangle( abs_x1, abs_y1, abs_x2, abs_y2, outline="red", width=2, tags="pryamougolnik", dash=(5, 2) ) narisovat_uglovye_markery(abs_x1, abs_y1, abs_x2, abs_y2) sohranit_koord_pryam(abs_x1, abs_y1, abs_x2, abs_y2) except ValueError: pass def sohranit_koord_pryam(x1, y1, x2, y2): global sohran_koord_pryam sohran_koord_pryam = [x1, y1, x2, y2] def narisovat_uglovye_markery(x1, y1, x2, y2): razmer_markera = 8 # Левый верхний holst.create_oval( x1 - razmer_markera, y1 - razmer_markera, x1 + razmer_markera, y1 + razmer_markera, fill="red", outline="white", width=1, tags="ugol" ) # Правый верхний holst.create_oval( x2 - razmer_markera, y1 - razmer_markera, x2 + razmer_markera, y1 + razmer_markera, fill="red", outline="white", width=1, tags="ugol" ) # Левый нижний holst.create_oval( x1 - razmer_markera, y2 - razmer_markera, x1 + razmer_markera, y2 + razmer_markera, fill="red", outline="white", width=1, tags="ugol" ) # Правый нижний holst.create_oval( x2 - razmer_markera, y2 - razmer_markera, x2 + razmer_markera, y2 + razmer_markera, fill="red", outline="white", width=1, tags="ugol" ) def ochistit_pryam(): global tekush_pryam holst.delete("pryamougolnik") holst.delete("ugol") tekush_pryam = None def nachalo_peremesh(event): global peremesh_aktivno, ugol_dlya_peremesh, nach_koord_peremesh if not tekush_pryam: return x, y = event.x, event.y koord_pryam = holst.coords(tekush_pryam) if not koord_pryam: return tolshina_klika = 15 ugli = [ (koord_pryam[0], koord_pryam[1], "levyy_verhniy"), (koord_pryam[2], koord_pryam[1], "pravyy_verhniy"), (koord_pryam[0], koord_pryam[3], "levyy_nizhniy"), (koord_pryam[2], koord_pryam[3], "pravyy_nizhniy") ] for ugol_x, ugol_y, tip_ugla in ugli: if abs(x - ugol_x) <= tolshina_klika and abs(y - ugol_y) <= tolshina_klika: peremesh_aktivno = True ugol_dlya_peremesh = tip_ugla nach_koord_peremesh = (x, y, koord_pryam[0], koord_pryam[1], koord_pryam[2], koord_pryam[3]) holst.config(cursor="crosshair") return def peremeshchenie(event): global peremesh_aktivno, ugol_dlya_peremesh, nach_koord_peremesh if not peremesh_aktivno or not tekush_pryam: return x, y = event.x, event.y nach_x, nach_y, x1, y1, x2, y2 = nach_koord_peremesh dx = x - nach_x dy = y - nach_y x = max(0, min(x, holst.winfo_width())) y = max(0, min(y, holst.winfo_height())) if ugol_dlya_peremesh == "levyy_verhniy": nov_koord = [x, y, x2, y2] if nov_koord[2] - nov_koord[0] < 10: nov_koord[0] = nov_koord[2] - 10 if nov_koord[3] - nov_koord[1] < 10: nov_koord[1] = nov_koord[3] - 10 elif ugol_dlya_peremesh == "pravyy_verhniy": nov_koord = [x1, y, x, y2] if nov_koord[2] - nov_koord[0] < 10: nov_koord[2] = nov_koord[0] + 10 if nov_koord[3] - nov_koord[1] < 10: nov_koord[1] = nov_koord[3] - 10 elif ugol_dlya_peremesh == "levyy_nizhniy": nov_koord = [x, y1, x2, y] if nov_koord[2] - nov_koord[0] < 10: nov_koord[0] = nov_koord[2] - 10 if nov_koord[3] - nov_koord[1] < 10: nov_koord[3] = nov_koord[1] + 10 elif ugol_dlya_peremesh == "pravyy_nizhniy": nov_koord = [x1, y1, x, y] if nov_koord[2] - nov_koord[0] < 10: nov_koord[2] = nov_koord[0] + 10 if nov_koord[3] - nov_koord[1] < 10: nov_koord[3] = nov_koord[1] + 10 else: return holst.coords(tekush_pryam, *nov_koord) holst.delete("ugol") narisovat_uglovye_markery(*nov_koord) obnovit_poly_vvoda() sohranit_koord_pryam(*nov_koord) def konets_peremesh(event): global peremesh_aktivno, ugol_dlya_peremesh, nach_koord_peremesh if peremesh_aktivno: peremesh_aktivno = False ugol_dlya_peremesh = None nach_koord_peremesh = None holst.config(cursor="") def sohranit_v_bazu(): if not tekush_put_k_file or not tekush_pryam: messagebox.showwarning("Нет данных", "Нет изображения или прямоугольника для сохранения") return try: koord = holst.coords(tekush_pryam) if not koord or len(koord) < 4: messagebox.showwarning("Ошибка", "Нет координат прямоугольника") return if poz_izobr: img_x, img_y = poz_izobr x1 = int(koord[0] - img_x) y1 = int(koord[1] - img_y) x2 = int(koord[2] - img_x) y2 = int(koord[3] - img_y) rect_width = abs(x2 - x1) rect_height = abs(y2 - y1) img_width = pil_izobr_obj.width if pil_izobr_obj else 0 img_height = pil_izobr_obj.height if pil_izobr_obj else 0 cursor.execute(''' INSERT INTO annotations (filename, image_width, image_height, x1, y1, x2, y2, rectangle_width, rectangle_height, image_left, image_bottom, image_top, image_right, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', ( os.path.basename(tekush_put_k_file), img_width, img_height, x1, y1, x2, y2, rect_width, rect_height, int(levo_metka.cget("text").split(": ")[1]) if "Left: " in levo_metka.cget("text") else 0, int(niz_metka.cget("text").split(": ")[1]) if "Bottom: " in niz_metka.cget("text") else 0, int(verh_metka.cget("text").split(": ")[1]) if "Top: " in verh_metka.cget("text") else 0, int(pravo_metka.cget("text").split(": ")[1]) if "Right: " in pravo_metka.cget("text") else 0, datetime.datetime.now() )) conn.commit() messagebox.showinfo("Успех", "Данные успешно сохранены!") except Exception as e: messagebox.showerror("Ошибка", f"Не удалось сохранить данные: {str(e)}") def zagruzit_iz_istorii(id_zapisi): """Загрузить изображение и прямоугольник из записи истории""" try: cursor.execute("SELECT * FROM annotations WHERE id = ?", (id_zapisi,)) record = cursor.fetchone() if not record: messagebox.showwarning("Ошибка", "Запись не найдена") return id_num, filename, img_w, img_h, x1, y1, x2, y2, rect_w, rect_h, img_l, img_b, img_t, img_r, created_at = record # Ищем файл изображения file_found = False possible_paths = [ os.path.join(os.path.dirname(tekush_put_k_file) if tekush_put_k_file else "", filename), filename, os.path.join("images", filename), os.path.join(os.getcwd(), filename) ] for path in possible_paths: if os.path.exists(path): # Загружаем изображение zagruzit(path) # Ждем пока изображение загрузится root.update() # Устанавливаем координаты изображения levo_metka.config(text=f"Left: {img_l}") niz_metka.config(text=f"Bottom: {img_b}") verh_metka.config(text=f"Top: {img_t}") pravo_metka.config(text=f"Right: {img_r}") # Устанавливаем координаты прямоугольника vvod_koord["X1"].delete(0, tk.END) vvod_koord["X1"].insert(0, str(x1)) vvod_koord["Y1"].delete(0, tk.END) vvod_koord["Y1"].insert(0, str(y1)) vvod_koord["X2"].delete(0, tk.END) vvod_koord["X2"].insert(0, str(x2)) vvod_koord["Y2"].delete(0, tk.END) vvod_koord["Y2"].insert(0, str(y2)) # Рисуем прямоугольник narisovat_pryam() file_found = True messagebox.showinfo("Успех", f"Загружена запись ID: {id_num}\nФайл: {filename}") break if not file_found: messagebox.showwarning("Файл не найден", f"Не удалось найти файл: {filename}\n\n" f"Пожалуйста, выберите файл вручную.") # Открываем диалог выбора файла put_k_file = filedialog.askopenfilename( title=f"Найдите файл: {filename}", filetypes=[("Image files", "*.png *.jpg *.jpeg *.gif *.bmp")] ) if put_k_file: zagruzit(put_k_file) # Повторно применяем параметры zagruzit_iz_istorii(id_zapisi) except Exception as e: messagebox.showerror("Ошибка", f"Не удалось загрузить данные: {str(e)}") def pokazat_istoriju(): try: cursor.execute("SELECT * FROM annotations ORDER BY id DESC") records = cursor.fetchall() if not records: messagebox.showinfo("История", "База данных пуста") return history_window = tk.Toplevel(root) history_window.title("История") history_window.geometry("1000x550") history_window.configure(bg=bg_color) text_frame = tk.Frame(history_window, bg=bg_color) text_frame.pack(fill='both', expand=True, padx=10, pady=10) text_widget = tk.Text(text_frame, bg='#1e1e1e', fg=text_color, font=("Courier", 10), wrap='none') scrollbar_y = tk.Scrollbar(text_frame, orient='vertical', command=text_widget.yview) scrollbar_x = tk.Scrollbar(text_frame, orient='horizontal', command=text_widget.xview) text_widget.configure(yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set) text_widget.grid(row=0, column=0, sticky='nsew') scrollbar_y.grid(row=0, column=1, sticky='ns') scrollbar_x.grid(row=1, column=0, sticky='ew') text_frame.grid_rowconfigure(0, weight=1) text_frame.grid_columnconfigure(0, weight=1) col_widths = {'id': 4, 'filename': 25, 'img_size': 20, 'rect_coords': 25, 'rect_size': 20, 'date': 20} header_format = "{:>{id_width}} | {:<{filename_width}} | {:^{img_size_width}} | {:<{rect_coords_width}} | {:^{rect_size_width}} | {:<{date_width}} | {:^15}" headers = header_format.format( "ID", "Файл", "Размер изображения", "Прямоугольник", "Размер прямоуг.", "Дата создания", "Действия", id_width=col_widths['id'], filename_width=col_widths['filename'], img_size_width=col_widths['img_size'], rect_coords_width=col_widths['rect_coords'], rect_size_width=col_widths['rect_size'], date_width=col_widths['date'] ) text_widget.insert('end', headers + "\n") text_widget.insert('end', "-" * len(headers) + "\n") record_format = "{:>{id_width}} | {:<{filename_width}.{filename_width}} | {:^{img_size_width}} | {:<{rect_coords_width}} | {:^{rect_size_width}} | {:<{date_width}}" for record in records: id_num, filename, img_w, img_h, x1, y1, x2, y2, rect_w, rect_h, img_l, img_b, img_t, img_r, created_at = record img_size = f"{img_w}x{img_h}" rect_coords = f"({x1},{y1})-({x2},{y2})" rect_size = f"{rect_w}x{rect_h}" date_str = created_at[:19] if isinstance(created_at, str) else str(created_at)[:19] line = record_format.format( id_num, filename, img_size, rect_coords, rect_size, date_str, id_width=col_widths['id'], filename_width=col_widths['filename'], img_size_width=col_widths['img_size'], rect_coords_width=col_widths['rect_coords'], rect_size_width=col_widths['rect_size'], date_width=col_widths['date'] ) line_with_id = line + f" | ID:{id_num:>5}" text_widget.insert('end', line_with_id + "\n") text_widget.config(state='disabled') button_frame = tk.Frame(history_window, bg=bg_color) button_frame.pack(pady=10) id_frame = tk.Frame(button_frame, bg=bg_color) id_frame.pack(pady=5) tk.Label(id_frame, text="ID записи:", bg=bg_color, fg=text_color).pack(side='left', padx=5) entry_id = tk.Entry(id_frame, width=10, font=("Arial", 10)) entry_id.pack(side='left', padx=5) def zagruzit_po_id(): try: id_zapisi = int(entry_id.get()) zagruzit_iz_istorii(id_zapisi) history_window.destroy() # Закрываем окно истории except ValueError: messagebox.showwarning("Ошибка", "Введите корректный ID записи") btn_open = tk.Button(button_frame, text="Загрузить выбранную запись", command=zagruzit_po_id, bg=accent_color, fg=text_color, font=("Arial", 10), relief='flat', padx=10, pady=5) btn_open.pack(pady=5) btn_close = tk.Button(button_frame, text="Закрыть", command=history_window.destroy, bg='#d94a4a', fg=text_color, font=("Arial", 10), relief='flat', padx=10, pady=5) btn_close.pack(pady=5) except Exception as e: messagebox.showerror("Ошибка", f"Не удалось загрузить историю: {str(e)}") ramka_knopok = tk.Frame(ramka_pryam, bg=frame_color) ramka_knopok.pack(pady=10) knopka_ris = tk.Button(ramka_knopok, text="Нарисовать", command=narisovat_pryam, bg=accent_color, fg=text_color, font=("Arial", 9), relief='flat', padx=5, pady=3) knopka_ris.pack(side='left', padx=5) knopka_ochist = tk.Button(ramka_knopok, text="Очистить", command=ochistit_pryam, bg='#d94a4a', fg=text_color, font=("Arial", 9), relief='flat', padx=5, pady=3) knopka_ochist.pack(side='left', padx=5) razdelitel2 = tk.Frame(panel, height=2, bg=accent_color) razdelitel2.pack(fill='x', padx=20, pady=20) def vibor(): global tekush_put_k_file, pil_izobr_obj put_k_file = filedialog.askopenfilename( title="Выберите изображение", filetypes=[("Image files", "*.png *.jpg *.jpeg *.gif *.bmp")] ) if put_k_file: tekush_put_k_file = put_k_file zagruzit(put_k_file) knopka_vibor = tk.Button(panel, text="Выбрать изображение", command=vibor, bg=accent_color, fg=text_color, font=("Arial", 10), relief='flat', padx=10, pady=5) knopka_vibor.pack(pady=10) knopka_sohranit = tk.Button(panel, text="Сохранить в базу данных", command=sohranit_v_bazu, bg='#2ecc71', fg=text_color, font=("Arial", 10, "bold"), relief='flat', padx=10, pady=5) knopka_sohranit.pack(pady=10) knopka_istorija = tk.Button(panel, text="Показать историю", command=pokazat_istoriju, bg='#9b59b6', fg=text_color, font=("Arial", 10), relief='flat', padx=10, pady=5) knopka_istorija.pack(pady=10) holst = tk.Canvas(root, bg='#1e1e1e', width=600, height=600, highlightthickness=0) holst.pack(side='left', fill='both', expand=True) holst.bind("", nachalo_peremesh) holst.bind("", peremeshchenie) holst.bind("", konets_peremesh) def zagruzit(put_k_file): global tekush_izobr, poz_izobr, tekush_pryam, pil_izobr_obj try: pil_izobr_obj = Image.open(put_k_file) holst.delete("all") tekush_pryam = None shir_holst = holst.winfo_width() vys_holst = holst.winfo_height() img_width = pil_izobr_obj.width img_height = pil_izobr_obj.height scale_w = shir_holst / img_width scale_h = vys_holst / img_height scale = min(scale_w, scale_h, 1.0) new_width = int(img_width * scale) new_height = int(img_height * scale) pil_izobr_obj = pil_izobr_obj.resize((new_width, new_height), Image.Resampling.LANCZOS) tekush_izobr = ImageTk.PhotoImage(pil_izobr_obj) x_centr = (shir_holst - new_width) // 2 y_centr = (vys_holst - new_height) // 2 id_izobr = holst.create_image(x_centr, y_centr, anchor='nw', image=tekush_izobr) koord_izobr = holst.bbox(id_izobr) poz_izobr = (koord_izobr[0], koord_izobr[1]) levo_metka.config(text=f"Left: {koord_izobr[0]}") niz_metka.config(text=f"Bottom: {koord_izobr[1]}") verh_metka.config(text=f"Top: {koord_izobr[2]}") pravo_metka.config(text=f"Right: {koord_izobr[3]}") for i, metka in enumerate(metki_koord): vvod_koord[metka.replace(":", "")].delete(0, tk.END) vvod_koord[metka.replace(":", "")].insert(0, znach_po_umolch[i]) except Exception as e: holst.delete("all") holst.create_text( 300, 300, text="Ошибка загрузки изображения", fill=text_color, font=("Arial", 12), anchor='center' ) def obnovit(event=None): if tekush_izobr and pil_izobr_obj: holst.delete("all") tekush_pryam = None shir_holst = holst.winfo_width() vys_holst = holst.winfo_height() if shir_holst <= 1 or vys_holst <= 1: return img_width = pil_izobr_obj.width img_height = pil_izobr_obj.height scale_w = shir_holst / img_width scale_h = vys_holst / img_height scale = min(scale_w, scale_h, 1.0) new_width = int(img_width * scale) new_height = int(img_height * scale) resized_img = pil_izobr_obj.resize((new_width, new_height), Image.Resampling.LANCZOS) tekush_izobr = ImageTk.PhotoImage(resized_img) x_centr = (shir_holst - new_width) // 2 y_centr = (vys_holst - new_height) // 2 id_izobr = holst.create_image(x_centr, y_centr, anchor='nw', image=tekush_izobr) koord_izobr = holst.bbox(id_izobr) poz_izobr = (koord_izobr[0], koord_izobr[1]) levo_metka.config(text=f"Left: {koord_izobr[0]}") niz_metka.config(text=f"Bottom: {koord_izobr[1]}") verh_metka.config(text=f"Top: {koord_izobr[2]}") pravo_metka.config(text=f"Right: {koord_izobr[3]}") holst.bind("", obnovit) holst.create_text( 300, 300, text="Нажмите 'Выбрать изображение'\nчтобы загрузить картинку", fill=text_color, font=("Arial", 12), anchor='center', justify='center' ) def zakryt_bazu(): conn.close() root.destroy() root.protocol("WM_DELETE_WINDOW", zakryt_bazu) root.mainloop()