From eab9ada507cc21be94ba3a3d18bed5e9c880afb1 Mon Sep 17 00:00:00 2001 From: Marcelo Rovai Date: Fri, 30 Aug 2024 16:33:59 -0400 Subject: [PATCH] Add files via upload --- image_classification.qmd | 1539 ++++++++++++++++++++++++++++++++++++++ setup.qmd | 579 ++++++++++++++ style.scss | 24 + 3 files changed, 2142 insertions(+) create mode 100644 image_classification.qmd create mode 100644 setup.qmd diff --git a/image_classification.qmd b/image_classification.qmd new file mode 100644 index 00000000..c4405d72 --- /dev/null +++ b/image_classification.qmd @@ -0,0 +1,1539 @@ +# Image Classification {.unnumbered} + +![*DALL·E prompt - A cover image for an 'Image Classification' chapter in a Raspberry Pi tutorial, designed in the same vintage 1950s electronics lab style as previous covers. The scene should feature a Raspberry Pi connected to a camera module, with the camera capturing a photo of the small blue robot provided by the user. The robot should be placed on a workbench, surrounded by classic lab tools like soldering irons, resistors, and wires. The lab background should include vintage equipment like oscilloscopes and tube radios, maintaining the detailed and nostalgic feel of the era. No text or logos should be included.*](images/jpeg/img_class_cover.jpg) + +## Introduction + +Image classification is a fundamental task in computer vision that involves categorizing an image into one of several predefined classes. It's a cornerstone of artificial intelligence, enabling machines to interpret and understand visual information in a way that mimics human perception. + +Image classification refers to assigning a label or category to an entire image based on its visual content. This task is crucial in computer vision and has numerous applications across various industries. Image classification's importance lies in its ability to automate visual understanding tasks that would otherwise require human intervention. + +### Applications in Real-World Scenarios + +Image classification has found its way into numerous real-world applications, revolutionizing various sectors: + +- Healthcare: Assisting in medical image analysis, such as identifying abnormalities in X-rays or MRIs. +- Agriculture: Monitoring crop health and detecting plant diseases through aerial imagery. +- Automotive: Enabling advanced driver assistance systems and autonomous vehicles to recognize road signs, pedestrians, and other vehicles. +- Retail: Powering visual search capabilities and automated inventory management systems. +- Security and Surveillance: Enhancing threat detection and facial recognition systems. +- Environmental Monitoring: Analyzing satellite imagery for deforestation, urban planning, and climate change studies. + +### Advantages of Running Classification on Edge Devices like Raspberry Pi + +Implementing image classification on edge devices such as the Raspberry Pi offers several compelling advantages: + +1. Low Latency: Processing images locally eliminates the need to send data to cloud servers, significantly reducing response times. + +2. Offline Functionality: Classification can be performed without an internet connection, making it suitable for remote or connectivity-challenged environments. + +3. Privacy and Security: Sensitive image data remains on the local device, addressing data privacy concerns and compliance requirements. + +4. Cost-Effectiveness: Eliminates the need for expensive cloud computing resources, especially for continuous or high-volume classification tasks. + +5. Scalability: Enables distributed computing architectures where multiple devices can work independently or in a network. + +6. Energy Efficiency: Optimized models on dedicated hardware can be more energy-efficient than cloud-based solutions, which is crucial for battery-powered or remote applications. + +7. Customization: Deploying specialized or frequently updated models tailored to specific use cases is more manageable. + +We can create more responsive, secure, and efficient computer vision solutions by leveraging the power of edge devices like Raspberry Pi for image classification. This approach opens up new possibilities for integrating intelligent visual processing into various applications and environments. + +In the following sections, we'll explore how to implement and optimize image classification on the Raspberry Pi, harnessing these advantages to create powerful and efficient computer vision systems. + +## Setting Up the Environment + +### Updating the Raspberry Pi + +First, ensure your Raspberry Pi is up to date: + +```bash +sudo apt update +sudo apt upgrade -y +``` + +### Installing Required Libraries + +Install the necessary libraries for image processing and machine learning: + +```bash +sudo apt install python3-pip +sudo rm /usr/lib/python3.11/EXTERNALLY-MANAGED +pip3 install --upgrade pip +``` + +### Setting up a Virtual Environment (Optional but Recommended) + +Create a virtual environment to manage dependencies: + +```bash +python3 -m venv ~/tflite +source ~/tflite/bin/activate +``` + +### Installing TensorFlow Lite + +We are interested in performing **inference**, which refers to executing a TensorFlow Lite model on a device to make predictions based on input data. To perform an inference with a TensorFlow Lite model, we must run it through an **interpreter**. The TensorFlow Lite interpreter is designed to be lean and fast. The interpreter uses a static graph ordering and a custom (less-dynamic) memory allocator to ensure minimal load, initialization, and execution latency. + +We'll use the [TensorFlow Lite runtime](https://pypi.org/project/tflite-runtime/) for Raspberry Pi, a simplified library for running machine learning models on mobile and embedded devices, without including all TensorFlow packages. + +```bash +pip install tflite_runtime --no-deps +``` + +> The wheel installed: `tflite_runtime-2.14.0-cp311-cp311-manylinux_2_34_aarch64.whl` + +### Installing Additional Python Libraries + +Install required Python libraries for use with Image Classification: + +If you have another version of Numpy installed, first uninstall it. + +```bash +pip3 uninstall numpy +``` + +Install `version 1.23.2`, which is compatible with the tflite_runtime. + +```bash + pip3 install numpy==1.23.2 +``` + +```bash +pip3 install Pillow matplotlib +``` + +### Creating a working directory: + +If you are working on the Raspi-Zero with the minimum OS (No Desktop), you may not have a user-pre-defined directory tree (you can check it with `ls`. So, let's create one: + +```bash +mkdir Documents +cd Documents/ +mkdir TFLITE +cd TFLITE/ +mkdir IMG_CLASS +cd IMG_CLASS +mkdir models +cd models +``` + +> On the Raspi-5, the /Documents should be there. + +**Get a pre-trained Image Classification model**: + +An appropriate pre-trained model is crucial for successful image classification on resource-constrained devices like the Raspberry Pi. **MobileNet** is designed for mobile and embedded vision applications with a good balance between accuracy and speed. Versions: MobileNetV1, MobileNetV2, MobileNetV3. Let's download the V2: + +```bash +wget https://storage.googleapis.com/download.tensorflow.org/models/ +tflite_11_05_08/mobilenet_v2_1.0_224_quant.tgz + +tar xzf mobilenet_v2_1.0_224_quant.tgz +``` + +Get its labels: + +```bash +wget https://raw.githubusercontent.com/tensorflow/tensorflow/master/tensorflow/ +lite/java/demo/app/src/main/assets/labels_mobilenet_quant_v1_224.txt -O labels.txt +``` + +In the end, you should have the models in its directory: + +![](images/png/models_dir.png) + +> We will only need the `mobilenet_v2_1.0_224_quant.tflite` model and the `labels.txt`. You can delete the other files. + +### Setting up Jupyter Notebook (Optional) + +If you prefer using Jupyter Notebook for development: + +```bash +pip3 install jupyter +jupyter notebook --generate-config +``` + +To run Jupyter Notebook, run the command (change the IP address for yours): + +```bash +jupyter notebook --ip=192.168.4.210 --no-browser +``` + +On the terminal, you can see the local URL address to open the notebook: + +![](images/png/notebook_token.png) + +You can access it from another device by entering the Raspberry Pi's IP address and the provided token in a web browser (you can copy the token from the terminal). + +![](images/png/image-20240823145059675.png) + +Define your working directory in the Raspi and create a new Python 3 notebook. + +### Verifying the Setup + +Test your setup by running a simple Python script: + +```python +import tflite_runtime.interpreter as tflite +import numpy as np +from PIL import Image + +print("NumPy:", np.__version__) +print("Pillow:", Image.__version__) + +# Try to create a TFLite Interpreter +model_path = "./models/mobilenet_v2_1.0_224_quant.tflite" +interpreter = tflite.Interpreter(model_path=model_path) +interpreter.allocate_tensors() +print("TFLite Interpreter created successfully!") +``` + +You can create the Python script using nano on the terminal, saving it with `CTRL+0` + `ENTER` + `CTRL+X` + +![](images/png/nano.png) + +And run it with the command: + +![](images/png/test_result.png) + +Or you can run it directly on the [Notebook](https://github.com/Mjrovai/EdgeML-with-Raspberry-Pi/blob/main/IMG_CLASS/notebooks/setup_test.ipynb): + +![](images/png/notebook_test.png) + +## Making inferences with Mobilenet V2 + +In the last section, we set up the environment, including downloading a popular pre-trained model, Mobilenet V2, trained on ImageNet's 224x224 images (1.2 million) for 1,001 classes (1,000 object categories plus 1 background). The model was converted to a compact 3.5MB TensorFlow Lite format, making it suitable for the limited storage and memory of a Raspberry Pi. + +![](images/png/mobilinet_zero.png) + +Let's start a new [notebook](https://github.com/Mjrovai/EdgeML-with-Raspberry-Pi/blob/main/IMG_CLASS/notebooks/10_Image_Classification.ipynb) to follow all the steps to classify one image: + +Import the needed libraries: + +```python +import time +import numpy as np +import matplotlib.pyplot as plt +from PIL import Image +import tflite_runtime.interpreter as tflite +``` + +Load the TFLite model and allocate tensors: + +```python +model_path = "./models/mobilenet_v2_1.0_224_quant.tflite" +interpreter = tflite.Interpreter(model_path=model_path) +interpreter.allocate_tensors() +``` + +Get input and output tensors. + +```python +input_details = interpreter.get_input_details() +output_details = interpreter.get_output_details() +``` + +**Input details** will give us information about how the model should be fed with an image. The shape of (1, 224, 224, 3) informs us that an image with dimensions (224x224x3) should be input one by one (Batch Dimension: 1). + +![](images/png/input_details.png) + +The **output details** show that the inference will result in an array of 1,001 integer values. Those values result from the image classification, where each value is the probability of that specific label being related to the image. + +![](images/png/output_details.png) + +Let's also inspect the dtype of input details of the model + +```python +input_dtype = input_details[0]['dtype'] +input_dtype +``` + +``` +dtype('uint8') +``` + +This shows that the input image should be raw pixels (0 - 255). + +Let's get a test image. You can transfer it from your computer or download one for testing. Let's first create a folder under our working directory: + +```bash +mkdir images +cd images +wget https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg +``` + +Let's load and display the image: + +```python +# Load he image +img_path = "./images/Cat03.jpg" +img = Image.open(img_path) + +# Display the image +plt.figure(figsize=(8, 8)) +plt.imshow(img) +plt.title("Original Image") +plt.show() +``` + +![](images/png/cat_original.png) + +We can see the image size running the command: + +```python +width, height = img.size +``` + +That shows us that the image is an RGB image with a width of 1600 and a height of 1600 pixels. So, to use our model, we should reshape it to (224, 224, 3) and add a batch dimension of 1, as defined in input details: (1, 224, 224, 3). The inference result, as shown in output details, will be an array with a 1001 size, as shown below: + +![](images/png/process_img.png) + +So, let's reshape the image, add the batch dimension, and see the result: + +```python +img = img.resize((input_details[0]['shape'][1], input_details[0]['shape'][2])) +input_data = np.expand_dims(img, axis=0 +input_data.shape +``` + +The input_data shape is as expected: (1, 224, 224, 3) + +Let's confirm the dtype of the input data: + +```python +input_data.dtype +``` + +``` +dtype('uint8') +``` + +The input data dtype is 'uint8', which is compatible with the dtype expected for the model. + +Using the input_data, let's run the interpreter and get the predictions (output): + +```python +interpreter.set_tensor(input_details[0]['index'], input_data) +interpreter.invoke() +predictions = interpreter.get_tensor(output_details[0]['index'])[0] +``` + +The prediction is an array with 1001 elements. Let’s get the Top-5 indices where their elements have high values: + +```python +top_k_results = 5 +top_k_indices = np.argsort(predictions)[::-1][:top_k_results] +top_k_indices +``` + +The top_k_indices is an array with 5 elements: `array([283, 286, 282])` + +So, 283, 286, 282, 288, and 479 are the image's most probable classes. Having the index, we must find to what class it appoints (such as car, cat, or dog). The text file downloaded with the model has a label associated with each index from 0 to 1,000. Let’s use a function to load the .txt file as a list: + +```python +def load_labels(filename): + with open(filename, 'r') as f: + return [line.strip() for line in f.readlines()] +``` + +And get the list, printing the labels associated with the indexes: + +```python +labels_path = "./models/labels.txt" +labels = load_labels(labels_path) + +print(labels[286]) +print(labels[283]) +print(labels[282]) +print(labels[288]) +print(labels[479]) +``` + +As a result, we have: + +```bash +Egyptian cat +tiger cat +tabby +lynx +carton +``` + +At least the four top indices are related to felines. The **prediction** content is the probability associated with each one of the labels. As we saw on output details, those values are quantized and should be dequantized and apply softmax. + +```python +scale, zero_point = output_details[0]['quantization'] +dequantized_output = (predictions.astype(np.float32) - zero_point) * scale +exp_output = np.exp(dequantized_output - np.max(dequantized_output)) +probabilities = exp_output / np.sum(exp_output) +``` + +Let's print the top-5 probabilities: + +```python +print (probabilities[286]) +print (probabilities[283]) +print (probabilities[282]) +print (probabilities[288]) +print (probabilities[479]) +``` + +```bash +0.27741462 +0.3732285 +0.16919471 +0.10319158 +0.023410844 +``` + +For clarity, let's create a function to relate the labels with the probabilities: + +```python +for i in range(top_k_results): + print("\t{:20}: {}%".format( + labels[top_k_indices[i]], + (int(probabilities[top_k_indices[i]]*100)))) +``` + +```bash +tiger cat : 37% +Egyptian cat : 27% +tabby : 16% +lynx : 10% +carton : 2% +``` + +### Define a general Image Classification function + +Let's create a general function to give an image as input, and we get the Top-5 possible classes: +
+```python +def image_classification(img_path, model_path, labels, top_k_results=5): + # load the image + img = Image.open(img_path) + plt.figure(figsize=(4, 4)) + plt.imshow(img) + plt.axis('off') + + # Load the TFLite model + interpreter = tflite.Interpreter(model_path=model_path) + interpreter.allocate_tensors() + + # Get input and output tensors + input_details = interpreter.get_input_details() + output_details = interpreter.get_output_details() + + # Preprocess + img = img.resize((input_details[0]['shape'][1], + input_details[0]['shape'][2])) + input_data = np.expand_dims(img, axis=0) + + # Inference on Raspi-Zero + interpreter.set_tensor(input_details[0]['index'], input_data) + interpreter.invoke() + + # Obtain results and map them to the classes + predictions = interpreter.get_tensor(output_details[0]['index'])[0] + + # Get indices of the top k results + top_k_indices = np.argsort(predictions)[::-1][:top_k_results] + + # Get quantization parameters + scale, zero_point = output_details[0]['quantization'] + + # Dequantize the output and apply softmax + dequantized_output = (predictions.astype(np.float32) - zero_point) * scale + exp_output = np.exp(dequantized_output - np.max(dequantized_output)) + probabilities = exp_output / np.sum(exp_output) + + print("\n\t[PREDICTION] [Prob]\n") + for i in range(top_k_results): + print("\t{:20}: {}%".format( + labels[top_k_indices[i]], + (int(probabilities[top_k_indices[i]]*100)))) +``` +
+And loading some images for testing, we have: + +![](images/jpeg/img_class_func.jpg) + +### Testing with a model trained from scratch + +Let's get a TFLite model trained from scratch. For that, you can follow the Notebook: + +[CNN to classify Cifar-10 dataset](https://colab.research.google.com/github/Mjrovai/UNIFEI-IESTI01-TinyML-2022.1/blob/main/00_Curse_Folder/2_Applications_Deploy/Class_16/cifar_10/CNN_Cifar_10_TFLite.ipynb#scrollTo=iiVBUpuHXEtw) + + In the notebook, we trained a model using the CIFAR10 dataset, which contains 60,000 images from 10 classes of CIFAR (*airplane, automobile, bird, cat, deer, dog, frog, horse, ship, and truck*). CIFAR has 32x32 color images (3 color channels) where the objects are not centered and can have the object with a background, such as airplanes that might have a cloudy sky behind them! In short, small but real images. + +The CNN trained model (*cifar10_model.keras*) had a size of 2.0MB. Using the *TFLite Converter*, the model *cifar10.tflite* became with 674MB (around 1/3 of the original size). + +![](images/png/cifar10_model.png) + +On the notebook [Cifar 10 - Image Classification on a Raspi with TFLite](https://github.com/Mjrovai/EdgeML-with-Raspberry-Pi/blob/main/IMG_CLASS/notebooks/20_Cifar_10_Image_Classification.ipynb) (which can be run over the Raspi), we can follow the same steps we did with the `mobilenet_v2_1.0_224_quant.tflite`. Below are examples of images using the *General Function for Image Classification* on a Raspi-Zero, as shown in the last section. + +![](images/png/infer-cifar10.png) + + + +### Installing Picamera2 + +[Picamera2](https://github.com/raspberrypi/picamera2), a Python library for interacting with Raspberry Pi’s camera, is based on the *libcamera* camera stack, and the Raspberry Pi foundation maintains it. The Picamera2 library is supported on all Raspberry Pi models, from the Pi Zero to the RPi 5. It is already installed system-wide on the Raspi, but we should make it accessible within the virtual environment. + +1. First, activate the virtual environment if it's not already activated: + + ```bash + source ~/tflite/bin/activate + ``` + +2. Now, let's create a .pth file in your virtual environment to add the system site-packages path: + + ```bash + echo "/usr/lib/python3/dist-packages" > $VIRTUAL_ENV/lib/python3.11/ + site-packages/system_site_packages.pth + ``` + + > Note: If your Python version differs, replace `python3.11` with the appropriate version. + +3. After creating this file, try importing picamera2 in Python: + + ```bash + python3 + >>> import picamera2 + >>> print(picamera2.__file__) + ``` + +The above code will show the file location of the `picamera2` module itself, proving that the library can be accessed from the environment. + +```python +/home/mjrovai/tflite/lib/python3.11/site-packages/picamera2/__init__.py +``` + +You can also list the available cameras in the system: + +```python +>>> print(Picamera2.global_camera_info()) +``` + +In my case, with a USB installed, I got: + +![](images/png/cam_installed.png) + +Now that we've confirmed picamera2 is working in the environment with an `index 0`, let's try a simple Python script to capture an image from your USB camera: + +```python +from picamera2 import Picamera2 +import time + +# Initialize the camera +picam2 = Picamera2() # default is index 0 + +# Configure the camera +config = picam2.create_still_configuration(main={"size": (640, 480)}) +picam2.configure(config) + +# Start the camera +picam2.start() + +# Wait for the camera to warm up +time.sleep(2) + +# Capture an image +picam2.capture_file("usb_camera_image.jpg") +print("Image captured and saved as 'usb_camera_image.jpg'") + +# Stop the camera +picam2.stop() +``` + +Use the Nano text editor, the Jupyter Notebook, or any other editor. Save this as a Python script (e.g., `capture_image.py`) and run it. This should capture an image from your camera and save it as "usb_camera_image.jpg" in the same directory as your script. + +![](images/png/capture_test.png) + +If the Jupyter is open, you can see the captured image on your computer. Otherwise, transfer the file from the Raspi to your computer. + +![](images/png/img_test_result.png) + +> If you are working with a Raspi-5 with a whole desktop, you can open the file directly on the device. + +## Image Classification Project + +Now, we will develop a complete Image Classification project using the Edge Impulse Studio. As we did with the Movilinet V2, the trained and converted TFLite model will be used for inference. + +### The Goal + +The first step in any ML project is to define its goal. In this case, it is to detect and classify two specific objects present in one image. For this project, we will use two small toys: a robot and a small Brazilian parrot (named Periquito). We will also collect images of a *background* where those two objects are absent. + +![](images/jpeg/project_goal.jpg) + +### Data Collection + +Once we have defined our Machine Learning project goal, the next and most crucial step is collecting the dataset. We can use a phone for the image capture, but we will use the Raspi here. Let's set up a simple web server on our Raspberry Pi to view the `QVGA (320 x 240)` captured images in a browser. + +1. First, let's install Flask, a lightweight web framework for Python: + + ```bash + pip3 install flask + ``` + +2. Let's create a new Python script combining image capture with a web server. We'll call it `get_img_data.py`: + +
+```python +from flask import Flask, Response, render_template_string, request, redirect, url_for +from picamera2 import Picamera2 +import io +import threading +import time +import os +import signal + +app = Flask(__name__) + +# Global variables +base_dir = "dataset" +picam2 = None +frame = None +frame_lock = threading.Lock() +capture_counts = {} +current_label = None +shutdown_event = threading.Event() + +def initialize_camera(): + global picam2 + picam2 = Picamera2() + config = picam2.create_preview_configuration(main={"size": (320, 240)}) + picam2.configure(config) + picam2.start() + time.sleep(2) # Wait for camera to warm up + +def get_frame(): + global frame + while not shutdown_event.is_set(): + stream = io.BytesIO() + picam2.capture_file(stream, format='jpeg') + with frame_lock: + frame = stream.getvalue() + time.sleep(0.1) # Adjust as needed for smooth preview + +def generate_frames(): + while not shutdown_event.is_set(): + with frame_lock: + if frame is not None: + yield (b'--frame\r\n' + b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') + time.sleep(0.1) # Adjust as needed for smooth streaming + +def shutdown_server(): + shutdown_event.set() + if picam2: + picam2.stop() + # Give some time for other threads to finish + time.sleep(2) + # Send SIGINT to the main process + os.kill(os.getpid(), signal.SIGINT) + +@app.route('/', methods=['GET', 'POST']) +def index(): + global current_label + if request.method == 'POST': + current_label = request.form['label'] + if current_label not in capture_counts: + capture_counts[current_label] = 0 + os.makedirs(os.path.join(base_dir, current_label), exist_ok=True) + return redirect(url_for('capture_page')) + return render_template_string(''' + + + + Dataset Capture - Label Entry + + +

Enter Label for Dataset

+
+ + +
+ + + ''') + +@app.route('/capture') +def capture_page(): + return render_template_string(''' + + + + Dataset Capture + + + +

Dataset Capture

+

Current Label: {{ label }}

+

Images captured for this label: {{ capture_count }}

+ + +
+ +
+
+ +
+
+ +
+ + + ''', label=current_label, capture_count=capture_counts.get(current_label, 0)) + +@app.route('/video_feed') +def video_feed(): + return Response(generate_frames(), + mimetype='multipart/x-mixed-replace; boundary=frame') + +@app.route('/capture_image', methods=['POST']) +def capture_image(): + global capture_counts + if current_label and not shutdown_event.is_set(): + capture_counts[current_label] += 1 + timestamp = time.strftime("%Y%m%d-%H%M%S") + filename = f"image_{timestamp}.jpg" + full_path = os.path.join(base_dir, current_label, filename) + + picam2.capture_file(full_path) + + return redirect(url_for('capture_page')) + +@app.route('/stop', methods=['POST']) +def stop(): + summary = render_template_string(''' + + + + Dataset Capture - Stopped + + +

Dataset Capture Stopped

+

The capture process has been stopped. You can close this window.

+

Summary of captures:

+ + + + ''', capture_counts=capture_counts) + + # Start a new thread to shutdown the server + threading.Thread(target=shutdown_server).start() + + return summary + +@app.route('/check_shutdown') +def check_shutdown(): + return {'shutdown': shutdown_event.is_set()} + +if __name__ == '__main__': + initialize_camera() + threading.Thread(target=get_frame, daemon=True).start() + app.run(host='0.0.0.0', port=5000, threaded=True) +``` +
+ +4. Run this script: + + ```bash + python3 get_img_data.py + ``` + +3. Access the web interface: + + - On the Raspberry Pi itself (if you have a GUI): Open a web browser and go to `http://localhost:5000` + - From another device on the same network: Open a web browser and go to `http://:5000` (Replace `` with your Raspberry Pi's IP address). For example: `http://192.168.4.210:5000/` + +This Python script creates a web-based interface for capturing and organizing image datasets using a Raspberry Pi and its camera. It's handy for machine learning projects that require labeled image data. + +#### Key Features: + +1. **Web Interface**: Accessible from any device on the same network as the Raspberry Pi. +2. **Live Camera Preview**: This shows a real-time feed from the camera. +3. **Labeling System**: Allows users to input labels for different categories of images. +4. **Organized Storage**: Automatically saves images in label-specific subdirectories. +5. **Per-Label Counters**: Keeps track of how many images are captured for each label. +6. **Summary Statistics**: Provides a summary of captured images when stopping the capture process. + +#### Main Components: + +1. **Flask Web Application**: Handles routing and serves the web interface. +2. **Picamera2 Integration**: Controls the Raspberry Pi camera. +3. **Threaded Frame Capture**: Ensures smooth live preview. +4. **File Management**: Organizes captured images into labeled directories. + +#### Key Functions: + +- `initialize_camera()`: Sets up the Picamera2 instance. +- `get_frame()`: Continuously captures frames for the live preview. +- `generate_frames()`: Yields frames for the live video feed. +- `shutdown_server()`: Sets the shutdown event, stops the camera, and shuts down the Flask server +- `index()`: Handles the label input page. +- `capture_page()`: Displays the main capture interface. +- `video_feed()`: Shows a live preview to position the camera +- `capture_image()`: Saves an image with the current label. +- `stop()`: Stops the capture process and displays a summary. + +#### Usage Flow: + +1. Start the script on your Raspberry Pi. +2. Access the web interface from a browser. +3. Enter a label for the images you want to capture and press `Start Capture`. + +![](images/png/enter_label.png) + +4. Use the live preview to position the camera. +5. Click `Capture Image` to save images under the current label. + +![](images/png/capture.png) + +6. Change labels as needed for different categories, selecting `Change Label`. +7. Click `Stop Capture` when finished to see a summary. + +![](images/png/stop.png) + +#### Technical Notes: + +- The script uses threading to handle concurrent frame capture and web serving. +- Images are saved with timestamps in their filenames for uniqueness. +- The web interface is responsive and can be accessed from mobile devices. + +#### Customization Possibilities: + +- Adjust image resolution in the `initialize_camera()` function. Here we used QVGA (320X240). +- Modify the HTML templates for a different look and feel. +- Add additional image processing or analysis steps in the `capture_image()` function. + +#### Number of samples on Dataset: + +Get around 60 images from each category (`periquito`, `robot` and `background`). Try to capture different angles, backgrounds, and light conditions. On the Raspi, we will end with a folder named `dataset`, witch contains 3 sub-folders *periquito,* *robot*, and *background*. one for each class of images. + +You can use `Filezilla` to transfer the created dataset to your main computer. + +## Training the model with Edge Impulse Studio + +We will use the Edge Impulse Studio to train our model. Go to the [Edge Impulse Page](https://edgeimpulse.com/), enter your account credentials, and create a new project: + +![](images/png/new-proj-ei.png) + +> Here, you can clone a similar project: [Raspi - Img Class](https://studio.edgeimpulse.com/public/510251/live). + +### Dataset + +We will walk through four main steps using the EI Studio (or Studio). These steps are crucial in preparing our model for use on the Raspi: Dataset, Impulse, Tests, and Deploy (on the Edge Device, in this case, the Raspi). + +> Regarding the Dataset, it is essential to point out that our Original Dataset, captured with the Raspi, will be split into *Training*, *Validation*, and *Test*. The Test Set will be separated from the beginning and reserved for use only in the Test phase after training. The Validation Set will be used during training. + +On Studio, follow the steps to upload the captured data: + +1. Go to the `Data acquisition` tab, and in the `UPLOAD DATA` section, upload the files from your computer in the chosen categories. +2. Leave to the Studio the splitting of the original dataset into *train and test* and choose the label about +3. Repeat the procedure for all three classes. At the end, you should see your "raw data" in the Studio: + +![](images/png/data-Aquisition.png) + +The Studio allows you to explore your data, showing a complete view of all the data in your project. You can clear, inspect, or change labels by clicking on individual data items. In our case, a straightforward project, the data seems OK. + +![](images/png/data-esplorer.png) + +## The Impulse Design + +In this phase, we should define how to: + +- Pre-process our data, which consists of resizing the individual images and determining the `color depth` to use (be it RGB or Grayscale) and + +- Specify a Model. In this case, it will be the `Transfer Learning (Images)` to fine-tune a pre-trained MobileNet V2 image classification model on our data. This method performs well even with relatively small image datasets (around 180 images in our case). + +Transfer Learning with MobileNet offers a streamlined approach to model training, which is especially beneficial for resource-constrained environments and projects with limited labeled data. MobileNet, known for its lightweight architecture, is a pre-trained model that has already learned valuable features from a large dataset (ImageNet). + +![](images/jpeg/model_1.jpg) + +By leveraging these learned features, we can train a new model for your specific task with fewer data and computational resources and achieve competitive accuracy. + +![](images/jpeg/model_2.jpg) + +This approach significantly reduces training time and computational cost, making it ideal for quick prototyping and deployment on embedded devices where efficiency is paramount. + +Go to the Impulse Design Tab and create the *impulse*, defining an image size of 160x160 and squashing them (squared form, without cropping). Select Image and Transfer Learning blocks. Save the Impulse. + +![](images/png/impulse.png) + +### Image Pre-Processing + +All the input QVGA/RGB565 images will be converted to 76,800 features (160x160x3). + +![](images/png/preproc.png) + +Press `Save parameters` and select `Generate features` in the next tab. + +### Model Design + +MobileNet is a family of efficient convolutional neural networks designed for mobile and embedded vision applications. The key features of MobileNet are: + +1. Lightweight: Optimized for mobile devices and embedded systems with limited computational resources. +2. Speed: Fast inference times, suitable for real-time applications. +3. Accuracy: Maintains good accuracy despite its compact size. + +[MobileNetV2](https://arxiv.org/abs/1801.04381), introduced in 2018, improves the original MobileNet architecture. Key features include: + +1. Inverted Residuals: Inverted residual structures are used where shortcut connections are made between thin bottleneck layers. +2. Linear Bottlenecks: Removes non-linearities in the narrow layers to prevent the destruction of information. +3. Depth-wise Separable Convolutions: Continues to use this efficient operation from MobileNetV1. + +In our project, we will do a `Transfer Learning` with the `MobileNetV2 160x160 1.0`, which means that the images used for training (and future inference) should have an *input Size* of 160x160 pixels and a *Width Multiplier* of 1.0 (full width, not reduced). This configuration balances between model size, speed, and accuracy. + +### Model Training + +Another valuable deep learning technique is **Data Augmentation**. Data augmentation improves the accuracy of machine learning models by creating additional artificial data. A data augmentation system makes small, random changes to the training data during the training process (such as flipping, cropping, or rotating the images). + +Looking under the hood, here you can see how Edge Impulse implements a data Augmentation policy on your data: + +``` python +# Implements the data augmentation policy +def augment_image(image, label): + # Flips the image randomly + image = tf.image.random_flip_left_right(image) + + # Increase the image size, then randomly crop it down to + # the original dimensions + resize_factor = random.uniform(1, 1.2) + new_height = math.floor(resize_factor * INPUT_SHAPE[0]) + new_width = math.floor(resize_factor * INPUT_SHAPE[1]) + image = tf.image.resize_with_crop_or_pad(image, new_height, new_width) + image = tf.image.random_crop(image, size=INPUT_SHAPE) + + # Vary the brightness of the image + image = tf.image.random_brightness(image, max_delta=0.2) + + return image, label +``` + +Exposure to these variations during training can help prevent your model from taking shortcuts by "memorizing" superficial clues in your training data, meaning it may better reflect the deep underlying patterns in your dataset. + +The final dense layer of our model will have 0 neurons with a 10% dropout for overfitting prevention. Here is the Training result: + +![](images/png/result-train.png) + +The result is excellent, with a reasonable 35ms of latency (for a Rasp-4), which should result in around 30 fps (frames per second) during inference. A Raspi-Zero should be slower, and the Rasp-5, faster. + +### Trading off: Accuracy versus speed + +If faster inference is needed, we should train the model using smaller alphas (0.35, 0.5, and 0.75) or even reduce the image input size, trading with accuracy. However, reducing the input image size and decreasing the alpha (width multiplier) can speed up inference for MobileNet V2, but they have different trade-offs. Let's compare: + +1. Reducing Image Input Size: + +Pros: + +- Significantly reduces the computational cost across all layers. +- Decreases memory usage. +- It often provides a substantial speed boost. + +Cons: + +- It may reduce the model's ability to detect small features or fine details. +- It can significantly impact accuracy, especially for tasks requiring fine-grained recognition. + +2. Reducing Alpha (Width Multiplier): + +Pros: + +- Reduces the number of parameters and computations in the model. +- Maintains the original input resolution, potentially preserving more detail. +- It can provide a good balance between speed and accuracy. + +Cons: + +- It may not speed up inference as dramatically as reducing input size. +- It can reduce the model's capacity to learn complex features. + +Comparison: + +1. Speed Impact: + - Reducing input size often provides a more substantial speed boost because it reduces computations quadratically (halving both width and height reduces computations by about 75%). + - Reducing alpha provides a more linear reduction in computations. + +2. Accuracy Impact: + - Reducing input size can severely impact accuracy, especially when detecting small objects or fine details. + - Reducing alpha tends to have a more gradual impact on accuracy. + +3. Model Architecture: + - Changing input size doesn't alter the model's architecture. + - Changing alpha modifies the model's structure by reducing the number of channels in each layer. + +Recommendation: + +1. If our application doesn't require detecting tiny details and can tolerate some loss in accuracy, reducing the input size is often the most effective way to speed up inference. +2. Reducing alpha might be preferable if maintaining the ability to detect fine details is crucial or if you need a more balanced trade-off between speed and accuracy. +3. For best results, you might want to experiment with both: + - Try MobileNet V2 with input sizes like 160x160 or 92x92 + - Experiment with alpha values like 1.0, 0.75, 0.5 or 0.35. +4. Always benchmark the different configurations on your specific hardware and with your particular dataset to find the optimal balance for your use case. + +> Remember, the best choice depends on your specific requirements for accuracy, speed, and the nature of the images you're working with. It's often worth experimenting with combinations to find the optimal configuration for your particular use case. + +### Model Testing + +Now, you should take the data set aside at the start of the project and run the trained model using it as input. Again, the result is excellent (92.22%). + +### Deploying the model + +As we did in the previous section, we can deploy the trained model as .tflite and use Raspi to run it using Python. + +On the `Dashboard` tab, go to Transfer learning model (int8 quantized) and click on the download icon: + +![](images/png/model.png) + +> Let's also download the float32 version for comparasion + +Transfer the model from your computer to the Raspi (./models), for example, using FileZilla. Also, capture some images for inference (./images). + +Import the needed libraries: + +```python +import time +import numpy as np +import matplotlib.pyplot as plt +from PIL import Image +import tflite_runtime.interpreter as tflite +``` + +Define the paths and labels: + +```python +img_path = "./images/robot.jpg" +model_path = "./models/ei-raspi-img-class-int8-quantized-model.tflite" +labels = ['background', 'periquito', 'robot'] +``` + +> Note that the models trained on the Edge Impulse Studio will output values with index 0, 1, 2, etc., where the actual labels will follow an alphabetic order. + +Load the model, allocate the tensors, and get the input and output tensor details: + +```python +# Load the TFLite model +interpreter = tflite.Interpreter(model_path=model_path) +interpreter.allocate_tensors() + +# Get input and output tensors +input_details = interpreter.get_input_details() +output_details = interpreter.get_output_details() +``` + +One important difference to note is that the `dtype` of the input details of the model is now `int8`, which means that the input values go from -128 to +127, while each pixel of our image goes from 0 to 256. This means that we should pre-process the image to match it. We can check here: + +```python +input_dtype = input_details[0]['dtype'] +input_dtype +``` + +``` +numpy.int8 +``` + +So, let's open the image and show it: + +```python +img = Image.open(img_path) +plt.figure(figsize=(4, 4)) +plt.imshow(img) +plt.axis('off') +plt.show() +``` + +![](images/png/infer_robot.png) + +And perform the pre-processing: + +```python +scale, zero_point = input_details[0]['quantization'] +img = img.resize((input_details[0]['shape'][1], + input_details[0]['shape'][2])) +img_array = np.array(img, dtype=np.float32) / 255.0 +img_array = (img_array / scale + zero_point).clip(-128, 127).astype(np.int8) +input_data = np.expand_dims(img_array, axis=0) +``` + +Checking the input data, we can verify that the input tensor is compatible with what is expected by the model: + +```python +input_data.shape, input_data.dtype +``` + +``` +((1, 160, 160, 3), dtype('int8')) +``` + +Now, it is time to perform the inference. Let's also calculate the latency of the model: + +```python +# Inference on Raspi-Zero +start_time = time.time() +interpreter.set_tensor(input_details[0]['index'], input_data) +interpreter.invoke() +end_time = time.time() +inference_time = (end_time - start_time) * 1000 # Convert to milliseconds +print ("Inference time: {:.1f}ms".format(inference_time)) +``` + +The model will take around 125ms to perform the inference in the Raspi-Zero, which is 3 to 4 times longer than a Raspi-5. + +Now, we can get the output labels and probabilities. It is also important to note that the model trained on the Edge Impulse Studio has a softmax in its output (different from the original Movilenet V2), and we should use the model's raw output as the “probabilities.” + +```python +# Obtain results and map them to the classes +predictions = interpreter.get_tensor(output_details[0]['index'])[0] + +# Get indices of the top k results +top_k_results=3 +top_k_indices = np.argsort(predictions)[::-1][:top_k_results] + +# Get quantization parameters +scale, zero_point = output_details[0]['quantization'] + +# Dequantize the output +dequantized_output = (predictions.astype(np.float32) - zero_point) * scale +probabilities = dequantized_output + +print("\n\t[PREDICTION] [Prob]\n") +for i in range(top_k_results): + print("\t{:20}: {:.2f}%".format( + labels[top_k_indices[i]], + probabilities[top_k_indices[i]] * 100)) +``` + +![](images/png/infer-result.png) + +Let’s modify the function created before so that we can handle different type of models: + +
+```python +def image_classification(img_path, model_path, labels, top_k_results=3, + apply_softmax=False): + # Load the image + img = Image.open(img_path) + plt.figure(figsize=(4, 4)) + plt.imshow(img) + plt.axis('off') + + # Load the TFLite model + interpreter = tflite.Interpreter(model_path=model_path) + interpreter.allocate_tensors() + + # Get input and output tensors + input_details = interpreter.get_input_details() + output_details = interpreter.get_output_details() + + # Preprocess + img = img.resize((input_details[0]['shape'][1], + input_details[0]['shape'][2])) + + input_dtype = input_details[0]['dtype'] + + if input_dtype == np.uint8: + input_data = np.expand_dims(np.array(img), axis=0) + elif input_dtype == np.int8: + scale, zero_point = input_details[0]['quantization'] + img_array = np.array(img, dtype=np.float32) / 255.0 + img_array = (img_array / scale + zero_point).clip(-128, 127).astype(np.int8) + input_data = np.expand_dims(img_array, axis=0) + else: # float32 + input_data = np.expand_dims(np.array(img, dtype=np.float32), axis=0) / 255.0 + + # Inference on Raspi-Zero + start_time = time.time() + interpreter.set_tensor(input_details[0]['index'], input_data) + interpreter.invoke() + end_time = time.time() + inference_time = (end_time - start_time) * 1000 # Convert to milliseconds + + # Obtain results + predictions = interpreter.get_tensor(output_details[0]['index'])[0] + + # Get indices of the top k results + top_k_indices = np.argsort(predictions)[::-1][:top_k_results] + + # Handle output based on type + output_dtype = output_details[0]['dtype'] + if output_dtype in [np.int8, np.uint8]: + # Dequantize the output + scale, zero_point = output_details[0]['quantization'] + predictions = (predictions.astype(np.float32) - zero_point) * scale + + if apply_softmax: + # Apply softmax + exp_preds = np.exp(predictions - np.max(predictions)) + probabilities = exp_preds / np.sum(exp_preds) + else: + probabilities = predictions + + print("\n\t[PREDICTION] [Prob]\n") + for i in range(top_k_results): + print("\t{:20}: {:.1f}%".format( + labels[top_k_indices[i]], + probabilities[top_k_indices[i]] * 100)) + print ("\n\tInference time: {:.1f}ms".format(inference_time)) + +``` +
+ +And test it with different images and the int8 quantized model (**160x160 alpha =1.0**). + +![](images/png/infer-int8-160.png) + +Let's download a smaller model, such as the one trained for the [Nicla Vision Lab](https://studio.edgeimpulse.com/public/353482/live) (int8 quantized model (96x96 alpha = 0.1), as a test. We can use the same function: + +![](images/png/infer-int8-96.png) + +The model lost some accuracy, but it is still OK once our model does not look for many details. Regarding latency, we are around **ten times faster** on the Rasp-Zero. + +## Live Image Classification + +Let's develop an app to capture images with the USB camera in real time, showing its classification. + +Using the nano on the terminal, save the code below, such as `img_class_live_infer.py`. +
+```python +from flask import Flask, Response, render_template_string, request, jsonify +from picamera2 import Picamera2 +import io +import threading +import time +import numpy as np +from PIL import Image +import tflite_runtime.interpreter as tflite +from queue import Queue + +app = Flask(__name__) + +# Global variables +picam2 = None +frame = None +frame_lock = threading.Lock() +is_classifying = False +confidence_threshold = 0.8 +model_path = "./models/ei-raspi-img-class-int8-quantized-model.tflite" +labels = ['background', 'periquito', 'robot'] +interpreter = None +classification_queue = Queue(maxsize=1) + +def initialize_camera(): + global picam2 + picam2 = Picamera2() + config = picam2.create_preview_configuration(main={"size": (320, 240)}) + picam2.configure(config) + picam2.start() + time.sleep(2) # Wait for camera to warm up + +def get_frame(): + global frame + while True: + stream = io.BytesIO() + picam2.capture_file(stream, format='jpeg') + with frame_lock: + frame = stream.getvalue() + time.sleep(0.1) # Capture frames more frequently + +def generate_frames(): + while True: + with frame_lock: + if frame is not None: + yield (b'--frame\r\n' + b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') + time.sleep(0.1) + +def load_model(): + global interpreter + if interpreter is None: + interpreter = tflite.Interpreter(model_path=model_path) + interpreter.allocate_tensors() + return interpreter + +def classify_image(img, interpreter): + input_details = interpreter.get_input_details() + output_details = interpreter.get_output_details() + + img = img.resize((input_details[0]['shape'][1], + input_details[0]['shape'][2])) + input_data = np.expand_dims(np.array(img), axis=0)\ + .astype(input_details[0]['dtype']) + + interpreter.set_tensor(input_details[0]['index'], input_data) + interpreter.invoke() + + predictions = interpreter.get_tensor(output_details[0]['index'])[0] + # Handle output based on type + output_dtype = output_details[0]['dtype'] + if output_dtype in [np.int8, np.uint8]: + # Dequantize the output + scale, zero_point = output_details[0]['quantization'] + predictions = (predictions.astype(np.float32) - zero_point) * scale + return predictions + +def classification_worker(): + interpreter = load_model() + while True: + if is_classifying: + with frame_lock: + if frame is not None: + img = Image.open(io.BytesIO(frame)) + predictions = classify_image(img, interpreter) + max_prob = np.max(predictions) + if max_prob >= confidence_threshold: + label = labels[np.argmax(predictions)] + else: + label = 'Uncertain' + classification_queue.put({'label': label, + 'probability': float(max_prob)}) + time.sleep(0.1) # Adjust based on your needs + +@app.route('/') +def index(): + return render_template_string(''' + + + + Image Classification + + + + +

Image Classification

+ +
+ + +
+ + +
+
Waiting for classification...
+ + + ''') + +@app.route('/video_feed') +def video_feed(): + return Response(generate_frames(), + mimetype='multipart/x-mixed-replace; boundary=frame') + +@app.route('/start', methods=['POST']) +def start_classification(): + global is_classifying + is_classifying = True + return '', 204 + +@app.route('/stop', methods=['POST']) +def stop_classification(): + global is_classifying + is_classifying = False + return '', 204 + +@app.route('/update_confidence', methods=['POST']) +def update_confidence(): + global confidence_threshold + confidence_threshold = float(request.form['confidence']) + return '', 204 + +@app.route('/get_classification') +def get_classification(): + if not is_classifying: + return jsonify({'label': 'Not classifying', 'probability': 0}) + try: + result = classification_queue.get_nowait() + except Queue.Empty: + result = {'label': 'Processing', 'probability': 0} + return jsonify(result) + +if __name__ == '__main__': + initialize_camera() + threading.Thread(target=get_frame, daemon=True).start() + threading.Thread(target=classification_worker, daemon=True).start() + app.run(host='0.0.0.0', port=5000, threaded=True) + +``` +
+ +On the terminal, run: + +```bash +python3 img_class_live_infer.py +``` + +And access the web interface: + +- On the Raspberry Pi itself (if you have a GUI): Open a web browser and go to `http://localhost:5000` +- From another device on the same network: Open a web browser and go to `http://:5000` (Replace `` with your Raspberry Pi's IP address). For example: `http://192.168.4.210:5000/` + +Here are some screenshots of the app running on an external desktop + +![](images/png/app-inference.png) + +Here, you can see the app running on the YouTube: + +{{< video https://www.youtube.com/watch?v=o1QsQrpCMw4 >}} + +The code creates a web application for real-time image classification using a Raspberry Pi, its camera module, and a TensorFlow Lite model. The application uses Flask to serve a web interface where is possible to view the camera feed and see live classification results. + +#### Key Components: + +1. **Flask Web Application**: Serves the user interface and handles requests. +2. **PiCamera2**: Captures images from the Raspberry Pi camera module. +3. **TensorFlow Lite**: Runs the image classification model. +4. **Threading**: Manages concurrent operations for smooth performance. + +#### Main Features: + +- Live camera feed display +- Real-time image classification +- Adjustable confidence threshold +- Start/Stop classification on demand + +#### Code Structure: + +1. **Imports and Setup**: + - Flask for web application + - PiCamera2 for camera control + - TensorFlow Lite for inference + - Threading and Queue for concurrent operations + +2. **Global Variables**: + - Camera and frame management + - Classification control + - Model and label information + +3. **Camera Functions**: + - `initialize_camera()`: Sets up the PiCamera2 + - `get_frame()`: Continuously captures frames + - `generate_frames()`: Yields frames for the web feed + +4. **Model Functions**: + - `load_model()`: Loads the TFLite model + - `classify_image()`: Performs inference on a single image + +5. **Classification Worker**: + - Runs in a separate thread + - Continuously classifies frames when active + - Updates a queue with the latest results + +6. **Flask Routes**: + - `/`: Serves the main HTML page + - `/video_feed`: Streams the camera feed + - `/start` and `/stop`: Controls classification + - `/update_confidence`: Adjusts the confidence threshold + - `/get_classification`: Returns the latest classification result + +7. **HTML Template**: + - Displays camera feed and classification results + - Provides controls for starting/stopping and adjusting settings + +8. **Main Execution**: + - Initializes camera and starts necessary threads + - Runs the Flask application + +#### Key Concepts: + +1. **Concurrent Operations**: Using threads to handle camera capture and classification separately from the web server. +2. **Real-time Updates**: Frequent updates to the classification results without page reloads. +3. **Model Reuse**: Loading the TFLite model once and reusing it for efficiency. +4. **Flexible Configuration**: Allowing users to adjust the confidence threshold on the fly. + +#### Usage: + +1. Ensure all dependencies are installed. +2. Run the script on a Raspberry Pi with a camera module. +3. Access the web interface from a browser using the Raspberry Pi's IP address. +4. Start classification and adjust settings as needed. + +## Conclusion: + +Image classification has emerged as a powerful and versatile application of machine learning, with significant implications for various fields, from healthcare to environmental monitoring. This chapter has demonstrated how to implement a robust image classification system on edge devices like the Raspi-Zero and Rasp-5, showcasing the potential for real-time, on-device intelligence. + +We've explored the entire pipeline of an image classification project, from data collection and model training using Edge Impulse Studio to deploying and running inferences on a Raspi. The process highlighted several key points: + +1. The importance of proper data collection and preprocessing for training effective models. +2. The power of transfer learning, allowing us to leverage pre-trained models like MobileNet V2 for efficient training with limited data. +3. The trade-offs between model accuracy and inference speed, especially crucial for edge devices. +4. The implementation of real-time classification using a web-based interface, demonstrating practical applications. + +The ability to run these models on edge devices like the Raspi opens up numerous possibilities for IoT applications, autonomous systems, and real-time monitoring solutions. It allows for reduced latency, improved privacy, and operation in environments with limited connectivity. + +As we've seen, even with the computational constraints of edge devices, it's possible to achieve impressive results in terms of both accuracy and speed. The flexibility to adjust model parameters, such as input size and alpha values, allows for fine-tuning to meet specific project requirements. + +Looking forward, the field of edge AI and image classification continues to evolve rapidly. Advances in model compression techniques, hardware acceleration, and more efficient neural network architectures promise to further expand the capabilities of edge devices in computer vision tasks. + +This project serves as a foundation for more complex computer vision applications and encourages further exploration into the exciting world of edge AI and IoT. Whether it's for industrial automation, smart home applications, or environmental monitoring, the skills and concepts covered here provide a solid starting point for a wide range of innovative projects. + +## Resources + +- [Dataset Example](https://github.com/Mjrovai/EdgeML-with-Raspberry-Pi/tree/main/IMG_CLASS/dataset) + +- [Setup Test Notebook on a Raspi](https://github.com/Mjrovai/EdgeML-with-Raspberry-Pi/blob/main/IMG_CLASS/notebooks/setup_test.ipynb) + +- [Image Classification Notebook on a Raspi](https://github.com/Mjrovai/EdgeML-with-Raspberry-Pi/blob/main/IMG_CLASS/notebooks/10_Image_Classification.ipynb) + +- [CNN to classify Cifar-10 dataset at CoLab](https://colab.research.google.com/github/Mjrovai/UNIFEI-IESTI01-TinyML-2022.1/blob/main/00_Curse_Folder/2_Applications_Deploy/Class_16/cifar_10/CNN_Cifar_10_TFLite.ipynb#scrollTo=iiVBUpuHXEtw) + +- [Cifar 10 - Image Classification on a Raspi](https://github.com/Mjrovai/EdgeML-with-Raspberry-Pi/blob/main/IMG_CLASS/notebooks/20_Cifar_10_Image_Classification.ipynb) + +- [Python Scripts](https://github.com/Mjrovai/EdgeML-with-Raspberry-Pi/tree/main/IMG_CLASS/python_scripts) + +- [Edge Impulse Project](https://studio.edgeimpulse.com/public/510251/live) diff --git a/setup.qmd b/setup.qmd new file mode 100644 index 00000000..3484fc87 --- /dev/null +++ b/setup.qmd @@ -0,0 +1,579 @@ +# Setup {.unnumbered} + +![*DALL·E prompt - An electronics laboratory environment inspired by the 1950s, with a cartoon style. The lab should have vintage equipment, large oscilloscopes, old-fashioned tube radios, and large, boxy computers. The Raspberry Pi 5 board is prominently displayed, accurately shown in its real size, similar to a credit card, on a workbench. The Pi board is surrounded by classic lab tools like a soldering iron, resistors, and wires. The overall scene should be vibrant, with exaggerated colors and playful details characteristic of a cartoon. No logos or text should be included.*](images/jpeg/rasp_setup_portada.jpg) + +This chapter will guide you through setting up Raspberry Pi Zero 2 W (*Raspi-Zero*) and Raspberry Pi 5 (*Raspi-5*) models. We'll cover hardware setup, operating system installation, initial configuration, and tests. + +> The general instructions for the *Rasp-5* also apply to the older Raspberry Pi versions, such as the Rasp-3 and Raspi-4. + +## Introduction + +The Raspberry Pi is a powerful and versatile single-board computer that has become an essential tool for engineers across various disciplines. Developed by the [Raspberry Pi Foundation](https://www.raspberrypi.org/), these compact devices offer a unique combination of affordability, computational power, and extensive GPIO (General Purpose Input/Output) capabilities, making them ideal for prototyping, embedded systems development, and advanced engineering projects. + +### Key Features + +1. **Computational Power**: Despite their small size, Raspberry Pis offers significant processing capabilities, with the latest models featuring multi-core ARM processors and up to 8GB of RAM. + +2. **GPIO Interface**: The 40-pin GPIO header allows direct interaction with sensors, actuators, and other electronic components, facilitating hardware-software integration projects. + +3. **Extensive Connectivity**: Built-in Wi-Fi, Bluetooth, Ethernet, and multiple USB ports enable diverse communication and networking projects. + +4. **Low-Level Hardware Access**: Raspberry Pis provides access to interfaces like I2C, SPI, and UART, allowing for detailed control and communication with external devices. + +5. **Real-Time Capabilities**: With proper configuration, Raspberry Pis can be used for soft real-time applications, making them suitable for control systems and signal processing tasks. + +6. **Power Efficiency**: Low power consumption enables battery-powered and energy-efficient designs, especially in models like the Pi Zero. + +### Raspberry Pi Models (covered in this book) + +1. **Raspberry Pi Zero 2 W** (*Raspi-Zero*): + - Ideal for: Compact embedded systems + - Key specs: 1GHz single-core CPU (ARM Cortex-A53), 512MB RAM, minimal power consumption + +2. **Raspberry Pi 5** (*Raspi-5*): + - Ideal for: More demanding applications such as edge computing, computer vision, and edgeAI applications, including LLMs. + - Key specs: 2.4GHz quad-core CPU (ARM Cortex A-76), up to 8GB RAM, PCIe interface for expansions + +### Engineering Applications + +1. **Embedded Systems Design**: Develop and prototype embedded systems for real-world applications. + +2. **IoT and Networked Devices**: Create interconnected devices and explore protocols like MQTT, CoAP, and HTTP/HTTPS. + +3. **Control Systems**: Implement feedback control loops, PID controllers, and interface with actuators. + +4. **Computer Vision and AI**: Utilize libraries like OpenCV and TensorFlow Lite for image processing and machine learning at the edge. + +5. **Data Acquisition and Analysis**: Collect sensor data, perform real-time analysis, and create data logging systems. + +6. **Robotics**: Build robot controllers, implement motion planning algorithms, and interface with motor drivers. + +7. **Signal Processing**: Perform real-time signal analysis, filtering, and DSP applications. + +8. **Network Security**: Set up VPNs, firewalls, and explore network penetration testing. + +This tutorial will guide you through setting up the most common Raspberry Pi models, enabling you to start on your machine learning project quickly. We'll cover hardware setup, operating system installation, and initial configuration, focusing on preparing your Pi for Machine Learning applications. + +## Hardware Overview + +### Raspberry Pi Zero 2W + +![](images/jpeg/zero-hardware.jpg) + +- **Processor**: 1GHz quad-core 64-bit Arm Cortex-A53 CPU +- **RAM**: 512MB SDRAM +- **Wireless**: 2.4GHz 802.11 b/g/n wireless LAN, Bluetooth 4.2, BLE +- **Ports**: Mini HDMI, micro USB OTG, CSI-2 camera connector +- **Power**: 5V via micro USB port + +### Raspberry Pi 5 + +![](images/jpeg/r5-hardware.jpg) + +- **Processor**: + - Pi 5: Quad-core 64-bit Arm Cortex-A76 CPU @ 2.4GHz + - Pi 4: Quad-core Cortex-A72 (ARM v8) 64-bit SoC @ 1.5GHz +- **RAM**: 2GB, 4GB, or 8GB options (8GB recommended for AI tasks) +- **Wireless**: Dual-band 802.11ac wireless, Bluetooth 5.0 +- **Ports**: 2 × micro HDMI ports, 2 × USB 3.0 ports, 2 × USB 2.0 ports, CSI camera port, DSI display port +- **Power**: 5V DC via USB-C connector (3A) + +## Installing the Operating System + +### The Operating System (OS) + +An operating system (OS) is fundamental software that manages computer hardware and software resources, providing standard services for computer programs. It is the core software that runs on a computer, acting as an intermediary between hardware and application software. The OS manages the computer's memory, processes, device drivers, files, and security protocols. + +1. **Key functions**: + - Process management: Allocating CPU time to different programs + - Memory management: Allocating and freeing up memory as needed + - File system management: Organizing and keeping track of files and directories + - Device management: Communicating with connected hardware devices + - User interface: Providing a way for users to interact with the computer +2. **Components**: + - Kernel: The core of the OS that manages hardware resources + - bash: The user interface for interacting with the OS + - File system: Organizes and manages data storage + - Device drivers: Software that allows the OS to communicate with hardware + +The Raspberry Pi runs a specialized version of Linux designed for embedded systems. This operating system, typically a variant of Debian called Raspberry Pi OS (formerly Raspbian), is optimized for the Pi's ARM-based architecture and limited resources. + +> The latest version of Raspberry Pi OS is based on [Debian Bookworm](https://www.raspberrypi.com/news/bookworm-the-new-version-of-raspberry-pi-os/). + +**Key feature**s: + +1. Lightweight: Tailored to run efficiently on the Pi's hardware. +2. Versatile: Supports a wide range of applications and programming languages. +3. Open-source: Allows for customization and community-driven improvements. +4. GPIO support: Enables interaction with sensors and other hardware through the Pi's pins. +5. Regular updates: Continuously improved for performance and security. + +Embedded Linux on the Raspberry Pi provides a full-featured operating system in a compact package, making it ideal for projects ranging from simple IoT devices to more complex edge machine-learning applications. Its compatibility with standard Linux tools and libraries makes it a powerful platform for development and experimentation. + +### Installation + +To use the Raspberry Pi, we will need an operating system. By default, Raspberry Pis checks for an operating system on any SD card inserted in the slot, so we should install an operating system using [Raspberry Pi Imager.](https://www.raspberrypi.com/software/) + +*Raspberry Pi Imager* is a tool for downloading and writing images on *macOS*, *Windows*, and *Linux*. It includes many popular operating system images for Raspberry Pi. We will also use the Imager to preconfigure credentials and remote access settings. + +Follow the steps to install the OS in your Raspi. + +1. [Download](https://www.raspberrypi.com/software/) and install the Raspberry Pi Imager on your computer. +2. Insert a microSD card into your computer (a 32GB SD card is recommended) . +3. Open Raspberry Pi Imager and select your Raspberry Pi model. +4. Choose the appropriate operating system: + - **For Raspi-Zero**: For example, you can select: + `Raspberry Pi OS Lite (64-bit)`. + + ![img](images/png/zero-burn.png) + + > Due to its reduced SDRAM (512MB), the recommended OS for the Rasp Zero is the 32-bit version. However, to run some machine learning models, such as the YOLOv8 from Ultralitics, we should use the 64-bit version. Although Raspi-Zero can run a *desktop*, we will choose the LITE version (no Desktop) to reduce the RAM needed for regular operation. + + - For **Raspi-5**: We can select the full 64-bit version, which includes a desktop: + `Raspberry Pi OS (64-bit)` + + ![](images/png/r5-burn.png) +5. Select your microSD card as the storage device. +6. Click on `Next` and then the `gear` icon to access advanced options. +7. Set the *hostname*, the Raspi *username and password*, configure *WiFi* and *enable SSH* (Very important!) + +![](images/jpeg/setup.jpg) + +8. Write the image to the microSD card. + +> In the examples here, we will use different hostnames: raspi, raspi-5, raspi-Zero, etc. You should replace by the one that you are using. + +### Initial Configuration + +1. Insert the microSD card into your Raspberry Pi. +2. Connect power to boot up the Raspberry Pi. +3. Please wait for the initial boot process to complete (it may take a few minutes). + +> You can find the most common Linux commands to be used with the Raspi [here](https://www.jwdittrich.people.ysu.edu/raspberrypi/UsefulRaspberryPiCommands.pdf) or [here](https://www.codecademy.com/learn/learn-raspberry-pi/modules/raspberry-pi-command-line-module/cheatsheet). + +## Remote Access + +### SSH Access + +The easiest way to interact with the Rasp-Zero is via SSH ("Headless"). You can use a Terminal (MAC/Linux), [PuTTy (](https://www.putty.org/)Windows), or any other. + +1. Find your Raspberry Pi's IP address (for example, check your router). + +2. On your computer, open a terminal and connect via SSH: + ```bash + ssh username@[raspberry_pi_ip_address] + ``` + Alternatively, if you do not have the IP address, you can try the following: + ```bash + ssh username@hostname.local + ``` + for example, `ssh mjrovai@rpi-5.local` , `ssh mjrovai@raspi.local` , etc. + + ![img](images/png/ssh.png) + + When you see the prompt: + + ```bash + mjrovai@rpi-5:~ $ + ``` + + It means that you are interacting remotely with your Raspi. + It is a good practice to update/upgrade the system regularly. For that, you should run: + + ```bash + sudo apt-get update + sudo apt upgrade + ``` + You should confirm the Raspi IP address. On the terminal, you can use: + ```bash + hostname -I + ``` + +![](images/png/pasted_graphic_11_ILcmyOYU7X.png) + + +### To shut down the Raspi via terminal: + +When you want to turn off your Raspberry Pi, there are better ideas than just pulling the power cord. This is because the Raspi may still be writing data to the SD card, in which case merely powering down may result in data loss or, even worse, a corrupted SD card. + +For safety shut down, use the command line: + +```bash +sudo shutdown -h now +``` + +> To avoid possible data loss and SD card corruption, before removing the power, you should wait a few seconds after shutdown for the Raspberry Pi's LED to stop blinking and go dark. Once the LED goes out, it’s safe to power down. + +### Transfer Files between the Raspi and a computer + +Transferring files between the Raspi and our main computer can be done using a pen drive, directly on the terminal (with scp), or an FTP program over the network. + +#### Using Secure Copy Protocol (`scp`): + +##### Copy files to your Raspberry Pi + +Let's create a text file on our computer, for example, `test.txt`. + +![](images/png/test_txt.png) + +> You can use any text editor. In the same terminal, an option is the `nano`. + +To copy the file named `test.txt` from your personal computer to a user’s home folder on your Raspberry Pi, run the following command from the directory containing `test.txt`, replacing the `` placeholder with the username you use to log in to your Raspberry Pi and the `` placeholder with your Raspberry Pi’s IP address: + +```bash +$ scp test.txt @:~/ +``` + +> Note that `~/` means that we will move the file to the ROOT of our Raspi. You can choose any folder in your Raspi. But you should create the folder before you run `scp`, since `scp` won’t create folders automatically. + +For example, let's transfer the file `test.txt` to the ROOT of my Raspi-zero, which has an IP of `192.168.4.210`: + +```bash +scp test.txt mjrovai@192.168.4.210:~/ +``` + +![](images/png/transfer_file.png) + +I use a different profile to differentiate the terminals. The above action happens **on your computer**. Now, let's go to our Raspi (using the SSH) and check if the file is there: + +![](images/png/list_archives.png) + +##### Copy files from your Raspberry Pi + +To copy a file named `test.txt` from a user’s home directory on a Raspberry Pi to the current directory on another computer, run the following command **on your Host Computer**: + +```bash +$ scp @:myfile.txt . +``` + +For example: + +On the Raspi, let's create a copy of the file with another name: + +```bash +cp test.txt test_2.txt +``` + +And on the Host Computer (in my case, a Mac) + +```bash +scp mjrovai@192.168.4.210:test_2.txt . +``` + +![](images/png/tranfer-text-mac.png) + +#### Transferring files using FTP + +Transferring files using FTP, such as [FileZilla FTP Client](https://filezilla-project.org/download.php?type=client), is also possible. Follow the instructions, install the program for your Desktop OS, and use the Raspi IP address as the `Host`. For example: + +```bash +sftp://192.168.4.210 +``` + +and enter your Raspi `username and password`. Pressing `Quickconnect` will open two windows, one for your host computer desktop (right) and another for the Raspi (left). + +![](images/png/filezila.png) + +## Increasing SWAP Memory + +Using `htop`, a cross-platform interactive process viewer, you can easily monitor the resources running on your Raspi, such as the list of processes, the running CPUs, and the memory used in real-time. To lunch `hop`, enter with the command on the terminal: + +```bash +htop +``` + +![](images/png/htop.png) + +Regarding memory, among the devices in the Raspberry Pi family, the Raspi-Zero has the smallest amount of SRAM (500MB), compared to a selection of 2GB to 8GB on the Raspis 4 or 5. For any Raspi, it is possible to increase the memory available to the system with "Swap." Swap memory, also known as swap space, is a technique used in computer operating systems to temporarily store data from RAM (Random Access Memory) on the SD card when the physical RAM is fully utilized. This allows the operating system (OS) to continue running even when RAM is full, which can prevent system crashes or slowdowns. + +Swap memory benefits devices with limited RAM, such as the Raspi-Zero. Increasing swap can help run more demanding applications or processes, but it's essential to balance this with the potential performance impact of frequent disk access. + +By default, the Rapi-Zero's SWAP (Swp) memory is only 100MB, which is very small for running some more complex and demanding Machine Learning applications (for example, YOLO). Let's increase it to 2MB: + +First, turn off swap-file: + +```bash +sudo dphys-swapfile swapoff +``` + +Next, you should open and change the file `/etc/dphys-swapfile`. For that, we will use the nano: + +```bash +sudo nano /etc/dphys-swapfile +``` + +Search for the **CONF_SWAPSIZE** variable (default is 200) and update it to **2000**: + +```bash +CONF_SWAPSIZE=2000 +``` + +And save the file. + +Next, turn on the swapfile again and reboot the Rasp-zero: + +```bash +sudo dphys-swapfile setup +sudo dphys-swapfile swapon +sudo reboot +``` + +When your device is rebooted (you should enter with the SSH again), you will realize that the maximum swap memory value shown on top is now something near 2GB (in my case, 1.95GB). + +> To keep the *htop* running, you should open another terminal window to interact continuously with your Raspi. + +## Installing a Camera + +The Raspi is an excellent device for computer vision applications; a camera is needed for it. We can install a standard USB webcam on the micro-USB port using a USB OTG adapter (Raspi-Zero and Rasp-5) or a camera module connected to the Raspi CSI (Camera Serial Interface) port. + +> USB Webcams generally have inferior quality to the camera modules that connect to the CSI port. They can also not be controlled using the `raspistill` and `rasivid` commands in the terminal or the `picamera` recording package in Python. Nevertheless, there may be reasons why you want to connect a USB camera to your Raspberry Pi, such as because of the benefit that it is much easier to set up multiple cameras with a single Raspberry Pi, long cables, or simply because you have such a camera on hand. + +### Installing a USB WebCam + +1. Power off the Raspi: + +```bash +sudo shutdown -h no +``` + +2. Connect the USB Webcam (USB Camera Module 30fps,1280x720) to your Raspi (In this example, I am using the Raspi-Zero, but the instructions work for all Raspis). + +![](images/jpeg/usb-cam-2.jpg) + + + +3. Power on again and run the SSH +3. To check if your USB camera is recognized, run: + +```bash +lsusb +``` + +You should see your camera listed in the output. + +![](images/png/USB-CAM-2.png) + +5. To take a test picture with your USB camera, use: + +```bash +fswebcam test_image.jpg +``` + +This will save an image named "test_image.jpg" in your current directory. + +![](images/png/image-test.png) + +6. Since we are using SSH to connect to our Rapsi, we must transfer the image to our main computer so we can view it. We can use FileZilla or SCP for this: + +Open a terminal **on your host computer** and run: + +```bash +scp mjrovai@raspi-zero.local:~/test_image.jpg . +``` + +> Replace "mjrovai" with your username and "raspi-zero" with Pi's hostname. + +![](images/png/cam-2_test.png) + + + +7. If the image quality isn't satisfactory, you can adjust various settings; for example, define a resolution that is suitable for YOLO (640x640): + +```bash +fswebcam -r 640x640 --no-banner test_image_yolo.jpg +``` + +This captures a higher-resolution image without the default banner. + +![](images/png/usb-cam-test-2.png) + +An ordinary USB Webcam can also be used: + +![](images/jpeg/usb_camera.jpg) + +And verified using `lsusb` + +![](images/png/usb-cam-test.png) + +#### Video Streaming + +For stream video (which is more resource-intensive), we can install and use mjpg-streamer: + +First, install Git: + +```bash +sudo apt install git +``` + +Now, we should install the necessary dependencies for mjpg-streamer, clone the repository, and proceed with the installation: + +```bash +sudo apt install cmake libjpeg62-turbo-dev +git clone https://github.com/jacksonliam/mjpg-streamer.git +cd mjpg-streamer/mjpg-streamer-experimental +make +sudo make install +``` + +Then start the stream with: + +```bash +mjpg_streamer -i "input_uvc.so" -o "output_http.so -w ./www" +``` + +We can then access the stream by opening a web browser and navigating to: + +`http://:8080`. In my case: http://192.168.4.210:8080 + +We should see a webpage with options to view the stream. Click on the link that says "Stream" or try accessing: + +```bash +http://:8080/?action=stream +``` + +![](images/png/streem.png) + +### Installing a Camera Module on the CSI port + +There are now several Raspberry Pi camera modules. The original 5-megapixel model was [released ](https://www.raspberrypi.com/news/camera-board-available-for-sale/)in 2013, followed by an [8-megapixel Camera Module 2,](https://www.raspberrypi.com/products/camera-module-v2/) released in 2016. The latest camera model is the [12-megapixel Camera Module 3,](https://www.raspberrypi.com/documentation/accessories/camera.html#:~:text=the 12-megapixel-,Camera Module 3,-which was released) released in 2023. + +The original 5MP camera (**Arducam OV5647**) is no longer available from Raspberry Pi but can be found from several alternative suppliers. Below is an example of such a camera on a Raspi-Zero. + +![](images/jpeg/rasp-zero-with-cam.jpg) + +Here is another example of a v2 Camera Module, which has a **Sony IMX219** 8-megapixel sensor: + +![](images/png/raspi-5-cam.png) + +Any camera module will work on the Raspis, but for that, the `onfiguration.txt` file must be updated: + +```bash +sudo nano /boot/firmware/config.txt +``` + +At the bottom of the file, for example, to use the 5MP Arducam OV5647 camera, add the line: + +```bash +dtoverlay=ov5647,cam0 +``` + +Or for the v2 module, wich has the 8MP Sony IMX219 camera: + +```bash +dtoverlay=imx219,cam0 +``` + +Save the file (CTRL+O [ENTER] CRTL+X) and reboot the Raspi: + +```bash +Sudo reboot +``` + +After the boot, you can see if the camera is listed: + +```bash +libcamera-hello --list-cameras +``` + +![](images/jpeg/list_cams_raspi-zero.jpg) + +![](images/png/list_cams_raspi-5.png) + +> [libcamera ](https://www.raspberrypi.com/documentation/computers/camera_software.html#libcamera)is an open-source software library that supports camera systems directly from the Linux operating system on Arm processors. It minimizes proprietary code running on the Broadcom GPU. + +Let's capture a jpeg image with a resolution of 640 x 480 for testing and save it to a file named `test_cli_camera.jpg` + +```bash +rpicam-jpeg --output test_cli_camera.jpg --width 640 --height 480 +``` + +if we want to see the file saved, we should use `ls -f`, which lists all current directory content in long format. As before, we can use scp to view the image: + +![](images/png/test_camera_raspi-5.png) + + + +## Running the Raspi Desktop remotely + +While we've primarily interacted with the Raspberry Pi using terminal commands via SSH, we can access the whole graphical desktop environment remotely if we have installed the complete Raspberry Pi OS (for example, `Raspberry Pi OS (64-bit)`. This can be particularly useful for tasks that benefit from a visual interface. To enable this functionality, we must set up a VNC (Virtual Network Computing) server on the Raspberry Pi. Here's how to do it: + +1. Enable the VNC Server: + - Connect to your Raspberry Pi via SSH. + - Run the Raspberry Pi configuration tool by entering: + ```bash + sudo raspi-config + ``` + - Navigate to `Interface Options` using the arrow keys. + + ![](images/png/vnc-1.png) + + - Select `VNC` and `Yes` to enable the VNC server. + + ![](images/png/vnc-2.png) + + - Exit the configuration tool, saving changes when prompted. + + ![](images/png/vnc-3.png) + +2. Install a VNC Viewer on Your Computer: + - Download and install a VNC viewer application on your main computer. Popular options include RealVNC Viewer, TightVNC, or VNC Viewer by RealVNC. We will install [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer) by RealVNC. + +3. Once installed, you should confirm the Raspi IP address. For example, on the terminal, you can use: + + ```bash + hostname -I + ``` + + ![](images/png/vnc-4.png) + +4. Connect to Your Raspberry Pi: + - Open your VNC viewer application. + + ![](images/png/vnc-5.png) + + - Enter your Raspberry Pi's IP address and hostname. + - When prompted, enter your Raspberry Pi's username and password. + + ![](images/png/vnc-7.png) + +5. The Raspberry Pi 5 Desktop should appear on your computer monitor. + + ![](images/png/vnc-8.png) + +6. Adjust Display Settings (if needed): + + - Once connected, adjust the display resolution for optimal viewing. This can be done through the Raspberry Pi's desktop settings or by modifying the config.txt file. + - Let's do it using the desktop settings. Reach the menu (the Raspberry Icon at the left upper corner) and select the best screen definition for your monitor: + +![](images/png/vnc-9.png) + +## Updating and Installing Software + +1. Update your system: + ```bash + sudo apt update && sudo apt upgrade -y + ``` +2. Install essential software: + ```bash + sudo apt install python3-pip -y + ``` +3. Enable pip for Python projects: + ```bash + sudo rm /usr/lib/python3.11/EXTERNALLY-MANAGED + ``` + +## Model-Specific Considerations + +### Raspberry Pi Zero +- Limited processing power, best for lightweight projects +- Use headless setup (SSH) to conserve resources. +- Consider increasing swap space for memory-intensive tasks. + +### Raspberry Pi 4 or 5 + +- Suitable for more demanding projects, including AI and machine learning. +- Can run full desktop environment smoothly. +- For Pi 5, consider using an active cooler for temperature management during intensive tasks. + +Remember to adjust your project requirements based on the specific Raspberry Pi model you're using. The Pi Zero is great for low-power, space-constrained projects, while the Pi 4/5 models are better suited for more computationally intensive tasks. + diff --git a/style.scss b/style.scss index e41803b2..9ed2d83a 100644 --- a/style.scss +++ b/style.scss @@ -174,3 +174,27 @@ div.callout-hint.callout > .callout-header::before { padding-left: 1em; padding-right: 1em; } + +/* scrolling windows for long Python scripts in the HTML e-book version */ +.scroll-code-block { + max-height: 400px; + overflow-y: auto; + border: 1px solid #e0e0e0; + border-radius: 4px; + padding: 10px; + background-color: #f8f8f8; +} + +.scroll-code-block pre { + margin: 0; + padding: 0; + border: none; + background-color: transparent; +} + +.scroll-code-block code { + display: block; + white-space: pre; + word-wrap: normal; + overflow-x: auto; +}