# Audit ISP lineaire IMX477 - niveau noir ISP (black_level) 0 vs 64

Date : 2026-09-29 - Pi4 `polarscope@10.0.0.5` (`pinox`, kernel 6.18.50+rpt-rpi-v8).
Depot distant : `/home/polarscope/Documents/c++/polarscope`.
Aucun fichier de production modifie : seuls les outils/fichiers QA listes en fin de
rapport ont ete ajoutes ou synchronises.

## 1. Objectif

Determiner empiriquement si l'ecart de pente ~1.065 observe entre le buffer ISP VC4
et le dematriçage logiciel EIP (avec `black_level` tuning a 4096, soit 64 en 10 bits)
provenait de la **renormalisation de plage du pedestal** par l'ISP. Pour cela on
refait la capture appairee avec un tuning dont `rpi.black_level.black_level = 0` et
un EIP au niveau noir 0, puis on compare les pentes/offsets par canal aux deux
conditions.

## 2. Protocole

- Banc : `tools/diagnostics/isp_linear_bench.cpp` (outil QA uniquement).
- Une **meme Request** libcamera produit deux flux appaires :
  - RAW `SBGGR12_CSI2P`, 2028x1520, stride 3072 ;
  - Viewfinder `RGB888`, 2028x1520, stride 6144 (60 octets de padding/ligne).
- Reference EIP : `image::bayerPackedToRgb888` applique **au meme RAW** avec gains
  WB unite (1.0, 1.0), gamma desactive et un niveau noir CLI (nouvel argument
  optionnel, defaut 64). Le benchmark compare donc le **vrai `BayerRawCodec`/EIP**
  au buffer ISP de la meme Request, pas une approximation.
- Mode capteur `bin2x2(2028x1520)`, exposition 20000 us, gain analogique 3.5,
  10 trames par condition, derniere trame sauvegardee.
- Tuning selectionne via `LIBCAMERA_RPI_TUNING_FILE` (log IPA
  `Using tuning file ...` confirme l'application).
- Service `imaging-server.service` arrete pendant les captures, restauration par
  `trap EXIT` (port 8765 verifie apres chaque execution).
- Analyse : `qa/isp-linear/analyze_captures.py` recalcule tout (aucune stat C++),
  corrige stride + ordre memoire B,G,R -> R,G,B, exclut 24 px de bord et les
  pixels satures (canal >= 254).

### Conditions capturees

| Condition  | Tuning                                          | ISP black | EIP black |
|------------|-------------------------------------------------|-----------|-----------|
| official   | `/usr/share/libcamera/ipa/rpi/vc4/imx477.json`  | (defaut)  | 64        |
| linear     | `qa/isp-linear/imx477_linear.json`              | 4096 (64) | 64        |
| linear2    | idem linear (repetabilite)                      | 4096 (64) | 64        |
| black64    | idem linear, controle contemporain              | 4096 (64) | 64        |
| black0     | `qa/isp-linear/imx477_linear_black0.json`       | 0         | 0         |

## 3. Blocs actifs / configuration actuelle

Log RPiController pour le tuning lineaire :

- Actifs : `rpi.black_level`, `rpi.dpc`, `rpi.lux`, `rpi.noise`, `rpi.geq`,
  `rpi.sdn`, `rpi.awb`, `rpi.agc`, `rpi.contrast`, `rpi.ccm`, `rpi.sharpen`,
  `rpi.sync`.
- Desactives : `rpi.nn.awb`, `rpi.alsc`, `rpi.hdr`.

Neutralisations du tuning lineaire (`make_linear_tuning.py`) : `dpc.strength=0`,
`geq=(0,0)`, `sdn.strength=0`, `contrast.ce_enable=0` + gamma identite
`[0,0,65535,65535]`, `ccm` identite, `sharpen.strength=0`/`limit=0`,
`alsc.enabled=false`, `hdr.enabled=false`, `nn.awb.enabled=false`. Seule la
variante `black0` differe par `rpi.black_level.black_level = 0`.

### Repartition des leviers : controles libcamera vs tuning VC4

Le banc combine deux plans de reglage distincts.

**a) Pilotage par controles libcamera (`CameraSettings` / Request) :**

| Controle | Valeur dans ce banc | Remarque |
|----------|---------------------|----------|
| `AeEnable` (`autoExposure`) | false | exposition manuelle |
| `ExposureTime` (`exposureUs`) | 20000 us | reel confirme : 19999 us (metadata `frame_metadata`) |
| `AnalogueGain` (`analogGain`) | 3.5 | reel confirme : 3.49488 (metadata `frame_metadata`) |
| `AwbEnable` (`autoWhiteBalance`) | false | AWB inhibee |
| `ColourGains` | (1.0, 1.0) | gains unite |
| `FrameDurationLimits` | 21000 us demandes | `applyRequestControls` (autoExposure=false) impose `exposureUs + max(1000, exposureUs/20)` = 21000 us pour 20000 us d'exposition ; metadata 22131 us (contrainte/arrondi capteur) |

**b) Neutralisation par le tuning VC4 (`LIBCAMERA_RPI_TUNING_FILE`) :**

| Bloc tuning | Etat neutralise |
|-------------|-----------------|
| `rpi.black_level` | `black_level` = 0 (variante black0) ou 4096 (= 64 en 10 bits) |
| `rpi.ccm` | matrice identite |
| `rpi.contrast` | `ce_enable=0` + gamma identite `[0,0,65535,65535]` |
| `rpi.dpc` | `strength=0` |
| `rpi.alsc` | `enabled=false` |
| `rpi.geq` | `offset=0`, `slope=0` |
| `rpi.sdn` | `strength=0` |
| `rpi.sharpen` | `strength=0`, `limit=0` |
| `rpi.hdr` | `enabled=false` |
| `rpi.nn.awb` | `enabled=false` |

`rpi.lux`, `rpi.noise`, `rpi.agc` et `rpi.sync` sont conserves (pedestal,
exposition, gain, synchro). Le bloc `rpi.awb` du tuning reste present et enabled,
mais AWB est effectivement inhibee par `AwbEnable=false` + `ColourGains=(1,1)`.

### Controle libcamera vs tuning VC4

Le tuning est bien pris en compte par l'IPA (fichier `Using tuning file ...` et
etat enabled/disabled des algorithmes). `pkg-config libcamera` fournit
`-lcamera -lcamera-base`. La verification croisee est numerique : la condition
`official` conserve le CCM et le gamma du tuning officiel (AWB inhibee par
controls, comme pour toutes les conditions), et la relation ISP/EIP n'y est pas
lineaire, alors qu'en `linear` (CCM identite + gamma identite) elle l'est
(R2 >= 0.996). Cela confirme que le fichier lineaire est reellement applique et
non le tuning par defaut.

## 4. Tuning minimal

Derivation QA par `qa/isp-linear/make_linear_tuning.py` depuis la copie officielle
`imx477_official.json` ; la variante `imx477_linear_black0.json` est **identique**
au tuning lineaire a la seule exception de `rpi.black_level.black_level = 0`
(verifie programmatiquement).

## 5. Resultats - regression par canal ISP = pente * EIP + offset

Pixels valides (bords + saturation exclus) : linear 2 663 615, black64 2 660 361,
black0 2 658 957.

### Condition black0 (ISP black=0, EIP black=0)

| Canal | pente  | offset | R2      | RMSE   | MAE    |
|-------|--------|--------|---------|--------|--------|
| R     | 1.0012 | -0.314 | 0.99587 | 0.894  | 0.687  |
| G     | 0.9983 | +0.119 | 0.99988 | 0.505  | 0.299  |
| B     | 0.9984 | -0.143 | 0.99883 | 0.987  | 0.747  |

Pente moyenne black0 = **0.9993**.

### Condition black64 (ISP black=4096, EIP black=64)

| Canal | pente  | offset | R2      | RMSE   | MAE    |
|-------|--------|--------|---------|--------|--------|
| R     | 1.0684 | -0.279 | 0.99618 | 0.920  | 0.675  |
| G     | 1.0650 | +0.111 | 0.99987 | 0.567  | 0.461  |
| B     | 1.0652 | -0.125 | 0.99890 | 1.027  | 0.777  |

Pente moyenne black64 = **1.0662** (l'ecart ~1.065 est reproduit).

### Test de la renormalisation de plage

Hypothese : l'ISP etale `(RAW - black)` sur 0..255 tandis que l'EIP fait
`(RAW - black)/4`. Prediction 10 bits :

- `slope_black64 = 4 * 255 / (1023 - 64) = 1020/959 = 1.0636` ;
- `slope_black0  = 4 * 255 / 1023        = 1020/1023 = 0.9971` ;
- rapport `slope_black0 / slope_black64 = 959/1023 = 0.93744`.

Mesure du rapport par canal : R 0.93713, G 0.93738, B 0.93726 (moyenne ~0.9372),
soit un ecart < 0.04 % a la prediction. Les pentes mesurees (1.0662 et 0.9993)
encadrent les predictions (1.0636 et 0.9971) a ~0.2 %.

Relations pedestal directes (meme RAW, memes pixels) :

- `ISP_black0` vs `ISP_black64` : pente ~0.9365, offset ~**+16.0** LSB, R2 >= 0.993 ;
- `EIP_black0` vs `EIP_black64` : pente ~0.998, offset ~**+16.0** LSB, R2 >= 0.997.

L'offset de 16 LSB correspond exactement a `64/4`, et la pente 0.9365 correspond a
`959/1023 = 0.9375`. Autrement dit, l'ISP en `black0` vaut
`0.9375 * ISP_black64 + 16`, ce qui est la signature exacte d'une renormalisation
de plage : **l'ecart ~1.065 avec black=64 vient bien de la renormalisation de
plage du pedestal par l'ISP**. En alignant ISP et EIP sur le meme niveau noir
(0), la pente tombe a ~1.00.

## 6. Spatial / contours / bruit (condition lineaire, dont black0)

- **Alignement** : orientation `identity` correlation 1.0 (pas de flip), decalage
  entier (0,0), pic sous-pixel (0,0) -> appariement pixel a pixel.
- **Haute frequence** : `hf_slope` 0.9998-0.9999 et ratio de variance locale
  1.000-1.006 sur R/G/B -> ni accentuation ni lissage ISP (sharpen et sdn
  neutralises).
- **Vignettage** : ratio ISP/fit min 0.991-0.997, median 1.0002, max <= 1.0045,
  ecart-type <= 0.0014 -> pas de correction de shading (alsc desactive).
- **Matrice de melange** apres lissage 5x5 : black0 ~ identite avec gains
  (1.006, 0.999, 0.993) et hors-diagonale < 0.003 ; black64 ~ identite avec gains
  (1.068, 1.066, 1.061). La matrice pleine resolution est contaminee par
  l'interpolation de dematriçage (hors-diagonale ~0.2) et n'est interpretable
  qu'apres lissage.
- **Repetabilite linear vs linear2** : ISP moyenne |delta| 0.89 LSB, ecart-type
  1.28, p99 4 ; EIP moyenne |delta| 0.82, ecart-type 1.23. Bruit temporel ~1 LSB.

## 7. Limites

- Scene unique (aucune mire), exposition/gain uniques ; valable pour ce point de
  fonctionnement.
- Le RAW 12 bits est normalise en 10 bits par le codec, le modele de prediction
  est ecrit en 10 bits.
- En `black0`, aucun pedestal n'est soustrait : les deux flux (ISP et EIP)
  conservent le niveau noir capteur, d'ou l'offset residuel ~16 LSB entre
  conditions mais une pente unite entre ISP et EIP.
- L'EIP est un dematriçage bilineaire ; l'ISP utilise son propre algorithme, ce
  qui explique le RMSE residuel ~0.5-1.0 LSB, independamment du niveau noir.
- Gain numerique : aucun controle `DigitalGain` independant n'est applique par le
  banc, et aucune metadata de gain numerique n'est lue. Seuls l'exposition reelle
  (19999 us) et le gain analogique reel (3.49488) sont confirmes par
  `frame_metadata`. Une eventuelle composante de gain numerique interne a l'ISP
  n'est donc pas quantifiee ici ; on ne peut pas affirmer que le gain numerique
  est explicitement controle.
- La condition `official` n'est pas comparable lineairement (CCM et gamma
  officiels actifs ; AWB inhibee par controls) et sert uniquement de contexte.

## 8. Conclusion factuelle

La pente ISP/EIP passe de **1.0662** (black_level ISP 64) a **0.9993**
(black_level ISP 0), avec des R2 de 0.996 a 0.9999 et un rapport mesure
0.9372 contre 0.93744 predit. L'ecart ~1.065 est donc integralement explique par
la renormalisation de plage du pedestal : l'ISP etale `(RAW - black)` sur la
pleine echelle. Une fois ISP et EIP au meme niveau noir, le dematriçage logiciel
EIP reproduit le buffer ISP du meme Request a ~0.1 % de pente pres.

Cette concordance est obtenue avec des gains de balance des blancs unite (1,1) :
ISP et EIP presentent donc le meme aspect colore natif (dominante verte/turquoise).
C'est precisement le resultat attendu de l'experience et cela ne constitue en rien
une correction couleur ; aucune correction chromatique n'est revendiquee ici.

## 9. Verification finale

- `imaging-server.service` : `active`, `enabled`.
- Port 8765 : en ecoute (`ss -ltn`).
- `python3 qa/isp-linear/analyze_captures.py` : reexecute avec succes.

## 10. Fichiers QA ajoutes / synchronises

- `qa/isp-linear/imx477_linear_black0.json` (nouveau, derive de `imx477_linear.json`).
- `tools/diagnostics/isp_linear_bench.cpp` (argument CLI niveau noir EIP, defaut 64).
- `qa/isp-linear/run_final_black0_pi.sh` (capture black0 + controle black64, trap service).
- `qa/isp-linear/analyze_captures.py` (conditions black0/black64, comparaison niveaux noirs).
- `qa/isp-linear/results/captures/final_black0_*` et `final_black64_*`.
- `qa/isp-linear/results/{json/measurements.json,csv/black_level_slopes.csv,images/*,plots/*}`.

Commande de compilation du banc sur le Pi (identique a l'ensemble de sources du
build serveur) :

```
g++ -std=c++17 -O2 \
  -DEIP_POLARSCOPE_ONLY -DPOLARSCOPE_HAS_LIBCAMERA -DEIP_HAS_LIBCAMERA \
  -DPOLARSCOPE_HAS_JPEG -DEIP_HAS_JPEG \
  -Isrc/bootstrap -Isrc/apps/polarscope -Isrc/model -Isrc/model/astronomy \
  -Isrc/model/camera -Isrc/model/hardware -Isrc/model/image -Isrc/model/network \
  -Isrc/model/polar -Isrc/utils $(pkg-config --cflags libcamera) \
  tools/diagnostics/isp_linear_bench.cpp \
  src/model/camera/LibcameraCaptureBackend.cpp src/model/camera/CameraCaptureBackend.cpp \
  src/model/camera/CameraSettings.cpp src/model/camera/CameraFrame.cpp \
  src/model/polar/PolarScopeSettings.cpp src/model/image/BayerRawCodec.cpp \
  -o /tmp/ispbuild/isp_linear_bench \
  $(pkg-config --libs libcamera) -fopenmp -lpthread
```
