Spaces:
Sleeping
Sleeping
File size: 3,586 Bytes
568914f 4247de9 568914f 4247de9 568914f 4247de9 568914f 4247de9 568914f 4247de9 568914f 4247de9 568914f 4247de9 568914f 4247de9 568914f 4247de9 568914f 4247de9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | """RAW decode + camera-space white balance from an illuminant chromaticity.
White balance is applied the way the iOS app does it (`CIRAWFilter.neutralChromaticity`):
as per-channel multipliers in the camera's *native* space, derived from the target
illuminant through the camera's own XYZ->camera matrix. The earlier Bradford-CAT-in-sRGB
approach used the wrong operator in the wrong space and produced a desaturated,
magenta/hazy result; this neutralises the illuminant the way the app does.
This is deliberately a *white-balance* preview, not a full ISP: no auto-exposure, tone
curve, or contrast. 'before' and 'after' share identical decode settings and differ
only in the WB multipliers, so the comparison shows the correction alone.
"""
import numpy as np
HALF_SIZE = True
MAX_LONG_EDGE = 2000
# Shared postprocess: linear 16-bit, no auto-exposure, half-res. WB + output colour
# are supplied per call so before/after differ only by white balance.
_POST = dict(output_bps=16, gamma=(1, 1), no_auto_bright=True, half_size=HALF_SIZE)
def xy_to_XYZ(x, y, Y=1.0):
return np.array([Y * x / y, Y, Y * (1.0 - x - y) / y])
def open_raw(path):
"""imread and return the RawPy object. The caller may delete the file right
after — the unpacked sensor data stays in RAM, so depth re-runs re-render from
memory (no second read) and the upload is still discarded for privacy."""
import rawpy
return rawpy.imread(str(path))
def wb_from_chromaticity(raw, illuminant_xy):
"""Raw WB multipliers [R, G, B, G2] that render `illuminant_xy` neutral, derived
from the camera's XYZ->camera matrix (`raw.rgb_xyz_matrix`). This is the camera-
space equivalent of CIRAWFilter.neutralChromaticity. Validated: feeding D65 here
reproduces `raw.daylight_whitebalance` to 4 decimals."""
M = np.asarray(raw.rgb_xyz_matrix, dtype=float)[:3] # XYZ -> camera
cam = M @ xy_to_XYZ(*illuminant_xy)
cam = np.where(np.abs(cam) < 1e-9, 1e-9, cam)
mul = 1.0 / cam
mul = mul / mul[1] # normalise green = 1
return [float(mul[0]), 1.0, float(mul[2]), 1.0]
def baseline_wb(raw):
"""'before' WB — the camera's as-shot balance (the photo as captured), falling
back to the daylight balance. Green-normalised."""
cw = np.asarray(raw.camera_whitebalance, dtype=float)
if cw.size < 3 or cw[1] <= 0 or not np.all(np.isfinite(cw[:3])):
cw = np.asarray(raw.daylight_whitebalance, dtype=float)
cw = cw / cw[1]
return [float(cw[0]), 1.0, float(cw[2]), 1.0]
def render(raw, wb, max_long_edge=MAX_LONG_EDGE):
"""postprocess `raw` with WB multipliers `wb` -> uint8 sRGB display image."""
import rawpy
rgb16 = raw.postprocess(user_wb=list(wb), output_color=rawpy.ColorSpace.sRGB, **_POST)
linear = _resize_max(rgb16.astype(np.float32) / 65535.0, max_long_edge)
return _encode_display(linear)
def _encode_display(linear_rgb):
lin = np.clip(linear_rgb, 0.0, 1.0)
srgb = np.where(lin <= 0.0031308, lin * 12.92, 1.055 * np.power(lin, 1 / 2.4) - 0.055)
return (np.clip(srgb, 0.0, 1.0) * 255.0 + 0.5).astype(np.uint8)
def _resize_max(img, max_long_edge):
from PIL import Image
h, w = img.shape[:2]
if max(h, w) <= max_long_edge:
return img
scale = max_long_edge / max(h, w)
new_w, new_h = max(1, round(w * scale)), max(1, round(h * scale))
chans = [
np.asarray(Image.fromarray(img[..., c], mode="F").resize((new_w, new_h), Image.BILINEAR))
for c in range(img.shape[2])
]
return np.stack(chans, axis=-1)
|