Gradación de Color con Fidelidad Fotográfica
Cuándo Usarlo
- El usuario proporciona una imagen de referencia y una foto a ajustar, solicitando 'ajusta el tono de esta foto según la referencia'
- El usuario tiene un lote de fotos ya editadas y desea consolidar ese estilo para aplicarlo a un nuevo lote
- El usuario quiere ajustes por zonas, como 'oscurece solo el cielo, no toques el suelo ni la cara'
- El usuario desea aplicar un Look específico por lotes, exigiendo 'no cambiar el contenido ni redibujar'
- El usuario quiere verificar si un estilo mantiene la fidelidad: sin bandas, dominantes de color, o aplastamiento de luces/sombras
- El usuario sube una imagen preguntando '¿esta foto está bien ajustada?', sin imagen de referencia
- El material del usuario podría ser metraje log / sin convertir (fotogramas S-Log3, V-Log, HLG exportados desde RAW de cámara)
- El usuario se queja de que la edición generativa 'cambió la cara' o 'perdió resolución': este es el escenario ideal para esta skill
No aplica: Tareas que requieran cambiar la dirección de la luz, reemplazar el cielo, añadir objetos o redibujar semánticamente. Esta skill solo produce mapeo de color, no puede modificar contenido.
Modelo Central
- Un 'estilo' = LUT base
.cube+ LUTs de segmentación opcionales (uno por zona) + registro de versiones + archivo de lotes + etiquetas de tema, almacenado encolor-styles/<name>/ - Aplicar un estilo = búsqueda en tabla píxel a píxel, luego superposición de LUTs de zona mediante máscaras semánticas. La salida tiene exactamente el mismo tamaño que la imagen original, con SSIM estructural igual a 1.0
- Dos formas de generar un estilo:
fit: aprendizaje a partir de pares 'original / retocado', preciso (ΔE2000 medido 0.55–0.9), requiere 3+ paresref: transferencia estadística desde una imagen de referencia, rápido pero aproximado (ΔE2000 medido ≈ 4), funciona con una sola imagen, ideal para arranque en frío
- Cada
fites una 'confirmación' que se consolida automáticamente: se determina a qué estilo pertenece → si se fusiona, se reajusta con todas las muestras históricas → se archiva → se etiqueta con tema. No entrena modelos ni requiere GPU
Qué Soluciona la Segmentación
Un LUT global solo puede responder 'a qué color se asigna este color'. Cuando un mismo color debe tener resultados diferentes en distintas zonas —el caso más típico es oscurecer un cielo gris claro mientras se conserva un pavimento igualmente gris claro—, un mapeo global no tiene solución matemática; los mínimos cuadrados toman un valor intermedio que no satisface a ninguna.
Medido (una escena callejera nublada, solo oscurecer el cielo −28%):
| Cielo ΔE2000 | No cielo ΔE2000 | |
|---|---|---|
| LUT global | 11.27 | 0.322 |
| Global + cielo segmentado | 1.50 | 0.324 |
El error en las zonas no cielo es idéntico, lo que demuestra que la segmentación no introduce contaminación adicional; simplemente completa la parte que el LUT global no podía aprender.
Dimensiones de segmentación: sky (cielo) / people (personas) / veg (vegetación) / water (agua) / ground (suelo) / arch (arquitectura). Por defecto: sky,people,veg, proporcionadas por SegFormer-B0 integrado (4.4 MB) que genera máscaras suaves.
Flujo de Trabajo
1. Verificar Dependencias
python -c "import numpy, PIL; print('ok')"
python -c "import onnxruntime; print('seg ok')" # Necesario para segmentación y etiquetado de temas
python -c "import tifffile; print('tif ok')" # Necesario para lectura/escritura TIFF de 16 bits
python -c "import rawpy; print('raw ok')" # Necesario para lectura directa de RAW, opcional
Si falta alguna: python -m pip install numpy pillow onnxruntime tifffile rawpy. La falta de onnxruntime no rompe nada, solo degrada a gradación puramente global.
2. Primero, Inspeccionar el Material (Recomendado, Ahorra Trabajo Inútil)
python <SKILL_DIR>/scripts/grade.py check --in ./photos/original --person
No necesita imagen de referencia; responde solo dos preguntas:
- ¿Qué material es?:
transfer.verdict=display/ambiguous/log. Aplicarfitdirectamente a material log incorporaría la curva log en el LUT, produciendo un resultado visualmente razonable pero incorrecto. La opción--personejecuta una segmentación, para que la valoración del tono de piel tenga sentido (no se evalúa si no hay personas). - ¿Tiene la imagen final defectos graves?: Luminancia media, punto negro, proporción de blancos/negros puros, número de desviaciones de color, riqueza cromática. Los valores fuera de rango se incluyen en
warnings.
Tanto apply como fit ejecutan automáticamente estas dos comprobaciones internamente, por lo que solo es necesario hacer este paso una vez antes de la entrega por lotes.
3. Confirmar un Lote de Material (Acción Central)
python <SKILL_DIR>/scripts/grade.py fit \
--src ./photos/original --tgt ./photos/retouched
--src/--tgtse emparejan por el nombre base del archivo; también se puede usar--pair original retocadapara repetir pares específicos- No importa si en el directorio hay fotos de diferentes tamaños, pero cada par debe tener el mismo tamaño interno
- Si no se usa
--name, se usa el modo automático: se compara la distancia con los estilos existentes; si es suficientemente cercano, se fusiona; si no, se crea uno nuevo --name warm_filmes una especificación explícita: si el nombre ya existe, se fusiona; si no, se crea--regions sky,people,veghabilita la segmentación (valor por defecto). Escribir--regions ""vuelve al modo puramente global--auto-threshold 1.5es el umbral ΔE para considerar 'el mismo estilo'
4. Aplicar
python <SKILL_DIR>/scripts/grade.py apply \
--style warm_film --in ./photos/new --out ./out --strength 1.0
--style auto primero identifica el tema de la foto de entrada y luego selecciona el estilo con la etiqueta más coincidente (el resultado se explica en el campo auto).
Primero revisar look y warnings antes de entregar. Si look.median_luma cambia de [0.195, 0.302] a [0.55, 0.95], significa que esta imagen es mucho más brillante que el material con el que se aprendió el estilo; al aplicar el LUT, las luces se saturan. En este caso solo hay tres opciones: reducir la intensidad con --strength 0.7, alinear la exposición al nivel aprendido con --match-key on (solo apto para ráfagas con la misma relación de luz), o aceptar un fondo más claro. No decidas por el usuario sin preguntar.
--match-key off|on|auto (por defecto off): primero alinea la luminancia media de la imagen al nivel con el que se aprendió el estilo y luego aplica el LUT. Puede dañar al sujeto en contraluces; ver la regla de fidelidad n.º 14.
Formato de Salida y Calidad de Imagen
Por defecto se generan dos archivos por imagen; no envíes solo el primero:
| Producto | Por defecto | Uso |
|---|---|---|
<nombre>__<estilo>_v<N>.jpg | q98, 4:4:4, conserva ICC/EXIF/DPI originales | Entrega, compartir, redes sociales |
<nombre>__<estilo>_v<N>__master.png | 8 bits sin pérdida, con tramado ±0.5 LSB | Para seguir editando, ampliar o imprimir |
--quality 98: calidad JPEG. De q95 a q98, el PSNR medido pasa de 48.7 a 50.6 dB, y el tamaño de 337 a 512 KB. Reducir a q92 ahorra la mitad de volumen y pierde 3 dB, visible a simple vista en cielos degradados.--master png|tif|none:tifproduce 16 bits (requieretifffile; si no se puede instalar, se degrada automáticamente a PNG de 8 bits y se escribemaster_note, sin dañar el JPG ya generado). En procesamiento por lotes, si molesta el tamaño, usa--master none.- Si el archivo de origen ya es
.png/.tif/.bmp, no se genera un archivo maestro: el producto principal ya es sin pérdida. - ICC, EXIF, DPI se transfieren desde la imagen original. Perder el ICC es lo que más se percibe como 'pérdida de calidad': una imagen en espacio P3/AdobeRGB, si se escribe sin etiqueta RGB, cualquier visor la interpreta como sRGB y el color se vuelve gris y apagado.
- El tramado solo se aplica en formatos sin pérdida. El error de cuantificación JPEG ya es un orden de magnitud mayor que 1 LSB; aplicar tramado allí solo desperdicia tasa de bits.
Si el usuario dice 'está comprimido', primero verifica la imagen original, no ajustes los parámetros. apply señala en source_note: si el JPEG original tiene menos de 0.10 bytes/píxel, es una copia reenviada por aplicaciones de mensajería (1080p con solo 100 KB); los detalles se perdieron antes de este paso, y la salida por muy sin pérdida que sea no puede recuperarlos. Otros dos errores comunes: juzgar la calidad viendo la vista previa en la ventana de chat (la plataforma la re-comprime) y juzgar por la imagen de vista previa final_sheet.jpg (reducida a la mitad).
5. Verificar
python <SKILL_DIR>/scripts/grade.py verify \
--style warm_film --src ./photos/original/a.jpg --tgt ./photos/retouched/a.jpg \
--threshold 2.0
Si se usa --threshold y se supera, el comando termina con código de salida 2, útil para puntos de control en pipelines.
6. Exportar para Uso Externo
python <SKILL_DIR>/scripts/grade.py export --style warm_film --out ./for_lightroom --full
--full genera además una nota de conexión (capa base sobre la imagen completa → capas de zona superpuestas con máscara suave → difuminado del 1-2% del tamaño del cuadro).
Los estilos con segmentación no se pueden representar con un solo .cube: un LUT 3D individual es una función del color del píxel, no puede expresar 'mismo color, diferente procesamiento según la zona'.
7. Deshacer y Trazabilidad
python <SKILL_DIR>/scripts/grade.py batches --style warm_film # Ver de qué lotes se compone este estilo
python <SKILL_DIR>/scripts/grade.py undo --style warm_film --batch b20261108_101500
python <SKILL_DIR>/scripts/grade.py styles # Todos los estilos + etiquetas + descripción breve
python <SKILL_DIR>/scripts/grade.py styles --name warm_film # Manifiesto completo de un solo estilo
undo no elimina versiones: elimina ese lote de la composición, reajusta con los lotes restantes y genera una nueva versión. Las versiones antiguas se conservan siempre.
Interpretación de la Salida
Todos los comandos imprimen JSON en stdout.
| Campo | Significado | Interpretación |
|---|---|---|
quality.dE2000_mean | Diferencia de color media en el par de entrenamiento (incluyendo segmentación) | < 2 aceptable; 2–4 marginal; > 4 indica que el par no es una relación puramente cromática |
decision | Cómo se procesó esta confirmación | create-new / merge-nearest / merge-into-named |
auto.distance | Distancia al estilo más cercano | Significativamente menor que el umbral pero se creó uno nuevo, o mayor que el umbral pero se fusionó, indica un error de juicio |
auto.nearest | Los tres estilos más cercanos y su distancia | Útil para entender 'por qué se decidió así' |
learned_from | Cuántas muestras históricas se usaron en la fusión | Si solo hay new_samples y no archived, es la primera vez |
diagnostics.coverage_pct | Porcentaje de puntos de la LUT cubiertos por datos | 30–50% es normal (las imágenes naturales solo ocupan una parte del espacio de color) |
diagnostics.lut_clipped_pct_observed | Porcentaje de datos que se comprimen a 0/1 en la zona con datos | > 2% hay que indicar posible pérdida de luces/sombras |
regions.active | Zonas de segmentación realmente activas | Vacío indica que estas zonas no aparecieron en el material del lote |
region_diagnostics.<r>.skipped | Motivo por el que se omitió la zona | too few pixels es normal; no residual learned indica que la zona se comporta igual que la global |
description.summary | Descripción breve del estilo | Directamente para el usuario |
description.region_extra | Qué hizo cada zona adicionalmente respecto al global | Solo cuando hay segmentación |
tags | Tema determinado automáticamente | portrait / landscape / street / indoor / night |
diagnostics.prior_shrink | Intensidad a priori, 1.0 significa uso completo | Cada ajuste prueba 1.0 / 0.5 / 0.2 / 0.05 y selecciona el mejor; prior_trials da sus respectivos dE |
health.monotonic_violations | Número de violaciones de monotonicidad | Por defecto se fuerza a cero; solo aparece si se desactiva con --no-monotonic |
fit_quality.geometry_preserved | Si se conserva la geometría | Debe ser true |
key_match.applied | Si se alineó la exposición de esta imagen con la del estilo | Por defecto off; si está activado, da los stops, gamma y la mediana ajustada |
transfer.verdict | Si el material es referencia de display, log, o incierto | log debe convertirse antes de fit; ambiguous requiere que el usuario confirme la fuente |
warnings[].metric / .note | Problemas detectados por la autocomprobación sin referencia | Transmitir al usuario literalmente la note; un array vacío significa que no se encontraron defectos graves |
look | [original, resultado]: luminancia media, proporción de blancos puros, saturación | Útil para responder '¿qué ha hecho este estilo en esta imagen?' |
input_guard | Si al hacer fit se detectó que la imagen original no es de referencia de display | blocking: false, solo es un aviso, no bloquea |
input_transfer | Si al hacer apply se mezclaron imágenes que no son de display | Da el veredicto por archivo, igual que arriba |
Reglas de Fidelidad Clave
- RAW es una decodificación aproximada. Libraw usa el balance de blancos de la cámara y una matriz de color genérica, no el DCP de la cámara. Un mismo estilo aplicado a diferentes cuerpos de cámara se desviará, y no es el renderizado de Lightroom. Para una entrega precisa, haz que el usuario exporte TIFF de 16 bits.
- El espacio de anclaje del LUT es sRGB gamma 2.2. Al exportar
.cubepara otros, hay que especificarlo; de lo contrario, si se aplica en el espacio incorrecto, todos los colores serán erróneos. - Un estilo con segmentación no puede expresarse con un solo
.cube.vN.cubees solo la capa base global; el mapeo por zonas está envN_<region>.cubey requiere una máscara de combinación. Al exportar, usaexport --full, que proporciona tanto las capas de zona como las instrucciones de conexión. - Los píxeles sobreexpuestos pasan tal cual. Al final del renderizado, los píxeles con el canal más brillante ≥0.97 se mezclan suavemente con la imagen original: en una zona sobreexpuesta no hay información de color, cualquier mapeo es invención. En un contraluz con un disco solar, se ha medido que la segmentación por zonas lo convertía en una mancha gris azulada; con esta protección, se recupera como una fuente de luz blanca cálida. El coste es que las luces altas saturadas de alto brillo ya no se reasignan (en una carta de color de prueba, el ΔE de entrenamiento pasa de 1.15 a 1.21). Para comprimir también las zonas sobreexpuestas, usa
--no-clip-guard. - Las máscaras suaves tienen una zona de transición en el borde, y es intencionado: dos mapeos diferentes que se encuentran bruscamente producirían un contorno visible. El coste es que en la unión del cielo con las copas de los árboles puede haber una franja de unos pocos píxeles de 'semitono'.
- La máscara tiene una resolución de solo 128×128. Las estructuras más finas que aproximadamente 1/128 del encuadre (ramas secas, cables, barandillas finas) se asignan a la zona en la que se encuentran. Estas estructuras suelen ser casi siluetas, y al oscurecerse no se notan; si se requiere precisión a nivel de píxel, usa herramientas de máscara externas.
- Las zonas de baja confianza no se ajustan. Cielos sobreexpuestos por contraluz hasta quedar sin color, o cielos fragmentados por el follaje, el modelo no puede producir una máscara fiable; esas partes se devuelven al LUT global. Mejor ajustar menos que ajustar mal.
- Los primeros planos de animales pueden identificarse como personas. ADE20K clasifica un primer plano de un gato como persona (medido en el 88% del encuadre). Esta skill utiliza un colchón con el 'porcentaje de tono de piel dentro de la región person', y si es inferior al 5%, se anula a cero. Aun así, mascotas de color muy claro pueden escapar.
- El modo
refno debe promocionarse como 'copia exacta'. Solo alinea la distribución Lab; el ΔE medido es de aproximadamente 4; para precisión se necesitafit. - Los valores numéricos de los deslizadores de la capa de descripción son estimaciones. La columna
measuredes reproducible directamente;estimate_lightroomno lo es: no hay una solución única de LUT a deslizadores. - La autocomprobación sin referencia solo detecta 'errores evidentes', no juzga 'si es bonito'. Utiliza objetivos estadísticos clásicos (luminancia media 0.45, punto negro 0.02, tono de piel 20°), pero una foto nocturna debería tener una luminancia media baja, y una puesta de sol debería ser cálida, por lo que cada elemento tiene una banda de tolerancia; la advertencia significa 'échale un vistazo', no 'está mal'. Dos umbrales estrictos: el tono de piel solo se evalúa si la segmentación detecta personas; de lo contrario, una escena callejera cálida podría malinterpretar el 30% del encuadre como piel; el balance de blancos solo se evalúa si hay suficientes píxeles neutros en la escena.
- La detección de log se basa en la forma del histograma, no en los metadatos. La imagen en este punto suele ser ya un PNG o el resultado de libraw, y los EXIF se han perdido. Por lo tanto, se cuentan: elevación del punto negro / luces contenidas / rango dinámico estrecho / baja saturación. Si coinciden tres o más, se diagnostica
log; si coinciden dos,ambiguous. Una imagen final con un estilo desaturado y aplanado puede informarse comoambiguous, es un falso positivo conocido. - El estilo tiene una propiedad de exposición; el LUT solo es fiable en el rango de brillo en el que se aprendió. Un look que eleva la mediana de 0.19 a 0.28, aplicado a una imagen con mediana 0.55, llevará la mediana a 0.95: no es un bug, es que el look en sí no tiene margen para tantas luces. Medido: el ajuste solo observó el rango de la imagen original 0~0.42, p99 original 0.42 / p99 objetivo 0.56; aplicado a una imagen con p99 0.72, la parte que excede se extrapola linealmente, lo que equivale a saturar las luces.
- No alinees la exposición por defecto sin consultar.
--match-keypuede alinear primero la mediana de la imagen al nivel aprendido por el estilo y luego aplicar el LUT, pero en contraluces tiene el efecto contrario: en una foto real con contraluz (cara 0.27, fondo 0.62), al alinear con la mediana de toda la imagen, la cara se reduce a 0.08, dejando al sujeto completamente negro. Por eso está desactivado por defecto; solo debe activarse cuando en una ráfaga con la misma relación de luz y el mismo encuadre, la exposición no es coherente. El criterio es simple: ¿la relación entre el brillo del sujeto y el del fondo en esta imagen es la misma que en la imagen de referencia? - Cuando en un fotograma el sujeto es más oscuro que el fondo, y en el fotograma de referencia el sujeto es lo más brillante, ningún mapeo global monótono puede complacer a ambos. Para satisfacer ambos, solo es posible un ajuste local. En este caso, prioriza al sujeto y acepta que el fondo se aclare; si se prioriza el fondo, la cara se aplastará.
- Una dominante cálida existente en el material se combinará con un look cálido, produciendo un tono naranja rojizo. Una foto de retrato con iluminación tenue y una dominante cálida de 0.46 stops, al aplicarle un look cálido, la cara pasa de marrón cálido a naranja rojizo, el cast se dispara a 0.77 y aparecen manchas en las mejillas. En este caso, no neutralices primero el balance de blancos: el único 'color neutro' de la escena suele ser la pared iluminada por la misma luz; si se corrige según ella, la pared se volverá verde y el tono de la piel, amarillo verdoso (medido probando 0.5/0.75 en dos ocasiones, ambos peores). El orden correcto es: primero reducir
--strength(a unos 0.6); si aún no satisface, abandona la aplicación de color y opta por una curva de luminancia pura o una ganancia lineal de exposición: solo se modifica el brillo, no el tono, y el look cálido se conserva tal cual. - Cada vez que se guarda un JPEG, se pierde un poco, y es irreversible. Entrega con q98 de una sola vez; si hay que volver a editarlo, hazlo desde
__master.png, no sobrescribas el JPEG ya guardado. Guardar el mismo LUT tres veces con q95: el PSNR pasa de 48.7 a 48.6, no parece mucho, pero cada vez se acumula un escalón en los degradados suaves (cielos realzados, fondos desenfocados). Para determinar quién perdió la calidad: si la imagen original ya estaba comprimida (<0.10 bytes/píxel), la culpa es de la fuente; no busques la causa en los parámetros de codificación.
Línea Base de Rendimiento
| Escenario | Tiempo para imagen de 10.8 MP |
|---|---|
| Renderizado puramente global | 3.6 s |
| Renderizado con segmentación (2 zonas activas) | 6.7 s |
| Inferencia de segmentación (512px) + sobremuestreo | 0.7 s |
Un fit (10 pares, con segmentación y etiquetado) | Aprox. 12 s |
El cuello de botella está en la recolección píxel a píxel del LUT (8 lecturas aleatorias × píxel), no en las ramas del algoritmo. En procesamiento por lotes, lo más rentable es paralelizar por imagen con multiproceso.
Tareas Comunes
| Lo que dice el usuario | Qué hacer |
|---|---|
| 'Ajusta el tono de mis fotos según esta referencia' | Generar estilo con ref → apply por lotes |
| 'Estas ya están retocadas, quiero este estilo a partir de ahora' | fit --src original --tgt retocada → apply |
| 'He retocado otro lote, también con este estilo' | Al hacer fit con --name apuntando al mismo estilo, se fusiona y reajusta automáticamente |
| 'Solo oscurece el cielo, no toques el suelo ni la cara' | La segmentación ya está activada por defecto; verifica que regions.active incluya sky |
| 'Este estilo es demasiado intenso' | apply --strength 0.6, sin necesidad de reajustar |
| 'Esta versión es peor que la anterior' | apply --version <número de versión anterior> |
| 'El lote anterior se hizo mal, deshacerlo' | Encontrar batch_id con batches → undo |
| '¿Qué estilo debería usar para este lote?' | apply --style auto, ver auto.ranking |
| '¿Qué ha cambiado realmente este estilo?' | styles --name X, leer description.summary |
| 'Exportar para DaVinci Resolve' | export --full; recordar anclar a sRGB gamma 2.2, las capas de zona necesitan máscaras suaves |
| 'Contraluz/amanecer, que el sol no se vuelva gris' | Por defecto ya está protegido (protección de sobreexposición); si aún se ve gris, verificar si el sol está realmente sobreexpuesto |
| '¿Está bien ajustada esta foto? ¿Dónde está el problema?' | check --in X --person, transmitir las warnings una por una, sin añadir juicios propios |
| '¿Es este material log?' | check --in X, ver transfer.verdict y transfer.signals |
| '¿Quemará las luces al aplicar este estilo?' | apply ya da la comparación antes/después en look; warnings lo señalará directamente |
| 'Esta imagen es mucho más brillante que la referencia; al aplicar el estilo queda pálida' | Primero ver la diferencia en look.median_luma; reducir --strength, o en ráfagas con la misma relación de luz usar --match-key on |
| 'El sujeto es más oscuro que el fondo; al aplicar el estilo, el sujeto queda negro' | No activar --match-key; la relación de luz entre la referencia y esta imagen es diferente, solo se puede ajustar localmente |
| 'Esta imagen ya tiene una dominante cálida; al aplicar el estilo, la cara se vuelve naranja' | Primero reducir --strength a 0.6; no neutralizar el balance de blancos (volvería la pared verde), ver regla de fidelidad 16 |
Referencias
references/style-schema.md: Estructura de directorios de estilos, campos destyle.json, semántica de versiones y lotesreferences/color-pipeline.md: Gestión del color, detalles del algoritmo, limitaciones conocidas y sugerencias de ajuste