Skip to content

Commit 7fc1cc9

Browse files
authored
Create IrisClassifier.py
1 parent a91e07e commit 7fc1cc9

1 file changed

Lines changed: 269 additions & 0 deletions

File tree

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
"""
2+
IrisClassifier v1.1 - Interactive GUI
3+
Classify Iris species using a trained scikit-learn model
4+
Drag & drop CSV files, browse CSVs, or enter measurements manually
5+
"""
6+
7+
import os, sys, threading
8+
import pandas as pd
9+
import tkinter as tk
10+
from tkinter import filedialog, messagebox, ttk
11+
12+
import ttkbootstrap as tb
13+
from ttkbootstrap.constants import *
14+
15+
try:
16+
from tkinterdnd2 import TkinterDnD, DND_FILES
17+
DND_ENABLED = True
18+
except ImportError:
19+
DND_ENABLED = False
20+
print("Drag & Drop requires tkinterdnd2: pip install tkinterdnd2")
21+
22+
from sklearn.datasets import load_iris
23+
from sklearn.ensemble import RandomForestClassifier
24+
from sklearn.model_selection import train_test_split
25+
from sklearn.preprocessing import StandardScaler
26+
27+
# ---------------------- UTIL ----------------------
28+
def resource_path(file_name):
29+
base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
30+
return os.path.join(base_path, file_name)
31+
32+
# ---------------------- MODEL ----------------------
33+
class IrisModel:
34+
def __init__(self):
35+
data = load_iris()
36+
self.X = data.data
37+
self.y = data.target
38+
self.target_names = data.target_names
39+
self.scaler = StandardScaler()
40+
X_scaled = self.scaler.fit_transform(self.X)
41+
X_train, X_test, y_train, y_test = train_test_split(X_scaled, self.y, test_size=0.2, random_state=42)
42+
self.clf = RandomForestClassifier(n_estimators=100, random_state=42)
43+
self.clf.fit(X_train, y_train)
44+
45+
def predict(self, X):
46+
X_scaled = self.scaler.transform(X)
47+
preds = self.clf.predict(X_scaled)
48+
return [self.target_names[p] for p in preds]
49+
50+
# ---------------------- WORKER ----------------------
51+
class ClassifierWorker:
52+
def __init__(self, files, callbacks):
53+
self.files = files
54+
self.callbacks = callbacks
55+
self._running = True
56+
self.model = IrisModel()
57+
58+
def stop(self):
59+
self._running = False
60+
61+
def run(self):
62+
total = len(self.files)
63+
for i, file in enumerate(self.files):
64+
if not self._running:
65+
break
66+
try:
67+
df = pd.read_csv(file)
68+
# Expect columns: sepal_length,sepal_width,petal_length,petal_width
69+
if set(df.columns) >= {"sepal_length","sepal_width","petal_length","petal_width"}:
70+
X = df[["sepal_length","sepal_width","petal_length","petal_width"]].values
71+
preds = self.model.predict(X)
72+
if "found" in self.callbacks:
73+
self.callbacks["found"](file, preds)
74+
except Exception as e:
75+
if "found" in self.callbacks:
76+
self.callbacks["found"](file, [f"Error: {str(e)}"])
77+
if "progress" in self.callbacks:
78+
self.callbacks["progress"](int((i+1)/total*100))
79+
if "finished" in self.callbacks:
80+
self.callbacks["finished"]()
81+
82+
# ---------------------- MAIN APP ----------------------
83+
class IrisClassifierApp:
84+
APP_NAME = "IrisClassifier"
85+
APP_VERSION = "1.1"
86+
87+
def __init__(self):
88+
if DND_ENABLED:
89+
self.root = TkinterDnD.Tk()
90+
else:
91+
self.root = tb.Window(themename="darkly")
92+
self.root.title(f"{self.APP_NAME} v{self.APP_VERSION}")
93+
self.root.minsize(1000, 700)
94+
self.worker_obj = None
95+
self.file_set = set()
96+
self.smooth_value = 0
97+
self.target_progress = 0
98+
self.model = IrisModel()
99+
100+
self._build_ui()
101+
self._apply_styles()
102+
self.root.after(15, self.animate_progress)
103+
104+
# ---------------------- UI ----------------------
105+
def _build_ui(self):
106+
main = tb.Frame(self.root, padding=10)
107+
main.pack(fill=BOTH, expand=True)
108+
109+
tb.Label(main, text=f"🌸 {self.APP_NAME} - Iris Flower Classifier",
110+
font=("Segoe UI", 20, "bold")).pack(pady=(0,10))
111+
tb.Label(main, text="Classify Iris species from CSV files or manual input",
112+
font=("Segoe UI", 10, "italic"), foreground="#9ca3af").pack(pady=(0,15))
113+
114+
# Row 1: File selection
115+
row1 = tb.Frame(main)
116+
row1.pack(fill=X, pady=(0,6))
117+
118+
self.path_input = tb.Entry(row1, width=80)
119+
self.path_input.pack(side=LEFT, fill=X, expand=True, padx=(0,6))
120+
self.path_input.insert(0, "Drag & drop CSV files here…")
121+
122+
browse_btn = tb.Button(row1, text="📂 Browse", bootstyle=INFO, command=self.browse)
123+
browse_btn.pack(side=LEFT, padx=3)
124+
125+
self.start_btn = tb.Button(row1, text="🚀 Classify CSV", bootstyle=SUCCESS, command=self.start)
126+
self.start_btn.pack(side=LEFT, padx=3)
127+
128+
self.cancel_btn = tb.Button(row1, text="⏹ Cancel", bootstyle=DANGER, command=self.cancel)
129+
self.cancel_btn.pack(side=LEFT, padx=3)
130+
self.cancel_btn.config(state=DISABLED)
131+
132+
export_btn = tb.Button(row1, text="💾 Export Results", bootstyle=PRIMARY, command=self.export_results)
133+
export_btn.pack(side=LEFT, padx=3)
134+
135+
# Progress bar
136+
self.progress = tb.Progressbar(main, bootstyle="success-striped", maximum=100)
137+
self.progress.pack(fill=X, pady=(0,6))
138+
139+
# Treeview for CSV results
140+
columns = ("selected","filename","prediction")
141+
self.tree = ttk.Treeview(main, columns=columns, show="headings", selectmode="extended", height=15)
142+
self.tree.heading("selected", text="✅")
143+
self.tree.heading("filename", text="Filename", anchor=W)
144+
self.tree.heading("prediction", text="Prediction", anchor=W)
145+
self.tree.column("selected", width=50, anchor=CENTER)
146+
self.tree.column("filename", width=600)
147+
self.tree.column("prediction", width=200)
148+
self.tree.pack(fill=BOTH, expand=True, pady=(0,6))
149+
150+
# Manual input frame
151+
manual_frame = tb.Labelframe(main, text="Manual Input", padding=10)
152+
manual_frame.pack(fill=X, pady=(10,6))
153+
154+
self.manual_entries = {}
155+
labels = ["Sepal Length","Sepal Width","Petal Length","Petal Width"]
156+
default_vals = [5.1, 3.5, 1.4, 0.2]
157+
for i, (label, val) in enumerate(zip(labels, default_vals)):
158+
tb.Label(manual_frame, text=label).grid(row=0, column=i*2, sticky=W, padx=(0,2))
159+
entry = tb.Entry(manual_frame, width=8)
160+
entry.grid(row=0, column=i*2+1, sticky=W, padx=(0,6))
161+
entry.insert(0, str(val))
162+
self.manual_entries[label] = entry
163+
164+
predict_btn = tb.Button(manual_frame, text="🔮 Predict", bootstyle=INFO, command=self.manual_predict)
165+
predict_btn.grid(row=0, column=8, padx=10)
166+
167+
self.manual_result = tb.Label(manual_frame, text="Prediction: ---", font=("Segoe UI", 12, "bold"))
168+
self.manual_result.grid(row=1, column=0, columnspan=9, pady=(6,0), sticky=W)
169+
170+
# Drag & Drop
171+
if DND_ENABLED:
172+
self.tree.drop_target_register(DND_FILES)
173+
self.tree.dnd_bind("<<Drop>>", self.on_drop)
174+
175+
# ---------------------- Browse / DnD ----------------------
176+
def browse(self):
177+
files = filedialog.askopenfilenames(filetypes=[("CSV Files","*.csv")])
178+
if files:
179+
threading.Thread(target=self._queue_files_thread, args=(files,), daemon=True).start()
180+
181+
def on_drop(self, event):
182+
dropped_paths = self.root.tk.splitlist(event.data)
183+
threading.Thread(target=self._queue_files_thread, args=(dropped_paths,), daemon=True).start()
184+
185+
def _queue_files_thread(self, paths):
186+
for path in paths:
187+
if path not in self.file_set and os.path.isfile(path) and path.endswith(".csv"):
188+
self.file_set.add(path)
189+
self.tree.insert("", END, values=("☑️", path, "Queued"))
190+
191+
# ---------------------- Actions ----------------------
192+
def start(self):
193+
selected_files = [self.tree.item(i)['values'][1] for i in self.tree.get_children()
194+
if self.tree.item(i)['values'][0]=="☑️"]
195+
if not selected_files:
196+
messagebox.showwarning("No files selected", "Select CSV files using the checkboxes first")
197+
return
198+
self.progress["value"] = 0
199+
self.smooth_value = 0
200+
self.target_progress = 0
201+
self.start_btn.config(state=DISABLED)
202+
self.cancel_btn.config(state=NORMAL)
203+
self.worker_obj = ClassifierWorker(selected_files, callbacks={
204+
"found": self.add_prediction,
205+
"progress": self.set_target,
206+
"finished": self.finish
207+
})
208+
threading.Thread(target=self.worker_obj.run, daemon=True).start()
209+
210+
def add_prediction(self, file, preds):
211+
for i in self.tree.get_children():
212+
if self.tree.item(i)['values'][1]==file:
213+
self.tree.item(i, values=("☑️", file, ", ".join(map(str,preds))))
214+
break
215+
216+
def manual_predict(self):
217+
try:
218+
values = [float(self.manual_entries[label].get()) for label in self.manual_entries]
219+
pred = self.model.predict([values])[0]
220+
self.manual_result.config(text=f"Prediction: {pred}", foreground="#10b981")
221+
except Exception as e:
222+
self.manual_result.config(text=f"Error: {str(e)}", foreground="#f87171")
223+
224+
def set_target(self, v):
225+
self.target_progress = v
226+
227+
def animate_progress(self):
228+
if self.smooth_value < self.target_progress:
229+
self.smooth_value += 1
230+
self.progress["value"] = self.smooth_value
231+
self.root.after(15, self.animate_progress)
232+
233+
def cancel(self):
234+
if self.worker_obj:
235+
self.worker_obj.stop()
236+
self.finish()
237+
238+
def finish(self):
239+
self.start_btn.config(state=NORMAL)
240+
self.cancel_btn.config(state=DISABLED)
241+
self.progress["value"] = 100
242+
243+
# ---------------------- Export ----------------------
244+
def export_results(self):
245+
selected = [self.tree.item(i)['values'] for i in self.tree.get_children()
246+
if self.tree.item(i)['values'][0]=="☑️"]
247+
if not selected:
248+
messagebox.showwarning("Export", "No selected files to export")
249+
return
250+
path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text Files","*.txt")])
251+
if path:
252+
with open(path,"w",encoding="utf-8") as f:
253+
for s in selected:
254+
f.write(f"{s[1]} | {s[2]}\n")
255+
messagebox.showinfo("Export", "Export completed")
256+
257+
# ---------------------- Styles ----------------------
258+
def _apply_styles(self):
259+
self.root.style = tb.Style(theme="darkly")
260+
self.root.style.configure("TProgressbar", troughcolor="#1b1f3a", background="#7c3aed", thickness=14)
261+
262+
# ---------------------- Run ----------------------
263+
def run(self):
264+
self.root.mainloop()
265+
266+
# ---------------------- RUN ----------------------
267+
if __name__ == "__main__":
268+
app = IrisClassifierApp()
269+
app.run()

0 commit comments

Comments
 (0)