Skip to content

Commit 13806c1

Browse files
authored
Merge pull request #18 from labthings/update_labthings
Update labthings
2 parents 72626aa + 012617d commit 13806c1

File tree

6 files changed

+119
-91
lines changed

6 files changed

+119
-91
lines changed

.github/workflows/publish.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ jobs:
3333

3434
publish-to-testpypi:
3535
name: Publish Python 🐍 distribution 📦 to TestPyPI
36+
if: startsWith(github.ref, 'refs/tags/testpypi-v') # only publish to PyPI on tag pushes
3637
needs:
3738
- build
3839
runs-on: ubuntu-latest

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "labthings-picamera2"
3-
version = "0.0.1-dev1"
3+
version = "0.0.1-dev2"
44
authors = [
55
{ name="Richard Bowman", email="richard.bowman@cantab.net" },
66
]
@@ -14,7 +14,7 @@ classifiers = [
1414
]
1515
dependencies = [
1616
"picamera2~=0.3.12",
17-
"labthings-fastapi>=0.0.6",
17+
"labthings-fastapi>=0.0.7",
1818
"numpy",
1919
"scipy",
2020
]

src/labthings_picamera2/recalibrate_utils.py

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
picamera.lens_shading_table = lst
3030
```
3131
"""
32+
3233
from __future__ import annotations
34+
import gc
3335
import logging
3436
import time
3537
from typing import List, Literal, Optional, Tuple
@@ -38,6 +40,7 @@
3840
from scipy.ndimage import zoom
3941

4042
from picamera2 import Picamera2
43+
import picamera2
4144

4245

4346
def load_default_tuning(cam: Picamera2) -> dict:
@@ -245,31 +248,34 @@ def adjust_white_balance_from_raw(
245248
camera.configure(config)
246249
camera.start()
247250
channels = channels_from_bayer_array(camera.capture_array("raw"))
248-
#logging.info(f"White balance: channels were retrieved with shape {channels.shape}.")
251+
# logging.info(f"White balance: channels were retrieved with shape {channels.shape}.")
249252
if luminance is not None and Cr is not None and Cb is not None:
250253
# Reconstruct a low-resolution image from the lens shading tables
251254
# and use it to normalise the raw image, to compensate for
252255
# the brightest pixels in each channel not coinciding.
253-
grids = grids_from_lst(np.array(luminance)**luminance_power, Cr, Cb)
254-
channel_gains = 1/grids
256+
grids = grids_from_lst(np.array(luminance) ** luminance_power, Cr, Cb)
257+
channel_gains = 1 / grids
255258
if channel_gains.shape[1:] != channels.shape[1:]:
256259
channel_gains = upsample_channels(channel_gains, channels.shape[1:])
257260
logging.info(f"Before gains, channel maxima are {np.max(channels, axis=(1,2))}")
258261
channels = channels * channel_gains
259262
logging.info(f"After gains, channel maxima are {np.max(channels, axis=(1,2))}")
260263
if method == "centre":
261264
_, h, w = channels.shape
262-
blue, g1, g2, red = np.mean(
263-
channels[:, 9*h//20:11*h//20, 9*w//20:11*w//20],
264-
axis=(1,2),
265-
) - 64
265+
blue, g1, g2, red = (
266+
np.mean(
267+
channels[:, 9 * h // 20 : 11 * h // 20, 9 * w // 20 : 11 * w // 20],
268+
axis=(1, 2),
269+
)
270+
- 64
271+
)
266272
else:
267273
# TODO: read black level from camera rather than hard-coding 64
268274
blue, g1, g2, red = np.percentile(channels, percentile, axis=(1, 2)) - 64
269275
green = (g1 + g2) / 2.0
270276
new_awb_gains = (green / red, green / blue)
271277
if Cr is not None and Cb is not None:
272-
# The LST algorithm normalises Cr and Cb by their minimum.
278+
# The LST algorithm normalises Cr and Cb by their minimum.
273279
# The lens shading correction only ever boosts the red and blue values.
274280
# Here, we decrease the gains by the minimum value of Cr and Cb.
275281
new_awb_gains = (green / red * np.min(Cr), green / blue * np.min(Cb))
@@ -320,40 +326,47 @@ def get_16x12_grid(chan: np.ndarray, dx: int, dy: int):
320326
"""
321327
for i in range(11):
322328
for j in range(15):
323-
grid.append(np.mean(chan[dy*i:dy*(1+i), dx*j:dx*(1+j)]))
324-
grid.append(np.mean(chan[dy*i:dy*(1+i), 15*dx:]))
329+
grid.append(np.mean(chan[dy * i : dy * (1 + i), dx * j : dx * (1 + j)]))
330+
grid.append(np.mean(chan[dy * i : dy * (1 + i), 15 * dx :]))
325331
for j in range(15):
326-
grid.append(np.mean(chan[11*dy:, dx*j:dx*(1+j)]))
327-
grid.append(np.mean(chan[11*dy:, 15*dx:]))
332+
grid.append(np.mean(chan[11 * dy :, dx * j : dx * (1 + j)]))
333+
grid.append(np.mean(chan[11 * dy :, 15 * dx :]))
328334
"""
329335
return as np.array, ready for further manipulation
330336
"""
331337
return np.reshape(np.array(grid), (12, 16))
332338

339+
333340
def upsample_channels(grids: np.ndarray, shape: tuple[int]):
334341
"""Zoom an image in the last two dimensions
335342
336343
This is effectively the inverse operation of `get_16x12_grid`
337344
"""
338-
zoom_factors = [1,] + list(np.ceil(np.array(shape)/np.array(grids.shape[1:])))
339-
return zoom(grids, zoom_factors, order=1)[:, :shape[0], :shape[1]]
345+
zoom_factors = [
346+
1,
347+
] + list(np.ceil(np.array(shape) / np.array(grids.shape[1:])))
348+
return zoom(grids, zoom_factors, order=1)[:, : shape[0], : shape[1]]
349+
340350

341351
def downsampled_channels(channels: np.ndarray, blacklevel=64) -> list[np.ndarray]:
342352
"""Generate a downsampled, un-normalised image from which to calculate the LST
343353
344354
TODO: blacklevel probably ought to be determined from the camera...
345355
"""
346356
channel_shape = np.array(channels.shape[1:])
347-
lst_shape = np.array([12,16])
348-
step = np.ceil(channel_shape/lst_shape).astype(int)
357+
lst_shape = np.array([12, 16])
358+
step = np.ceil(channel_shape / lst_shape).astype(int)
349359
return np.stack(
350360
[
351-
get_16x12_grid(channels[i, ...].astype(float) - blacklevel, step[1], step[0])
361+
get_16x12_grid(
362+
channels[i, ...].astype(float) - blacklevel, step[1], step[0]
363+
)
352364
for i in range(channels.shape[0])
353365
],
354366
axis=0,
355367
)
356368

369+
357370
def lst_from_channels(channels: np.ndarray) -> LensShadingTables:
358371
"""Given the 4 Bayer colour channels from a white image, generate a LST.
359372
@@ -373,32 +386,34 @@ def lst_from_grids(grids: np.ndarray) -> LensShadingTables:
373386
# TODO: make consistent with
374387
https://git.linuxtv.org/libcamera.git/tree/utils/raspberrypi/ctt/ctt_alsc.py
375388
"""
376-
r: np.ndarray = grids[3, ...]
389+
r: np.ndarray = grids[3, ...]
377390
g: np.ndarray = np.mean(grids[1:3, ...], axis=0)
378391
b: np.ndarray = grids[0, ...]
379392

380393
# What we actually want to calculate is the gains needed to compensate for the
381394
# lens shading - that's 1/lens_shading_table_float as we currently have it.
382395
luminance_gains: np.ndarray = np.max(g) / g # Minimum luminance gain is 1
383396
cr_gains: np.ndarray = g / r
384-
#cr_gains /= cr_gains[5, 7] # Normalise so the central colour doesn't change
397+
# cr_gains /= cr_gains[5, 7] # Normalise so the central colour doesn't change
385398
cb_gains: np.ndarray = g / b
386-
#cb_gains /= cb_gains[5, 7]
399+
# cb_gains /= cb_gains[5, 7]
387400
return luminance_gains, cr_gains, cb_gains
388401

402+
389403
def grids_from_lst(lum: np.ndarray, Cr: np.ndarray, Cb: np.ndarray) -> np.ndarray:
390404
"""Convert form luminance/chrominance dict to four RGGB channels
391-
405+
392406
Note that these will be normalised - the maximum green value is always 1.
393407
Also, note that the channels are BGGR, to be consistent with the
394408
`channels_from_raw_image` function. This should probably change in the
395409
future.
396410
"""
397-
G = 1/np.array(lum)
398-
R = G/np.array(Cr)
399-
B = G/np.array(Cb)
411+
G = 1 / np.array(lum)
412+
R = G / np.array(Cr)
413+
B = G / np.array(Cb)
400414
return np.stack([B, G, G, R], axis=0)
401415

416+
402417
def set_static_lst(
403418
tuning: dict,
404419
luminance: np.ndarray,
@@ -423,30 +438,23 @@ def set_static_lst(
423438
]
424439
alsc["luminance_lut"] = np.reshape(luminance, (-1)).round(3).tolist()
425440

426-
def set_static_ccm(
427-
tuning: dict,
428-
c: list
429-
) -> None:
441+
442+
def set_static_ccm(tuning: dict, c: list) -> None:
430443
"""Update the `rpi.alsc` section of a camera tuning dict to use a static correcton.
431444
432445
`tuning` will be updated in-place to set its shading to static, and disable any
433446
adaptive tweaking by the algorithm.
434447
"""
435448
ccm = Picamera2.find_tuning_algo(tuning, "rpi.ccm")
436-
ccm["ccms"] = [{
437-
"ct": 2860,
438-
"ccm": c
439-
}
440-
]
449+
ccm["ccms"] = [{"ct": 2860, "ccm": c}]
441450

442-
def get_static_ccm(
443-
tuning: dict
444-
) -> None:
445-
"""Get the `rpi.ccm` section of a camera tuning dict
446-
"""
451+
452+
def get_static_ccm(tuning: dict) -> None:
453+
"""Get the `rpi.ccm` section of a camera tuning dict"""
447454
ccm = Picamera2.find_tuning_algo(tuning, "rpi.ccm")
448455
return ccm["ccms"]
449456

457+
450458
def lst_is_static(tuning: dict) -> bool:
451459
"""Whether the lens shading table is set to static"""
452460
alsc = Picamera2.find_tuning_algo(tuning, "rpi.alsc")
@@ -472,7 +480,7 @@ def set_static_geq(
472480
def _geq_is_static(tuning: dict) -> bool:
473481
"""Whether the green equalisation is set to static"""
474482
geq = Picamera2.find_tuning_algo(tuning, "rpi.geq")
475-
return alsc["offset"] == 65535
483+
return geq["offset"] == 65535
476484

477485

478486
def index_of_algorithm(algorithms: list[dict], algorithm: str):
@@ -499,6 +507,7 @@ def lst_from_camera(camera: Picamera2) -> LensShadingTables:
499507
channels = raw_channels_from_camera(camera)
500508
return lst_from_channels(channels)
501509

510+
502511
def raw_channels_from_camera(camera: Picamera2) -> LensShadingTables:
503512
"""Acquire a raw image and return a 4xNxM array of the colour channels."""
504513
if camera.started:
@@ -521,6 +530,16 @@ def raw_channels_from_camera(camera: Picamera2) -> LensShadingTables:
521530
return channels_from_bayer_array(raw_image)
522531

523532

533+
def recreate_camera_manager():
534+
"""Delete and recreate the camera manager.
535+
536+
This is necessary to ensure the tuning file is re-read.
537+
"""
538+
del Picamera2._cm
539+
gc.collect()
540+
Picamera2._cm = picamera2.picamera2.CameraManager()
541+
542+
524543
if __name__ == "__main__":
525544
"""This block is untested but has been updated."""
526545
with Picamera2() as cam:

0 commit comments

Comments
 (0)