diff --git a/general/package/legacy/ambarella-osdrv-s3l/ambarella-osdrv-s3l.mk b/general/package/legacy/ambarella-osdrv-s3l/ambarella-osdrv-s3l.mk index 8ae5608d3..581120123 100644 --- a/general/package/legacy/ambarella-osdrv-s3l/ambarella-osdrv-s3l.mk +++ b/general/package/legacy/ambarella-osdrv-s3l/ambarella-osdrv-s3l.mk @@ -6,7 +6,7 @@ AMBARELLA_OSDRV_S3L_SITE_METHOD = local AMBARELLA_OSDRV_S3L_SITE = $(AMBARELLA_OSDRV_S3L_PKGDIR)/src -AMBARELLA_OSDRV_S3L_LICENSE = MIT (load_ucode), PROPRIETARY (modules, microcode) +AMBARELLA_OSDRV_S3L_LICENSE = MIT (load_ucode, ISP generator), PROPRIETARY (modules, microcode) # load_ucode puts the DSP microcode in place. files/firmware is the generic set # from Ambarella's S3L SDK release 2.5.0_20181109 (2018/10/28, 284712.282649), @@ -21,8 +21,19 @@ AMBARELLA_OSDRV_S3L_LICENSE = MIT (load_ucode), PROPRIETARY (modules, microcode) AMBARELLA_OSDRV_S3L_DEPENDENCIES = linux AMBARELLA_OSDRV_S3L_IAV_OFFSETS = INODE_I_CDEV:272 FILE_PRIVATE_DATA:112 +# The ISP configuration Divinus loads into the DSP (/etc/sensors/.bin) is generated +# here from src/isp's presets, one per sensor; nothing comes from a camera's firmware. Only +# Divinus reads it (Majestic does not), so it is built and installed only with Divinus. +ifeq ($(BR2_PACKAGE_DIVINUS),y) +AMBARELLA_OSDRV_S3L_ISP_SENSORS = ov4689 +AMBARELLA_OSDRV_S3L_DEPENDENCIES += host-python3 +endif + define AMBARELLA_OSDRV_S3L_BUILD_CMDS $(MAKE) $(TARGET_CONFIGURE_OPTS) -C $(@D) load_ucode + for s in $(AMBARELLA_OSDRV_S3L_ISP_SENSORS); do \ + $(HOST_DIR)/bin/python3 $(@D)/isp/s3l_isp_gen.py $(@D)/isp/$$s.bin --preset $(@D)/isp/$$s.yaml || exit 1; \ + done $(LINUX_MAKE_ENV) $(MAKE) -C $(LINUX_DIR) $(LINUX_MAKE_FLAGS) \ M=$(@D)/abi abi_probe.s for want in $(AMBARELLA_OSDRV_S3L_IAV_OFFSETS); do \ @@ -37,8 +48,10 @@ define AMBARELLA_OSDRV_S3L_BUILD_CMDS endef define AMBARELLA_OSDRV_S3L_INSTALL_TARGET_CMDS - $(INSTALL) -m 755 -d $(TARGET_DIR)/etc/sensors - # $(INSTALL) -m 644 -t $(TARGET_DIR)/etc/sensors $(AMBARELLA_OSDRV_S3L_PKGDIR)/files/sensor/config/*.cfg + $(if $(AMBARELLA_OSDRV_S3L_ISP_SENSORS),$(INSTALL) -m 755 -d $(TARGET_DIR)/etc/sensors) + for s in $(AMBARELLA_OSDRV_S3L_ISP_SENSORS); do \ + $(INSTALL) -m 644 $(@D)/isp/$$s.bin $(TARGET_DIR)/etc/sensors/$$s.bin || exit 1; \ + done $(INSTALL) -m 755 -d $(TARGET_DIR)/lib/modules/3.10.104/ambarella $(INSTALL) -m 644 -t $(TARGET_DIR)/lib/modules/3.10.104/ambarella $(AMBARELLA_OSDRV_S3L_PKGDIR)/files/kmod/*.ko diff --git a/general/package/legacy/ambarella-osdrv-s3l/src/isp/ov4689.yaml b/general/package/legacy/ambarella-osdrv-s3l/src/isp/ov4689.yaml new file mode 100644 index 000000000..bde66e291 --- /dev/null +++ b/general/package/legacy/ambarella-osdrv-s3l/src/isp/ov4689.yaml @@ -0,0 +1,11 @@ +# OmniVision OV4689: S3L ISP configuration, generated at build time (no vendor tuning). +# Values here are the sensor's, shared by every OV4689 board; a board that needs its own file points the +# streamer at it (Divinus: system.sensor_config), which takes precedence over /etc/sensors/ov4689.bin. +width: 2688 +height: 1520 +bayer: 1 +black_level: 244 # the sensor's BLC target 0x40 (10-bit) is a nominal 256; a dark frame's median sits at ~244 +wb_gain: 1.5,1.8 # starting point only: the streamer's auto white balance replaces it from the first frames +dbp: 4,32 # dynamic bad-pixel correction +sharpness: medium +temporal_nr: med # motion-compensated temporal noise reduction diff --git a/general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py b/general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py new file mode 100644 index 000000000..272fa1e5d --- /dev/null +++ b/general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py @@ -0,0 +1,432 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""S3L ISP configuration generator: a sensor preset in, an S3L ISP configuration file out (the file Divinus loads +into the DSP's IMG buffer before preview starts: the LISO, HDR and CE image blocks and the tables they point at). + + s3l_isp_gen.py OUT.bin [--preset FILE.yaml]... [--set key=value]... [--show] + +Presets are `key: value` lines, applied in order, then --set on top. Keys (OPTIONS below): + width, height, bayer sensor output size and Bayer phase (0..3); default 2688x1520, bayer 1 (OV4689) + black_level sensor black level in 12-bit raw units, 0..2047 + wb_gain starting white balance 'R,B' or 'R,G,B', each 0.25..8 (the streamer's AWB takes over) + dbp dynamic bad-pixel correction: off | MODE,THRESH (mode 1..4, threshold 0..255) + sharpness final sharpening: off | mild | medium | strong + temporal_nr motion-compensated temporal noise reduction: off | low | med | high +Everything not set by a preset is neutral: identity colour with gamma 0.45, BT.601, no local exposure, contrast +enhancement or HDR (a linear sensor). + +Where the format comes from: the block layout, field offsets, table sizes and encodings, and the limits the DSP +enforces were worked out by reverse engineering the S3L ISP library and DSP microcode, checked against the camera. +The values written are this file's own: neutral or identity settings, values measured on an OV4689 camera (black +level, white balance), and noise-reduction and sharpening curves designed and measured here. Nothing is copied from +a camera's firmware or tuning files. The layout has been run on the IP4M-1026E with the DSP microcode OpenIPC ships +(SDK 2.5.0, 2018/10/28 284712) and with that camera's own (2017/02/22 267837). + +Output (S3LISP03): "S3LISP03", u32 ops, u32 img_len, u32 img_off, u32 nrel; ops x {u8 kind, u8 nr, u16 0, +u32 offset, u32 flags}; nrel x u32 relocation; then img_len bytes of the IMG buffer from img_off. Offsets are from +the start of the IMG buffer. Table pointers inside the blocks are written as IMG offsets, and each relocation names a +pointer word the loader adds the IMG buffer's DSP address to, so the file does not depend on where the buffer is. +""" +import math, os, struct, sys + +IMG_LEN = 0x600000 +ALLOC_START = 0x1c0000 # well clear of the command rings at +0x500000 +ALLOC_END = 0x500000 + +# --- format --------------------------------------------------------------------------------------------------------- +LISO_SIZE, HDR_SIZE, CE_SIZE = 1024, 68, 8 +# LISO pointer fields of the tables written here; the rest stay 0 (table not used). +LISO_PTRS = {0x100: 'le_gain_curve', 0x120: 'chroma_gain_curve', 0x124: 'cc_reg', 0x128: 'cc_3d', + 0x12c: 'tone_curve', 0x160: 'dbp_dark', 0x164: 'dbp_hot', 0x22c: 'video_mctf', 0x230: 'final_sharpen'} +TABLE_SIZES = {'le_gain_curve': 512, 'chroma_gain_curve': 256, 'cc_reg': 2304, 'cc_3d': 16384, 'tone_curve': 1024, + 'dbp_dark': 768, 'dbp_hot': 768, 'video_mctf': 10080, + 'final_sharpen': 12800} # the DSP reads 12752 bytes of it +# The image-block ioctls (kind 's' = set, ioctl nr), sent in this order before preview starts, which applies every +# sub-filter: contrast enhancement (0x65), HDR (0x64), then LISO (0x60). The flags word is unused for 's'. +OPS = [('s', 0x65, 'ce'), ('s', 0x64, 'hdr'), ('s', 0x60, 'liso')] + + +class Config: + def __init__(self, width, height, bayer): + self.liso = bytearray(LISO_SIZE) + self.tables = {n: None for n in TABLE_SIZES} + L = self.liso + struct.pack_into(' ALLOC_END: + raise SystemExit('ISP layout does not fit') + place[name] = pos + img[pos:pos + len(data)] = data + pos += len(data) + + for name, data in self.tables.items(): + if data is not None: + alloc(name, data) + for i, (_, _, blk) in enumerate(OPS): + if blk == 'ce': + alloc(f'ce{i}', bytes(CE_SIZE)) # descriptor {0, 0}: contrast enhancement off + elif blk == 'hdr': + alloc(f'hdr{i}', bytes(HDR_SIZE)) # HDR blending off (linear sensor) + else: + alloc(f'liso{i}', self.liso) + for fo, name in LISO_PTRS.items(): + if name in place: + struct.pack_into(' start and not any(img[end - 0x1000:end]): # trim trailing zero pages + end -= 0x1000 + end = max(end, max(place.values()) + 0x400) + with open(path, 'wb') as f: + f.write(b'S3LISP03' + struct.pack(' 16x16x16 LUT (+0x128) -> output LUT (+0x12c). The input LUT has 352 knots, +# spaced (count, step) in 12-bit units: +CC_IN_RUNS = [(64, 1), (32, 2), (32, 4), (96, 8), (64, 16), (64, 32)] + + +def color_chain(cfg): + """Identity colour, with the display gamma 0.45 in the input LUT.""" + knots, x = [], 0 + for n, step in CC_IN_RUNS: + for _ in range(n): + knots.append(x) + x += step + e = [min(4095, round(4095 * (k / 4096) ** 0.45)) for k in knots] + words = [e[2 * i] | e[2 * i + 1] << 12 for i in range(176)] + [0] * 16 + cfg.tables['cc_reg'] = bytearray(struct.pack('<192I', *words) * 3) + q = lambda k: round(k * 1023 / 15) + cfg.tables['cc_3d'] = bytearray(struct.pack('<4096I', *(q(a) | q(b) << 10 | q(c) << 20 + for a in range(16) for b in range(16) for c in range(16)))) + vals = [min(4 * i, 1023) for i in range(256)] # identity, 8-bit index to 10-bit output + cfg.tables['tone_curve'] = bytearray(struct.pack('<256I', *(x | x << 10 | x << 20 for x in vals))) + + +def rgb2yuv_bt601(cfg): + """RGB->YUV (+0x130, Q10, 1024 = 1.0) and offsets: BT.601, studio range.""" + m = ((0.257, 0.504, 0.098), (-0.148, -0.291, 0.439), (0.439, -0.368, -0.071)) + struct.pack_into('<9h3h', cfg.liso, 0x130, *[round(c * 1024) for row in m for c in row], 16, 128, 128) + + +FS_UPDATE = 0x3fff0000 # LISO +0x234 bits 16-29: the final-sharpen sub-tables to apply + + +def final_sharpen_off(cfg): + """Final sharpen off: zeros with byte 0 = 1 (mcts_en: without it the DSP never writes the output buffer, which + shows as a line pattern).""" + t = bytearray(TABLE_SIZES['final_sharpen']) + t[0] = 0x01 + cfg.tables['final_sharpen'] = t + struct.pack_into(' (S, q[25])""" + for S in range(4, -1, -1): + q = [round(FIR_UNIT * k / 2 ** (4 - S)) for k in taps[:24]] + q.append(-2 * sum(q)) + if all(-(1 << (w - 1)) <= v < (1 << (w - 1)) for v, w in zip(q, FIR_W)): + return S, q + raise SystemExit('sharpness: kernel too strong to encode') + + +def fs_fir_record(t, off, q): + for i in range(24): + t[off + FIR_BYTE[i]] = q[i] & 0xff + struct.pack_into('= t1 else round(a + (255 - a) * (i - t0) / (t1 - t0)) for i in range(64)]) + + +def mctf_lvl(m, base, vals): + """MCTF level-control record (0x50 bytes, y/cb/cr per slot) giving the same value at every level: low, base and + high value = vals. As for fs_lvl_const, the thresholds, deltas and area then do not matter and stay 0.""" + for slot in (0x30, 0x38, 0x40): + m[base + slot: base + slot + 3] = bytes(vals) + + +def opt_temporal_nr(cfg, v): + """off: an all-zero table (verified to pass video through). low/med/high: curves B and C hold the current-frame + weight per motion bin (alpha floor for static areas, a ramp, 255 above); curve D passes through (255). The mode, + median and temporal-adjust fields stay 0: measured on a static scene at high gain and on a moving pattern, setting + them changed neither the noise nor the detail, and no setting showed ghosting.""" + m = bytearray(TABLE_SIZES['video_mctf']) + if v != 'off': + if v not in TNR_PRESETS: + raise SystemExit(f'temporal_nr {v!r}: off, ' + ', '.join(TNR_PRESETS)) + p = TNR_PRESETS[v] + gy, gc = p['gate'] + mctf_lvl(m, 0x108, (gy, gc, gc)) # iso filter gate + mctf_lvl(m, 0x158, (gy, gc, gc)) # dir filter gate + mctf_lvl(m, 0x218, (255, 255, 255)) # overall max change 255 (no limit) + m[0x210:0x213] = b'\xff\xff\xff' # tf_max_change_from_SB2: 255 (no limit) + struct.pack_into('