From 43754a46b2bb1cfd0b316bfdda65c1c92b97377f Mon Sep 17 00:00:00 2001 From: davidb2311 Date: Thu, 28 May 2026 01:16:04 +0200 Subject: [PATCH 1/2] feat: added growth and color models and routes in API --- API/api.py | 46 ++++++++++++++++++++++++++++- API/growth_color_models.py | 33 +++++++++++++++++++++ API/weights/X_mean1.pth | Bin 0 -> 1180 bytes API/weights/X_mean2.pth | Bin 0 -> 1180 bytes API/weights/X_std1.pth | Bin 0 -> 1175 bytes API/weights/X_std2.pth | Bin 0 -> 1175 bytes API/weights/color_hidd_weights.pth | Bin 0 -> 2714 bytes API/weights/color_out_weights.pth | Bin 0 -> 1940 bytes API/weights/lr_weights.pth | Bin 0 -> 1514 bytes 9 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 API/growth_color_models.py create mode 100644 API/weights/X_mean1.pth create mode 100644 API/weights/X_mean2.pth create mode 100644 API/weights/X_std1.pth create mode 100644 API/weights/X_std2.pth create mode 100644 API/weights/color_hidd_weights.pth create mode 100644 API/weights/color_out_weights.pth create mode 100644 API/weights/lr_weights.pth diff --git a/API/api.py b/API/api.py index 0a1e853..bfc3cc2 100644 --- a/API/api.py +++ b/API/api.py @@ -2,6 +2,7 @@ Unified Plant Care API - Plant health detection (EfficientNetB0 + colour ensemble) - Plant recommendation (fuzzy matching on care criteria) +- Growth/color prediction model """ import io, base64, time, threading import numpy as np @@ -13,6 +14,8 @@ from fastapi.responses import RedirectResponse from pydantic import BaseModel, Field import uvicorn +from growth_color_models import GrowthPredictor, ColorPredictor +import torch # 1. Plant data for recommendation @@ -346,6 +349,22 @@ def _infer(pil_img): }, } +# Pydantic models for growth/color models +class DataVector(BaseModel): + days_passed: float + avg_direct_light: float + avg_indirect_light: float + avg_nighttime: float + avg_temp: float + min_temp: float + max_temp: float + times_watered: float + initial_height: float + color_before: List[int] + +# Loading grwoth/color models +growth_model = GrowthPredictor(9) +color_model = ColorPredictor(14, 20) # 5. FastAPI app – combined endpoints @@ -417,7 +436,32 @@ def detect_b64(body: DetectBase64Request): return res except Exception as e: raise HTTPException(status_code=500, detail=str(e)) - + +# --- Prediction endpoints --- +@app.post("/growth") +async def fwd(vector: DataVector): + + flat_vector = list(vector.model_dump().values()) + flat_vector.pop() + inp = torch.tensor(flat_vector, dtype=torch.float32).unsqueeze(0) + + inp_norm = (inp - growth_model.X_mean) / (growth_model.X_std + 1e-8) + + inp_c = torch.tensor(flat_vector).unsqueeze(0) + inp_cb = torch.tensor(vector.color_before).unsqueeze(0) + inp_c_norm = (inp_c - color_model.X_mean) / (color_model.X_std) + inp_c_final = torch.cat([inp_c_norm, inp_cb], dim=1) + + growth_model.eval() + color_model.eval() + with torch.no_grad(): + pred = growth_model(inp_norm).item() + + logits = color_model(inp_c_final) + probs = torch.softmax(logits, dim=1) + color = torch.argmax(probs, dim=1).item() + + return {"guess" : pred, "color": color} # 6. Run the server diff --git a/API/growth_color_models.py b/API/growth_color_models.py new file mode 100644 index 0000000..167fc40 --- /dev/null +++ b/API/growth_color_models.py @@ -0,0 +1,33 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +class GrowthPredictor(nn.Module): + def __init__(self, n_inp): + super().__init__() + self.out = nn.Linear(n_inp, 1) + self.out.load_state_dict(torch.load("weights/lr_weights.pth", weights_only=True)) + self.X_std = torch.load("weights/X_std1.pth", weights_only=True) + self.X_mean = torch.load("weights/X_mean1.pth", weights_only=True) + + + def forward(self, input_layer): + out = self.out(input_layer) + + return out + +class ColorPredictor(nn.Module): + def __init__(self, n_inp, n_hidd): + super().__init__() + self.hidd = nn.Linear(n_inp, n_hidd) + self.out = nn.Linear(n_hidd, 5) + self.hidd.load_state_dict(torch.load("weights/color_hidd_weights.pth", weights_only=True)) + self.out.load_state_dict(torch.load("weights/color_out_weights.pth", weights_only=True)) + self.X_std = torch.load("weights/X_std2.pth", weights_only=True) + self.X_mean = torch.load("weights/X_mean2.pth", weights_only=True) + + def forward(self, input_layer): + x = F.relu(self.hidd(input_layer)) + out = self.out(x) + + return out \ No newline at end of file diff --git a/API/weights/X_mean1.pth b/API/weights/X_mean1.pth new file mode 100644 index 0000000000000000000000000000000000000000..44e13b93c6081ad922618a7bdcbbeddc1b9f6665 GIT binary patch literal 1180 zcma)6!EVz)5M3vYlNc#&xgn%*C=x*e*G}vJm4IR)U-p1W1w>qoW3O5Z6L;-RB{=0m zrG5fE^uibL0Z81T2Yvx=oH=l0rb&!#NX1xB>-FxNc{8(XhgMezX*9@Rwni?Jq5Fz? zV|_F5gs1MmjBKk#o)>Id@kN~Yd#XDTVU#Lv!bX!Y3S7a)X`HwRU4@fQXIKaSDeUxi z8JA`h#g1Z6?94tYyfTzvkH)LRDvbXAgs;%1;I+q;P>D7VxlEf>I^^{u-;bk+`67(R zsq!cZSi*vbp)VBPxIW;(Q3m{i>d<<_c{V*g25$&wX{PJsT9)bPwO}#2w5^R&s)f84 zlPDA-Vi@b~-S3xVh=W)jt3~c*v9PL}nr&_UPoPD>(n!d3n~)cPFM!blLf!+803QLL z?jOH9-9~laKA(KJ3d#`6)vb~%nYsg(q)=hox_q9zo`T-Lp6ouUfM>*V?`_h}ysKx) zrz{D*D175#FWoS(E%m%$rQC6<>qg%&y0^N$+s2)qX=;XUYG8W5YxZ?r>lvEXH~KwP zsc!6;9Z-mHsDxs(lb=5+Yq(Ib@;xZC^Sxg$Fn_>o!OqTBnVxS>t3ZD${h`l}OPQN5 z{c3^xGsm4bvrNucq+K8npgIfP|L&DTW%HV%6n}}EFC(Q58$t>LNCXMe*!gNIff5V6oCGE)5V087Ikl#4+_Nv0VCq68 zCL|^V5))JVH?XlXAT~C{#>~LTy(YG7Ln@y2^n7>k_rC88BCP-vi|`k%!Fd>)d(@h$ z>l2Gx_5EE}*UIoXL5EqJd%nF>H)q^&1KITH*35AyCa2TD^UVWQW>Bk*3&MUNG_7sQ zGINNmyPm}#%r|A09cP42gGpmaNPBCP)9vyyuE36?KOEay&)hN@gXPkOyQr9l0QcdI? zE?kFmm!hv1H-B8n3qQ!qeU;%>a{eSl2F|6<$+D>tEh)kFvrt@^b4{ zP$TR0)8qr{JC^G_x3HI{GtssBS*sY8_0 zf)S|3ppZF&8AvA=loqmhbCwjc26{7iGkSBj6|#AAdNZ~avUesY=jY_4CYNO9=M{7L z7p0^YrKY%KCYNv(a%ct>0`+hO6>{qZc(Zc^gg2a>1vCwW1919OfI$Q1)1=Ch)O?VM zZcZxbUPbjdl$VoPQj(Jjva8khu_PPN9uUTD7cYZ7%q~!<>KnK@SrcUuR@fmR;)LyKH(H`VFgZzReDnOTkFmBiJF(|-X zSC(2-3=Cj5CmFoXGv)%BesNxUC=<|R5XNn~EQ2x3bUm==i&Kj-6LT`F5tA!EMZ`1Xkun!Vq|V)Y++()Y-(y?VrXgr1cqitre=nwrk19L7N$mqMj%)6*_@i9 z0dye<2Y54r91BnR$SH*%B!L3Z@&USL zK-Y>K?tCa(zaX?iV-j62a%jk+=nVpf4Affe@f6_A#-;<+BFC%?*USoK!Dw4hjs}7s i;GhNtIvc1QU@!$1IUoR%4)A6L zf)S|3ppZF&8AvA=loqmhbCwjc26{7iGkSBj6|#AAdNZ~avUesY=jY_4CYNO9=M{7L z7p0^YrKY%KCYNv(a%ct>0`+hO6>{qZc(Zc^gg2a>1vCwW1919OfI$Q1)1=Ch)O?VM zZcZxbUPbjdl$VoPQj(Jjva8khu_PPN9uUTD7cYZ7%q~!<>KnK@SrcUuR@Wf_cNrt5({Uz}Q$nV6GVl?aN;_{uoX~C6&01k3L?xu)e2=nZP69AxIC1)(6v@ME>dJ4GvIoFjh?;d-1+}AbMJS* z|IUBzz0orBxfsUbVE-dR%nFldXfm{kDz!q9xJ9W>Rq1v9ie!DVU*=|wG}0VPNJuD{ zBWH{>O1WO0k*?#e(JGW$rDCO8uIG}NR}5+98E?rzPkEDb zo|#2jt#&fglbp3OOvabl7LnF6TbWrA$t_W_G;H+byclwRJabkoMV+i8ZBGBce{m*N*Kv*Kia`AL*JLz&g+m!{kWR zlBfvOKhl7|n>0oh+=@fI@k^2;@3^8Z3GPH(yBp!kUy6>Ir%_w(JOQ_|QMAnSA249< zPJiLOk7~<10+OtLs%GI8w8SVR`o`=CpWqlMD!YmJ(mYDL#vb6D4Lx+=GS#t3K#Mqg z;J5Ef!SdvG^rm@*Bzu25_4IZhuD!sa_ddG}JKx#}srx>HiL0C7NIpRyt+z+!2XpbQ zX(bXjp;2P%FN9xKounM!X+-?u^>p}$9g>iSufe~j28Juwqqr|8@J5am@%F(JX#L&_ zC>m*m=E@srQN;t)ab^H*o$pKkEy#mdyu*=LJQPNM9(EL0?OFv%d#<*8!)t<2w=NX8 zy#U3(K93Nma_H1v7rHSeh#oF^1ZOg1(L(RjaA4mkoNOPVp8V(uenWqx65%QAGzf^b zr=qB#^7m2A_$^4cT?3JQKf`4|GopIK1yuT14`ST;J|%^Av~s@!&M$R=@9&l2b^Cup z71k=~h&l%Ck_t3>w-bGs+KR$rDE!C5dPqN54u*RU^w~HOy0yXr&-RG|C**_NJ`EsJ z3V5_na~z5t5kXI+3z7P13)~Lw12@h9YQ1BC!3hC~dgZ8~^ANt#P)Ger>qqCk9Y_7r znvY_QonT$}kZQi}1;ra1VQ{P*C9R9Wy9RsFFvLRc2~Q$IoQfa%st8V z56}7CS)|zh2PpsEn(AsihA;V;3u#p=sOIW!czl!~c2h>gOYA{LXD>KcdV$iooJNZA z-SAU{GyQ`d0DkU5xa+m=pu6T_OIB}BcuoS1nioj`g^7sf57J@V`Fsc{$i(l|zlj=t zy$fpn9q7jXc(@*ZhAJCc2LG;Vf-A?8;jWQK^FQa|N5TidVV)DT`Gy0(aVeDXZos?5 zLr5sQhDP>ZLSOd_3Aow~e{bu6kODqEVeLXx^XuTzpG)!T(c_?5dQ0ag}q@aqTgik*D1B?WQ}@TGTX=# z)e32(`~O2AASiTsz;aP&sCap3XrOp`P_QU~k>KEvkig(Tai}PS*@;BKEK9dL&DT5` zhO9=*%viE7CXeY2QpkRoA+MN2W&=%cgDx{bbWHCx0raA~&PJMk?VM*Ib(=<-YNE4Y zrr(B{FlU%nZGzAwOyF#o>AzSogG~}ME}4LNevxM5O#ca9GjOh%#xZ?8qGgtrKF{4< v&3#`0vC{`D|6RXgQpL*F=MyI5$4+-x2$y|g82dR|W-)1Q$#}8$Q?~yBj4GM6 literal 0 HcmV?d00001 diff --git a/API/weights/color_out_weights.pth b/API/weights/color_out_weights.pth new file mode 100644 index 0000000000000000000000000000000000000000..e5d0069ae58502ea8fb2f71fcb36375da95d0ac9 GIT binary patch literal 1940 zcma)7e{2&~9KWtV*GyEyus{@W?+FHF7>w=EI3qgH#Gr1`7%&(UeXpIh?OO4heD7WE zeZQaY=Y8*U-*auiw)XayeC0C$<9x zejF4M{IVF{E$N?(1cZnX*dY34J)qWf0EN|~!O+QcP~ext5r44W8ixC7kf!AGThA}@huL0l)O3MO%Epd_f})G}C}$R7Zu zoMHeNlDTCvFm{0QXg{d%6ykjEGlI;MHh{_mw#kDZQo!5~ED7ShLPWy709Bkqk|bZf zo73vHjxAKzV8+DVc{^T3mFMWVZ=WoLNn2gc(nQNi+fx=2WjQ2}8UtPY zd4WX(=jWmJatTvXVM8OgC}jJ*p1n7wM3Dn0;Hu7-8Q-Y_G`IO6Q_$N2%Qw-;ZIYma z8G=t;U5~_tR~hf8Rus5h#yZF5;qoU(l%4K61}kFYld#P2X)Qf%^HfU*~xR|==7#nnCF(=hW29*!t2)qZR?zBR@Zn9 z$4x+^{}Mb}`^nTvUWqR68)EuqEi7HVkwM`{AyGn)_T0~-SZT!nO9^Z9DZ%D)uKu4& z7+qifP}d(<+DoIuGDl8ursgG`h;Jh+$ANub!WQPN+pIfF|8PxPfIivr4q zN$)>-2me|;CroZ-xH*kx-D2tr<^3rTGIxpnv!20iBG#u)t~ihU*C>19N>OCn&8ZXN L8k|Mqld*pT7q20{ literal 0 HcmV?d00001 diff --git a/API/weights/lr_weights.pth b/API/weights/lr_weights.pth new file mode 100644 index 0000000000000000000000000000000000000000..68e87c89c951a3efec846749d7b6b894fe2e3130 GIT binary patch literal 1514 zcmb7EJ8aWH7(OR$9xl*F3bZ^`gisJPaUN|ND5;1l3+a%W234{UlI1v7Y+>r2eJP?+ zC6-n^1{j!`DrOeg=*qx^genFWU}a>px_c0hBf9R`wxLtYS@y&Qw`sUWbKcUaNYLdv5z^ZKwnioklomS!u;jx)o%OHI|M3ikuqj&s4 zk@PD9n@$6y92qp@SfFI6gWn*-SlA#VJ~llHt_{2GyV%#?ZKPTNqk{-GvP2A3?J` z|K3)~)tPXC-X#Y{3L>a~e%)G%FlESa^$C>a)ia&qdxmRiwza0g0aC4|T)px?W~5@d zB$e{za-mc%=L)6bbY5an$`z;b#pxNTAmyc8erg(AK99T@N;8L$H7tPH{gcmkXbL_A z;*fiwn}Bb>(E!+GP0tbRw{ Date: Thu, 28 May 2026 01:18:03 +0200 Subject: [PATCH 2/2] feat: added growth page --- ui/main.py | 359 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 350 insertions(+), 9 deletions(-) diff --git a/ui/main.py b/ui/main.py index ca043cf..1878d90 100644 --- a/ui/main.py +++ b/ui/main.py @@ -2,6 +2,15 @@ from tkinter import simpledialog, messagebox, filedialog import matplotlib matplotlib.use("TkAgg") + +# Nastavi pisavo za emoji znake +import tkinter.font as tkfont +def _fix_emoji_font(root): + try: + if "Noto Color Emoji" in tkfont.families(root): + root.tk.eval('font configure TkDefaultFont -family {Noto Color Emoji}') + except Exception: + pass from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import matplotlib.pyplot as plt import matplotlib.patches as mpatches @@ -552,6 +561,72 @@ def on_leave(e): c.bind("", on_leave) +class GreenSlider(tk.Frame): + """Drsnik z vedno zelenim gumbom – deluje na vseh platformah.""" + + def __init__(self, parent, from_, to, variable, resolution=0.5, length=260, **kw): + super().__init__(parent, bg=BG_CARD, height=20) + self._from = from_ + self._to = to + self._var = variable + self._res = resolution + self._len = length + self._drag = False + + self._canvas = tk.Canvas(self, bg=BG_CARD, height=20, + width=length, highlightthickness=0) + self._canvas.pack(fill="x", expand=True) + + self._canvas.bind("", self._redraw) + self._canvas.bind("", self._on_press) + self._canvas.bind("", self._on_drag) + self._canvas.bind("", self._on_release) + self._var.trace_add("write", lambda *a: self._redraw()) + self._redraw() + + def _x_for_val(self, val, w): + ratio = (val - self._from) / (self._to - self._from) + return int(8 + ratio * (w - 16)) + + def _val_for_x(self, x, w): + ratio = (x - 8) / (w - 16) + ratio = max(0.0, min(1.0, ratio)) + raw = self._from + ratio * (self._to - self._from) + # Snap to resolution + snapped = round(raw / self._res) * self._res + return max(self._from, min(self._to, snapped)) + + def _redraw(self, *_): + c = self._canvas + w = c.winfo_width() or self._len + c.delete("all") + # Ozadje tira + c.create_rectangle(8, 8, w - 8, 12, fill=BG_CARD2, outline="", tags="trough") + # Zapolnjen del tira (levo od gumba) + val = self._var.get() + tx = self._x_for_val(val, w) + c.create_rectangle(8, 8, tx, 12, fill=ACCENT, outline="", tags="fill") + # Zeleni gumb + c.create_oval(tx - 8, 2, tx + 8, 18, fill=ACCENT, outline=ACCENT, tags="thumb") + + def _on_press(self, event): + self._drag = True + self._update(event.x) + + def _on_drag(self, event): + if self._drag: + self._update(event.x) + + def _on_release(self, event): + self._drag = False + self._update(event.x) + + def _update(self, x): + w = self._canvas.winfo_width() or self._len + val = self._val_for_x(x, w) + self._var.set(round(val, 2)) + + class AuthWindow(tk.Tk): def __init__(self): super().__init__() @@ -1479,15 +1554,9 @@ def slider_row(parent, label, from_, to, resolution, default): val_lbl.pack(side="right") scale_frame = tk.Frame(row, bg=BG_CARD) scale_frame.pack(fill="x", pady=(4, 0)) - # FIX 2: slider thumb always green via fg and activebackground - scale = tk.Scale( - scale_frame, from_=from_, to=to, resolution=resolution, - orient=tk.HORIZONTAL, variable=val_var, showvalue=False, - bg=BG_CARD, fg=ACCENT, troughcolor=BG_CARD2, - activebackground=ACCENT, - highlightthickness=0, bd=0, sliderrelief="flat", sliderlength=18, - length=260 - ) + # Drsnik z vedno zelenim gumbom + scale = GreenSlider(scale_frame, from_=from_, to=to, + variable=val_var, resolution=resolution, length=260) scale.pack(fill="x") minmax = tk.Frame(row, bg=BG_CARD) minmax.pack(fill="x") @@ -1701,6 +1770,276 @@ def _run_search(self): pass +class GrowthPage(BasePage): + """Stran za napovedovanje rasti rastline s klicem na API.""" + + # Vhodna polja in njihove oznake + FIELDS = [ + ("days_passed", "Days passed"), + ("avg_direct_light", "Avg direct light (hrs)"), + ("avg_indirect_light", "Avg indirect light (hrs)"), + ("avg_nighttime", "Avg nighttime (hrs)"), + ("avg_temp", "Avg temperature (°C)"), + ("min_temp", "Min temperature (°C)"), + ("max_temp", "Max temperature (°C)"), + ("times_watered", "Times watered"), + ("initial_height", "Initial height (cm)"), + ] + # Možne barve rastline + COLORS = ["green", "yellow", "brown", "pale", "black"] + + def _build(self): + self._entries = {} + self._color_var = tk.StringVar(value="green") + + outer = tk.Frame(self, bg=BG_MAIN) + outer.pack(fill="both", expand=True, padx=22, pady=18) + + # Naslov strani + tk.Label(outer, text="📈 Growth Prediction", font=("Segoe UI", 16, "bold"), + bg=BG_MAIN, fg=TEXT_PRI).pack(anchor="w") + tk.Label(outer, text="Enter plant conditions and predict height growth using the AI model.", + font=self.f_small, bg=BG_MAIN, fg=TEXT_SEC).pack(anchor="w", pady=(0, 16)) + + cols = tk.Frame(outer, bg=BG_MAIN) + cols.pack(fill="both", expand=True) + + # Leva plošča – vnosna polja + left = tk.Frame(cols, bg=BG_CARD, width=360, padx=18, pady=16) + left.pack(side="left", fill="y", padx=(0, 14)) + left.pack_propagate(False) + + tk.Label(left, text="Plant Conditions", font=self.f_title, + bg=BG_CARD, fg=TEXT_PRI).pack(anchor="w", pady=(0, 12)) + + # Vnosna polja za numerične vrednosti + for key, label in self.FIELDS: + row = tk.Frame(left, bg=BG_CARD) + row.pack(fill="x", pady=(0, 8)) + tk.Label(row, text=label, font=self.f_small, bg=BG_CARD, + fg=TEXT_SEC, width=22, anchor="w").pack(side="left") + entry = tk.Entry(row, bg=BG_CARD2, fg=TEXT_PRI, + insertbackground=TEXT_PRI, relief="flat", + font=("Segoe UI", 9), width=10) + entry.pack(side="left", ipady=5, padx=(6, 0)) + tk.Frame(row, bg=BORDER, height=1).pack(side="bottom", fill="x") + self._entries[key] = entry + + tk.Frame(left, bg=BORDER, height=1).pack(fill="x", pady=(4, 10)) + + # Izbira barve rastline + tk.Label(left, text="Plant colour (before)", font=self.f_small, + bg=BG_CARD, fg=TEXT_SEC).pack(anchor="w", pady=(0, 6)) + color_row = tk.Frame(left, bg=BG_CARD) + color_row.pack(fill="x", pady=(0, 12)) + self._color_btns = {} + + color_display = {"green": ACCENT, "yellow": YELLOW, "brown": "#a0522d", + "pale": TEXT_SEC, "black": "#555555"} + + for c in self.COLORS: + is_active = (c == "green") + btn = tk.Frame(color_row, + bg=ACCENT if is_active else BG_CARD2, + cursor="hand2", padx=10, pady=4) + btn.pack(side="left", padx=(0, 4)) + lbl = tk.Label(btn, text=c.capitalize(), font=self.f_small, + bg=ACCENT if is_active else BG_CARD2, + fg=BG_MAIN if is_active else TEXT_SEC) + lbl.pack() + self._color_btns[c] = (btn, lbl) + + def on_color(e, v=c): + self._color_var.set(v) + for k, (b, l) in self._color_btns.items(): + active = (k == v) + b.configure(bg=ACCENT if active else BG_CARD2) + l.configure(bg=ACCENT if active else BG_CARD2, + fg=BG_MAIN if active else TEXT_SEC) + + bind_tree(btn, "", on_color) + + # API URL polje + api_row = tk.Frame(left, bg=BG_CARD) + api_row.pack(fill="x", pady=(0, 10)) + tk.Label(api_row, text="API URL", font=self.f_small, bg=BG_CARD, + fg=TEXT_SEC).pack(anchor="w", pady=(0, 4)) + self._api_url_var = tk.StringVar(value="http://localhost:5000/growth") + api_entry = tk.Entry(api_row, textvariable=self._api_url_var, + bg=BG_CARD2, fg=TEXT_PRI, insertbackground=TEXT_PRI, + relief="flat", font=("Segoe UI", 9)) + api_entry.pack(fill="x", ipady=5) + tk.Frame(api_row, bg=BORDER, height=1).pack(fill="x") + + # Gumb za pošiljanje napovedi + predict_btn = tk.Frame(left, bg=ACCENT, cursor="hand2") + predict_btn.pack(fill="x", pady=(14, 0)) + tk.Label(predict_btn, text="📈 Predict Growth", font=self.f_label, + bg=ACCENT, fg=BG_MAIN, pady=9).pack() + bind_tree(predict_btn, "", lambda e: self._run_prediction()) + hover(predict_btn, ACCENT, "#00c98a") + + # Desna plošča – rezultati + right = tk.Frame(cols, bg=BG_MAIN) + right.pack(side="left", fill="both", expand=True) + + tk.Label(right, text="Prediction Result", font=self.f_title, + bg=BG_MAIN, fg=TEXT_PRI).pack(anchor="w", pady=(0, 10)) + + self._result_frame = tk.Frame(right, bg=BG_MAIN) + self._result_frame.pack(fill="both", expand=True) + self._show_empty() + + def _show_empty(self): + """Prikaže prazno stanje brez rezultatov.""" + for w in self._result_frame.winfo_children(): + w.destroy() + container = tk.Frame(self._result_frame, bg=BG_CARD, padx=20, pady=40) + container.pack(fill="both", expand=True) + tk.Label(container, text="📈", font=("Segoe UI", 40), bg=BG_CARD).pack(pady=(20, 8)) + tk.Label(container, text="Fill in the plant conditions and click Predict Growth.", + font=self.f_body, bg=BG_CARD, fg=TEXT_SEC, justify="center").pack() + + def _show_loading(self): + """Prikaže animacijo med čakanjem na odgovor API.""" + for w in self._result_frame.winfo_children(): + w.destroy() + container = tk.Frame(self._result_frame, bg=BG_CARD, padx=20, pady=40) + container.pack(fill="both", expand=True) + tk.Label(container, text="Contacting growth model...", + font=("Segoe UI", 11, "bold"), bg=BG_CARD, fg=TEXT_PRI).pack(pady=(30, 8)) + self._loading_lbl = tk.Label(container, text="⬤ ⬤ ⬤", + font=("Segoe UI", 14), bg=BG_CARD, fg=ACCENT) + self._loading_lbl.pack(pady=(12, 0)) + self._dot_idx = 0 + self._animate() + + def _animate(self): + patterns = ["⬤ ○ ○", "○ ⬤ ○", "○ ○ ⬤", "○ ⬤ ○"] + try: + self._loading_lbl.config(text=patterns[self._dot_idx % len(patterns)]) + self._dot_idx += 1 + self._anim_job = self.after(400, self._animate) + except Exception: + pass + + def _run_prediction(self): + """Zbere vrednosti, zgradi zahtevo in jo pošlje na API v ozadni niti.""" + # Preveri, da so vsa polja izpolnjena + data = {} + for key, label in self.FIELDS: + val = self._entries[key].get().strip() + if not val: + messagebox.showwarning("Missing Input", f"Please fill in: {label}") + return + try: + data[key] = float(val) + except ValueError: + messagebox.showerror("Invalid Input", f"'{label}' must be a number.") + return + + # Kodiranje barve v one-hot vektor + color = self._color_var.get() + vector = [1 if c == color else 0 for c in self.COLORS] + + payload = {**data, "color_before": vector} + api_url = self._api_url_var.get().strip() + + self._show_loading() + + def _call(): + try: + import requests as req_lib + response = req_lib.post(api_url, json=payload, timeout=10) + if response.status_code == 200: + rep = response.json() + self.after(0, lambda: self._show_result(rep)) + else: + msg = f"Server returned status {response.status_code}.\n{response.text[:200]}" + self.after(0, lambda m=msg: self._show_error(m)) + except Exception as ex: + self.after(0, lambda e=ex: self._show_error(str(e))) + + threading.Thread(target=_call, daemon=True).start() + + def _show_result(self, rep): + """Prikaže rezultat napovedi iz API odgovora.""" + if hasattr(self, "_anim_job"): + try: self.after_cancel(self._anim_job) + except: pass + for w in self._result_frame.winfo_children(): + w.destroy() + + color_name = self.COLORS[rep.get("color", 0)] if "color" in rep else "unknown" + guess = rep.get("guess", "N/A") + + # Kartica z rezultatom + card = tk.Frame(self._result_frame, bg=BG_CARD, padx=20, pady=20) + card.pack(fill="x", pady=(0, 10)) + + tk.Label(card, text="Prediction Complete", font=("Segoe UI", 12, "bold"), + bg=BG_CARD, fg=ACCENT).pack(anchor="w", pady=(0, 12)) + + # Napovedana višina + height_row = tk.Frame(card, bg=BG_CARD2, padx=16, pady=14) + height_row.pack(fill="x", pady=(0, 8)) + tk.Label(height_row, text="Predicted height growth", + font=self.f_small, bg=BG_CARD2, fg=TEXT_SEC).pack(anchor="w") + tk.Label(height_row, text=f"{guess:.3f} cm", + font=("Segoe UI", 28, "bold"), bg=BG_CARD2, fg=ACCENT).pack(anchor="w") + + # Napovedana barva + color_row = tk.Frame(card, bg=BG_CARD2, padx=16, pady=14) + color_row.pack(fill="x", pady=(0, 8)) + tk.Label(color_row, text="Predicted plant colour", + font=self.f_small, bg=BG_CARD2, fg=TEXT_SEC).pack(anchor="w") + color_colors = {"green": ACCENT, "yellow": YELLOW, "brown": "#a0522d", + "pale": TEXT_SEC, "black": "#888888"} + tk.Label(color_row, text=color_name.capitalize(), + font=("Segoe UI", 16, "bold"), bg=BG_CARD2, + fg=color_colors.get(color_name, TEXT_PRI)).pack(anchor="w") + + # Surovi odgovor API + raw_card = tk.Frame(self._result_frame, bg=BG_CARD, padx=16, pady=12) + raw_card.pack(fill="x") + tk.Label(raw_card, text="Raw API response", font=self.f_small, + bg=BG_CARD, fg=TEXT_MUT).pack(anchor="w", pady=(0, 6)) + import json + raw_txt = tk.Text(raw_card, bg=BG_CARD2, fg=TEXT_SEC, font=("Courier", 8), + relief="flat", height=6, state="normal") + raw_txt.insert("end", json.dumps(rep, indent=2)) + raw_txt.config(state="disabled") + raw_txt.pack(fill="x") + + # Gumb za ponovni poskus + retry_btn = tk.Frame(self._result_frame, bg=BG_CARD2, cursor="hand2", + padx=14, pady=8) + retry_btn.pack(anchor="e", pady=(10, 0)) + tk.Label(retry_btn, text="Predict Again", font=self.f_label, + bg=BG_CARD2, fg=ACCENT).pack() + bind_tree(retry_btn, "", lambda e: self._run_prediction()) + hover(retry_btn, BG_CARD2, BG_CARD) + + def _show_error(self, msg: str): + """Prikaže sporočilo o napaki pri klicu API.""" + if hasattr(self, "_anim_job"): + try: self.after_cancel(self._anim_job) + except: pass + for w in self._result_frame.winfo_children(): + w.destroy() + card = tk.Frame(self._result_frame, bg=BG_CARD, padx=20, pady=20) + card.pack(fill="both", expand=True) + tk.Label(card, text="Prediction Failed", font=("Segoe UI", 12, "bold"), + bg=BG_CARD, fg=RED).pack(pady=(10, 8)) + tk.Label(card, text=msg, font=self.f_body, bg=BG_CARD, fg=TEXT_SEC, + wraplength=400, justify="center").pack(pady=(0, 12)) + retry_btn = tk.Frame(card, bg=BG_CARD2, cursor="hand2", padx=14, pady=8) + retry_btn.pack() + tk.Label(retry_btn, text="Try Again", font=self.f_label, bg=BG_CARD2, fg=ACCENT).pack() + bind_tree(retry_btn, "", lambda e: self._run_prediction()) + hover(retry_btn, BG_CARD2, BG_CARD) + + class SettingsPage(BasePage): def _build(self): pad = tk.Frame(self, bg=BG_MAIN) @@ -1754,6 +2093,7 @@ def toggle(e): ("📅", "History"), ("🔬", "Detection"), ("💡", "Recommendation"), + ("📈", "Growth"), ("⚙️", "Settings"), ] @@ -1763,6 +2103,7 @@ def toggle(e): "History": HistoryPage, "Detection": DetectionPage, "Recommendation": RecommendationSystemPage, + "Growth": GrowthPage, "Settings": SettingsPage, }