Training star identification AI using PyTorch

We’ve finished our previous article TRAINING SET WITH STELLARIUM II having the training set ready to go. It was up to Sebi to the next step and do the actual star identification AI training. Sebi ended up to be sort of reluctant making it in a post so it came to explore a form we haven’t used up to this point – an Interview.

Jan: Where do we start Sebi?

Seb: I’ve been interested which will be the better for our job – whether the PyTorch or the TensorFlow, and after some initial research I’ve picked PyTorch as being more popular.

So I’ve started watching following video tutorial about PyTorch.

After watching it I wrote my first code based on that video. Here it goes:

from random import random
import torch
import torch.optim as optim
from torch.optim import lr_scheduler
from torchvision import models, transforms
from import DataLoader, random_split
from torchvision.datasets import ImageFolder
import os
import torch.nn as nn
import time
import copy
import signal
projectDir = os.path.dirname(os.path.realpath(__file__))+"/"
dataDir = projectDir+'../data/train_224_224_monochrome_big'
deviceName = "cuda:0" if torch.cuda.is_available() else "cpu"
device = torch.device(deviceName)
trainData = ImageFolder(dataDir, transform=transforms.Compose([transforms.Grayscale(num_output_channels=1), transforms.ToTensor()]))
testData = ImageFolder(projectDir+'../data/train_224_224_monochrome', transform=transforms.Compose([transforms.Grayscale(num_output_channels=1), transforms.ToTensor()]))
modelName = "resnet18"
batchSize = 1000
logName = "resnet18.log"
model = models.resnet18(pretrained=False)
model.conv1 = nn.Conv2d(1, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
model.fc = nn.Linear(model.fc.in_features, len(trainData.classes))
if os.path.exists(projectDir+modelName+".pth"):
def main():
  trainDataLoader = DataLoader(trainData, batch_size=batchSize, shuffle=True, num_workers=3)
  testDataLoader = DataLoader(testData, batch_size=batchSize, shuffle=True, num_workers=3)
  criterion = nn.CrossEntropyLoss()
  optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
  scheduler = lr_scheduler.StepLR(optimizer, step_size=1, gamma=0.8) # every step_size epochs multiply learning rate by gamma
  train_model(model, optimizer, criterion, scheduler, trainDataLoader, testDataLoader, 1, test=True)
def train_model(model, optimizer, criterion, scheduler, trainDataLoader, testDataLoader, numEpochs, test=False):
  start = time.time()
  best_model_wts = copy.deepcopy(model.state_dict())
  best_acc = 0.0
  best_train_acc = 0.0
  running_incorrects = {star:{star:0 for star in trainData.classes} for star in trainData.classes}
  if test:
    log("the next epochs are for testing:")
  for epoch in range(numEpochs):
    log('Epoch {}/{}'.format(epoch+1, numEpochs))
    log('-' * 10)
    for phase in ["train", "test"] if test == False else ["test"]:
      running_loss = 0.0
      running_corrects = 0
      if phase == "train":
      for index, (inputs, labels) in enumerate(trainDataLoader if phase == "train" else testDataLoader):
        print('Batch {}/{}'.format((index+1)*len(labels), len(trainDataLoader if phase == "train" else testDataLoader)*len(labels))+' Accuracy: {}/{} Percent: %{:.4f}'.format(running_corrects, len(trainData if phase == "train" else testData), (running_corrects / ((len(labels)*(index)) + 0.00001)) * 100), end="\r")
        inputs =
        labels =
        with torch.set_grad_enabled(phase == "train"):
          outputs = model(inputs)
          _, preds = torch.max(outputs, 1)
          loss = criterion(outputs, labels)
          if phase == "train":
        running_loss += loss.item() * inputs.size(0)
        running_corrects += torch.sum(preds ==
        for index, (prediction, label) in enumerate(zip(preds,
          if prediction != label:
            running_incorrects[trainData.classes[label.item()]][trainData.classes[prediction.item()]] += 1
      epoch_loss = running_loss / len(trainData if phase == "train" else testData)
      epoch_acc = running_corrects.double() / len(trainData if phase == "train" else testData)
      if phase == 'train':
        if epoch_acc > best_train_acc:
          best_train_acc = epoch_acc
      # log("failed classes: "+str({star:sorted(running_incorrects[star].items(), key=lambda item: item[1], reverse=True) for star in running_incorrects}))
      log("failed classes: "+str(running_incorrects))
      log('{} Loss: {:.4f} Acc: {:.4f}'.format(
        phase, epoch_loss, epoch_acc))
      log('Accuracy: {}/{}'.format(running_corrects, len(trainData if phase == "train" else testData)))
      # deep copy the model
      if phase == "test" and epoch_acc > best_acc and test == False:
        best_acc = epoch_acc
        best_model_wts = copy.deepcopy(model.state_dict())
        log("updated model")
      time_elapsed = time.time() - start
      log('Epoch time stamp: {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
  log('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
  log('Best val Acc: {:4f}'.format(best_acc))
  # load best model weights
  return model
def save_model(model, name=modelName):, projectDir+name+".pth")
  log("saved model: "+modelName)
def handler(signum, frame):
def log(message, name=logName):
  with open(projectDir+name, 'a') as file:
signal.signal(signal.SIGINT, handler)

Jan: What’s that doing? Looks messy.

Sebi: No. It is beautiful! Ok. It takes from pre-build network Resnet18 from PyTorch. (Resnet networks are in general famous for its image processing abilities.) I’ve changed it to have a correct number of inputs and outputs.

Jan: Like what?

Sebi: Inputs – greyscale and 64 x 64 points resulutions and Outputs – number of training classes (20 stars).

Jan: What next?

Sebi: Then it starts training that model through the back-propagation neural network and started showing how it progresses. It stops after several epochs and saves itself (trained weights).

Jan: What was a first result?

Sebi: So on a first epoch on a first go, I’ve ended up with accuracy of 0.01 accuracy. It kept improving over 33 epochs and it came up with 60% accuracy against the testing dataset.

Jan: That’s pretty cool for a first run. Would you share that log with us please?


# learning rate: 0.01, momentum: 0.0, step_size: 7, gamma: 0.1
saved model: resnet18
Epoch 1/50
train Loss: 2.8978 Acc: 0.0972
Epoch time stamp: 22m 34s
test Loss: 2.7925 Acc: 0.1225
saved model: resnet18
updated model
Epoch time stamp: 23m 24s
Epoch 2/50
train Loss: 2.6922 Acc: 0.1929
Epoch time stamp: 45m 48s
test Loss: 2.6295 Acc: 0.2250
saved model: resnet18
updated model
Epoch time stamp: 46m 32s

… many mode lines.

Epoch 32/50
train Loss: 2.0006 Acc: 0.6081
Epoch time stamp: 762m 41s
test Loss: 2.0135 Acc: 0.5955
Epoch time stamp: 763m 35s
Epoch 33/50
train Loss: 2.0006 Acc: 0.6106
Epoch time stamp: 786m 42s
test Loss: 2.0042 Acc: 0.6065
saved model: resnet18
updated model
Epoch time stamp: 787m 36s

Jan: Ok, what happened next?

Sebi: I’ve been playing with few training constants like it follows on a smaller dataset.

saved model: resnet18
ended session
# learning rate: 0.1, momentum: 0.9, step_size: 5, gamma: 0.5
saved model: resnet18
Epoch 1/10
train Loss: 0.4430 Acc: 0.8650
Epoch time stamp: 41m 11s
test Loss: 0.0202 Acc: 0.9960
saved model: resnet18
updated model
Epoch time stamp: 42m 34s

.. making it clear that those new training constants have quite significant impact on how it all operates (much better now). As you can see below – it finished in achieving 100% accuracy.

saved model: resnet18
Epoch 1/3
train Loss: 0.0003 Acc: 1.0000
Accuracy: 18000/18000
Epoch time stamp: 22m 11s
test Loss: 0.0001 Acc: 1.0000
Accuracy: 2000/2000
Epoch time stamp: 22m 52s

Jan: Amazing – almost unbelievable. It means that we can identify 20 brightest stars with 100% accuracy now?

Sebi: Not exactly. When we started training on a much bigger dataset and it achieved on the training set just 99% accuracy, while on the testing 95%.

Jan: Why do you think it didn’t reach 100%?

Sebi: Because that dateset is corrupted … definitely.

Jan: I don’t believe you.

Sebi: Checkout this.

Epoch 1/1
failed classes: [('Arcturus', 877), ('Canopus', 157), ('Achernar', 0), ('Acrux', 0), ('Aldebaran', 0), ('Altair', 0), ('Antares', 0), ('Betelgeuse', 0), ('Capella', 0), ('Deneb', 0), ('Fomalhaut', 0), ('Hadar', 0), ('Mimosa', 0), ('Pollux', 0), ('Procyon', 0), ('Rigel', 0), ('Rigel Kentaurus', 0), ('Sirius', 0), ('Spica', 0), ('Vega', 0)]
test Loss: 0.1494 Acc: 0.9483
Accuracy: 18966/20000
Epoch time stamp: 9m 28s

Why do you think it would fail on just two stars?

Jan: That original data set was generated programatically and I am pretty sure there is no problem.

Sebi: Fine, look at this:

Training complete in 0m 25s
Best val Acc: 0.000000
saved model: resnet18
the next epochs are for testing:
Epoch 1/1
failed classes: {'Achernar': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Acrux': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Aldebaran': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Altair': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Antares': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Arcturus': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 79, 'Capella': 798, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Betelgeuse': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Canopus': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 157, 'Spica': 0, 'Vega': 0}, 'Capella': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Deneb': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Fomalhaut': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Hadar': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Mimosa': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Pollux': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Procyon': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Rigel': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Rigel Kentaurus': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Sirius': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Spica': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}, 'Vega': {'Achernar': 0, 'Acrux': 0, 'Aldebaran': 0, 'Altair': 0, 'Antares': 0, 'Arcturus': 0, 'Betelgeuse': 0, 'Canopus': 0, 'Capella': 0, 'Deneb': 0, 'Fomalhaut': 0, 'Hadar': 0, 'Mimosa': 0, 'Pollux': 0, 'Procyon': 0, 'Rigel': 0, 'Rigel Kentaurus': 0, 'Sirius': 0, 'Spica': 0, 'Vega': 0}}
test Loss: 0.1494 Acc: 0.9483
Accuracy: 18966/20000
Epoch time stamp: 9m 55s

… just four stars identification results are strangely incorrect. See the table below.

Jan: Well – it is a neural network, it is ok to have few wrong. I actually think this is an awesome result! Thank you Sebi!

Sebi: FYI I also tried running it with Resnet30 and …

saved model: resnet30
Epoch 1/1
train Loss: 0.0008 Acc: 1.0000
Accuracy: 199995/200000
Epoch time stamp: 549m 7s
failed classes: {"Achernar": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Acrux": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Aldebaran": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 1, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Altair": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Antares": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Arcturus": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 271, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Betelgeuse": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Canopus": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 2, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 840, "Hadar": 0, "Mimosa": 0, "Pollux": 1, "Procyon": 0, "Rigel": 8, "Rigel Kentaurus": 0, "Sirius": 92, "Spica": 0, "Vega": 57}, "Capella": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Deneb": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Fomalhaut": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Hadar": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 1, "Sirius": 0, "Spica": 0, "Vega": 0}, "Mimosa": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Pollux": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 1, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Procyon": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 2, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Rigel": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Rigel Kentaurus": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Sirius": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Spica": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}, "Vega": {"Achernar": 0, "Acrux": 0, "Aldebaran": 0, "Altair": 0, "Antares": 0, "Arcturus": 0, "Betelgeuse": 0, "Canopus": 0, "Capella": 0, "Deneb": 0, "Fomalhaut": 0, "Hadar": 0, "Mimosa": 0, "Pollux": 0, "Procyon": 0, "Rigel": 0, "Rigel Kentaurus": 0, "Sirius": 0, "Spica": 0, "Vega": 0}}
test Loss: 0.7076 Acc: 0.9365
Accuracy: 18729/20000
saved model: resnet30
updated model
Epoch time stamp: 566m 14s
Training complete in 566m 14s
Best val Acc: 0.936450
saved model: resnet30
Epoch 1/1

… here the accuracy got even worse – getting just 94%. I find interesting that those invalid results with Resnet18 are not correlating with Resnet30. It just doesn’t make sense …

Jan: Well for me both results look impressive. Thank you for answering all my questions!

Having this stage of our project covered, what lies in front of us next? We need to finish whole loop here which I currently imagine like this:

  1. Stellarium randomly picks a place on Earth in a certain date-time
  2. Sellarium takes pictures of 4 brightest stars saves those locally and records their inclination
  3. Pre-trained AI attempts to identify all 4 stars and potentially runs some basic checks if those are forming a plausible scenario (can be seen from Earth at some stage / above horizon).
  4. In the next stage application uses inclinations of identified stars and looks up star charts for specified date to identify celestial position.
  5. Finally it translates celestial position to GPS coordinates.

However this needs to wait for another day. πŸ™‚

BBBlimp on Hydrogen Connect Summit Brisbane 2022

On 8th Sept we had an opportunity to present our project on a Hydrogen Connect Summit Brisbane 2022 organised by H2Q.

It was a long day and we met number of awesome people interested in our project. I took few pictures and will keep adding into following gallery as getting more from other sources.

Whole event culminated by presenting our project in front of ~400 summit attendees, including our technological demonstrator.

Sebi & Jan presenting Blimpy

PowerPoint presentation itself here:

Whole team took a picture altogether with organisers.

Huge thanks to H2Q for inviting us to this event. We’ve learned a lot and got some awesome contacts to follow up on with!

UPDATE 2022-09-12: Received this video from Phil – Seb demonstrating our Blimp’s control with ramping up motors to a full power to show off. πŸ˜‰

Troubles with The Arm

Working on our new super-cool gimbal arm for a while we’ve hit a bummer. The most stressed part of our design – the joint between the main arm and attachment jig – apparently is not strong enough to withstand required forces, even more, making it pretty susceptible to a so-called hangar-rash.

It took us couple hours to do a minor design change – running upper connection bolts all the way through the attachment jig to reinforce that whole thing.

Slicing on cura – that’s that easy part.

However running out of filament was a major problem πŸ˜€

Luckily one of our generous sponsors (Vilem) came back with an instant supply of 4 big rolls of it! Thank you Vilem!!! πŸ™‚

Printing it was then easy, it just took whole weekend to get 4 of those as each print took 7hrs.

Bit of sanding + drilling and mounting and voila – it feels much stronger now.

Let’s see where it breaks next time. πŸ™‚ Still, it keeps me wonder how all that 3D printing (Additive Manufacturing) made things possible for us. You mess up / break something and isn’t24 hrs later you have a better or replacement part in your hands without much stress and hustle.

Checking some articles on this I have found this one – Intro to Additive Manufacturing: Prototyping vs. Production, which is very nicely describing challenges of redesigning legacy products and prototyping with 3D printing provides tangible benefits.

They are mentioning 5 key points where 3D printing helps:

  • Accelerate Development TimeΒ – Exceeding the typical design cycle schedule by 3D printing your ideas overnight and having your parts available the next morning.
  • Fail Fast, Fail OftenΒ – 3D printing enables the engineering team to identify mistakes early in the process.Β Products are hardly ever right the first time, this process is mitigating the time lost with rapid prototyping.
  • Cost EffectiveΒ – The traditional methods of developing a prototype can be time consuming and expensive. Multiple fabrication methods, reserving time on production equipment or not having access to the right technology can be costly.
  • Enhanced Creativity – Rapid prototyping is an efficient tool for engineers to quickly evaluate and improve on their ideas.
  • Product TestingΒ – Form, fit and function. Feel theΒ ergonomicsΒ of a new prototype, squeeze pieces to pressure fit an assembly or drop test the part to evaluate functional strength. Easy with 3D printing.

Sounds pretty handy, doesn’t it? πŸ™‚

Training set with Stellarium II

Checking where next with our “super-awesome” 42GB training set, I’ve realized that for a next step we’ll need much lower resolution. We are now on 1920 x 1080, while many of those traditional neuron networks like ResNet are happily coping with much more modest 224 x 224.

So well onto some tiny Python scripting to get our set converted to 224 x 224 – grayscale! Following script does that magic in a matter of 90 minutes.

#!/usr/bin/env python3

from argparse import ArgumentParser

from PIL import Image, ImageOps
import os
from glob import glob

def transform(im, args):
  left = im.width/2-args.width/2*args.scale
  top = im.height/2-args.height/2*args.scale
  right = im.width/2+args.width/2*args.scale
  bottom = im.height/2+args.height/2*args.scale
  return left, top, right, bottom

def crop(args):
  result = [y for x in os.walk(args.input) for y in glob(os.path.join(x[0], '*.png'))]
  counter = 0

  for filename in result:
    with as im:
      im2 = im.crop(transform(im, args))
      im2 = im2.resize((args.width, args.height))
      if args.grayscale:
        im2 = ImageOps.grayscale(im2)
      filename_out = filename.replace(args.input, args.output)
      os.makedirs(os.path.dirname(filename_out), exist_ok=True)
    counter += 1
    pct = round(counter/len(result)*100,4);
    print("Finished processing for", filename_out, "\t[", pct, "%]")

def main():
  parser = ArgumentParser(description='Recursive crop & transformation for image files')

  parser.add_argument('--input', default='./train', help='input folder')
  parser.add_argument('--output', default='./train_224_224_monochrome', help='output folder')
  parser.add_argument('--height', default=224, help='image height')
  parser.add_argument('--width', default=224, help='image width')
  parser.add_argument('--grayscale', default=True, help='convert output to grayscale')
  parser.add_argument('--scale', default=2, help='scale down coefficient')

  args = parser.parse_args()

  print('input  folder: ', args.input)
  print('output folder: ', args.output)

  if args:




This whole operation worked out reducing our initial 48.5GB monster train set to much more convenient 1.4GB.

Result seems to be bit radical, but well it is like it is.

Sirius 1920 x 1080 colour (before)
Sirius 224 x 224 colour (after)

Meanwhile Sebi kept reading and experimenting with machine learning and got some fantastic results there, but that’s for another post. πŸ™‚

Training set with Stellarium

Thinking about training our AI to locate stars Sebi ran a test demonstrating a simplified training set we’ll be needing images of 20 brightest stars – 10.000 per each having:

  • reasonable focus / zoom in & out
  • any possible rotation
  • good quality
  • no atmospheric effects
  • no horizon
  • no planets / sun / moon / satellites

The obvious question came instantly – where to get such a training set for our project? The idea came to use Stellarium.

Stellarium is already a pretty old application (started 2001) which can emulate a planetarium for your computer. It shows a realistic sky in 3D, just like what you see with the naked eye, binoculars or a telescope.

Getting and running Stellarium on a Linux is piece of cake:

$ git clone
$ cd stellarium
$ mkdir -p build/unix
$ cd build/unix
$ cmake -DCMAKE_INSTALL_PREFIX=/opt/stellarium ../.. 
$ make -j4
<wait here>
$ ./src/stellarium

Application throws you straight to a nice screen – showing very late morning in North Brisbane. πŸ™‚

Well, this is a moment when things got little bit more complicated. I had to get familiar with the Stellarium scripting engine, its API, Julian calendar and also PyTorch training set layout and ended up with submitting an actual patch to the Stellarim guys with all that wrapped up.

Let’s start with a patch which seems to be trivial – just allows creating a subfolder when needed:

From 6d2fcc079705385f4803c78902099a38ddc4e89f Mon Sep 17 00:00:00 2001
From: Jan Bilek <>
Date: Sat, 13 Aug 2022 22:22:59 +1000
Subject: [PATCH] Screenshot to attempt to create folder when missing

 src/StelMainView.cpp | 19 ++++++++++---------
 1 file changed, 10 insertions(+), 9 deletions(-)

diff --git a/src/StelMainView.cpp b/src/StelMainView.cpp
index 288832dc75..d72b1c14f6 100644
--- a/src/StelMainView.cpp
+++ b/src/StelMainView.cpp
@@ -1718,16 +1718,17 @@ void StelMainView::doScreenshot(void)
 			screenshotDir = StelFileMgr::getUserDir().append(screenshotDirSuffix);
+		StelApp::getInstance().getSettings()->setValue("main/screenshot_dir", screenshotDir);
+	}
-		try
-		{
-			StelFileMgr::setScreenshotDir(screenshotDir);
-			StelApp::getInstance().getSettings()->setValue("main/screenshot_dir", screenshotDir);
-		}
-		catch (std::runtime_error &e)
-		{
-			qDebug("Error: cannot create screenshot directory: %s", e.what());
-		}
+	//Always check if destination folder exists and attempt to recreate it if not
+	try
+	{
+		StelFileMgr::setScreenshotDir(screenShotDir.isEmpty() ? StelFileMgr::getScreenshotDir() : screenShotDir);
+	}
+	catch (std::runtime_error &e)
+	{
+		qDebug("Error: cannot create screenshot directory: %s", e.what());
 	if (screenShotDir == "")

Don’t get distracted by that – it looks much worse then what it is – it is just couple lines, all the rest is computer generated fluff to make it look cool (and needed to be working with the original code).

Interesting part comes now – following script generates a PyTorch star training set for our celestial navigation project – 20 x 10000 pictures of the brightest stars. πŸ™‚

var stars = ["Sirius", "Canopus", "Arcturus", "Rigel", "Vega", "Capella", "Rigel Kentaurus", "Procyon", "Betelgeuse", "Achernar", "Hadar", "Altair", "Acrux", "Aldebaran", "Spica", "Antares", "Pollux", "Fomalhaut", "Deneb", "Mimosa"]; //20 brightest stars


var pictsInSet = 10000;     //Size of a training set per star
var minMjDay = 50000.000000 //Min Julian calendar date to consider for randomization
var maxMjDay = 60000.000000 //Max Julian calendar date to consider for randomization
var minZoom = 10 	     //Min zoom to consider for randomization
var maxZoom = 30 	     //Max zoom to consider for randomization

//Helper functions
function randomFloat(min, max) {
    return min + (max - min) * Math.random();
function showAStar(cName) {
  core.setMJDay(randomFloat(minMjDay, maxMjDay));
  core.selectObjectByName(cName, true);
  StelMovementMgr.zoomTo(randomFloat(minZoom, maxZoom),0);
  core.wait(0.1); //Making sure that UI has a moment to catch up

//Disabling all

core.wait(1); //Making sure that UI has a moment to catch up

for (i=0; i<stars.length;++i) {
  for (ii=0; ii<pictsInSet; ++ii) {
    var fileName =  ii;
    core.debug(DIR + stars[i] + "/" + fileName +".png");
    core.screenshot(fileName, false, DIR + stars[i], true);


Well then it took almost 5 days (I had some problems with stability), but it worked out well. We’ve ended up with 42.3 GB images of 20 brightest stars – 10.000 each!

Even that directory listing looks impressive!

Adding a first three images of Sirius here for reference.

Huge thanks here belongs to the Stellarium team – this wouldn’t be possible without you!

Next stage – onto some serious machine learning! πŸ™‚

How many candy are in the jar

Sebi came with a new project on his own, which I thought it is perfect – let’s use machine learning to guess how many candy are in the jar!

I love Craion!

Somehow coincidentally few days later we’ve ended up on the Cyber wargaming in a Lego City MeetUp arranged by Chris Boden.

We’ve enjoyed MeetUp with Sebi a lot and had a good chat with guys from TLR afterwards (namely Billy was just awesome white-hat hacker).

We also caught up with Chris B. and while Sebi briefly introduced him to his new project idea, Chris instantly proposed to use today’s state-of-art machine learning computer – Jetson Nano for it!

You never heard of Jetson Nano? Jetson Nano is a small, powerful computer that lets you run multiple neural networks in parallel for applications like image classification, object detection, segmentation, and speech processing. All in an easy-to-use platform that runs in as little as 5 watts – all magic in size of a cigarette pack.

Well, Chris didn’t just told us about it, but instantly gave a call to Ben and asked him on our behalf if we can borrow one … and Ben agreed – apparently having one spare on his desk!

Jetson Nano not idling on Ben’s desk anymore!

We had to visit JayCar next day as we haven’t had a power supply strong enough to feed this hungry beast, but Sebi ended up having it working in couple days. Now just to teach it to count lollies! πŸ˜€

Getting to the end of our post – allow me here to say huge thank you to Chris Bowen and Ben Duncan – your help allowed us to jump instantly forward a mile! It is awesome to have such a great support from you Noosa guys and always feels welcome for more then a decade!

Thank you all again!

16MB ArduCam with Autofocus

Reading through my common news-feed couple months ago, I’ve stumbled on a new 16MB ArduCam with Autofocus.

Even not having any actual usage for it we decided to get one for $29 AUD.

It took us couple months to find a moment to start experimenting with it, but finally Sebi mounted it on our RPI.

I’ve left all the rest with Sebi and simply following How-To from here he made it working in ~30 minutes. And here comes our first picture!

Our first picture with ArduCam!

Not being that impressive we’ve captured one more with Eddie. πŸ™‚

Second ArduCam picture with Eddie

Asking Sebi how he did it I’ve been provided with that link above and following line from terminal:

sudo libcamera-still -o images/test.jpg --autofocus

Gimbal arm Mk II

As per our previous article Gimbal arm, we’ve got seriously misled by this project in past three months. There were too many things happening so let’s just go through several highlights.

Design update

Trying to get around that criteria of having our arm light, but still very firm we’ve progressed for a multiple channels arm:

To a single channel arm with clamps and 20% filling:

Current complete 3D model of our arm then looks like this.


While it sounds like a trivial thingy, it took quite an effort to get it right. The thing you are seeing on picture below is practically just a garbage due to poor design, where each of these prints is ~200g of wasted filament + 24-36hrs of printing.

Printing challenge

While printing became somewhat common for us, some of those latest 36hrs prints came in as something new. Particular problem was our garage sound proofing – it is far from perfect and while it can be happily ignored through the day it resonates through a whole house through the night causing some unwelcome family discussions.

Let’s have a look how such print looks like in a time-lapse.

Here you may ask why it is being printed in that most inefficient way – but trust me, this is the best way we identified. Bending forces imposed through the printing due to the temperature differences can come with an incredible power on these lengths and kept ripping the print in a pieces without any problems.

Well at the end we’ve finished having pretty nice collection. πŸ™‚

Cabling mayhem

Blueprint below depicts our cabling plan. Whatever is there – in short it actually means that we needed to get 7 different cables through the arm’s central shaft to have it all working.

In some cases it took up to several hours to make it happen! Chickening out on our first go – I cut out a bit of material to actually make it happen, but it really wasn’t needed later on.

This included also changes to all the connectors around and new power bridge development (I’m keeping this one out of this post as there is more about it). Anyway gimbals ended up needed rewiring as well.

All four arms assembly and cabling completed looked pretty satisfying. It took us two whole weekends just to do this.

Trusses replacement

In comparison to all the other tasks this was a back to basics. Old steel stabiliser rods connecting our thrusters needed to be replaced with a light-weight wooden variant before mounting in our new arms. This could be considered as a cosmetic, but still took us some effort to do it right.

The last bit missing was to drill an oval hole in each support leg to be able to lead our cabling through it.

New arms

Finally we can present you – new arms! Enjoy!

Aren’t they beautiful?

As always, if you read all the way here, please drop us a note on what you think – your feedback is our best motivation. πŸ™‚

Next time – new 6V and 25V power bridges and let’s get back to the ArduPilot again!

Fun with anemometer

With all those EDFs (Electric Ducted Fans) all around our project (six of them), we’ve been thinking if their design is good or bad and the only thing to work it out seemed to measure their power. Some ideas were pretty wild, till .. you ask Google how to do this. The answer was clear – just get an anemometer!

Kept searching for something less steampunkish and ended up with BENETECH Anemometer GT8907 Sensor with ability to measure wind speeds up to 45m/s (162km/h).

It arrived after after a while, but in an excellent condition couple weeks later.

Well, long story short. Sebi will show you how it went.

So we did three tests and what we’ve found out?

Test #1Test #2Test #3
Tube diameter (mm)55mm6480
ft/min (LFM)3,740.163,346.462,559.06

940.99 m3/hr

261.39 L/s

Well, what we did find out? Unfortunately no idea. I suppose we’ll need to keep these values for reference and see how our model performs first. πŸ™‚

To wrap this up I am adding one more video showing our gimbal test.

Tooli G3 laser cutter – part I

Months ago, when planning for putting together our new gas bags design, I’ve realised that we’ll need something more reliable to do all the cutting. Spreading a word, we suddenly ended up with a Tooli G3 Laser cutter form Toolbotics!

It looks even better on their Ad – CNC, Laser Toolbotics Tooli 3G Launch!

While it may sound like a fantastic news, it ended up being a big story – it actually might be a good time to get yourself a good cup tea before you’ll start reading.

So it begins, we’ve did the obvious first – assembled the tool and ran it!

Even Sebi & Oli had pretty promising go on that.

However we’ve hit the first obstacle here, to be able to do something sensible we needed to produce a gcode which would tell it what to do. Checking on the vendor’s page solution seems to be simple just get their Plotter! Well not that fast as it needs the Art2Gcode program which does not open because it needs the Adobe Flash service, which doesn’t exist for past X years. Bummer.

So, trying to get something we wrote an email to to get some support there. It worked out that Toolbotics practically doesn’t exist anymore and their support is limited. Anyway someone finally responded and we’ve got our hands on that Art2Gcode app and Sebi has been able to run it somehow on his Windows PC.

We’ve loaded resulting Gcode and loaded it on a SD card, plugged it in the device and .. it couldn’t find any files loaded. So we got back to Toolbotics support again, asking for any advice and they came back with:

… and then many more iterations later it became – that it all can be an SD card reader problem. And that was a proper opportunity to check what’s inside that box! πŸ™‚

Very interesting. Information on boards provided us with few ideas. Main thing here – you can see that the board is modified Mega controller from Makerlab. Its description says that it is a Single board solution, Remix of Arduino mega and RAMPS and it is actually pretty cool!

However we are interested mainly in that SD card support (you can see connections in a blue frame saying LCD/SD support with Mini Panel above). Thinking obvious – SD Card reader is broken, we did a quick run to grab a new one for $5.95.

Well, swapping it with the new one changed nothing – no files seems to be loaded / detected by that thing! Getting already bit frustrated, I’ve asked for another advice and been given that it is very likely main board problem and it needs to be replaced. So I asked if we can be provided with a source code or a firmware to attempt to do some debugging ourselves and ended up in some sort of strange Catch 22 situation where firmware cannot be provided as it is their IP so even they recommend board replacement, there is no way to reload it.

This argument seemed to be good enough that Toolbotics support actually ended up providing whole FW source code! Having it checked by Andrew – he discovered that it is based on a Open-Source 3D printer FW called Marlin, just version 1.x, while they already moved forward a bit.

Still, Andrew’s been able to revive it to get it compile again. Next step was obvious – load it! And this is where things are getting interesting a lot again, by loading the firmware provided over the one present in there, everything stopped working, even those things which seemed to be half-working before. Darn.

I’ll keep you hanging here as this is getting pretty long, while rest assured that this story is not over!

Airships with Craiyon

Andrew came back having some fun with CrAIyon – “Craiyon, formerly DALLΒ·E mini, is an AI model that can draw images from any text prompt!”. Some cool pics came out of it, so I put them in a small gallery.

It is fascinating what this technology can do!

UPDATE 2022-09-06 from Viktor K:

father with two sons and dog constructing blimp in the garden next to the house in australia

Let us know if you’ll get some cool ones, we’ll get them posted here as well!

10 things I like about Airship Design by Charles P. Burgess

While reading through the Fatal Flight from Bill Hammack, I’ve noticed numerous references on another book – Airship Design by Charles P. Burgess. I couldn’t resist an opportunity and bought a copy. It arrived shortly and I’ve ended up reading it for past 2 months! There were was so much of interesting information related to our project that I’ve lost track of all of them after while, but dedicated to coming back and do at least a minimal review on some highlights – I came with a plan to pick my top 10 highlights.

While plan is laid, I would still like to start with synopsis on the book’s booklet itself: Originally published in 1927, this volume was intended to fill the dual role of textbook for the student of airship design and handbook for the practical engineer. The design of airships, particularly of the rigid type, is mainly a structural problem; and theoretical aerodynamics has nothing like the relative importance which it bears in airplane design. This is to be expected when we consider that the gross lift of an airship depends solely on the specific gravity of the gas and the bulk of the gas container, and not at all on shape or other aerodynamic characteristics which determine the lift of airplanes. … and it is all there!

Now let’s start with our list itself.

1/ Beautiful historical pictures and schematics

This book is full of them. I’m picking up two of them, bu I’ll keep picking more through our list.

2/ Hydrogen vs. Helium lifting performance.

It is well known that weight difference between weight of Hydrogen and Helium is just about 5%. However it is not that apparent how it translates to the gas lifting power. One of the paragraphs in the Size and Performance section covers this topic in a detail clearly stating that usage of Hydrogen increases overall performance of the airship by incredible 54.5% – this roughly translates into larger payload / reach radius / operations ceiling in general.

Hydrogen increases overall performance of the airship by incredible 54.5%

3/ Testing with models

Imagine 1920s – no computers, 3D visualisation, well … no calculators, no super computers. Pinnacle of the modern technology was mechanical Enigma Machine. What you do? You use wind tunnels to test your aerodynamics, and underwater models for testing all sort of sheering moments and stress forces. Then you’ll come with equations which will describe how all those observations scale up. Then you build it and learn from your mistakes and repeat. Purely amazing!

4/ Venting & Exhaust trunks

Rapid pressure changes caused by the airship’s steep ascend, descend or just gas temperature changes are clearly one of the prime dangers every airship is facing. Practical way to tackle that is to have some good-sized vents which can prevent popping its envelope. Yep, even this is in, including practical calculation example relating to the overall volume.

There is also an equation providing required vent area on the next page together with a description of exhaust trunks for safe Hydrogen venting from gas bags.

5/ Airship size and its performance

You have an airship of a size X and you are designing it to travel with a certain speed, how many horse powers you need? This is called the effect of increased speed – every cyclist has pretty good practical experience with this. With bit of basic calculations and following graphs you’ll know that for your airship being able to carry 16.000lbs payload with speed of 60 knots, you’ll need it to mount it with power plants totalling 1.450Hp.

6/ Slenderness and Elongation

Those two super-important fitness parameters are main parameters coming from airship’s linear dimensions and volume. There parameters have a direct effect on the airship’s performance. This book provides multiple graphs showing these effects, while to my surprise at the end of this chapter it leaves designer free hand as it seems like some of those parameters and their effects are not that clear.

7/ Materials

Let me introduce you to the state-of-art materials for building airship in 1920s: duralumin, high-tensile steel and hick cotton, painted with aircraft dope containing aluminium to reduce solar heating for envelope! While doing its job, all these materials are today superseded new ones which are lighter, stronger and overall more reliable than anything they could imagine that time. It is actually surprising how creative and resourceful engineers of their time were.

8/ Airship resistance – skin friction

There are multiple parameters affecting overall airship resistance – ability to efficiently propagate through the air and operate. While all of them are important, the most interesting for me was the skin friction – the tangential force of the air actin upon the surface of body. While there are equations provided with indications on how to get these forces, apparently any coarse buckram, unglazed fabric or any other surface imperfection can have significant impact on this ranging with up to 15% performance penalty. Still it is recommended to keep testing, keep testing and then learn from other projects.

9/ Build your own

Whole chapter (called Steps in Design) is dedicated for engineers to be able to assist them with step-by-step design which will fulfil certain performance requirements. It again recommends to check on other projects for inspiration and learn from them. All main and expected design aspects are covered, while paying detail to its controls, ballast, mooring mechanism and finishing with details on passenger cars.

10/ Common airship fallacies

One of the most interesting and the final chapter of this book – what not to do when you are designing your airship. The book clearly warns not to use following inventions:

a) The vacuum airship
b) Compressing gas or air for ballast
c) Artificial control of super-heat
d) Combined heavier and lighter-than-air craft
e) Channel through hull to reduce resistance
f) Wind screen at mooring mast

You might be not surprised that except one – we’ve been clearly considering all of them in our design! I’ll leave you to guess which one was it. πŸ™‚

And that’s all! Really enjoyed this book as well written and while technical still very readable. It already found its place in our library and inspired several updates to our design. Thank you Mr. Burges, your effort won’t be forgotten!

Pressure control

In our previous we’ve got all ready – now wrapping it all up – gas cell’s pressure control!

So Sebi prepared this board below to do the job.

There is an Arduino Nano (that central blue-ish thingy), while that main magic is that black micro-chip-y thing, which is actually an ULN2003 16-Pin Darlington Transistor Array IC. This micro-chip integrates something like SUPPRESSION DIODES FOR INDUCTIVE LOADS – which is quite fancy word for a current relays. It is there to allow Arduino to handle higher voltages for selenoid and air pump operations.

Took few pictures when it’s all mounted to our gas cell.

Having it all connected, we did a first pressurisation test.

So Sebi actually did all the design and coding and testing and here is his working code (if you are really trying to give it a go don’t forget to get this library too).

#include <HX710B.h>

HX710B pressure_sensor;

float currentPsi = 0.0807;
float targetPsi = 0.083;
float targetRadius = 0.001;

String conStatus = "normal";

bool moreOverRide = false;
bool lessOverRide = false;

void setup() {
  pressure_sensor.begin(2, 3);
  pinMode(4, OUTPUT); //input
  pinMode(5, OUTPUT); //output
  pinMode(6, OUTPUT); //motor

void loop() {

  if (pressure_sensor.is_ready()) {

    Serial.print("current PSI: ");

    currentPsi = pressure_sensor.psi();

    Serial.print(currentPsi, 4);
    Serial.print(" target PSI: ");
    Serial.println(targetPsi, 4);

    Serial.println((moreOverRide || lessOverRide));

  if (Serial.available()) {
    targetPsi = Serial.readString().toFloat();

  if ((currentPsi + targetRadius < targetPsi) || (moreOverRide && currentPsi < targetPsi)) {

    digitalWrite(4, HIGH);
    digitalWrite(6, HIGH);
    digitalWrite(5, LOW);

    moreOverRide = true;
    lessOverRide = false;

    conStatus = "need more - 4 - on, 5 - off";
  } else if ((currentPsi - targetRadius > targetPsi) || (lessOverRide && currentPsi > targetPsi)) {

    digitalWrite(4, LOW);
    digitalWrite(6, LOW);
    digitalWrite(5, HIGH);

    moreOverRide = false;
    lessOverRide = true;

    conStatus = "need less - 4 - off, 5 - on";
  } else {
    digitalWrite(4, LOW);
    digitalWrite(6, LOW);
    digitalWrite(5, LOW);

    moreOverRide = false;
    lessOverRide = false;

    conStatus = "all good - 4 - off, 5 - off";

Then finally – voila – here comes it all working!

Well, this is it. Now just to build many of them and scale up! πŸ™‚

UPDATE 14th June 2022

Sebi assumed that this is a good opportunity to test his new PCBs and reorganise those electronic parts from breadboard to there.

We also did few more tests with that new setup.

Gas cell

With preps started – in the next stage we started planing for an actual gas bag. Using our brand new whiteboard we’ve drafted this.

Well it might not look that super clear, so I’ve prepared a sketch in the OpenSCAD.

If you are interested, here comes the code.

module GasBagMiddle() {
  D() {
    union() {
      %D() {
        cylinder(25, 25, 25);
        cylinder(25 -.02, 25-.02, 25-.02);
        cylinder(25 +.02, 5, 5);
      D() {
        cylinder(25, 5, 5);
        cylinder(25 -.02, 5-.02, 5-.02);
      for(w=[0:360/n:359]) {
        Rz(w)T(15, 0, 12.5)
        cube([20,0.1,25], center = true);
    cylinder(25+0.02, 5-.01, 5-.01);

You may now wonder what are you looking at, so that is a model of our gas cell – actually the middle one. It will be wrapped around one of our central ducts – that’s why there is that central hole. It also comes with internal partitioning, to support holding a shape and one quarter is missing as it forms an internal opening for airship’s technical background.

So we started drawing and cutting – a lot.

.. and then gluing for several days …

Last picture comes with the adaptor already glued in, but I took couple close ones when gluing it in as well.

Whole that envelope was quite stable so we couldn’t resist and mounted it on our jet cart.

It looked pretty wild already – and it is not over! Stay tuned!

Air pump, clamps and hose adaptor

In preparation of our gas cell project we restocked few parts – couple of air valve selenoids, pack of digital pressure meters and 12V air pump.

First thing first, when pump arrived, I noticed that it is missing some sort of clamp/mount. Got some inspiration from Google pictures around and printed couple of those.

About an hour later got this model out of the OpenSCAD.

After printing it, I couldn’t resist and did another improvement πŸ™‚

.. but then forgot to upload the latest model in a printer, so ended up with the first one anyway. Still it looks pretty good when mounted. What you think?

Second part of our preparations was to come up with an adaptor which would allow connecting three silicon tubes to our gas cell – one for its inflation, one for deflation and third one for a pressure sensor – something like this.

Printed out it came out nicely on the first try.

Sebi dry-connected it to see if is going to do the job.

As before – all happy so we are ready for another stage.

BLIMPS NOW IN 3D – new STL plugin

As a part our our WordPress plan upgrade to Pro, we can now use embedded STL containers. I actually need to test it somewhere so made it in a post here like this. Hope you will like it as you’ll be seeing all those models around much more from now. πŸ™‚

The model below served as a template for all our renderings so far and tested in OpenVSP.

Note: Model is quite large, it may take up to a couple minutes to load, while that new plugin is unfortunately not showing the loading progress.

We have a New whiteboard!

Our generous sponsor supplied us with a brand new whiteboard (Magnetic Glass Board 1500 x 1000mm) so we can finally get bit organised in our tasks with Sebi.

It looks awesome, doesn’t it? Check out left bottom corner – yes, our new whiteboard came without Marker & Eraser Holder – luckily we are proud owners of a 3D printer so it wasn’t a big deal.

This time I resisted to design it myself so went to and grabbed a random one which seemed to be doing well – thank you MagChange! Anyway seeing that STL model, I thought that it would be excellent to have an option to place those on our blog. Again, it wasn’t cheap, but here it comes – our STL plugin premiere!

[woo3dviewer model_url=”//” material_url=”” thumbnail_url=”” canvas_width=”700″ canvas_height=”700″ canvas_border=”true” display_mode=”3d_model” display_mode_mobile=”3d_model” rendered_file_url=”” model_color=”#1e73be” background_color=”#FFFFFF” background_transparency=”false” model_transparency=”opaque” model_shininess=”plastic” show_grid=”true” grid_color=”#898989″ show_ground=”true” ground_color=”#c1c1c1″ show_shadow=”false” show_mirror=”false” auto_rotation=”true” rotation_x=”0″ rotation_y=”0″ rotation_z=”0″ offset_z=”1.4210854715202004e-14″ light_source1=”false” light_source2=”true” light_source3=”false” light_source4=”false” light_source5=”false” light_source6=”true” light_source7=”false” light_source8=”false” light_source9=”false” remember_camera_position=”true” show_controls=”true” camera_position_x=”89.70550924252764″ camera_position_y=”155.951953125″ camera_position_z=”-201.48187258049617″ camera_lookat_x=”-0.33209907840944697″ camera_lookat_y=”-0.5773502691896255″ camera_lookat_z=”0.7459067426872231″ controls_target_x=”0″ controls_target_y=”0″ controls_target_z=”0″]

How do you like it? Pretty fun, isn’t it? Printed out it worked out pretty cool as well.

So that’s it for tonight, huge thanks to Veronika again for her never-ending support!


We’ve been elaborating with Andrew (and others) on how to get some evidence that “it’s going to fly” for a while. Mainly after reading through the “Airship Design” book by Charles P. Burgess some ideas came in. I really liked the one inspired by the book – let’s dive our model in a water!

As nicely summed by Chris “It should be possible to make baby-scale blimps and test them under water to observe accurate performance characteristics (at least of the envelope etc – doesn’t scale so well for the thrusters)”. Quite surprisingly this is exactly what they did 100 years ago.

Anyway, venturing this way would be enormous distraction for our project. So we’ve been thinking about some more modern approach like putting our model in some sort of Game-engine which would test its physics.

Looking around, I found OpenVSP.

OpenVSP, also known as Open Vehicle Sketch Pad, is an open source parametric aircraft geometry tool originally developed by NASA. It can be used to create 3D models of aircraft and to support engineering analysis of those models.


Apparently this tool should have all we need to be able to design and test our airship. I couldn’t wait and refreshed our OpenSCAD airship design and exported it into STL (it took 4 hrs of rendering). OpenVSP was clever enough to import it instantly.

While watching tutoring videos on OpenVSP, I realised that this goes far beyond my 9pm brain capacities (I never thought that propellers are such a science!) so I condescended to sort of randomly applying whatever methods while testing if I’ll get any sensible output.

Comp Geom – Mesh, Intersect, Trim

This above actually seems to be giving all surface areas. As STL model comes in unit-less, output area is the same.

Planar Slicing

This method actually starts giving some interesting values – yet again without any units – looks like it is possible to get cross-section areas through our whole airship. These values are absolutely essential to be able to predict airship’s performance through its surface area coefficients and also the slenderness ratio.

Mass properties

Well, here I am sort of lost. I actually think that OpenVSP assumes here that our airship is filled with some sort of material, while it is practically hollow. Still it gave interesting graphical output.

Parasitic Drag

Browsing through all analysis available – I reached the most interesting one – a Parasitic drag. Getting a help from Wikipedia:

Parasitic drag, also known as profile drag,β€Š is a type of aerodynamic drag that acts on any object when the object is moving through a fluid. Parasitic drag is a combination of form drag and skin friction drag. It affects all objects regardless of whether they are capable of generating lift.

Total drag on an aircraft is made up of parasitic drag and lift-induced drag. Parasitic drag comprises all types of drag except lift-induced drag.


Unfortunately I haven’t been able to convince OpenVSP to give me any reasonable output on this as it somehow needs to work with a “reference wing” model.

OpenVSP link on the tool description here.

Thinking about OpenVSP now, it’s been an interesting trip. This is clearly a complex and powerful application which can help a lot. Question here is, if it is worth time-investment at this stage, or we can utilise our time better (building a physical model). Whatever it’s going to be, we are not seeing this application for the last time!

Gimbal arm Mk I

As softly indicated in our Rendering post, our current focus is on designing a gimbal arms. These will serve to extend gimbals configuration away from gondola and align them with main thrusters.

To be able to print such a monster (60+ cm end to end) it got separated in three sections.

1/ The gondola clamp

I think that pictures will tell more than explaining here what and how.

Overall this clamp is pretty stable and does the job, however joint with that mid-section is too klanky and needs re-designing.

2/ Mid arm section

How simple it looks, it took several hours to design a shape which starts with ellipse cross-section of one size (6cm) and skews sideways into another ellipse of a different size (4cm). I am not going to spam here with a code for the others, but I am actually pretty proud of this part so here it comes!


#parameterised 2D ellipse
function ellipse(r1, r2) = [for (phi = [1 : 1 : 360]) [r1 * cos(phi), r2 * sin(phi)]];

module tube(r1, r2, rr, R, th) {
  D() {
      ellipse((r1+(r1-r2)/fn*i)/rr, r1+(r1-r2)/fn*i))]);
    assign(r1 = r1-th,r2 = r2-th)
      ellipse((r1+(r1-r2)/fn*i)/rr, r1+(r1-r2)/fn*i))]);

module mid_section() {
  D() {
    Ry(10) {
     skew([0, 0, 45, 0, 0, 0])
     tube(r1 = 3, r2 = 1.5, rr = 6, R = 25, th = 1);


Design above allows printing all that monstrosity quite efficiently “laying flat”

It clearly was our longest print so far – it took 18hrs – and seeing how that worked out it already needs redesign. An internal reinforcing rib is needed and joints on both sides needs some sort of clamps to be more solid.

3/ Gimbal adapter section

When having that above mid-section in hands, designing this part was much easier. Practically the same, but smaller + bit of fun when scaling cylinders to make them fit that ellipsoid cross-section. Problematic part came when attempting to join it with the gimbal as there was not enough material to make me confident about it withstand some reasonable sheer forces (400N). Simple “test by Ondrej” demonstrated that I was right.

Video from printing here:

Anyway, at the end it all clicked together (with bit of sanding, heating and persuasion) to the point where we could demonstrate its first prototype!

As you can see above, there is quite a sag on that arm which is caused by multiple design weaknesses, but mainly the joint between the main clamp and the long arm. That needs to be surely reinforced. I am still liking it as it really looks pretty close to that original rendering, just this is real! πŸ˜€

As always, let us know your ideas or any comments whatsoever, all welcome!

3D part for Vilda’s car

Vilda, knowing that we are having too much fun with our 3D printer, asked us for a favour and improve one of his car’s slots with power sockets. Well me, knowing that Sebi is having too much fun with our 3D printer, asked him to do that job for Vilda! πŸ™‚

Anyway, Sebi spent few day on in and it worked out so well that I decided to document it in one of our posts. Here comes screens from OpenSCAD.

This is how it worked out in a slicer (CURA).

As always it took “just” few hours to print it and we’ve ended up with a final product!