Loading…
Loading…
Museum specimen classifier: 7 broad animal groups, transfer learning, TensorFlow Lite.
Primary use: the Evolutionsmuseet visitor app. A visitor photographs a specimen in an exhibit and the app shows its broad taxonomic group, to encourage engagement.
Photos taken by the team at Evolutionsmuseet Zoology Section, Uppsala, on 14 October 2026, with a smartphone. The photos are deliberately varied: different angles and distances, visible backgrounds, reflections on the display glass, and partial views. They were sorted by hand into TRAINING and TESTING folders for each of the 7 classes, in JPG, JPEG and PNG format. Before training, 191 photos that were stored with an EXIF rotation flag were turned upright, and every image was converted to RGB JPEG.
Training set: 280 images. 15% of these were held out as a validation set for early stopping. Test set: 56 images, 8 per class, which were never seen during training. 2 training images that were byte-identical copies of test images were removed so the test set stays unseen (INSECT/IMG_8370.jpeg, MARINE_INVERTEBRATE/IMG_4346.jpg).
| Class | Training | Testing |
|---|---|---|
| Amphibians | 40 | 8 |
| Birds | 40 | 8 |
| Fish | 40 | 8 |
| Insects | 40 | 8 |
| Mammals | 40 | 8 |
| Marine invertebrates | 40 | 8 |
| Reptiles | 40 | 8 |
Results on the held-out test set (56 images): accuracy 87.5%, macro precision 89.2%, macro recall 87.5%, macro F1 87.5%.
| Class | Precision | Recall | F1 |
|---|---|---|---|
| Amphibians | 0.64 | 0.88 | 0.74 |
| Birds | 0.89 | 1.00 | 0.94 |
| Fish | 1.00 | 0.75 | 0.86 |
| Insects | 1.00 | 0.88 | 0.93 |
| Mammals | 1.00 | 1.00 | 1.00 |
| Marine invertebrates | 0.89 | 1.00 | 0.94 |
| Reptiles | 0.83 | 0.62 | 0.71 |
TFLite conversion and verification: the saved zootag.h5 model was reloaded and converted with the TensorFlow Lite converter, using dynamic-range quantisation (6.6 MB). The .tflite model was then run on every test image. Its top prediction agreed with the .h5 model on 98% of images. TFLite test accuracy was 85.7%, and the largest difference in any class probability was 0.060, caused by quantisation. The sample below has two test images per class. The full per-image results are in tflite_verification.csv.
| Test image | True label | Keras (.h5) | TFLite |
|---|---|---|---|
| IMG_8275.jpg | AMPHIBIAN | AMPHIBIAN (0.62) | AMPHIBIAN (0.65) |
| IMG_8277.jpg | AMPHIBIAN | AMPHIBIAN (0.98) | AMPHIBIAN (0.98) |
| IMG_4496.jpg | BIRD | BIRD (0.97) | BIRD (0.97) |
| IMG_4502.jpg | BIRD | BIRD (0.93) | BIRD (0.93) |
| IMG_4410.jpg | FISH | AMPHIBIAN (0.82) | AMPHIBIAN (0.81) |
| IMG_4437.jpg | FISH | MARINE_INVERTEBRATE (0.43) | MARINE_INVERTEBRATE (0.42) |
| Flipped1.jpg | INSECT | INSECT (0.79) | INSECT (0.77) |
| Flipped3.jpg | INSECT | INSECT (0.89) | INSECT (0.90) |
| IMG_4161.jpg | MAMMAL | MAMMAL (0.73) | MAMMAL (0.74) |
| IMG_4201.jpg | MAMMAL | MAMMAL (0.89) | MAMMAL (0.89) |
| IMG_4313.jpg | MARINE_INVERTEBRATE | MARINE_INVERTEBRATE (0.96) | MARINE_INVERTEBRATE (0.96) |
| IMG_4332.jpg | MARINE_INVERTEBRATE | MARINE_INVERTEBRATE (0.48) | MARINE_INVERTEBRATE (0.44) |
| IMG_4362.jpg | REPTILE | REPTILE (0.94) | REPTILE (0.94) |
| IMG_4364.jpg | REPTILE | REPTILE (0.97) | REPTILE (0.98) |
import numpy as np, tensorflow as tf
from PIL import Image
labels = open('labels.txt').read().split()
it = tf.lite.Interpreter(model_path='zootag.tflite'); it.allocate_tensors()
img = np.asarray(Image.open('photo.jpg').convert('RGB').resize((224, 224)), np.float32)
it.set_tensor(it.get_input_details()[0]['index'], img[None])
it.invoke()
probs = it.get_tensor(it.get_output_details()[0]['index'])[0]
print(labels[probs.argmax()], probs.max())