Analogue, digital and graphics
Analogue and digital signals, ADCs and DACs, bitmapped and vector graphics and when to use each.
Do this lesson in the simulatorA bit pattern has no meaning on its own. 01000001 could be the number 65, the letter A, a grey pixel, a moment of sound or a machine code instruction: it depends entirely on how the program is told to read it. This lesson starts with how real-world signals become bit patterns at all, then compares the two ways of storing a picture.
Bit patterns
The same 8 bits read four ways:
byte = 0b01000001
print("unsigned integer:", byte)
print("character:", chr(byte))
print("two's complement:", byte - 256 if byte & 0x80 else byte)
print("8-bit grey level:", round(byte / 255 * 100), "% brightness")
A file format, a data type or a protocol is the agreement that says which reading is correct. Get it wrong and the data is still there, just nonsense.
Analogue and digital
An analogue signal varies continuously: it can take any value in its range, at any moment. Sound pressure, light level, a battery's voltage and the true distance to a wall are all analogue.
A digital signal takes only discrete values, at discrete moments. Inside a computer that means a sequence of binary numbers.
Digital data can be stored, copied and sent without gradually losing quality, because each value only has to be told apart from a few others. Most of the world is analogue, so a computer that senses or acts needs converters at its edges.
ADC and DAC
An analogue to digital converter (ADC) turns an analogue signal into numbers. It does two things:
- Sampling: it measures the signal at regular intervals. The number of samples per second is the sampling rate.
- Quantisation: it rounds each measurement to the nearest of a fixed set of levels and outputs that level's binary code. An n-bit ADC has 2ⁿ levels.
A digital to analogue converter (DAC) does the reverse: it turns each code back into a voltage, holding it until the next code arrives. The result is a staircase that approximates the original; filtering smooths the steps.
More samples per second follow the signal's changes more closely; more bits per sample make the steps smaller. Both improve accuracy and both need more data. Every microphone recording is an ADC's output and every loudspeaker playing digital audio is fed by a DAC. On a robot, a battery voltage or a light sensor's output must pass through an ADC before a program can read it as a number.
import math
BITS = 3
levels = 2 ** BITS
codes = []
for k in range(16):
v = math.sin(2 * math.pi * 1.25 * k / 16) # the analogue value, -1 to 1
level = round((v + 1) / 2 * (levels - 1)) # quantise to one of 8 levels
codes.append(format(level, "03b"))
print(" ".join(codes))
Bitmapped graphics
A bitmap stores an image as a grid of pixels (picture elements), each holding a binary code for its colour. You met these at GCSE with the robot's camera; the A level vocabulary is exact:
- Size in pixels: width × height, for example 320 × 240.
- Resolution: the number of pixels per unit of length of the display or print, such as dots per inch. A bitmap with more pixels in the same space looks sharper.
- Colour depth: the number of bits per pixel. n bits give 2ⁿ possible colours.
Storage, ignoring metadata:
storage in bits = width in pixels × height in pixels × colour depth
One 320 × 240 frame at 24 bits per pixel is 320 × 240 × 24 = 1,843,200 bits, which is 230,400 bytes.
A bitmap file also stores metadata, data about the image: its width and height (needed to rebuild the grid from a stream of pixel values), its colour depth, and often the date, camera settings and location.
Vector graphics
A vector graphic stores an image as a list of objects, each described by its properties. A rectangle has a position, width, height, line colour and fill colour; a circle has a centre, a radius and colours. The file is the drawing list: a program redraws the picture from it every time it is shown.
drawing = [
{"type": "rect", "x": 2, "y": 2, "w": 10, "h": 6, "fill": "black"},
{"type": "circle", "cx": 22, "cy": 8, "r": 5, "fill": "black"},
]
scaled = [dict(shape) for shape in drawing]
for shape in scaled:
for key in ["x", "y", "w", "h", "cx", "cy", "r"]:
if key in shape:
shape[key] = shape[key] * 4 # scaling just multiplies the numbers
print(scaled)
Scaling a vector image changes a few numbers and loses nothing; scaling a bitmap up spreads the same pixels over more space, so edges go blocky.
Vector or bitmap?
| Bitmap | Vector | |
|---|---|---|
| Suits | photographs and scenes with continuous detail, like the camera's view | diagrams, logos, fonts, maps, plans |
| Scaling up | loses quality: pixels become visible | no loss: the shapes are recalculated |
| File size depends on | size in pixels and colour depth | the number and complexity of objects |
| Editing | change individual pixels | change or move whole objects |
| Showing it | pixels are sent straight to the screen | must be rendered into pixels first |
A photograph as vectors would need an object for almost every pixel, so bitmaps win for photos. A logo as a bitmap would need a new, larger file for every size, so vectors win there. Every vector image becomes a bitmap in the end, because screens are grids of pixels: turning objects into pixels is called rasterising, and it is what the task below does.
Task: vector to bitmap
shapes is a vector drawing of two objects. A rectangle has x and y (its top-left corner) and w and h; a circle has a centre cx, cy and a radius r. All are in pixels. Rasterise it onto a bitmap WIDTH 32 pixels wide and HEIGHT 16 pixels tall, 1 bit per pixel.
- The pixel in column
col(0 to 31) and rowrow(0 to 15) has its centre at(col + 0.5, row + 0.5). It is on when that centre is inside any shape: for a rectangle,x <= px < x + wandy <= py < y + h; for a circle, the squared distance from the centre is no more thanrsquared. - Print the 16 rows, top row first, each as 32 characters:
#for on and.for off. - Then print, calculating each number from
WIDTHandHEIGHT:1-bit bitmap: <n> bits,24-bit bitmap: <n> bits, and1-bit bitmap at 4 times the size: <n> bits(4 times the width and 4 times the height, still 1 bit per pixel).
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
WIDTH = 32
HEIGHT = 16
shapes = [
{"type": "rect", "x": 2, "y": 2, "w": 10, "h": 6},
{"type": "circle", "cx": 22, "cy": 8, "r": 5},
]
def inside(shape, px, py):
return False
Challenges
- Rasterise the drawing at 4 times the size by scaling the shapes, not the bitmap. Compare it with scaling the 32 × 16 bitmap up.
- Add a
lineshape with two end points and a thickness. - Estimate how many bytes the vector drawing takes as text, and compare it with the 24-bit bitmap.