Astro Image Classifier

An ensemble of two transfer-learned CNN branches that classifies astronomical images into 11 classes.

Architecture

vgg_ensemble_input      [None,224,224,3] -> vgg_branch_model      -> softmax(11)
densenet_ensemble_input [None,224,224,3] -> densenet_branch_model -> softmax(11)
                                         -> Average
  • VGG19 and DenseNet201 backbones, ImageNet-initialised and partially unfrozen
  • Custom heads: global average pooling, dense + batch norm, dropout
  • The VGG branch includes a data-augmentation block, inactive at inference
  • 40,056,982 parameters
  • Saved with Keras 3.10; optimizer state stripped, so this file is inference-only

Classes

black_hole, earth, galaxy, jupiter, mars, mercury, neptune, pluto, saturn, uranus, venus

Usage

Both branches preprocess internally โ€” the densenet branch through a Lambda(preprocess_input), the vgg branch through a channel-swap and mean-subtraction chain. Feed raw 0-255 RGB. Applying vgg19.preprocess_input or densenet.preprocess_input beforehand preprocesses twice and degrades predictions badly on some classes.

custom_objects is required: the densenet Lambda is serialised under the name preprocess_input, and Keras resolves it by that name at load time.

import numpy as np, tensorflow as tf, keras
from huggingface_hub import hf_hub_download
from PIL import Image
from tensorflow.keras.applications.densenet import preprocess_input

keras.mixed_precision.set_global_policy("float32")  # saved under mixed_float16

path = hf_hub_download("MA29/astro-image-classifier", "ensemble_model.keras")
model = tf.keras.models.load_model(
    path, custom_objects={"preprocess_input": preprocess_input}, compile=False
)

CLASSES = ["black_hole", "earth", "galaxy", "jupiter", "mars", "mercury",
           "neptune", "pluto", "saturn", "uranus", "venus"]

img = Image.open("example.jpg").convert("RGB").resize((224, 224))
x = np.expand_dims(np.array(img, dtype="float32"), 0)   # raw 0-255
names = [t.name.split(":")[0].split("/")[0] for t in model.inputs]

probs = model.predict({n: x for n in names}, verbose=0)[0]
print(CLASSES[int(np.argmax(probs))], probs.max())

Evaluation

Held-out test split: 446 images, reconstructed from the training notebook's image_dataset_from_directory(validation_split=0.3, seed=42) then temp_ds.skip(14). Per-class supports match the notebook's own reports.

Model Test accuracy
DenseNet201 branch alone 0.9888
VGG19 branch alone 0.9888
Ensemble (this model) 0.9910 (442/446)

Macro F1 0.990, weighted F1 0.991, mean confidence 98.4%.

class precision recall f1 support
black_hole 1.000 0.952 0.976 21
earth 1.000 1.000 1.000 35
galaxy 0.962 1.000 0.980 25
jupiter 1.000 1.000 1.000 39
mars 0.978 0.957 0.967 46
mercury 1.000 1.000 1.000 50
neptune 1.000 1.000 1.000 52
pluto 1.000 1.000 1.000 37
saturn 1.000 1.000 1.000 41
uranus 1.000 1.000 1.000 48
venus 0.962 0.981 0.971 52

All four errors: three venus/mars confusions, and one black_hole predicted as galaxy at 100% confidence โ€” worth knowing that high confidence is not a reliable correctness signal here.

Note the test split is drawn from the same curated dataset as training, so this figure reflects in-distribution performance only.

Limitations

  • Trained on a curated, fairly clean dataset of planet and deep-sky imagery; it has not been evaluated on telescope captures, noisy frames or unusual crops.
  • Closed-world over 11 classes โ€” every input is forced into one of them. There is no "none of the above", so out-of-distribution images get confident and meaningless labels.
  • Several classes are visually similar under poor lighting or low resolution; treat single-image predictions as indicative.
Downloads last month
22
Inference Providers NEW
This model isn't deployed by any Inference Provider. ๐Ÿ™‹ Ask for provider support