Backend for the Planter mobile app. A FastAPI application that accepts a photo, recognizes the wild plant species with a ResNet50 model (PyTorch) and returns its description from a PostgreSQL database. The model recognizes 62 species.
photo ──▶ POST /api/v1/recognize-plant/
│
▼
recognize/plant_recognition.py
ResNet50 (224x224, 62 classes) ──▶ class index (0-61)
│
▼
`plants` table (id = index + 1)
│
▼
JSON response
- FastAPI, Uvicorn
- PyTorch / torchvision: ResNet50 with a replaced
fclayer (62 classes) - PostgreSQL, SQLAlchemy 2, Alembic
- pydantic-settings: configuration from a
.envfile - pandas, h5py, xlrd: loading training data
planter-api/
├── main.py # FastAPI app and endpoints
├── database.py # SQLAlchemy engine and session
├── core/config.py # settings (.env)
├── models/plant.py # Plant model (plants table)
├── recognize/
│ ├── plant_recognition.py # inference and index -> plant name map
│ └── train_model_one.py # model training
├── database/
│ ├── migrations/ # Alembic migrations
│ └── seeders/plant_seeder.py # data for 62 plants (PL/EN)
├── scripts/
│ ├── clear_database.py # deletes all plants
│ └── inspect_hdf5_file.py # inspects an .hdf5 weights file
├── tests/test_plant.py
├── alembic.ini
└── requirements.txt
- Python 3.10+
- PostgreSQL
- Model weights at
result/plant_recognition_model_one.pth(not included in the repository, see Model)
git clone https://github.com/maszjan/planter-api.git
cd planter-api
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txtCreate a .env file in the project root (all fields are required):
APP_NAME=Planter API
ADMIN_EMAIL=admin@example.com
ITEMS_PER_USER=50
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=your_password
DB_NAME=plantercreatedb planter
# run migrations
alembic upgrade head
# fill the plants table (62 species)
python database/seeders/plant_seeder.pyPlace the trained weights in result/plant_recognition_model_one.pth. The path is relative to the directory from which you start the server.
uvicorn main:app --host 0.0.0.0 --port 8000 --reload- API: http://localhost:8000
- Swagger UI: http://localhost:8000/docs
{ "message": "Hello World" }multipart/form-data with a file field (the photo).
curl -X POST http://localhost:8000/api/v1/recognize-plant/ \
-F "file=@photo.jpg"Response:
{
"id": 18,
"name": "Mniszek lekarski",
"eng_name": "Dandelion",
"scientific_name": "Taraxacum",
"description": "...",
"eng_description": "...",
"image_url": null
}Errors are returned as JSON with HTTP status 200:
| Response | Cause |
|---|---|
{"error": "Plant recognition failed"} |
the model returned no result |
{"error": "Plant not found"} |
no plant with the given id in the database |
{"error": "Internal server error"} |
unexpected exception (details in the log) |
The plants table: id, name (Polish), eng_name, scientific_name, description (Polish), eng_description, image_url (optional, currently empty).
The record for a class n returned by the model is looked up by id = n + 1. The order in plant_seeder.py must therefore match the index_to_name map in recognize/plant_recognition.py, and the table must be filled starting from id = 1.
- Architecture: ResNet50, 62 output classes, 224x224 input with ImageNet normalization.
- Training:
recognize/train_model_one.py(SGD, learning rate 0.001, 25 epochs, a checkpoint after every batch). It needs a local dataset indatasets/wild_plants/(metadata.xls,.hdf5weights,dataset/anddataset-test/directories), which is not part of the repository. - The script saves its output to
models/plant_recognition_model_one.pth, while the API loadsresult/plant_recognition_model_one.pth, so copy the file after training.
python recognize/train_model_one.pyLogs are written to app.log in the directory where the server is started.
pip install pytest httpx
pytestThe current tests in tests/test_plant.py are outdated: they expect the response {"message": "Plant recognized"} and an endpoint /api/v1/detect-plant-disease/ that does not exist. They also require the model weights and a database.
- The model always returns the most likely class. There is no confidence threshold or "not recognized" answer, so a photo of something that is not a plant still gets a result.
- Errors are returned with status 200;
HTTPException(404, 422, 500) would be cleaner. - There is no file type validation. Images with an alpha channel (for example PNG) may fail because
.convert("RGB")is missing before inference. - Mapping a class to a record through
id + 1is fragile:clear_database.pydoes not reset the PostgreSQL sequence, so ids shift after re-seeding. Storing the class index in a separate column would be safer. requirements.txtliststensorflowandscikit-learn, which the code does not use, and lackspytestandhttpx.- The default
sqlalchemy.urlinalembic.inicontains credentials. It is overridden by.env, but it is better removed. - A Dockerfile and
docker-compose.yml(API and PostgreSQL) would simplify deployment. - Predictions can be wrong, and the descriptions mention edibility, so results should not be treated as authoritative.
- planter: the mobile app (Expo / React Native)