Using ffmpeg to take a snapshot with a webcam in Windows

I downloaded the BtbN Windows build of ffmpeg from here:

The specific file I downloaded was:

  • ffmpeg-n4.4.1-2-gcc33e73618-win64-gpl-4.4.zip (download)

I extracted the zip file, then opened a command window in the “bin” folder where ffmpeg.exe was located and took a photo with camera 0 (the default video capture device) using the following command:

ffmpeg.exe -f vfwcap -i 0 -vframes 1 out.jpg

To do the same without all the text output:

ffmpeg.exe -nostats -loglevel 0 -f vfwcap -i 0 -vframes 1 out.jpg

It should be possible to move ffmpeg.exe to whatever folder you want to take photos in.

Using DirectShow

To list available DirectShow devices:

ffmpeg -list_devices true -f dshow -i dummy

If a suitable device is listed, use that device name in the following command (instead of “Integrated Webcam”) to snap a photo from the camera:

ffmpeg -f dshow -i video="Integrated Webcam" out.jpg
Posted in Uncategorized | Leave a comment

Using a serial byte sent from an Arduino to launch an application on the PC

One of my students is building an Arduino-based robotic system that needs to run a program on the PC to do some image capture. The Python script below bridges the gap. It opens a serial port (you need to specify the COM port that Windows has assigned to the Arduino), then processes incoming characters, waiting for a particular character (‘p’) to be received. When a ‘p’ is received, it launches an application using os.system. In this example, the application launched is Notepad, but it could be whatever you want.

import serial
import os

# Open whatever COM port is assigned to the Arduino
# at the same baudrate that the Arduino is using
with serial.Serial('COM5', 9600, timeout=1) as ser:
    # Now process incoming characters one at a time forever
    while 1:
        c = ser.read()          # read one byte
        print(c)                # print out the byte
        print('\n')             # print new line
        if c == b'p':
            # The received byte was a 'p' character
            # so launch an application on the PC
            os.system("notepad.exe")
            print("Got a P\n")
        else:
            # The received byte (if any) was not a 'p'
            print("Not a P\n")

To test the Python program, I uploaded the following tiny program to the Arduino to send a ‘p’ character once every ten seconds.

void setup()
{
  Serial.begin(9600);
}

void loop()
{
  delay(10000);
  Serial.print("p");
}
Posted in Uncategorized | Leave a comment

Generating variations on a code analysis question in a Brightspace quiz

NB This post is a work in progress.

Download example files: example_question.zip

This post documents the process I used to programmatically generate multiple variations on a quiz question for the Brightspace virtual learning environment. The quiz question displays an example C program and asks what number it will print out. In each generated variation of the question, placeholder variable names (VAR1, VAR2, VAR3) and numerical values (VAL1, VAL2, VAL3) are replaced by different strings.

The quiz feature in Brightspace provides an “Arithmetic” question type that allows certain values in the question to be randomised and the correct answer to be calculated with a user-specified formula. Unfortunately, the set of supported mathematical operators is very limited. In my code analysis questions, changing the numerical values in the code can alter the flow of control through the program. This forces students to trace the flow of control very carefully, even when attempting the quiz multiple times. Calculating the value printed by each version of the program is more complex than what Brightspace’s Arithmetic question type supports.

My alternative approach for programmatically generating many variations of a question that use the same code structure:

  • Write template code (similar to that shown below) in which some variables and numerical values are represented by predefined text markers. Save this file as “code.c”.
  • Programmatically generate a CSV file, “questions_list.csv”, in which each line contains a list of variable names and numerical values to be substituted into the template code to create a unique version of the program.
  • Automatically process “questions_list.csv”, generating a different copy of the template code for each variation (“generated_code.c”). In each generated code file, the text markers (VAR1, VAL1, etc.) are replaced by the values specified in the corresponding line of “questions_list.csv”.
  • Using ImageMagick, generate a PNG image of the code in “generated_code.c”. This not only ensures that the code will appear neatly formatted in the Brightspace quiz, but also prevents students from copying and pasting the code into a file and running it.
  • Automatically compile each copy of “generated_code.c” using GCC, then automatically run the program and pipe its output into a file “answer.txt”.
  • Generate another file, “brightspace_import_file.csv”, which contains a paragraph of comma-delimited data for each variation of the question (including the filename of the corresponding PNG image, the correct program output, etc.).
  • Upload all PNG images of the generated code to the Brightspace module.
  • Import “brightspace_import_file.csv” into the Question Library of the Brightspace module so that the questions can then be added to a quiz.

Example template code (“code.c”) for a set of variations on the same code analysis question:

#include <stdio.h>

int main()
{
    int VAR1=VAL1, VAR2=VAL2, VAR3=VAL3;
    
    VAR3 = VAL1;
    if (VAL2)
    {
        VAR3 = VAL2;
    }
    
    printf("%d", VAR3);

    return 0;
}

Shown below is an example PNG image produced from one variation on the code template above. “VAR1” has been replaced by “alice”, “VAL1” has been replaced by “-1”, and so on. The modified code has been automatically rendered as a PNG image using ImageMagick. Students are asked to identify the number that the program prints out.

By automating the process of generating variations on a single code template, it’s possible to create code analysis quizzes that remain challenging to students even when attempted multiple times.

Preparation

On my computer (running Xubuntu Linux 20.04), using ImageMagick to generate a PNG image from each file of example C code required a change to its default security policy. As root, I commented out the following line in /etc/ImageMagick-6/policy.xml

<policy domain="path" rights="none" pattern="@*"/>

Generating the question list file

The following Python program is used to generate a question list file, with a line of text describing each variation on the question.

#
# generate_list.py - written by Ted Burke - 30-Nov-2021
#
# To generate a list of question variations:
#
#    python3 generate_list.py
#

from random import randrange

names = [('x','y','z'),('a','b','c'),('alice','bob','charlie')]

# This csv file will be imported into the Brightspace quiz
with open('question_list.csv', 'w') as brightspace_import_file:
    # Write a line to define the question base id, question title, question text
    brightspace_import_file.write('COMP2109-FlowOfControl-If1,if statements,What number will this program print?,,\n')
    # Write a line to define the text markers that will be substituted in the example code
    brightspace_import_file.write('VAR1,VAR2,VAR3,VAL1,VAL2,VAL3\n')
    for n in range(20):
        # Randomly select one of the three sets of variable names
        name1,name2,name3 = names[randrange(3)]
        # The following line can be edited to control the numerical range of each value generated in the code
        value1,value2,value3 = str(randrange(20)-10), str(randrange(20)-10), str(randrange(20)-10)
        brightspace_import_file.write(name1 + ',' + name2 + ',' + name3 + ',' + value1 + ',' + value2 + ',' + value3 + '\n') 

Shown below is an example of the “question_list.csv” file produced by the above Python program. With the exception of the first two lines, each line defines a different variation on the question. The first three items on each line are the strings to substitute for VAR1, VAR2 and VAR3. The last three items on each line are the strings to substitute for VAL1, VAL2 and VAL3. Different text markers (as many or few as required) can be used by editing line 2.

COMP2109-FlowOfControl-If1,if statements,What number will this program print?,,
VAR1,VAR2,VAR3,VAL1,VAL2,VAL3
alice,bob,charlie,-5,6,0
a,b,c,1,6,6
alice,bob,charlie,-7,1,-3
a,b,c,-4,-6,4
x,y,z,0,3,-10
alice,bob,charlie,3,-9,-7
alice,bob,charlie,-10,-8,0
x,y,z,-6,-5,8
x,y,z,-3,2,2
a,b,c,-2,2,2
alice,bob,charlie,-9,-1,-9
a,b,c,4,-1,8
x,y,z,9,6,8
a,b,c,-10,4,0
x,y,z,-2,4,1
a,b,c,-3,3,-6
alice,bob,charlie,-7,9,-9
alice,bob,charlie,-9,-10,-1
x,y,z,7,-6,4
alice,bob,charlie,8,-4,2

The first line of the above file specifies three items:

  1. The base question ID string. Each generated variation of the question will be assigned a unique question ID by appending a three-digit number to the base ID string.
  2. The question title that will be displayed in the Question Library in Brightspace.
  3. The question text that students will see when taking the quiz.

Generating the question variations – PNG images and Brightspace CSV import file

Once the file “question_list.csv” is generated, we’re ready to generate the code PNG images and the CSV file containing the information for each question which will be used to import the question variations into Brightspace.

#
# generate_questions.py - written by Ted Burke - 30-Nov-2021
#
# To generate variations on a single question:
#
#    python3 generate_questions.py
#

import os

# This csv file will be imported into the Brightspace quiz
with open('brightspace_import_file.csv', 'w') as brightspace_import_file:

    # Process the question generation list
    with open('question_list.csv', 'r') as list_file:
        # Read all lines from file
        lines = list_file.readlines()
        
        # First line contains question id, title and text
        question_info = lines[0].strip().split(',')
        question_id_base = question_info[0]
        question_title = question_info[1]
        question_text = question_info[2]
        
        # Second line contains the text markers to be replaced in the code
        keys = lines[1].strip().split(',')
        
        # Now generate many variations on the question
        count = 0
        for line in lines:
            # Only begin generating question variations on the third line
            count = count + 1
            if count <= 2:
                continue
            
            # Generate the unique question ID
            question_id = question_id_base + '-' + str(count-2).zfill(3)
            print('Generating question ' + question_id)
            
            # Parse values from current line for next variation of the question
            values = line.strip().split(',')
            
            # Read in the original code from file code.c
            with open('code.c', 'r') as code_file :
                code = code_file.read()
            
            # Replace the marker strings in the code
            for key,value in zip(keys,values):
                code = code.replace(key, value)
            
            # Write the updated code to generated_code.c
            with open('generated_code.c', 'w') as file:
                file.write(code)
                
            # Generate a png image of the code
            os.system('convert -size 1000x1000 xc:white -font "FreeMono" -pointsize 16 -fill black -annotate +15+15 @generated_code.c -trim -bordercolor "#FFF" -border 10 +repage ' + question_id + '.png')
            
            # Compile and run the generated code to get question answer
            os.system('gcc generated_code.c -o prog')
            os.system('./prog > answer.txt')
            with open('answer.txt', 'r') as answer_file :
                question_answer = answer_file.read()
            
            # Write the question data to brightspace_import_file.csv
            brightspace_import_file.write('NewQuestion,SA,,,\n')
            brightspace_import_file.write('ID,' + question_id + ',,,\n')
            brightspace_import_file.write('Title,' + question_id + ',,,\n')
            brightspace_import_file.write('QuestionText,' + question_text + ',,,\n')
            brightspace_import_file.write('Points,1,,,\n')
            brightspace_import_file.write('Difficulty,1,,,\n')
            brightspace_import_file.write('Image,images/' + question_id_base + '/' + question_id + '.png,,,\n')
            brightspace_import_file.write('InputBox,1,6,,\n')
            brightspace_import_file.write('Answer,100,' + question_answer + ',,\n')
            brightspace_import_file.write('Hint,,,,\n')
            brightspace_import_file.write('Feedback,,,,\n')
            brightspace_import_file.write(',,,,\n')

Shown below is an example of the Brightspace CSV import file, “brightspace_import_file.csv”. This is generated by the program above.

NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-001,,,
Title,COMP2109-FlowOfControl-If1-001,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-001.png,,,
InputBox,1,6,,
Answer,100,6,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-002,,,
Title,COMP2109-FlowOfControl-If1-002,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-002.png,,,
InputBox,1,6,,
Answer,100,6,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-003,,,
Title,COMP2109-FlowOfControl-If1-003,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-003.png,,,
InputBox,1,6,,
Answer,100,1,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-004,,,
Title,COMP2109-FlowOfControl-If1-004,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-004.png,,,
InputBox,1,6,,
Answer,100,-6,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-005,,,
Title,COMP2109-FlowOfControl-If1-005,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-005.png,,,
InputBox,1,6,,
Answer,100,3,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-006,,,
Title,COMP2109-FlowOfControl-If1-006,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-006.png,,,
InputBox,1,6,,
Answer,100,-9,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-007,,,
Title,COMP2109-FlowOfControl-If1-007,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-007.png,,,
InputBox,1,6,,
Answer,100,-8,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-008,,,
Title,COMP2109-FlowOfControl-If1-008,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-008.png,,,
InputBox,1,6,,
Answer,100,-5,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-009,,,
Title,COMP2109-FlowOfControl-If1-009,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-009.png,,,
InputBox,1,6,,
Answer,100,2,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-010,,,
Title,COMP2109-FlowOfControl-If1-010,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-010.png,,,
InputBox,1,6,,
Answer,100,2,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-011,,,
Title,COMP2109-FlowOfControl-If1-011,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-011.png,,,
InputBox,1,6,,
Answer,100,-1,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-012,,,
Title,COMP2109-FlowOfControl-If1-012,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-012.png,,,
InputBox,1,6,,
Answer,100,-1,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-013,,,
Title,COMP2109-FlowOfControl-If1-013,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-013.png,,,
InputBox,1,6,,
Answer,100,6,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-014,,,
Title,COMP2109-FlowOfControl-If1-014,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-014.png,,,
InputBox,1,6,,
Answer,100,4,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-015,,,
Title,COMP2109-FlowOfControl-If1-015,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-015.png,,,
InputBox,1,6,,
Answer,100,4,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-016,,,
Title,COMP2109-FlowOfControl-If1-016,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-016.png,,,
InputBox,1,6,,
Answer,100,3,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-017,,,
Title,COMP2109-FlowOfControl-If1-017,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-017.png,,,
InputBox,1,6,,
Answer,100,9,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-018,,,
Title,COMP2109-FlowOfControl-If1-018,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-018.png,,,
InputBox,1,6,,
Answer,100,-10,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-019,,,
Title,COMP2109-FlowOfControl-If1-019,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-019.png,,,
InputBox,1,6,,
Answer,100,-6,,
Hint,,,,
Feedback,,,,
,,,,
NewQuestion,SA,,,
ID,COMP2109-FlowOfControl-If1-020,,,
Title,COMP2109-FlowOfControl-If1-020,,,
QuestionText,What number will this program print?,,,
Points,1,,,
Difficulty,1,,,
Image,images/COMP2109-FlowOfControl-If1/COMP2109-FlowOfControl-If1-020.png,,,
InputBox,1,6,,
Answer,100,-4,,
Hint,,,,
Feedback,,,,
,,,,

Uploading the question variations to Brightspace

  • Go to the Brightspace module in which you want to create the quiz.
  • Go to “Module Tools -> Module Admin -> Manage Files”.
  • Click into the “images” folder.
  • Create a new folder with the same name as the base id of the question pool, e.g. “COMP2109-FlowOfControl-If1”.
  • Click into the new folder.
  • Click “Upload” and select the 20 code images generated above.
  • Go to “Assessment -> Quizzes -> Question Library”.
  • Click “New -> Section”.
  • Set the “Section Title” to the base id of the new question, tick the check boxes for “Hide Section Title from learners” and “Hide Section Text from learners”, and click “Save”.
  • Click into the newly created section to navigate into it, then click on “Import -> Upload a File” and select the CSV file generated above, “brightspace_import_file.csv”.
  • Go to “Assessment -> Quizzes” and click on the quiz to which you want to add the new question.
  • Click “Add/Edit Questions”.
  • Click “Add -> Question Pool”.
  • Set the question pool title to the base id of the new question, e.g. “COMP2109-FlowOfControl-If1”.
  • Click “Browse Question Library”.
  • Tick the check box beside the question library section of the new question, e.g. “COMP2109-FlowOfControl-If1” and click “Import” to add all question variations to the question pool for the new question.

Posted in Uncategorized | Leave a comment

How to merge two Garmin .fit files and upload to Strava on Ubuntu

First, install gpsbabel and (optionally) gpxviewer.

sudo apt install gpsbabel gpxviewer

Now, use gpsbabel to combine the two .fit files into a single .gpx file.

gpsbabel -i garmin_fit -f B3781326.FIT -f B3792523.FIT -o gpx -F merged.gpx

gpxviewer can be used to check the merged activity on a map.

gpxviewer merged.gpx

The merged activity file can now be uploaded to Strava using the “Upload activity” menu option.

The data in the merged file will be as if the watch was paused at the end of the first recording and restarted at the start of the second activity. Hence, the intervening time will be included in Strava’s elapsed time for the activity, but will be excluded from the moving time. To fully merge the two activities so that there is no discontinuity, the .gpx file can be edited in a text editor. Simply remove the closing </trk> tag of the first activity and the opening <trk> tag of the second activity, which will appear on successive lines, then save and upload to Strava.

Posted in Uncategorized | Leave a comment

Moving statue of David Foster Wallace

I’m experimenting with adding different types of animated noise to static images to create an illusion of movement. Nothing very successful so far, but I’m enjoying the periodic modulation of the pixelation effect on this picture of David Foster Wallace. I’m just making a snapshot of this experiment now so that I can return to it later.

Click on the gif below to view it at full size. The original image is included below the code.

//
// pixelator.c - a funny kinda pixelator
// Written by Ted Burke - 11-11-2019
//
// This program loads an image from a PNM file ("dfw1280.pnm"),
// then outputs a series of images, each of which is a "pixelated"
// version of the original. The pixelation process consists of
// iterating through every pixel in the image in random order,
// replacing each one with a dot of the same colour, but of
// random size at the same x,y location. The maximum dot size
// varies from frame to frame.
//
// To compile:
//
//    gcc -o pixelator pixelator.c -lm
//
// To combine frames into a single gif using ImageMagick:
//
//    convert -loop 0 -delay 12 frame* dfw.gif
//

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define W 1280
#define H 842

// Pixel buffers for input and output images
unsigned char pi[H][W][3];
unsigned char po[H][W][3];

// An array of these stuctures is used to store the image pixels
// in a sortable form. When the order of the pixels is shuffled,
// each pixel in the array remembers its colour and x,y position.
typedef struct pixel
{
    int x;
    int y;
    int z;
    unsigned char r;
    unsigned char g;
    unsigned char b;
} Pixel;

Pixel px[W*H];

// This function is used by qsort to sort the pixels by z order
int zcompare(const void *p1, const void *p2)
{
    if (((const Pixel *)p1)->z > ((const Pixel *)p2)->z) return 1;
    else if (((const Pixel *)p1)->z < ((const Pixel *)p2)->z) return -1;
    return 0;
}

// This function renders a dot of specified size and color at
// the specified location in the output image pixel buffer 
void dot(int cx, int cy, int rad, unsigned char r, unsigned char g, unsigned char b)
{
    int x, y, rad2;
    
    rad2 = rad*rad; // radius squared
    
    for (y=cy-rad ; y<=cy+rad ; ++y) for (x=cx-rad ; x<=cx+rad ; ++x)
    {
        if (y>=0 && y<H && x>=0 && x<W && ((x-cx)*(x-cx)+(y-cy)*(y-cy))<=rad2)
        //if (y>=0 && y<H && x>=0 && x<W) // square dots
        {
            po[y][x][0] = r;
            po[y][x][1] = g;
            po[y][x][2] = b;
        }
    }
}

int main()
{
    int t, n, x, y, T=32;
    char buf[1024];
    char filename[256];
    const char input_filename[] = "dfw1280.pnm";
    FILE *f;
    
    // Read image from file
    fprintf(stderr, "Reading %s...", input_filename);
    f = fopen(input_filename, "r");
    for (n=0 ; n<3 ; ++n) fscanf(f, "%[^\n]\n", buf); // PNM header
    fread(pi, 3, W*H, f); // pixel data
    fclose(f);
    fprintf(stderr, "done\n");
    
    // Randomise pixel order
    for (y=0 ; y<H ; ++y) for (x=0 ; x<W ; ++x)
    {
        n = y*W + x;
        px[n].x = x;
        px[n].y = y;
        px[n].z = rand(); // assign random z order
        px[n].r = pi[y][x][0];
        px[n].g = pi[y][x][1];
        px[n].b = pi[y][x][2];
    }
    
    for (t=0 ; t<T ; ++t)
    {
        // Shuffle z order of pixels
        for (n=0 ; n<W*H ; ++n) px[n].z = rand();
        qsort(px, W*H, sizeof(Pixel), zcompare);
        
        // Modify image
        for (n=0 ; n<W*H ; ++n)
        {
            // Draw a random sized dot at the current pixel location
            dot(px[n].x, px[n].y, rand()%((int)(7.0+2.0*sin(t*2.0*M_PI/T))), px[n].r, px[n].g, px[n].b);
        }
        
        // Write image to file
        sprintf(filename, "frame%03d.pnm", t);
        fprintf(stderr, "Writing %s...", filename);
        f = fopen(filename, "w");
        fprintf(f, "P6\n%d %d\n255\n", W, H);
        fwrite(po, 3, W*H, f);
        fclose(f);
        fprintf(stderr, "done\n");
    }
}

The program requires the input image in PNM format. I converted the above PNG image to PNM format using ImageMagick’s convert command:

convert dfw1280.png dfw1280.pnm
Posted in Uncategorized | Leave a comment

TU Dubuntu 19.10 – a customised live USB version of Xubuntu Linux 19.10 for TU Dublin

This post documents the steps I followed to create a customised version of the Xubuntu 19.10 live CD. I called the modified image “TU Dubuntu” (after my university, TU Dublin), but it’s really just Xubuntu with some additional software packages installed that aren’t included in the default image. The steps I followed are based on the “LiveCDCustomization” article on the Ubuntu Community Wiki.

https://help.ubuntu.com/community/LiveCDCustomization

That article isn’t specific to this version of Xubuntu and it includes a lot of optional customisations that I didn’t use, so I wrote this post to capture the specific sequence of steps that produced my working image. Also, I added a step at the end to turn the iso file into a “hybrid” image. That step wasn’t included in the article on the Ubuntu Community Image, but my image wouldn’t boot without it.

Preparing to Create the Custom Image

First, we install some required packages:

sudo apt-get -y install squashfs-tools genisoimage syslinux-utils

First, create a directory and download the original Xubuntu iso image into it:

cd ~
mkdir livecdtmp
cd livecdtmp
wget http://cdimages.ubuntu.com/xubuntu/releases/19.10/release/xubuntu-19.10-desktop-amd64.iso

Create a sub-directory “mnt” and mount the downloaded iso image as a loopback filesystem. A warning may be displayed saying that the filesystem has been mounted read only, which is not a problem.

mkdir mnt
sudo mount -o loop xubuntu-19.10-desktop-amd64.iso mnt

Extract the contents of the installation CD from the downloaded iso image:

mkdir extract-cd
sudo rsync --exclude=/casper/filesystem.squashfs -a mnt/ extract-cd

Extract the SquashFS filesystem into the current directory, then rename the root folder as “edit”:

sudo unsquashfs mnt/casper/filesystem.squashfs
sudo mv squashfs-root edit

The “edit” folder will be the root directory of the chroot environment. Before chrooting into it, we copy some network configuration files into it and clone a couple of special directories as sub-directories of “edit”.

sudo cp /etc/resolv.conf edit/etc/
sudo mount -o bind /run/ edit/run
sudo cp /etc/hosts edit/etc/
sudo mount --bind /dev/ edit/dev

Now, we chroot into the edit folder, mount some special filesystems, and set a couple of environment variables.

sudo chroot edit
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devpts none /dev/pts
export HOME=/root
export LC_ALL=C

I actually don’t really understand what the following commands do, but I ran than because they were in the original instructions and everything worked out ok!

dbus-uuidgen > /var/lib/dbus/machine-id
dpkg-divert --local --rename --add /sbin/initctl
ln -s /bin/true /sbin/initctl

Customising the Image

The following command runs the timezone configuration. I selected “Europe” and “Dublin”.

dpkg-reconfigure tzdata
dpkg-reconfigure keyboard-configuration

Now install whatever packages are to be added using apt-get.

apt update
apt-get -y install inkscape octave geany hexedit mplayer ffmpeg blender python-scipy python-matplotlib python-serial obs-studio php texlive texstudio audacity xdotool pv gphoto2 exfat-utils

Note: I intended to add the Arduino IDE to the chroot filesystem at this point, but I forgot so I had to add it in later. The required commands are included later in the process.

Cleaning Up

The following commands basically undo those commands above that I didn’t understand.

rm /var/lib/dbus/machine-id
rm /sbin/initctl
dpkg-divert --rename --remove /sbin/initctl

The following commands unmount the special filesystems and exit chroot.

umount /proc
umount /sys
umount /dev/pts
exit
sudo umount edit/dev
sudo umount edit/run
sudo umount mnt

Assembling the Modified Filesystem

Regenerate manifest:

sudo chmod +w extract-cd/casper/filesystem.manifest
sudo su
chroot edit dpkg-query -W --showformat='${Package} ${Version}\n' > extract-cd/casper/filesystem.manifest
exit
sudo cp extract-cd/casper/filesystem.manifest extract-cd/casper/filesystem.manifest-desktop
sudo sed -i '/ubiquity/d' extract-cd/casper/filesystem.manifest-desktop
sudo sed -i '/casper/d' extract-cd/casper/filesystem.manifest-desktop

When I was installing pacakges earlier in the process using apt-get, I forgot to also download the Arduino IDE and unpack it into the /opt directory of the chroot environment so I did it at this point instead:

wget https://downloads.arduino.cc/arduino-1.8.10-linux64.tar.xz
tar xf arduino-1.8.10-linux64.tar.xz
rm arduino-1.8.10-linux64.tar.xz
sudo mv arduino-1.8.10 edit/opt/

The following command compresses the modified filesystem:

sudo mksquashfs edit extract-cd/casper/filesystem.squashfs

This recalculates the filesystem size:

sudo su
printf $(du -sx --block-size=1 edit | cut -f1) > extract-cd/casper/filesystem.size
exit

To change the image name to 'TUDubuntu 19.04 "Learned Lecturer" - Release amd64', we edit the first line of the file “extract-cd/README.diskdefines” using nano:

sudo nano extract-cd/README.diskdefines

The following command recalculates the md5sum:

cd extract-cd
sudo rm md5sum.txt
find -type f -print0 | sudo xargs -0 md5sum | grep -v isolinux/boot.cat | sudo tee md5sum.txt

Create the Bootable Image

This creates the iso image:

sudo mkisofs -D -r -V "$IMAGE_NAME" -cache-inodes -J -l -b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table -o ../TUDubuntu-19.10-desktop-amd64.iso .

This command runs isohybrid on the iso file to turn it into a “hybrid” image that will boot from a USB drive:

cd ..
sudo isohybrid TUDubuntu-19.10-desktop-amd64.iso

Finally, the image needs to be copied onto a USB drive. In my case, when I plugged in my USB key it appeared as “/dev/sda”, but you must check the correct device before running the following command!!! The entire contents of the device you specify (hopefully your USB key) will be erased.

WARNING: RUNNING THE NEXT COMMAND WITHOUT CHECKING WHICH DEVICE FILE IS THE USB DRIVE MAY WIPE YOUR ENTIRE OPERATING SYSTEM. MINE IS /dev/sda BUT THERE IS NO GUARANTEE THAT WILL BE THE CASE ON OTHER SYSTEMS.

sudo dd if=TUDubuntu-19.10-desktop-amd64.iso of=/dev/sda

The previous command may take a few minutes to complete because it has to copy approximately 2.7 GB of data to the USB drive.

That’s it! The USB drive should now be ready to boot.

Posted in Uncategorized | Leave a comment

Using Canon EOS 70D DSLR as a USB web cam in Xubuntu Linux 19.04

For a while now, I’ve been interested in using my Canon EOS 70D DSLR as a USB camera so that I can record (and potentially live stream) better quality tutorial videos. The quality of video from the 70D is super and it has a USB connector, so I thought this should be possible. However, although I think Canon provides some kind of software utility to support this kind of thing in Windows and Mac OS, it wasn’t immediately obvious how to do it in Linux. Anyway, I spent a while digging around to figure it, but the solution which finally worked for me (shown below) is basically straight from this post.

One drawback to using the 70D this way is that it doesn’t seem to be possible to power it via the USB connector, so you either need to periodically recharge the battery or buy some kind of AC adapter that plugs into the battery compartment (I think devices like this are available, but I haven’t tried any of them).

The solution is basically:

  • Connect the Canon 70D to the laptop via USB cable.
  • Use gphoto2 to initiate video capture on the camera and receive the data in real time from USB and pipe it onwards via stdout.
  • The output of gphoto2 is piped into gstreamer which does some data format conversion
  • The output of gstreamer is sent to a V4L2 loopback device (e.g. /dev/video2), so that other applications can access it the same way as a normal USB cam.
  • I’ve been using OBS to capture videos from the 70D (including audio), which seems to work extremely well, but in principle any V4L2 compatible software should now be able to access the camera.

First, I had to install a couple of packages:

sudo apt-get install gphoto2 v4l2loopback-utils

Then, the v4l2loopback kernel module needs to be inserted, since that will be used to make the video stream appear as a camera device (e.g. /dev/video2).

sudo modprobe v4l2loopback

Edit (25-7-2019): For some reason, to make the loopback video capture device visible to the Chrome browser, I need to insert the v4l2loopback module with the following additional option specified:

modprobe v4l2loopback exclusive_caps=1

To begin streaming video data:

gphoto2 --stdout --capture-movie | gst-launch-1.0 fdsrc ! decodebin3 name=dec ! queue ! videoconvert ! v4l2sink device=/dev/video2

Incidentally, the following command can be used to capture a still image to a file on the laptop without storing it on the camera’s memory card):

gphoto2 --capture-image-and-download --no-keep
Posted in Uncategorized | Tagged , , , , , , , , , , , , , | 2 Comments

TU Dubuntu – a customised live USB version of Xubuntu Linux 19.04 for TU Dublin

This post documents the steps I followed to create a customised version of the Xubuntu 19.04 live CD. I called the modified image “TU Dubuntu” (after my university, TU Dublin), but it’s really just Xubuntu with some additional software packages installed that aren’t included in the default image. The steps I followed are based on the “LiveCDCustomization” article on the Ubuntu Community Wiki.

https://help.ubuntu.com/community/LiveCDCustomization

That article isn’t specific to this version of Xubuntu and it includes a lot of optional customisations that I didn’t use, so I wrote this post to capture the specific sequence of steps that produced my working image. Also, I added a step at the end to turn the iso file into a “hybrid” image. That step wasn’t included in the article on the Ubuntu Community Image, but my image wouldn’t boot without it.

Preparing to Create the Custom Image

First, we install some required packages:

sudo apt install squashfs-tools genisoimage syslinux-utils

First, create a directory and download the original Xubuntu iso image into it:

cd ~
mkdir livecdtmp
cd livecdtmp
wget http://cdimages.ubuntu.com/xubuntu/releases/19.04/release/xubuntu-19.04-desktop-amd64.iso

Create a sub-directory “mnt” and mount the downloaded iso image as a loopback filesystem.

mkdir mnt
sudo mount -o loop xubuntu-19.04-desktop-amd64.iso mnt

Extract the contents of the installation CD from the downloaded iso image:

mkdir extract-cd
sudo rsync --exclude=/casper/filesystem.squashfs -a mnt/ extract-cd

Extract the SquashFS filesystem into the current directory, then rename the root folder as “edit”:

sudo unsquashfs mnt/casper/filesystem.squashfs
sudo mv squashfs-root edit

The “edit” folder will be the root directory of the chroot environment. Before chrooting into it, we copy some network configuration files into it and clone a couple of special directories as sub-directories of “edit”.

sudo cp /etc/resolv.conf edit/etc/
sudo mount -o bind /run/ edit/run
sudo cp /etc/hosts edit/etc/
sudo mount --bind /dev/ edit/dev

Now, we chroot into the edit folder, mount some special filesystems, and set a couple of environment variables.

sudo chroot edit
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devpts none /dev/pts
export HOME=/root
export LC_ALL=C

I actually don’t really understand what the following commands do, but I ran than because they were in the original instructions and everything worked out ok!

dbus-uuidgen > /var/lib/dbus/machine-id
dpkg-divert --local --rename --add /sbin/initctl
ln -s /bin/true /sbin/initctl

Customising the Image

The following command runs the timezone configuration. I selected “Europe” and “Dublin”.

dpkg-reconfigure tzdata

Now install whatever packages are to be added using apt-get.

apt-get -y install inkscape octave geany hexedit mplayer ffmpeg blender python-scipy python-matplotlib python-serial obs-studio php texlive texstudio audacity xdotool pv gphoto2 exfat-utils

Note: I intended to add the Arduino IDE to the chroot filesystem at this point, but I forgot so I had to add it in later. The required commands are included later in the process.

Cleaning Up

The following commands basically undo those commands above that I didn’t understand.

rm /var/lib/dbus/machine-id
rm /sbin/initctl
dpkg-divert --rename --remove /sbin/initctl

The following commands unmount the special filesystems and exit chroot.

umount /proc
umount /sys
umount /dev/pts
exit
sudo umount edit/dev
sudo umount edit/run
sudo umount mnt

Assembling the Modified Filesystem

Regenerate manifest:

sudo chmod +w extract-cd/casper/filesystem.manifest
sudo su
chroot edit dpkg-query -W --showformat='${Package} ${Version}\n' > extract-cd/casper/filesystem.manifest
exit
sudo cp extract-cd/casper/filesystem.manifest extract-cd/casper/filesystem.manifest-desktop
sudo sed -i '/ubiquity/d' extract-cd/casper/filesystem.manifest-desktop
sudo sed -i '/casper/d' extract-cd/casper/filesystem.manifest-desktop

When I was installing pacakges earlier in the process using apt-get, I forgot to also download the Arduino IDE and unpack it into the /opt directory of the chroot environment so I did it at this point instead:

wget https://downloads.arduino.cc/arduino-1.8.9-linux64.tar.xz
tar xf arduino-1.8.9-linux64.tar.xz
rm arduino-1.8.9-linux64.tar.xz
sudo mv arduino-1.8.9 edit/opt/

The following command compresses the modified filesystem:

sudo mksquashfs edit extract-cd/casper/filesystem.squashfs

This recalculates the filesystem size:

sudo su
printf $(du -sx --block-size=1 edit | cut -f1) > extract-cd/casper/filesystem.size
exit

To change the image name to 'TUDubuntu 19.04 "Learned Lecturer" - Release amd64', we edit the first line of the file “extract-cd/README.diskdefines” using nano:

sudo nano extract-cd/README.diskdefines

The following command recalculates the md5sum:

cd extract-cd
sudo rm md5sum.txt
find -type f -print0 | sudo xargs -0 md5sum | grep -v isolinux/boot.cat | sudo tee md5sum.txt

Create the Bootable Image

This creates the iso image:

sudo mkisofs -D -r -V "$IMAGE_NAME" -cache-inodes -J -l -b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table -o ../TUDubuntu-19.04-desktop-amd64.iso .

This command runs isohybrid on the iso file to turn it into a “hybrid” image that will boot from a USB drive:

cd ..
sudo isohybrid TUDubuntu-19.04-desktop-amd64.iso

Finally, the image needs to be copied onto a USB drive. In my case, when I plugged in my USB key it appeared as “/dev/sda”, but you must check the correct device before running the following command!!! The entire contents of the device you specify (hopefully your USB key) will be erased.

WARNING: RUNNING THE NEXT COMMAND WITHOUT CHECKING WHICH DEVICE FILE IS THE USB DRIVE MAY WIPE YOUR ENTIRE OPERATING SYSTEM. MINE IS /dev/sda BUT THERE IS NO GUARANTEE THAT WILL BE THE CASE ON OTHER SYSTEMS.

sudo dd if=TUDubuntu-19.04-desktop-amd64.iso of=/dev/sda

The previous command may take a few minutes to complete because it has to copy approximately 2.7 GB of data to the USB drive.

That’s it! The USB drive should now be ready to boot.

Posted in Uncategorized | Tagged , , , , , , , | Leave a comment

Using the PHP command line web server to transfer files between devices on a local network

When you install PHP, you get a simple built-in webserver as a bonus. This is very handy for testing web pages you’re writing, but I also sometimes use it as a simple way to transfer files between devices on my local wifi network. In particular, it’s a convenient way of getting files from my laptop onto my phone.

The basic idea is that I run the PHP web server in the folder that contains the files I want to transfer onto my phone, then use the browser on my phone to download the files. Let’s say that the file I’m transferring is called “myfile.txt” and it’s in the directory “/home/xubuntu/Downloads/“. I would type the following commands in a terminal to run the PHP web server in that directory:

cd /home/xubuntu/Downloads
php -S 0.0.0.0:8000

The “0.0.0.0” part is a dummy IP address that tells the PHP web server to listen on all network interfaces (e.g. 127.0.0.1 and 192.168.0.16 on my machine). I’ve also specified 8000 as the port number for the web server. To download the file onto my phone, the URL would be:

http//192.168.0.16:8000/myfile.txt

The phone needs to be connected to the same local network as the computer that the web server is running on. I have both connected to my wifi router.

If I have a few files to transfer, or if the filenames are long, it can be useful to make a little PHP script in the directory to generate a simple web page with links to each file. This is what I’m currently using (saved in the same directory as “index.php“):

<!DOCTYPE html>

<head>
    <style>
        body {font-size:200%;}
    </style>
</head>

<body>
    <br>

    <?php
    $i = 1;
    $g = glob("*");
    foreach ($g as $f)
    {
        if ($f == "index.php") continue;
        echo $i . ". " . "<a href=\"" . $f . "\">" . $f . "</a><br><br>";
        $i = $i + 1;
    }
    ?>
</body>

Here are the files stored in “/home/xubuntu/Downloads/”:

And here’s how it looks in the phone browser (the address is “192.168.0.16:8000”):

When I’m finished transferring files, I just press Ctrl-C in the terminal to close down the web server.

Posted in Uncategorized | Tagged , , , | Leave a comment

€5 PPG – photoplethysmogram amplifier / Arduino circuit

The photoplethysmogram (PPG) is a signal that measures changes in blood volume in some part of the body (e.g. the fingertip) by shining light into the skin and detecting small changes in the level of light absorption that occur due to the blood vessels enlarging and contracting. One common application is heart rate measurement. When the heart beats, blood vessels around the body swell slightly due to the increased blood pressure. This results is variable light absorption over the course of each cardiac cycle.

This circuit is a €5 PPG system that uses a TCRT5000 reflective infrared sensor, an LM358 opamp and an Arduino Nano.

The following Arduino code samples the analog voltage on pin A7 (the sample rate is approximately 100 Hz) and prints the values via the serial connection. The signal can therefore be plotted using the Arduino development environment’s Serial Plotter tool (located under the Tools menu). Normally, the Serial Plotter dynamically scales the vertical axis to fit the displayed signal. To prevent this (and maintain constant scaling), this program actually outputs two additional dummy signals – one which is always 0 and another which is always 1023. These hold the vertical axis limits at constant values.

//
// Photoplethysmogram (PPG) example
// Written by Ted Burke, 3-4-2019
//

void setup()
{
  pinMode(2, OUTPUT);
  Serial.begin(9600);
}

void loop()
{
  int du;

  du = analogRead(7);

  Serial.print("0 1023 ");
  Serial.println(du);

  delay(10);
}

This is an example PPG signal recorded using the above circuit (I rested my fingertip directly on top of the TCRT5000) and displayed in the Serial Plotter:

Since the PPG is a very low frequency signal, you may wish to reduce the gain of the amplifier at higher frequencies, which will tend to reduce the visible interference and smooth out the signal. This can be achieved by placing a 100nF capacitor in parallel with the 100kΩ resistor. The following signal was recorded with that capacitor in the circuit.

Posted in Uncategorized | Tagged , , , , , , , , , , | 8 Comments