Zoom on a Pi 4 (4GB)

It works using¬†chromium not the Zoom app (which only runs on x86, not ARM). I tested it with a two-person, two-video stream call. You need a screen (I happened to have a spare 7″ touchscreen). You also need a keyboard for the initial setup, and a mouse if you don’t have a touchscreen.

The really nice thing is that Video4Linux (bcm2835-v4l2) support has improved so it works with both v1 and v2 raspi cameras, and no need for options bcm2835-v4l2 gst_v4l2src_is_broken=1 ūüéČūüéČ



  • Install Raspian Buster
  • Connect the screen keyboard, mouse, camera and speaker/mic. I used a Sennheiser usb speaker / mic, and a standard 2.1 Raspberry pi camera.
  • Boot up.¬†I had to add lcd_rotate=2 in /boot/config.txt for my screen to rotate it 180 degrees.
  • Don’t forget to enable the camera in raspi-config
  • Enable bcm2835-v4l2 – add it to sudo nano /etc/modules
  • I increased swapsize using¬†sudo nano /etc/dphys-swapfile ->¬†CONF_SWAPSIZE=2000 ->¬†sudo /etc/init.d/dphys-swapfile restart
  • I increased GPU memory using sudo nano¬†/boot/config.txt ->¬†gpu_mem=512

You’ll need to set up Zoom and pass capchas using the keyboard and mouse. Once you have logged into Zoom you can often ssh in and start it remotely like this:

export DISPLAY=:0.0
/usr/bin/chromium-browser --kiosk --disable-infobars --disable-session-crashed-bubble --no-first-run https://zoom.us/wc/XXXXXXXXXX/join/

Note the url format – this is what you get when you click “join from my browser”. If you use the standard Zoom url you’ll need to click this url yourself, ignoring the Open xdg-open prompts.


You’ll still need to select the audio and start the video, including allowing it in the browser. You might need to select the correct audio and video, but I didn’t need to.

I experimented a bit with an ancient logitech webcam-speaker-mic and the speaker-mic part worked and video started but stalled – which made me think that a better / more recent webcam might just work.

Removing rivets

I wanted to stay away from the computer during a week off work so I had a plan to fix up some garden chairs whose wooden slats had gone rotten:


Looking more closely I realised the slats were riveted on. How do you get rivets off? I asked my hackspace buddies and Barney suggested drilling them out. They have an indentation in the back and you don’t have to drill very far to get them out.

The first chair took me two hours to drill out 15 rivets, and was a frustrating and sweaty experience. I checked YouTube to make sure I wasn’t doing anything stupid and tried a few different drill bits. My last chair today took 15 minutes, so!¬†My amateurish top tips / reminder for me next time:

  1. Find a drill bit the same size as the hole that the rivet’s gone though
  2. Make sure it’s a tough drill bit, and not too pointy. You are trying to pop off the bottom end of the rivet – it comes off like a ring – and not drill a hole into the rivet itself.
  3. Wear eye protection – there’s the potential for little bits of sharp metal to be flying around
  4. Give it some welly – I found it was really fast once I started to put some pressure on the drill
  5. Get the angle right – it seemed to work best when I was drilling exactly vertically down into to the rivet, and not at a slight angle.
  6. Once drilled, you might need to pop them out with a screwdriver or something of the right width plus a hammer


More about rivets.

Pi / openCV / Tensorflow again

Cat detector code updated for Raspian Buster. I used lite. A few things have changed since the last time. The code is here.

Download Raspian

I got Raspbian Buster Lite (from https://www.raspberrypi.org/downloads/raspbian/ )

Burn it onto a SD card.

touch /Volumes/boot/ssh
Add the wifi
nano /Volumes/boot/wpa_supplicant.conf

The file should containing something like:

ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev

then eject the card and put it in the pi.

ssh into it from your laptop

password: raspberry
sudo nano /etc/hosts
sudo nano /etc/hostname


sudo reboot

Set up a virtualenv for python

This is not strictly necessary but keeps things tidy. You can also just use the built in python, just make sure you are using python3 and pip3 if so.

ssh into the pi again, then:

sudo apt update
sudo apt-get install python3-pip
sudo pip3 install virtualenv
virtualenv env
source env/bin/activate
(env) pi@birdbot:~ $ python --version
Python 3.7.3 # or similar

Enable the camera

sudo raspi-config # and enable camera under 'interfacing'; reboot

Install Tensorflow

Increase the swap size:

sudo nano /etc/dphys-swapfile

The default value in Raspbian is:


We will need to change this to:


Restart the service that manages the swapfile own Raspbian:

sudo /etc/init.d/dphys-swapfile restart

Install tensorflow dependencies

sudo apt-get install libatlas-base-dev
sudo apt-get install git
pip install --upgrade tensorflow

(this takes a few minutes)

Test that tensorflow installed ok:

python -c "import tensorflow as tf; tf.enable_eager_execution(); print(tf.reduce_sum(tf.random_normal([1000, 1000])))"

You may see an error about hadoop –

HadoopFileSystem load error: libhdfs.so: cannot open shared object file: No such file or directory.

See also tensorflow/tensorflow#36141 and tensorflow/tensorflow#36141. That doesn’t seem to matter.

You could try some user built tensorflow binaries – I tried this one, which seemed to corrupt my SD card, but not tried this one. Tensorflow 2 would be better to learn (the apis all changed between 1.4 and 2).

Install OpenCV

sudo apt-get install libjasper-dev libqtgui4 libqt4-test libhdf5-dev libharfbuzz0b libilmbase-dev libopenexr-dev libgstreamer1.0-dev libavcodec-dev libavformat-dev libswscale5

pip install opencv-contrib-python== #(see this)


python -c 'import cv2; print(cv2.__version__)'

Install camera dependencies

pip install imutils picamera

Install speaking dependencies

sudo apt-get install espeak-ng


git clone https://github.com/libbymiller/cat_loving_robot
cd cat_loving_robot
python classify_image.py

If you want to add the servos and so on for cat detecting and running towards cats, or start it up automatically, there’s more info in github.

Links for my Pervasive Media Studio talk

I’m giving a talk this afternoon at the Pervasive Media Studio in Bristol about some low-resolution people experiments I’ve been making. Here are some related links:

Exploring the Affect of Abstract Motion in Social Human-Robot Interaction, John Harris and Ehud Sharlin

Libbybot 11 code and instructions

Zamia and scripts for running it on the Raspberry Pi 3

Real_libby code


Matt Jones – Butlers or Centaurs

Tim Cowlishaw‘s colab notebook for GPT-2 retraining

Janelle Shane‘s site and book

This is the voice synthesis code that my colleagues used. They are hoping to open up their version of it soon.

There’s a¬†voice assistant survey that uses the synthesised voices and there’s a blog post about that.

Pimoroni sell Respeaker 4-mics.

I’m libbymiller on twitter.


Tensorflow – saveModel for tflite

I want to convert an existing model to one that will run on a USB stick ‘accelerator’ called Coral. Conversion to tflite is needed for any small devices like these.

I’ve not managed this yet, but here are some notes. ¬†I’ve figured out some of it, but come unstuck in that some operations (‘ops’) are not supported in tflite yet. But maybe this is still useful to someone, and I want to remember what I did.

I’m trying to change a tensorflow model – for which I only have .meta and .index files – to one with .pb files or variables, which seems to be called a ‘savedModel’. These have some interoperability, and appear to be a prerequisite for making a tflite model.

Here’s what I have to start with:

ls models/LJ01-1/

Conversion to SavedModel

First, create a savedModel (this code is for Tensorflow 1.3, but 2.0 is a simple conversion using a command-line tool).

import tensorflow as tf
model_path = 'LJ01-1/model_gs_933k'
output_node_names = ['Merge_1/MergeSummary']    
loaded_graph = tf.Graph()

with tf.Session(graph=loaded_graph) as sess:
    # Restore the graph
    saver = tf.train.import_meta_graph(model_path+'.meta')
    # Load weights
    # Freeze the graph
    frozen_graph_def = tf.graph_util.convert_variables_to_constants(

    builder = tf.saved_model.builder.SavedModelBuilder('new_models')
    op = sess.graph.get_operations()
    input_tensor = [m.values() for m in op][1][0]
    output_tensor = [m.values() for m in op][len(op)-1][0]

    # https://sthalles.github.io/serving_tensorflow_models/
    tensor_info_input = tf.saved_model.utils.build_tensor_info(input_tensor)
    tensor_info_output = tf.saved_model.utils.build_tensor_info(output_tensor)
    prediction_signature = (
         inputs={'x_input': tensor_info_input},
         outputs={'y_output': tensor_info_output},
    builder.add_meta_graph_and_variables(sess, [tf.saved_model.tag_constants.SERVING],

I used

output_node_names = [n.name for n in tf.get_default_graph().as_graph_def().node]

to find out the names of the input and output ops.

That gives you a directory (new_models) like


Conversion to tflite

Once you have that, then you can use the command-line tool tflite_convert (examples) –¬†

tflite_convert --saved_model_dir=new_models --output_file=model.tflite --enable_select_tf_ops

This does the conversion to tflite. And it will probably fail, e.g. mine did this:

Some of the operators in the model are not supported by the standard TensorFlow Lite runtime. If those are native TensorFlow operators, you might be able to use the extended runtime by passing --enable_select_tf_ops, or by setting target_ops=TFLITE_BUILTINS,SELECT_TF_OPS when calling tf.lite.TFLiteConverter(). Otherwise, if you have a custom implementation for them you can disable this error with --allow_custom_ops, or by setting allow_custom_ops=True when calling tf.lite.TFLiteConverter(). Here is a list of builtin operators you are using: ABS, ADD, CAST, CONCATENATION, CONV_2D, DIV, EXP, EXPAND_DIMS, FLOOR, GATHER, GREATER_EQUAL, LOGISTIC, MEAN, MUL, NEG, NOT_EQUAL, PAD, PADV2, RSQRT, SELECT, SHAPE, SOFTMAX, SPLIT, SQUARED_DIFFERENCE, SQUEEZE, STRIDED_SLICE, SUB, SUM, TRANSPOSE, ZEROS_LIKE. Here is a list of operators for which you will need custom implementations: BatchMatMul, FIFOQueueV2, ImageSummary, Log1p, MergeSummary, PaddingFIFOQueueV2, QueueDequeueV2, QueueSizeV2, RandomUniform, ScalarSummary.

You can add¬†–allow_custom_ops to that, which will let everything through – but it still won’t work if there are ops that are not tflite supported – you have to write custom operators for the ones that don’t yet work (I’ve not tried this).

But it’s still useful to use –allow_custom_ops, i.e.

tflite_convert --saved_model_dir=new_models --output_file=model.tflite --enable_select_tf_ops --allow_custom_ops

because you can visualise the graph once you have a tflite file, using¬†netron. Which is quite interesting, although I suspect it doesn’t work for the bits which it passed through but doesn’t support.

>>> import netron 
>>> netron.start('model.tflite')
Serving 'model.tflite' at http://localhost:8080

Update – I forgot a link:

“This document outlines how to use TensorFlow Lite with select TensorFlow ops.¬†Note that this feature is experimental and is under active development.¬†“