A web system that recognises Javanese Edelweiss flowers in a photo and determines their health stage, for hikers and researchers monitoring this endemic mountain plant.
Javanese Edelweiss (Anaphalis javanica) grows along hiking trails on Indonesian mountains and is a protected species. The problem is that monitoring its condition takes a trained eye. A single clump can hold buds, flowers in bloom, seeds starting to ripen, and new shoots from regeneration. Hikers who pass through its habitat every weekend see the plant most often, yet are least able to tell those stages apart.
I built this system as my final project at Nusa Putra University. The idea was simple: hikers already carry a phone camera, so why not use it to record the condition of the Edelweiss as well.
The system has two parts that run separately. Laravel handles the web and the data, a Python service handles the model. When a user uploads a photo, Laravel passes the file to FastAPI, YOLOv11 finds the position of every flower in the image, each detected box is cropped, then an MLP decides its health stage. The result comes back as JSON with box coordinates, labels, and two confidence values (from YOLO and from the MLP). The browser redraws those boxes over the photo in colours that match the stage.
Splitting it into two stages was deliberate. YOLO is good at finding positions but less precise at separating stages that look alike. So I moved classification to an MLP trained specifically for it. The MLP's input is not raw pixels but a 64-dimension vector of HSV histograms, Sobel gradient histograms, and colour statistics from the crop. That keeps the model light, which matters because the service runs in a container with a 1.5 GB memory limit and no GPU.
The most troublesome part was not the model but the live camera. Sending every frame to the server made no sense. I put an 800 millisecond gap between frames, and more importantly, frames from the realtime loop never reach the database. An image is only stored when the user presses the capture button. I applied a similar rule to ordinary uploads: if no flower is detected, the photo is not stored, so the storage folder does not fill up with pictures that are not Edelweiss.
The second problem came from label ordering. Roboflow exports classes alphabetically, so Biji_Matang ends up at index zero even though that stage comes late in the life cycle. I decided the two systems did not need to agree. The model uses the alphabetical order in mlp_model.py, the web uses the life cycle order in config/edelweiss.php, and the web only ever accepts labels as strings, never as indexes. The side effect was pleasant: badge colours, chart colours, filter order, even the CSS classes in the PDF all read one configuration file. Adding a new stage means adding one entry and its translation, without touching a controller or a blade file.
Third, the accuracy that is still unfinished. The Pematangan_Biji and Biji_Matang stages look alike because both show drying petals, and that shows clearly in the evaluation. While Penyemaian_Baru and Mekar are near perfect, those two seed stages are what hold the overall figure back.
Finally, a failing ML service has to be reported honestly to the user. I separated the handling of a service that cannot be reached, a request that times out, and other errors, each with its own message. The admin panel also has a service status check, so I do not have to log into the server just to find out whether FastAPI is alive.
I did all of it myself: collecting and annotating the dataset in Roboflow, training both models in Google Colab, writing the FastAPI service, building the Laravel application and its interface, preparing both languages, and handling deployment. At first I installed the system manually on a VPS with Nginx and systemd. Later I moved it to Docker deployed through Coolify, so updates no longer depend on a shell script on the server.
The models were trained on 5,000 annotated images (3,987 training, 516 validation, 497 test). On the test set, YOLOv11n reached an mAP@0.5 of 91.8 percent with 82.1 percent precision and 89.5 percent recall, at 4.3 milliseconds inference per image. The MLP reached 90.1 percent validation accuracy across 6,540 flower crops. Per class, Penyemaian_Baru was the most accurate (mAP@0.5 of 99.5 percent) and Pematangan_Biji the weakest (78.7 percent).
What I want to work on next: more varied data for the two seed stages that still get confused, storing location coordinates on detection results so population spread can be mapped, and opening a public API so other researchers can use the model without going through the web interface.