ThinkPad X1 Carbon Gen 14 · Aura Edition

Fixing the MIPI camera

Five defects across the kernel sensor driver and libcamera's software ISP left this camera dark, heavily vignetted and tinted yellow-green. Here is what was wrong, how it was measured, and what changed.

Repository Prebuilt RPMs

The progression

Three stages of the same camera. Each image is what the pipeline actually produced at that point — including the middle one, where the fixes were half in and the result was briefly worse in one respect than where it started.

The first and last images are a controlled pair: same seat, same lamps, laptop not moved between them, and the stock side recaptured on a stock kernel with stock libcamera reinstalled rather than simulated. Both were held for 400 frames so that auto-exposure and auto white balance had fully settled — in a room this dim they need roughly 250, and a shorter capture flatters whichever side is measured first.

1 — Stock Fedora 44
Stock camera output: flat, desaturated, dark corners
Dark, green-cast and falling away badly at the corners. Stock kernel, stock libcamera 0.7.1. Auto-exposure is pinned against both its limits here and still cannot reach its own brightness target.
2 — Lens shading in, AWB still broken
A Meet call with a strong yellow-green cast
A real Meet call. The corners are fixed and the colour matrix is applied, but the white balance bug is still there — and the matrix faithfully amplifies it into a strong yellow-green cast.
3 — All five fixes
Final output: neutral colour, even illumination
Even illumination and neutral colour, at 2.9× the frame brightness of the stock capture. Red/green measures 1.047 against a USB webcam reference of 1.052 on the same scene. The visible grain is the sensor running at the full 16× gain.

Stage 2 is worth dwelling on. The colour matrix was correct the whole time — it preserves neutral to within 0.002% and behaved exactly as predicted on real captures. What it could not do was fix the white balance error being fed to it, and increasing saturation amplifies whatever imbalance it is given. Removing the matrix looked like the fix; finding the black level bug underneath it was the actual fix.

The hardware

A Sony IMX471 (16 MP, 4656×3496) behind an Intel IPU7. The IPU7 driver in Linux is input-system only — it delivers raw Bayer and never touches the hardware ISP — so every bit of processing is done by libcamera's software ISP on the CPU or GPU. That makes the software ISP's correctness the whole story.

What was wrong

1 · Crop rectangle returned an error — kernel

V4L2_SEL_TGT_CROP ended its case with break and fell through to return -EINVAL, and no crop rectangle was ever stored in the subdev state. libcamera says so itself:

imx471 0-001a: Unable to get rectangle 0 on pad 0/0: Invalid argument
imx471 0-001a: The sensor kernel driver needs to be fixed
imx471 0-001a: The analogue crop rectangle has been defaulted to the active area size

The sensor actually reads out 3856×2176 of its 4656×3496 array, so every geometry-dependent correction was being computed against the wrong rectangle.

2 · Test pattern generator did nothing — kernel

TEST_PATTERN_MODE was declared CCI_REG8 when 0x0600 is a 16-bit register, so selecting a pattern wrote the mode into the high byte and the sensor stayed on the live scene. The control read back correctly the whole time, which is what made it confusing. Every other 16-bit register in the driver was already correct; 0x0600 was the only outlier.

3 · Analogue gain capped 1.8 stops short — kernel

The gain law is gain = 1024 / (1024 - code). The driver capped the code at 800, giving 4.57×, where the sensor reaches 16× at code 960. Verified on hardware:

codepredictedmeasured
01.000×1.000×
5122.000×2.000×
8004.571×4.571×
96016.000×16.071×

4 · No lens shading correction existed — libcamera

libcamera's software ISP had no lens shading support for any sensor. On this camera the corners are about 2.7 stops down, and the falloff is channel-dependent: red needs 6.35× gain in the corners where blue needs 5.27×.

5 · White balance barely worked — libcamera

AWB statistics are summed in the sensor's native bit depth, but the black level subtracted from them is held in a uint8_t. For a 10-bit sensor black is under-subtracted fourfold, which inflates all three channels equally and drags every gain towards 1.0 — so the camera hardly white-balances at all.

Measured gains were 1.035 / 1.070 where grey world requires 1.316 / 1.806. This affects every RAW10 and RAW12 sensor using the software ISP, not just this one.

Measured on a flat painted ceiling, at identical coordinates in both frames, with a USB webcam on the same machine shooting the same room as the reference:

Low-light scene, both convergedR/GB/GFrame brightness
Stock0.8900.67231.1
All five fixes1.0470.82390.9
USB webcam reference1.0520.989—

Lens shading, isolated

The same scene seconds apart, with lens shading correction off and on. Centre-to-corner falloff goes from 2.78× to 1.09×.

Correction off
Heavy vignetting, dark corners
Correction on
Even illumination across the frame

Measured results

Channel ratios at the output in brighter light, with a USB webcam on the same machine as a reference. 1.000 is neutral. The vignetting column comes from the controlled flat-field pair above.

ConfigurationR/GB/GVignetting
Stock0.8710.6672.78×
+ lens shading0.9740.8551.09×
+ AWB fix and colour matrix1.0010.8701.09×
USB webcam reference0.990.97—

Where the tuning data came from

The lens shading grids and colour matrices were extracted from Intel's Windows camera driver package, which ships per-module tuning blobs in a CPFF container. The publicly available ipu6-camera-bins headers describe the format only partially — the 4×4 lens shading record in particular is named but not defined — so the layout was derived from the data and then checked.

Three independent checks that the decoding is right: every one of the 36 shading grids minimises at exactly the geometric centre with a value of precisely 1.000; the two green channels agree to three decimal places, which identifies them and confirms RGGB ordering; and channel falloff is ordered red > green > blue, as physics requires. Separately, libcamera master already carries a CameraSensorHelperImx471 with gain = 1024 / (1024 - code) and a black level of 64 DN, arrived at independently — both match.

Measuring the sensor itself

A red object filling the frame, used to verify Bayer order
Bayer order verified against a saturated red subject: red sits at grid (0,0) at 2.17× green, confirming SRGGB. The sensor's own colour-bar generator later confirmed it again — once patch 2 made it work.
A flat field capture showing radial falloff
A flat field showing the radial falloff the shading correction removes. Four attempts were needed: a very wide lens needs the source close enough to fill the frame, which then contributes its own falloff.

Using it

Prebuilt Fedora 44 packages are attached to the releases. The libcamera package is a drop-in: same soname, so PipeWire and WirePlumber are unaffected.

sudo dnf install ./libcamera-0.7.1-*.lsc2.fc44.x86_64.rpm \
                 ./libcamera-ipa-0.7.1-*.lsc2.fc44.x86_64.rpm \
                 ./libcamera-tools-0.7.1-*.lsc2.fc44.x86_64.rpm
systemctl --user restart wireplumber

The kernel patches apply to mainline and to the Fedora 7.2.8 package, whose copy of the driver is byte-identical. sudo dnf downgrade libcamera reverts.

Honest notes