A Raspberry Pi looks deceptively simple: a small printed-circuit board with a processor, memory, connectors and a row of pins. But technically it sits at an extremely useful intersection between an embedded system and a general-purpose Linux computer. It can communicate with sensors through GPIO, I2C, SPI and UART, drive displays, access networks, run Python and C++, host databases and web services, interface directly with cameras, and execute real-time image-processing pipelines.
This tutorial starts with the assumption that you have never held a Raspberry Pi in your hands. By the end, you will understand what the board actually is, how its hardware is organised, how to choose and power it correctly, how Raspberry Pi OS and Linux work, how networking and remote access work, how GPIO differs from ordinary microcontroller I/O, how the modern Raspberry Pi camera stack works, how images become NumPy arrays, and how OpenCV processes those arrays.
Most importantly, we will not stop at isolated examples. Near the end we will build a complete real-time computer-vision application using a Raspberry Pi 5 and Camera Module 3. The system will detect red objects, reject image noise, calculate object geometry, annotate frames, store inspection events and run either interactively or headlessly.
Version baseline: this tutorial targets the modern Raspberry Pi software stack and Raspberry Pi OS based on Debian 13 “Trixie”. The practical project uses Raspberry Pi 5 and Camera Module 3. Most concepts also apply to Raspberry Pi 4 and other recent boards, but connector types, performance and some hardware capabilities differ between models.
1. What a Raspberry Pi actually is
A Raspberry Pi is a single-board computer, commonly abbreviated as SBC. Unlike a traditional desktop computer, most of its essential components are mounted directly on one compact PCB.
At a high level, the system contains:
- a processor or System-on-Chip,
- RAM,
- non-volatile boot storage such as a microSD card or NVMe SSD,
- USB interfaces,
- Ethernet and wireless networking,
- display interfaces,
- camera interfaces,
- general-purpose digital I/O,
- power-management circuitry,
- and firmware that starts the computer before Linux takes control.
The important distinction is that a Raspberry Pi is not simply a large microcontroller development board.
Raspberry Pi versus a microcontroller
An STM32, ATmega, ESP32 or RP2040 normally executes firmware directly on the processor. The firmware owns the hardware almost completely. Timing can therefore be extremely deterministic, particularly when interrupts, DMA and hardware timers are used correctly.
A Raspberry Pi normally runs a multitasking operating system: Linux. Your program is only one process among many. The kernel schedules CPU time, manages memory, controls devices, handles networking and isolates processes from each other.
| Characteristic | Microcontroller | Raspberry Pi |
|---|---|---|
| Typical software | Bare-metal firmware or RTOS | Linux operating system |
| Boot time | Milliseconds | Seconds |
| Real-time determinism | Potentially excellent | Not guaranteed by normal Linux |
| Memory | Usually KB to MB | Usually GB |
| File system | Optional | Standard |
| Networking stack | Application-dependent | Built into Linux |
| GUI | Possible but constrained | Desktop-class interfaces possible |
| Computer vision | Possible on selected MCUs | Very practical |
| Hard real-time control | Good fit | Usually not the first choice |
This distinction matters. If I am designing a motor-control loop requiring precisely timed PWM and current sampling, I would normally put that responsibility on a microcontroller or dedicated control hardware. If I need networking, a user interface, image processing, data logging and high-level decision making, a Raspberry Pi becomes extremely attractive.
Professional systems often combine both architectures: the microcontroller handles deterministic low-level control while the Raspberry Pi handles Linux, communications, vision, data processing and system orchestration.
2. Choosing a Raspberry Pi
The Raspberry Pi family contains several types of boards. They should not be treated as interchangeable simply because they share the Raspberry Pi name.
Raspberry Pi 5
Raspberry Pi 5 is the reference platform for this tutorial because computer vision benefits from its CPU performance, improved I/O architecture and modern camera subsystem.
Its BCM2712 application processor contains four 64-bit Arm Cortex-A76 CPU cores running at up to 2.4 GHz. Raspberry Pi 5 also introduces the RP1 I/O controller, which handles much of the external I/O including GPIO, USB and other interfaces.
Important capabilities include:
- quad-core 64-bit Arm Cortex-A76 CPU,
- VideoCore VII GPU,
- LPDDR4X memory,
- Gigabit Ethernet,
- dual-band Wi-Fi,
- Bluetooth and Bluetooth Low Energy,
- two USB 3.0 and two USB 2.0 ports,
- a standard 40-pin GPIO header,
- two flexible MIPI camera/display connectors,
- microSD storage,
- a PCI Express interface suitable for devices such as an NVMe adapter,
- and a real power button.
Raspberry Pi 4
Raspberry Pi 4 remains useful for Linux, networking, automation and many vision applications. It is slower than Pi 5 and uses a different camera connector arrangement, but much of the Linux, Python and OpenCV knowledge in this tutorial transfers directly.
Raspberry Pi Zero family
Boards such as Raspberry Pi Zero 2 W are dramatically smaller and consume less power. They are excellent when physical size matters, but their performance is more constrained. Heavy OpenCV pipelines that are comfortable on Pi 5 may require lower resolutions, lower frame rates or substantial optimisation on a Zero-class board.
Compute Modules
Compute Modules are intended primarily for integration into custom products. Instead of providing all conventional connectors directly, they expose the computing platform through board-to-board connectors and are installed on a carrier board. This is often the right direction for commercial embedded products after a prototype has been validated on a standard Raspberry Pi.
3. What to buy for this tutorial
For the complete project I recommend the following hardware:
- Raspberry Pi 5, preferably with at least 4 GB RAM for a comfortable development environment,
- official or high-quality 5 V / 5 A USB-C power supply,
- Active Cooler or another appropriate cooling solution for sustained computer-vision workloads,
- 32 GB or larger quality microSD card,
- microSD card reader for your development PC,
- Raspberry Pi Camera Module 3,
- 15-pin-to-22-pin Standard-Mini camera cable for Raspberry Pi 5,
- micro-HDMI cable and monitor for the easiest first setup,
- USB keyboard and mouse,
- and optionally an Ethernet cable.
Once you understand headless operation, the monitor, keyboard and mouse are no longer required.
4. Power is part of the engineering design
One of the most common beginner mistakes is treating power as an afterthought. A Raspberry Pi is a computer with rapidly changing current consumption. CPU activity, USB peripherals, camera processing, storage and networking all influence the instantaneous load.
Raspberry Pi 5 is designed around a 5 V supply and the recommended full-power configuration uses a 5 V / 5 A USB-C power source. A lower-current supply can work under some conditions, but available current for connected peripherals may be restricted.
For a reliable engineering system, do not select a supply merely because the USB-C connector physically fits.
Symptoms of poor power
- unexpected reboots,
- USB devices disappearing,
- storage errors,
- unstable camera behaviour,
- reduced performance,
- or warnings from the operating system.
Intermittent power problems are especially dangerous because they are easily mistaken for software bugs.
5. Cooling and thermal throttling
Modern processors dynamically manage temperature and clock frequency. When a Raspberry Pi 5 performs continuous image processing, several CPU cores may remain heavily loaded for long periods.
If the processor becomes too hot, the platform protects itself by reducing performance. This is called thermal throttling.
The board is not destroyed simply because it reaches a throttling condition; the important engineering consequence is that execution performance becomes lower and potentially less predictable.
For sustained computer-vision, compilation, AI inference or similar high-load workloads, active cooling is therefore a sensible design choice.
You can inspect the current CPU temperature with:
vcgencmd measure_temp
You can also inspect throttling history with:
vcgencmd get_throttled
A value of zero means no currently reported throttling flags are set. When diagnosing a performance problem, thermal conditions and supply quality should be checked before rewriting software.
6. Storage: microSD versus NVMe
The operating system needs non-volatile storage. For a first system, microSD is the easiest option.
microSD
Advantages:
- cheap,
- simple,
- supported directly by the board,
- easy to re-image.
Disadvantages:
- limited write endurance compared with many SSDs,
- performance depends heavily on card quality,
- frequent logging or database writes can accelerate wear.
NVMe
Raspberry Pi 5 exposes PCI Express and can use an NVMe SSD through an appropriate adapter such as an M.2 HAT+. NVMe becomes attractive for larger datasets, extensive logging, databases, compilation and production-like systems.
For learning, start with microSD. Optimise storage after the system works.
7. Installing Raspberry Pi OS
Raspberry Pi OS is the official Linux distribution for Raspberry Pi computers. Current releases are based on Debian.
For this tutorial use the current 64-bit Raspberry Pi OS with Desktop. A desktop installation consumes more resources than Raspberry Pi OS Lite, but it makes the first camera and OpenCV experiments easier because a monitor can show image windows directly.
Step 1: install Raspberry Pi Imager
Install Raspberry Pi Imager on your Windows, Linux or macOS computer.
Step 2: insert the microSD card
Connect the microSD card to your PC through a card reader.
Step 3: select the target device
Choose Raspberry Pi 5 in Imager.
Step 4: select the operating system
Select Raspberry Pi OS 64-bit.
Step 5: select the storage device
Choose the correct microSD card. This step is destructive: the card will be overwritten.
Step 6: configure the OS before flashing
Imager can preconfigure important settings including:
- hostname,
- username,
- password,
- Wi-Fi SSID,
- Wi-Fi password,
- Wi-Fi regulatory country,
- time zone,
- keyboard layout,
- and SSH access.
For an engineering system, enable SSH from the beginning. Public-key authentication is preferable to password authentication when you already understand SSH keys.
Step 7: write and verify
Let Imager write and verify the card. When complete, safely remove it and insert it into the Raspberry Pi.
8. First boot
For your first boot:
- insert the microSD card,
- connect the monitor,
- connect keyboard and mouse,
- connect the network if using Ethernet,
- and connect power last.
Linux will boot and eventually display the desktop.
Open a terminal and update the system:
sudo apt update
sudo apt full-upgrade -y
sudo reboot
Current Raspberry Pi OS versions may request your user password when sudo is used. This is normal.
9. Understanding Linux before touching GPIO
Many Raspberry Pi problems are actually Linux misunderstandings. You do not need to become a Linux administrator before writing Python, but several concepts are essential.
The filesystem
Linux uses a single filesystem tree beginning at /.
| Path | Purpose |
|---|---|
/home |
User home directories |
/etc |
System configuration |
/usr |
Applications, libraries and shared resources |
/var |
Variable data such as logs |
/dev |
Device interfaces |
/boot/firmware |
Raspberry Pi boot configuration on current systems |
Essential commands
pwd
ls
ls -la
cd ~
mkdir my_project
cd my_project
cp source.txt destination.txt
mv old.txt new.txt
rm file.txt
cat file.txt
nano file.txt
clear
pwd prints the current working directory. ls lists files. cd changes directory. mkdir creates a directory.
Processes
A running application is a process. Useful inspection commands include:
ps aux
top
htop
If htop is not installed:
sudo apt install -y htop
Package management
Debian-based systems use APT:
sudo apt update
sudo apt install package-name
sudo apt remove package-name
apt update refreshes package metadata. It does not itself upgrade every installed application.
10. Finding out what hardware and software you actually have
Never troubleshoot from assumptions. Ask the system.
Kernel and architecture:
uname -a
Operating-system release:
cat /etc/os-release
CPU information:
lscpu
Memory:
free -h
Storage:
lsblk
df -h
USB devices:
lsusb
Network addresses:
ip addr
These commands are often more useful than screenshots when diagnosing remote systems.
11. Remote access with SSH
After the first setup, a Raspberry Pi often runs without a dedicated keyboard, mouse or monitor. This is known as a headless system.
SSH provides an encrypted remote terminal.
Find the Pi's IP address:
hostname -I
From another Linux or macOS system, or from modern Windows terminals:
ssh youruser@192.168.1.50
Replace the username and address with your own values.
Once connected, commands execute on the Raspberry Pi even though you type them on another computer.
Why SSH matters professionally
Embedded Linux devices are rarely developed by permanently attaching monitors. SSH enables remote diagnostics, software deployment, log inspection, file transfer and service management.
12. The 40-pin GPIO header
The 40-pin header is one of the features that turns the Raspberry Pi from a small PC into an embedded-development platform.
The header contains several categories of pins:
- 3.3 V supply,
- 5 V supply,
- ground,
- general-purpose GPIO,
- and GPIO pins that can be switched to alternate functions such as I2C, SPI or UART.
Critical electrical rule: normal Raspberry Pi GPIO uses 3.3 V logic. GPIO inputs are not general-purpose 5 V tolerant inputs. Do not connect a 5 V digital signal directly to a GPIO pin.
Physical numbering versus GPIO numbering
This causes endless beginner confusion.
Physical pin number means the actual position from 1 to 40 on the connector.
GPIO number identifies the logical GPIO signal.
For example, physical pin 11 corresponds to GPIO17. The numbers are not interchangeable.
Raspberry Pi OS includes a useful pinout utility:
pinout
Use it instead of relying on memory.
Never drive loads directly without checking current
GPIO is intended for logic signals, not for supplying motors, relays, solenoids, high-power LEDs or other substantial loads.
Depending on the load, use:
- a transistor,
- a MOSFET,
- a relay driver,
- an H-bridge,
- a dedicated motor driver,
- or an external I/O driver.
The engineering principle is simple: GPIO tells the power stage what to do; GPIO should not become the power stage.
13. I2C, SPI and UART
The same 40-pin connector gives access to important embedded communication interfaces.
I2C
I2C is a synchronous two-wire bus using:
- SDA for data,
- SCL for clock.
Multiple addressed devices can share one bus. It is widely used for sensors, EEPROMs, ADCs, DACs and configuration interfaces.
SPI
SPI normally uses:
- SCLK,
- MOSI,
- MISO,
- one or more chip-select signals.
It generally provides higher throughput and lower protocol overhead than I2C, but requires more wires.
UART
A basic UART connection uses TX and RX plus a common ground. It is asynchronous and remains one of the simplest ways to communicate with microcontrollers, modems, GNSS receivers and diagnostic consoles.
Always check voltage levels before connecting any serial interface. “UART” describes the protocol, not the electrical voltage.
14. Python on modern Raspberry Pi OS
Python 3 is installed by default and is one of the most productive languages for Raspberry Pi development.
Check the version:
python3 --version
From Raspberry Pi OS Bookworm onward, the system Python installation is treated as an externally managed environment. Installing arbitrary packages globally with sudo pip is therefore the wrong approach.
Use either:
aptfor system-packaged Python modules, or- a Python virtual environment for project-specific packages.
This separation exists to prevent pip from silently replacing files that the operating system package manager expects to control.
15. Python virtual environments
Create a project:
mkdir -p ~/projects
cd ~/projects
mkdir vision_inspector
cd vision_inspector
Install the virtual-environment support if necessary:
sudo apt install -y python3-full python3-venv
For Picamera2 projects, the system provides important camera bindings. We therefore create the environment with access to system Python packages:
python3 -m venv --system-site-packages .venv
source .venv/bin/activate
Your terminal should now show (.venv).
Leave the environment with:
deactivate
16. Understanding the Raspberry Pi camera architecture
Before writing code, understand the path taken by an image.
Scene
|
v
Lens
|
v
CMOS image sensor
|
v
MIPI CSI-2 data
|
v
Raspberry Pi camera / ISP pipeline
|
v
libcamera
|
+----> rpicam command-line applications
|
+----> Picamera2 Python API
|
v
NumPy array
|
v
OpenCV
|
v
Detection / measurement /
classification / storage
This architecture explains why copying an old tutorial based on raspistill, raspivid or the original Picamera library is a mistake on a modern system.
The supported architecture uses libcamera, rpicam-apps and Picamera2.
17. Camera Module 3
Camera Module 3 is an excellent general-purpose camera for learning computer vision.
It uses the Sony IMX708 image sensor and provides approximately 12 megapixels with a native image size of 4608 × 2592 pixels. Unlike older fixed-focus Raspberry Pi camera modules, Camera Module 3 provides powered autofocus.
Variants include:
- standard field of view,
- wide field of view,
- standard NoIR,
- wide NoIR.
NoIR versions omit the normal infrared-cut filter, which is useful for infrared illumination and night-vision applications but changes colour behaviour under ordinary lighting.
18. Connecting Camera Module 3 to Raspberry Pi 5
Turn the Raspberry Pi completely off and disconnect power before manipulating the camera ribbon cable.
Camera Module 3 itself uses the standard 15-pin connector. Raspberry Pi 5 uses the smaller 22-pin camera/display connector. Therefore Raspberry Pi 5 requires a Standard-Mini camera cable: 15-pin at the camera and 22-pin at the Pi.
Do not force the connector latch. These FPC connectors are mechanically delicate.
- Remove power.
- Open the connector latch carefully.
- Insert the ribbon cable fully and straight.
- Verify the contact orientation.
- Close the latch evenly.
- Connect the other end to the camera.
- Only then restore power.
If an Active Cooler is being installed, route or connect the camera cable before access to the connector becomes inconvenient.
19. Testing the camera before writing Python
Never start debugging OpenCV until the lower camera stack has been proven to work.
First update the system and reboot if necessary:
sudo apt update
sudo apt full-upgrade -y
sudo reboot
List detected cameras:
rpicam-hello --list-cameras
You should see the attached sensor. Camera Module 3 normally appears as an IMX708 device.
With a display attached, test a preview:
rpicam-hello
Run continuously:
rpicam-hello --timeout 0
Press Ctrl+C to stop it.
Capture a still image:
rpicam-still -o test.jpg
If this level does not work, do not blame Python. Check the cable, connector orientation, camera detection, operating-system version and updates first.
20. Installing Picamera2 and OpenCV
Recent Raspberry Pi OS images normally contain Picamera2, but explicitly installing the supported packages makes the project dependencies clear:
sudo apt update
sudo apt install -y python3-picamera2 python3-opencv python3-numpy
Return to the project and activate the environment:
cd ~/projects/vision_inspector
source .venv/bin/activate
Check imports:
python3 -c "import cv2; print('OpenCV:', cv2.__version__)"
python3 -c "from picamera2 import Picamera2; print('Picamera2 import OK')"
If Picamera2 works outside a virtual environment but cannot be imported inside it, the virtual environment was probably created without --system-site-packages.
21. Your first Python camera program
Create camera_test.py:
from time import sleep
import cv2
from picamera2 import Picamera2
from libcamera import controls
picam2 = Picamera2()
config = picam2.create_video_configuration(
main={"size": (1280, 720), "format": "RGB888"}
)
picam2.configure(config)
picam2.start()
if "AfMode" in picam2.camera_controls:
picam2.set_controls(
{"AfMode": controls.AfModeEnum.Continuous}
)
sleep(2)
try:
while True:
frame = picam2.capture_array()
cv2.imshow("Raspberry Pi Camera", frame)
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
break
finally:
picam2.stop()
cv2.destroyAllWindows()
Run it:
python3 camera_test.py
Press q to exit.
Why RGB888 is used
One detail deserves special attention. Picamera2 inherits pixel-format names from lower Linux and libcamera layers, and the naming can appear counter-intuitive from Python.
When Picamera2 captures the RGB888 format into a NumPy array, the bytes are arranged as B, G, R, which is exactly the channel order expected by most OpenCV functions.
Choosing BGR888 because “OpenCV wants BGR” can therefore produce swapped colours. This is an excellent example of why engineering should be based on documented memory layout rather than names alone.
22. What a digital image really is
OpenCV does not see a photograph. It sees numbers.
A colour frame with resolution 1280 × 720 can be represented as a three-dimensional NumPy array:
frame.shape
# Example:
# (720, 1280, 3)
The dimensions are:
- 720 rows,
- 1280 columns,
- 3 colour channels per pixel.
In the BGR representation:
pixel = frame[100, 200]
blue = pixel[0]
green = pixel[1]
red = pixel[2]
An image-processing algorithm is therefore a transformation or measurement applied to an array of pixel values.
23. Resolution, frame rate and computational cost
A common beginner assumption is that maximum camera resolution must produce the best vision system. Usually it does not.
Consider the pixel counts:
- 640 × 480 = 307,200 pixels,
- 1280 × 720 = 921,600 pixels,
- 1920 × 1080 = 2,073,600 pixels,
- 4608 × 2592 = 11,943,936 pixels.
If an algorithm processes every pixel in every frame, increasing resolution increases memory traffic and computation dramatically.
A good computer-vision engineer asks:
- What is the smallest feature that must be detected?
- How much field of view is required?
- What frame rate is required?
- How much latency is acceptable?
- Can processing occur on a lower-resolution stream while high-resolution images are captured only when required?
This is system engineering, not merely image processing.
24. BGR, grayscale and HSV
BGR
BGR represents blue, green and red intensities. It is convenient for displaying colour images but is not always the best representation for detection.
Grayscale
Grayscale reduces each pixel to an intensity value. It is useful when colour itself carries little information.
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
HSV
HSV separates colour into:
- Hue,
- Saturation,
- Value.
For many colour-segmentation problems, HSV is substantially easier to threshold than raw BGR because hue represents colour more directly.
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
25. Thresholding: turning an image into a decision
Suppose we want to find red objects.
We can classify each pixel as either:
- inside the chosen red range, or
- outside it.
The result is a binary mask.
OpenCV uses a hue range of 0 to 179 in its conventional 8-bit HSV representation. Red lies around the wrap-around point, so a robust red detector commonly uses two hue ranges.
lower_red_1 = np.array([0, 80, 70], dtype=np.uint8)
upper_red_1 = np.array([10, 255, 255], dtype=np.uint8)
lower_red_2 = np.array([170, 80, 70], dtype=np.uint8)
upper_red_2 = np.array([179, 255, 255], dtype=np.uint8)
mask_1 = cv2.inRange(hsv, lower_red_1, upper_red_1)
mask_2 = cv2.inRange(hsv, lower_red_2, upper_red_2)
mask = cv2.bitwise_or(mask_1, mask_2)
The thresholds are not universal constants. Lighting, camera white balance, material reflectance and exposure influence measured values. Production vision systems therefore require controlled lighting and calibration.
26. Morphological filtering
A raw threshold mask usually contains isolated pixels, small holes and fragmented regions.
Morphological operations use a small structuring element called a kernel.
Opening is useful for removing small isolated foreground noise.
Closing is useful for filling small gaps inside foreground regions.
kernel = np.ones((5, 5), dtype=np.uint8)
mask = cv2.morphologyEx(
mask,
cv2.MORPH_OPEN,
kernel,
iterations=1
)
mask = cv2.morphologyEx(
mask,
cv2.MORPH_CLOSE,
kernel,
iterations=2
)
This is not cosmetic filtering. It changes the topology of regions that later become detected objects.
27. Contours and object geometry
After segmentation, OpenCV can locate connected boundaries known as contours.
contours, _ = cv2.findContours(
mask,
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE
)
For every contour we can calculate:
- area,
- perimeter,
- bounding rectangle,
- centroid,
- minimum enclosing circle,
- orientation,
- shape descriptors.
For example:
area = cv2.contourArea(contour)
x, y, width, height = cv2.boundingRect(contour)
Rejecting contours below a minimum area is one of the simplest ways to prevent tiny segmentation noise from becoming a reported object.
28. Why lighting matters more than many algorithms
Image-processing tutorials frequently focus entirely on code. Real vision systems fail just as often because of optics and lighting.
Changing ambient light changes:
- pixel intensity,
- colour saturation,
- white balance,
- shadows,
- reflections,
- motion blur through changed exposure time.
If I need reliable measurements in a machine-vision system, I prefer controlled illumination over trying to repair uncontrolled illumination with increasingly complicated software.
Useful engineering techniques include:
- fixed camera mounting,
- fixed object distance,
- diffuse lighting,
- avoiding direct reflections,
- shielding the scene from sunlight,
- locking exposure after calibration where appropriate,
- and selecting a background that maximises contrast.
29. Latency and throughput
Frame rate and latency are not the same quantity.
A system could process 30 frames per second while still having hundreds of milliseconds of latency if frames accumulate in buffers.
For control-oriented vision systems, ask:
- When was this frame exposed?
- When did processing begin?
- When was the decision available?
- Is the software processing the newest frame or an old queued frame?
For real machines, stale information can be worse than a lower frame rate.
30. Security and reliability
Once a Raspberry Pi becomes part of a real product or network, security becomes an engineering requirement.
Do not expose SSH casually
Use strong authentication. Prefer SSH keys when practical. Do not forward SSH directly from the public internet merely because it is convenient.
Keep software updated
sudo apt update
sudo apt full-upgrade
Do not run applications as root without a reason
Root privileges remove important protection boundaries. A camera-processing program normally does not need unrestricted control over the whole operating system.
Handle shutdown properly
Do not repeatedly remove power while the filesystem is writing data. Use:
sudo poweroff
Then wait until the shutdown sequence has completed before removing power.
Design for storage failure
Production systems should consider:
- log rotation,
- write frequency,
- backup strategy,
- read-only partitions where appropriate,
- SSD storage for write-intensive applications,
- and recovery after unexpected power loss.
31. Practical project: real-time visual inspection and event logger
We now have enough knowledge to build a complete application.
The project will behave like a simplified industrial inspection station.
The camera continuously observes a scene. When a sufficiently large red object enters the image, the software:
- captures frames continuously,
- converts the frame from BGR to HSV,
- segments red pixels using two hue ranges,
- filters noise using morphology,
- extracts contours,
- chooses the largest valid object,
- calculates its bounding box and centroid,
- annotates the frame,
- saves one inspection image when a new object appears,
- and stores an event record in a CSV log.
That pipeline contains the essential architecture of many practical machine-vision systems:
Image acquisition
|
v
Colour-space conversion
|
v
Segmentation
|
v
Noise filtering
|
v
Feature extraction
|
v
Decision logic
|
+-----> Visualisation
|
+-----> Event image
|
+-----> CSV log
32. Preparing the project directory
cd ~/projects/vision_inspector
mkdir -p events
source .venv/bin/activate
Create the main file:
nano vision_inspector.py
33. Complete project code
#!/usr/bin/env python3
import argparse
import csv
import time
from datetime import datetime
from pathlib import Path
import cv2
import numpy as np
from libcamera import controls
from picamera2 import Picamera2
FRAME_WIDTH = 960
FRAME_HEIGHT = 540
DEFAULT_MIN_AREA = 2500.0
EVENT_COOLDOWN_SECONDS = 1.0
LOWER_RED_1 = np.array([0, 80, 70], dtype=np.uint8)
UPPER_RED_1 = np.array([10, 255, 255], dtype=np.uint8)
LOWER_RED_2 = np.array([170, 80, 70], dtype=np.uint8)
UPPER_RED_2 = np.array([179, 255, 255], dtype=np.uint8)
def create_argument_parser():
parser = argparse.ArgumentParser(
description="Raspberry Pi real-time red-object inspection system"
)
parser.add_argument(
"--headless",
action="store_true",
help="Run without OpenCV display windows",
)
parser.add_argument(
"--output",
type=Path,
default=Path("events"),
help="Directory used for event images and CSV log",
)
parser.add_argument(
"--min-area",
type=float,
default=DEFAULT_MIN_AREA,
help="Minimum contour area in pixels",
)
return parser
def prepare_output_directory(output_directory):
output_directory.mkdir(parents=True, exist_ok=True)
csv_path = output_directory / "events.csv"
if not csv_path.exists():
with csv_path.open("w", newline="", encoding="utf-8") as csv_file:
writer = csv.writer(csv_file)
writer.writerow(
[
"timestamp",
"area_px",
"center_x",
"center_y",
"bounding_x",
"bounding_y",
"bounding_width",
"bounding_height",
"image_file",
]
)
return csv_path
def append_event(
csv_path,
timestamp,
area,
center_x,
center_y,
bounding_box,
image_file,
):
x, y, width, height = bounding_box
with csv_path.open("a", newline="", encoding="utf-8") as csv_file:
writer = csv.writer(csv_file)
writer.writerow(
[
timestamp,
f"{area:.1f}",
center_x,
center_y,
x,
y,
width,
height,
image_file,
]
)
def create_red_mask(frame):
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask_1 = cv2.inRange(
hsv,
LOWER_RED_1,
UPPER_RED_1,
)
mask_2 = cv2.inRange(
hsv,
LOWER_RED_2,
UPPER_RED_2,
)
mask = cv2.bitwise_or(mask_1, mask_2)
kernel = np.ones((5, 5), dtype=np.uint8)
mask = cv2.morphologyEx(
mask,
cv2.MORPH_OPEN,
kernel,
iterations=1,
)
mask = cv2.morphologyEx(
mask,
cv2.MORPH_CLOSE,
kernel,
iterations=2,
)
return mask
def find_largest_valid_object(mask, min_area):
contours, _ = cv2.findContours(
mask,
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE,
)
if not contours:
return None
largest_contour = max(
contours,
key=cv2.contourArea,
)
area = cv2.contourArea(largest_contour)
if area < min_area:
return None
x, y, width, height = cv2.boundingRect(largest_contour)
moments = cv2.moments(largest_contour)
if moments["m00"] != 0:
center_x = int(moments["m10"] / moments["m00"])
center_y = int(moments["m01"] / moments["m00"])
else:
center_x = x + width // 2
center_y = y + height // 2
return {
"contour": largest_contour,
"area": area,
"bounding_box": (x, y, width, height),
"center": (center_x, center_y),
}
def annotate_frame(frame, detected_object):
annotated = frame.copy()
if detected_object is None:
cv2.putText(
annotated,
"NO TARGET",
(20, 40),
cv2.FONT_HERSHEY_SIMPLEX,
1.0,
(0, 255, 255),
2,
cv2.LINE_AA,
)
return annotated
x, y, width, height = detected_object["bounding_box"]
center_x, center_y = detected_object["center"]
area = detected_object["area"]
cv2.rectangle(
annotated,
(x, y),
(x + width, y + height),
(0, 255, 0),
2,
)
cv2.circle(
annotated,
(center_x, center_y),
6,
(255, 255, 255),
-1,
)
cv2.putText(
annotated,
"RED TARGET DETECTED",
(20, 40),
cv2.FONT_HERSHEY_SIMPLEX,
1.0,
(0, 255, 0),
2,
cv2.LINE_AA,
)
cv2.putText(
annotated,
f"Area: {area:.0f} px",
(20, 75),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
(0, 255, 0),
2,
cv2.LINE_AA,
)
cv2.putText(
annotated,
f"Center: ({center_x}, {center_y})",
(20, 105),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
(0, 255, 0),
2,
cv2.LINE_AA,
)
return annotated
def configure_camera():
picam2 = Picamera2()
configuration = picam2.create_video_configuration(
main={
"size": (FRAME_WIDTH, FRAME_HEIGHT),
"format": "RGB888",
}
)
picam2.configure(configuration)
picam2.start()
if "AfMode" in picam2.camera_controls:
picam2.set_controls(
{
"AfMode": controls.AfModeEnum.Continuous,
}
)
time.sleep(2.0)
return picam2
def main():
parser = create_argument_parser()
args = parser.parse_args()
csv_path = prepare_output_directory(args.output)
picam2 = configure_camera()
target_was_present = False
last_event_time = 0.0
print("Vision inspector started.")
print(f"Minimum object area: {args.min_area:.1f} pixels")
print(f"Event directory: {args.output.resolve()}")
if not args.headless:
print("Press Q to quit.")
print("Press S to save the current annotated frame manually.")
try:
while True:
frame = picam2.capture_array()
mask = create_red_mask(frame)
detected_object = find_largest_valid_object(
mask,
args.min_area,
)
target_present = detected_object is not None
annotated = annotate_frame(
frame,
detected_object,
)
current_time = time.monotonic()
new_target_event = (
target_present
and not target_was_present
and (
current_time - last_event_time
>= EVENT_COOLDOWN_SECONDS
)
)
if new_target_event:
timestamp = datetime.now().strftime(
"%Y-%m-%d_%H-%M-%S-%f"
)
image_name = f"event_{timestamp}.jpg"
image_path = args.output / image_name
cv2.imwrite(
str(image_path),
annotated,
)
x, y, width, height = detected_object["bounding_box"]
center_x, center_y = detected_object["center"]
append_event(
csv_path=csv_path,
timestamp=timestamp,
area=detected_object["area"],
center_x=center_x,
center_y=center_y,
bounding_box=(x, y, width, height),
image_file=image_name,
)
print(
f"Event saved: {image_path}"
)
last_event_time = current_time
target_was_present = target_present
if not args.headless:
cv2.imshow(
"Raspberry Pi Vision Inspector",
annotated,
)
cv2.imshow(
"Segmentation Mask",
mask,
)
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
break
if key == ord("s"):
timestamp = datetime.now().strftime(
"%Y-%m-%d_%H-%M-%S-%f"
)
manual_path = (
args.output
/ f"manual_{timestamp}.jpg"
)
cv2.imwrite(
str(manual_path),
annotated,
)
print(
f"Manual image saved: {manual_path}"
)
except KeyboardInterrupt:
print("Stopping after Ctrl+C.")
finally:
picam2.stop()
cv2.destroyAllWindows()
print("Vision inspector stopped.")
if __name__ == "__main__":
main()
34. Running the project
Make sure the virtual environment is active:
cd ~/projects/vision_inspector
source .venv/bin/activate
Run the application:
python3 vision_inspector.py
Place a clearly red object in front of the camera.
You should see:
- the live camera image,
- the red segmentation mask,
- a green bounding box around the detected object,
- a centroid marker,
- and the measured contour area.
When a new target appears, an annotated image is written to the events directory and one row is appended to events.csv.
35. Understanding the event logic
Why do we not save every frame in which red is visible?
At 30 frames per second, an object remaining in view for five seconds could otherwise produce roughly 150 images.
The software therefore distinguishes between a state and an event.
The state answers:
Is a valid target visible now?
The event answers:
Did the system just transition from no target to target?
This pattern is extremely important in embedded software. Events are often created from transitions between states.
36. Tuning the minimum object area
If small red noise is detected as an object, increase the contour-area threshold:
python3 vision_inspector.py --min-area 6000
If the desired target is small and not detected, reduce it:
python3 vision_inspector.py --min-area 1000
The correct value depends on:
- camera resolution,
- object size,
- camera distance,
- lens field of view,
- and segmentation quality.
A production system should derive such limits from actual validated samples rather than guessing.
37. Running without a monitor
Once everything works, run the program in headless mode:
python3 vision_inspector.py --headless
No OpenCV display windows are created, but detection, image storage and CSV logging continue.
Stop it with Ctrl+C.
38. Inspecting the recorded data
List event images:
ls -lh events
Read the event log:
cat events/events.csv
A typical structure is:
timestamp,area_px,center_x,center_y,bounding_x,bounding_y,bounding_width,bounding_height,image_file
2026-09-11_14-31-22-123456,18432.0,503,271,421,190,165,163,event_2026-09-11_14-31-22-123456.jpg
This is already a basic traceability system: an event has a time, measurements and corresponding image evidence.
39. Transferring results to your PC
From your PC, use SCP over SSH.
scp -r youruser@192.168.1.50:/home/youruser/projects/vision_inspector/events ./
For continuous industrial data transfer, you would normally move beyond manual SCP toward a network API, message broker, database or central storage system.
40. Performance optimisation
Before trying random optimisation tricks, measure the actual bottleneck.
Reduce image resolution
This is often the largest optimisation available. If 640 × 360 is sufficient, processing 4608 × 2592 frames is wasteful.
Process a region of interest
If the object can only occur in one region, crop first and process fewer pixels.
Do cheap operations before expensive ones
Reject obvious non-target frames before executing expensive algorithms.
Avoid unnecessary copies
Large images consume memory bandwidth. Repeated conversion and copying can become significant.
Separate acquisition from processing when necessary
More advanced architectures use threads, queues or processes to prevent slow processing from unnecessarily blocking acquisition. But queues must be bounded; otherwise latency can grow while the software happily processes old frames.
Measure frame processing time
start = time.perf_counter()
# Image-processing operations
elapsed = time.perf_counter() - start
print(
f"Processing time: {elapsed * 1000:.2f} ms"
)
If one frame requires 50 ms of processing, the theoretical processing ceiling is approximately 20 frames per second before considering other overhead.
41. Moving from colour detection to serious computer vision
The project intentionally begins with deterministic classical image processing because every step can be understood.
From here, several directions are possible.
Geometric inspection
Measure:
- object width,
- height,
- area,
- roundness,
- angle,
- edge position,
- hole presence,
- relative alignment.
Calibration
Pixels are not millimetres. Metric measurement requires calibration and an optical arrangement whose geometry is understood.
Feature-based vision
Edges, corners and descriptors can detect structures that are not represented adequately by colour alone.
Machine learning
Neural networks can perform:
- classification,
- object detection,
- semantic segmentation,
- pose estimation,
- anomaly detection.
But AI does not eliminate camera engineering. Bad focus, motion blur, saturation, insufficient light and inconsistent training data remain real problems.
42. CPU processing versus an AI accelerator
Classical OpenCV operations can run directly on the Raspberry Pi CPU. Many smaller neural networks can also run on the CPU, though throughput may be limited.
When model complexity or required frame rate grows, an accelerator becomes useful. Modern Raspberry Pi ecosystems can use dedicated inference hardware, and Raspberry Pi also offers camera and accelerator products intended for AI workloads.
Do not begin with an accelerator simply because AI sounds advanced. First define:
- the required model,
- input resolution,
- required FPS,
- latency target,
- power budget,
- thermal constraints,
- and expected accuracy.
Hardware should be selected from requirements, not the other way around.
43. Common mistakes
Following obsolete camera tutorials
Commands such as raspistill and the original Picamera stack belong to the legacy architecture. Use the modern rpicam tools and Picamera2.
Installing everything with sudo pip
Modern Raspberry Pi OS protects the system Python environment. Use APT or a virtual environment.
Creating an isolated venv and losing Picamera2
If your project relies on the system Picamera2 bindings, use:
python3 -m venv --system-site-packages .venv
Connecting 5 V logic to GPIO
This can damage the board. Use correct voltage translation.
Ignoring grounding
Two digital devices need an appropriate shared electrical reference unless the interface is galvanically isolated.
Powering motors from GPIO
Use a proper driver stage.
Debugging Python before testing rpicam
Prove the camera hardware and base software stack first.
Using maximum resolution unnecessarily
More pixels mean more computation and memory traffic, not automatically better decisions.
Ignoring lighting
A ten-line lighting improvement may outperform hundreds of lines of compensation code.
Removing power instead of shutting down
A writable filesystem deserves a controlled shutdown.
44. A professional troubleshooting hierarchy
When a system fails, debug from the bottom upward.
- Power: is supply voltage and current capacity correct?
- Physical hardware: are connectors, cables and modules installed correctly?
- Operating system: does Linux recognise the hardware?
- Driver stack: does the low-level camera application work?
- Library layer: can Picamera2 acquire an image?
- Image-processing layer: is OpenCV receiving the expected format?
- Algorithm: are segmentation and thresholds valid?
- Application logic: are events and outputs generated correctly?
This prevents the classic mistake of modifying an algorithm when the real fault is a ribbon cable.
45. A realistic learning path from beginner to expert
Stage 1: Linux confidence
Learn:
- filesystem navigation,
- APT,
- processes,
- SSH,
- permissions,
- system logs.
Stage 2: digital hardware
Learn:
- 3.3 V logic,
- GPIO inputs and outputs,
- pull-up and pull-down resistors,
- transistor drivers,
- I2C,
- SPI,
- UART.
Stage 3: Python engineering
Learn:
- functions,
- modules,
- classes,
- exceptions,
- virtual environments,
- logging,
- configuration files,
- unit tests.
Stage 4: image processing
Learn:
- NumPy arrays,
- colour spaces,
- filters,
- thresholding,
- morphology,
- contours,
- edge detection,
- geometry.
Stage 5: machine vision
Learn:
- lighting design,
- optics,
- camera calibration,
- measurement uncertainty,
- latency,
- triggering,
- traceability,
- repeatability.
Stage 6: AI vision
Only after the fundamentals are strong should you move into:
- CNNs,
- object-detection networks,
- segmentation networks,
- model optimisation,
- quantisation,
- hardware acceleration.
Stage 7: product engineering
A professional product requires more than a successful demo:
- watchdogs,
- automatic startup,
- recovery after failure,
- secure updates,
- version management,
- hardware protection,
- EMC considerations,
- thermal validation,
- long-term storage strategy,
- manufacturing and service procedures.
46. Final perspective
The real power of Raspberry Pi is not that it can blink an LED or run Python. Its value comes from bringing several engineering domains onto one compact platform: embedded hardware, Linux, networking, cameras, application software and high-level processing.
Once you understand the layers, the system becomes much less mysterious. Power and electronics form the physical foundation. Firmware starts the computer. Linux manages resources and drivers. Picamera2 exposes the camera pipeline to Python. NumPy represents frames as data. OpenCV transforms that data into measurable features. Application logic finally converts those features into useful decisions.
The practical vision project in this tutorial is intentionally understandable from beginning to end. From here it can evolve into a production counter, inspection machine, robot vision subsystem, laboratory instrument, security camera, sorting system or AI-enabled edge device.
If you are developing an embedded Linux, Raspberry Pi, electronics or computer-vision system and would like to discuss architecture, implementation or technical collaboration, get in touch.
DE