Laboratorio 01

Setup dell’Ambiente — CMake, Eigen e OpenCV

Cristiano De Michele

Obiettivo di oggi

Prima di scrivere una sola riga del nostro framework, dobbiamo costruire il cantiere. In questo laboratorio configuriamo i tre strumenti che useremo per tutto il corso:

  • CMake: il sistema di build che trasforma i nostri file .cpp in un eseguibile, in modo automatico e indipendente dal sistema operativo.
  • Eigen: la libreria C++ per l’algebra lineare — matrici, vettori, operazioni ottimizzate.
  • OpenCV: la libreria per la visione artificiale — caricheremo e visualizzeremo le immagini MNIST.

Al termine del laboratorio avrete un progetto che compila e stampa il risultato di un prodotto matrice-vettore.

L’ambiente di lavoro: il compilatore

Per compilare C++17 vi serve un compilatore moderno. Scegliete il vostro sistema:

Linux — tutto già disponibile:

sudo apt install build-essential cmake   # Ubuntu/Debian
sudo dnf install gcc-c++ cmake           # Fedora

macOS — il compilatore è il clang++ di Apple, dagli strumenti da riga di comando di Xcode; il resto arriva con Homebrew:

xcode-select --install
brew install cmake

Non installate gcc da Homebrew: CMake userebbe comunque clang++, e forzando g++ il collegamento con l’OpenCV di Homebrew (compilata con clang++) può fallire.

Windows — il consiglio è WSL 2 (Windows Subsystem for Linux): un terminale Linux reale dentro Windows, senza macchine virtuali:

wsl --install   # installa Ubuntu di default; riavviate poi

Poi seguite le istruzioni Linux. Per verificare:

g++ --version && cmake --version    # su macOS: clang++ --version

In WSL: dove tenere il progetto

Per chi usa Windows con WSL 2:

  • Tenete il progetto nella home di Linux (~/...), non in /mnt/c/...: dai dischi di Windows la lettura dei file è molto più lenta, e il CSV di MNIST, che leggerete più avanti, è grande.
  • VS Code lo apre con l’estensione WSL: dal terminale Linux, nella cartella del progetto, code .

L’ambiente di lavoro: l’editor

Il consiglio è Visual Studio Code — gratuito, multipiattaforma, estensibile.

Estensioni da installare subito dal pannello Extensions:

  • C/C++ (Microsoft) — syntax highlighting, IntelliSense, debugger integrato
  • CMake Tools (Microsoft) — compila con un click dalla barra di stato

Con CMake Tools i pulsanti Build e Run nella barra di stato (o F7 per compilare) sostituiscono i comandi da terminale: come usarli, subito dopo il CMakeLists.txt del corso. Usate comunque il terminale nelle prime settimane per capire cosa succede sotto.

Un progetto CMake: le basi

Il progetto è una cartella con due file; build/ la crea CMake:

basic_neural_plusplus/
├── CMakeLists.txt   # le istruzioni per CMake
├── main.cpp
└── build/           # creata da cmake -B build: si può cancellare e rigenerare

Il CMakeLists.txt minimo:

cmake_minimum_required(VERSION 3.15)    # versione minima di CMake
project(BasicNeuralPlusPlus)            # nome del progetto
set(CMAKE_CXX_STANDARD 17)              # compila in C++17
add_executable(basic_neural main.cpp)   # il bersaglio: l'eseguibile basic_neural, da main.cpp

Un bersaglio (target) è ciò che CMake costruisce. Le librerie gli si collegano con target_link_libraries, dopo add_executable: lo faremo nelle prossime due slide.

cmake -B build          # configura: legge CMakeLists.txt e prepara build/
cmake --build build     # compila
./build/basic_neural    # esegue

Installare Eigen

Eigen è header-only: non occorre compilarla. Scegliete il metodo più comodo:

Via package manager (consigliato):

sudo apt install libeigen3-dev    # Linux Ubuntu/Debian
brew install eigen                # macOS

e nel CMakeLists.txt, dopo add_executable:

find_package(Eigen3 QUIET)
target_link_libraries(basic_neural Eigen3::Eigen)

Manualmente (funziona su tutti i sistemi): scaricate Eigen da eigen.tuxfamily.org e copiate la cartella Eigen/ dentro libs/eigen/ nel vostro progetto; nel CMakeLists.txt, dopo add_executable:

target_include_directories(basic_neural PRIVATE libs/eigen)

Con QUIET, se non trova Eigen, find_package tace; se la trova imposta Eigen3_FOUND, che il CMakeLists.txt del corso (fra due slide) usa per scegliere fra i due casi.

Installare OpenCV

OpenCV è una libreria compilata: va installata e poi collegata al progetto.

sudo apt install libopencv-dev   # Linux, e Windows dentro WSL
brew install opencv              # macOS

Nel CMakeLists.txt aggiungete, come per Eigen dopo add_executable:

find_package(OpenCV REQUIRED)   # REQUIRED: se manca, CMake si ferma con un errore chiaro
target_link_libraries(basic_neural ${OpenCV_LIBS})

Per verificare che sia installata:

pkg-config --modversion opencv4   # Linux: es. 4.6.0
brew list --versions opencv       # macOS: es. opencv 5.0.0

La verifica che conta però è un’altra: quando lancerete cmake -B build, il CMakeLists.txt della prossima slide stamperà da solo la versione che ha trovato. Ubuntu 24.04 ha la 4.6, Homebrew la 5: il codice del corso compila con entrambe.

Il CMakeLists.txt del corso

Ecco il file completo: le righe delle tre slide precedenti, nell’ordine giusto. Lo useremo e aggiorneremo nel corso delle settimane:

cmake_minimum_required(VERSION 3.15)
project(BasicNeuralPlusPlus)

set(CMAKE_CXX_STANDARD 17)
1set(CMAKE_CXX_STANDARD_REQUIRED ON)
2set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")

find_package(Eigen3 QUIET)
find_package(OpenCV REQUIRED)
3message(STATUS "OpenCV ${OpenCV_VERSION} trovata")

add_executable(basic_neural main.cpp)

4if(Eigen3_FOUND)
    target_link_libraries(basic_neural Eigen3::Eigen)
else()
    message(STATUS "Eigen non trovata: uso libs/eigen")
    target_include_directories(basic_neural PRIVATE libs/eigen)
endif()
5target_link_libraries(basic_neural ${OpenCV_LIBS})
1
Senza questa riga, se il compilatore non supportasse il C++17 CMake ripiegherebbe in silenzio su uno standard più vecchio, e scoprireste il problema molto dopo.
2
-Wall accende gli avvisi del compilatore, e non è pignoleria: senza, alcuni errori veri restano invisibili. Ne incontrerete due, entrambi verificati — al Laboratorio 03 una ricorsione infinita che compila senza una parola, al Laboratorio 08 uno switch incompleto che azzera tutti i pesi in silenzio.
3
È la verifica più affidabile che OpenCV sia stata trovata: la stampa compare quando lanciate cmake -B build.
4
Trovare Eigen non basta: bisogna dire al bersaglio dove sono i suoi header. Ci pensa Eigen3::Eigen, il target che il pacchetto di Eigen definisce. Senza questa riga la compilazione fallisce con fatal error: Eigen/Dense: No such file or directory anche con Eigen regolarmente installata. Se invece Eigen manca davvero, ve lo dice già cmake -B build: “Eigen non trovata”.
5
Gli header di OpenCV arrivano da soli insieme alle librerie: non serve aggiungere ${OpenCV_INCLUDE_DIRS}.

In VS Code con CMake Tools

  • Aprite la cartella del progetto (File → Open Folder), quella con CMakeLists.txt: CMake Tools la riconosce e propone di configurarla (è il cmake -B build).
  • Scegliete il kit, cioè il compilatore, quando ve lo chiede: su macOS il Clang di Apple, su Linux e WSL GCC. [Unspecified] lascia scegliere a CMake, come da terminale.
  • Build nella barra di stato (o F7) compila, Run esegue basic_neural.
  • Run parte da build/: test.png finisce in build/test.png, non accanto a main.cpp (da terminale, invece, nella cartella da cui lanciate il programma).
  • #include <Eigen/Dense> in rosso, ma si compila? È IntelliSense che non conosce ancora CMake: accettate quando CMake Tools propone di configurarlo.
  • In WSL: VS Code si apre dal terminale Linux con code .; estensioni con Install in WSL.

I tipi fondamentali di Eigen

Useremo questi tipi per tutto il corso:

Tipo Cosa rappresenta Esempio
Eigen::MatrixXf Matrice di float, dimensioni dinamiche Pesi di uno strato
Eigen::VectorXf Vettore colonna di float Bias, uscita di uno strato
Eigen::ArrayXf Array 1D per operazioni elemento per elemento Funzioni di attivazione

Xf sta per dimensioni dinamiche (X), elementi float. Per una rete neurale la precisione del float basta, e occupa metà memoria del double. Ci sono varianti double (Xd) e a dimensioni fisse (Matrix3f, ecc.) che non useremo.

Operazioni fondamentali di Eigen

#include <Eigen/Dense>
int main() {
    Eigen::MatrixXf W = Eigen::MatrixXf::Random(3, 4); // 3x4, valori in [-1,1]
    Eigen::VectorXf x = Eigen::VectorXf::Random(4);    // 4x1

    // Prodotto matrice-vettore → risultato 3x1
    Eigen::VectorXf z = W * x;

    // Trasposta
1    Eigen::MatrixXf W_trasposta = W.transpose();  // diventa 4x3

    // Inizializzazioni utili
    Eigen::MatrixXf Z = Eigen::MatrixXf::Zero(3, 4);
    Eigen::MatrixXf I = Eigen::MatrixXf::Identity(4, 4);

    // Operazioni ELEMENTO PER ELEMENTO con .array(): servono per le attivazioni!
    Eigen::ArrayXf relu = z.array().max(0.0f);   // ReLU: max(0, zi)
    Eigen::ArrayXf sig  = 1.0f / (1.0f + (-z.array()).exp()); // Sigmoid
}
1
Non chiamatela Wt: fra qualche settimana Wt sarà il nome della matrice dei pesi di un layer, non della trasposta.

Attenzione: * tra due MatrixXf è sempre prodotto matriciale. Per il prodotto elemento per elemento usate .array().

Stampare in C++

In C stampavate con printf. In C++ si usa lo stream std::cout (header <iostream>), che sa stampare anche le matrici di Eigen:

#include <iostream>
#include <Eigen/Dense>
int main() {
    int n = 3;  float t = 0.5f;
    std::cout << "n = " << n << ", t = " << t << "\n";  // come printf("n = %d, t = %g\n", n, t)
    Eigen::VectorXf z = Eigen::VectorXf::Random(3);
    std::cout << "z =\n" << z << "\n";                  // tutto il vettore
    std::cout << z.rows() << "x" << z.cols() << "\n";   // le dimensioni: 3x1
}
  • << accoda i pezzi da stampare, e il tipo di ciascuno lo riconosce il compilatore: niente %d o %g.
  • printf funziona ancora (header <cstdio>), ma non sa stampare un oggetto di Eigen: servirebbe un ciclo sugli elementi z(i).
  • std:: indica che cout appartiene alla libreria standard; il perché del prefisso lo vedremo a lezione.

Task: Hello Eigen + OpenCV

Verificate che tutto funzioni scrivendo un main.cpp che:

  1. Crea una matrice W (\(3 \times 4\), valori casuali) e un vettore x (\(4 \times 1\)).
  2. Calcola \(z = W \cdot x\) e lo stampa con std::cout: le dimensioni devono essere \(3 \times 1\).
  3. Calcola z_relu = z.array().max(0.0f) e lo stampa: è il vostro primo forward pass (senza bias)!
  4. Crea un’immagine vuota OpenCV \(28 \times 28\) (scala di grigi) e la visualizza:
#include <iostream>
#include <Eigen/Dense>
#include <opencv2/opencv.hpp>

int main() {
    // punti 1-3: W, x, z = W * x e z_relu, stampati con std::cout

    cv::Mat img(28, 28, CV_8UC1, cv::Scalar(128));   // punto 4: grigio medio
    cv::imshow("Test MNIST size", img);
    cv::waitKey(0);   // un tasto, a finestra attiva (chiusa col mouse? Ctrl-C)
}

Se la finestra non si apre

Avviso

cv::imshow apre una finestra grafica. In WSL le finestre funzionano grazie a WSLg, che c’è su Windows 11 e su Windows 10 dalla build 19044 (21H2), purché WSL sia quello installato da wsl --install (la versione dello Store): il comando wsl --version in PowerShell risponde solo in quel caso, e wsl --update lo aggiorna. Collegati via ssh, invece, la finestra non si apre e il programma si ferma con un errore. In caso di problemi, sostituite imshow/waitKey con cv::imwrite("test.png", img); e aprite il file: il resto del corso non dipende dalle finestre.

Su macOS il primo avvio dopo l’installazione di OpenCV può richiedere decine di secondi: il programma non è bloccato, aspettate. Dal secondo avvio è immediato.

Se Eigen stampa e OpenCV apre la finestra (o scrive il PNG), l’ambiente è pronto per tutto il corso.

Task da completare

La soluzione di riferimento verrà discussa all’inizio del prossimo laboratorio.