Full grading pipeline: PIL -> GPU tensor -> kornia ops -> PIL.
(image: Image.Image, params: GradingParams)
| 203 | |
| 204 | |
| 205 | def grade_image(image: Image.Image, params: GradingParams) -> Image.Image: |
| 206 | """Full grading pipeline: PIL -> GPU tensor -> kornia ops -> PIL.""" |
| 207 | log.debug(f"Grading: params={params}") |
| 208 | kornia = _ensure_kornia() |
| 209 | arr = np.array(image).astype(np.float32) / 255.0 |
| 210 | tensor = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0) |
| 211 | tensor = tensor.to(device=devices.device, dtype=devices.dtype) |
| 212 | |
| 213 | # basic adjustments |
| 214 | if params.brightness != 0: |
| 215 | tensor = kornia.enhance.adjust_brightness(tensor, params.brightness) |
| 216 | if params.contrast != 0: |
| 217 | tensor = kornia.enhance.adjust_contrast(tensor, 1.0 + params.contrast) |
| 218 | if params.saturation != 0: |
| 219 | tensor = kornia.enhance.adjust_saturation(tensor, 1.0 + params.saturation) |
| 220 | if params.hue != 0: |
| 221 | tensor = kornia.enhance.adjust_hue(tensor, params.hue * math.pi) |
| 222 | if params.gamma != 1.0: |
| 223 | tensor = kornia.enhance.adjust_gamma(tensor, params.gamma) |
| 224 | if params.sharpness != 0: |
| 225 | tensor = kornia.enhance.sharpness(tensor, 1.0 + params.sharpness * 4.0) |
| 226 | if params.color_temp != 6500: |
| 227 | tensor = _apply_color_temp(tensor, params.color_temp) |
| 228 | |
| 229 | # tone adjustments |
| 230 | if params.shadows != 0 or params.midtones != 0 or params.highlights != 0: |
| 231 | tensor = _apply_shadows_midtones_highlights(tensor, params.shadows, params.midtones, params.highlights) |
| 232 | if params.clahe_clip > 0: |
| 233 | lab = kornia.color.rgb_to_lab(tensor) |
| 234 | L = lab[:, 0:1, :, :] / 100.0 |
| 235 | L = kornia.enhance.equalize_clahe(L, clip_limit=params.clahe_clip, grid_size=(params.clahe_grid, params.clahe_grid)) |
| 236 | lab[:, 0:1, :, :] = L * 100.0 |
| 237 | tensor = kornia.color.lab_to_rgb(lab).clamp(0, 1) |
| 238 | |
| 239 | # split toning |
| 240 | if params.shadows_tint != "#000000" or params.highlights_tint != "#ffffff": |
| 241 | tensor = _apply_split_toning(tensor, params.shadows_tint, params.highlights_tint, params.split_tone_balance) |
| 242 | |
| 243 | # effects |
| 244 | if params.vignette > 0: |
| 245 | tensor = _apply_vignette(tensor, params.vignette) |
| 246 | if params.grain > 0: |
| 247 | tensor = _apply_grain(tensor, params.grain) |
| 248 | |
| 249 | # convert back to PIL |
| 250 | tensor = tensor.clamp(0, 1) |
| 251 | arr = (tensor.squeeze(0).permute(1, 2, 0).float().cpu().numpy() * 255).astype(np.uint8) |
| 252 | result = Image.fromarray(arr) |
| 253 | result.info = image.info.copy() # Image.fromarray drops info; preserve so Process-tab metadata survives grading |
| 254 | |
| 255 | # LUT applied last (CPU, via pillow-lut-tools) |
| 256 | if params.lut_cube_file: |
| 257 | result = _apply_lut(result, params.lut_cube_file, params.lut_strength) |
| 258 | |
| 259 | return result |