Comencé este proyecto creando una colección de 13 000 imágenes de IA de 512x512 píxeles. El siguiente paso es cargar metadatos sobre las imágenes en una tabla de datos. Posteriormente, seleccionaré imágenes con colores similares para reemplazar partes de una imagen ampliada. La tabla tiene el siguiente aspecto:
The H,L,S and R,G,B values match the swatch color. The color should match some color in the sample.The H,L,S and R,G,B values match the swatch color. The color should match some color in the sample.The H,L,S and R,G,B values match the swatch color. The color should match some color in the sample.Los valores H, L, S y R, G, B coinciden con el color de la muestra. El color debe coincidir con algún color de la muestra.
Esto carga los nombres de los archivos, pero aún no las imágenes.
imageDir = "\\VBOXSVR\images\"; // ~13,000 512x512 AI generated images
files = Files In Directory( imageDir );
nf = N Items( files );
nfolds = 3; // the images are divided up into 3 sets to make sure the same images don't appear too soon
dt = New Table( "lookup",
Add Rows( nf ),
Set Cell Height( 51 ),
New Column( "filename", Character, setvalues( files ), Set Display Width( 356 ) ),
New Column( "wide" ),
New Column( "tall" ),
New Column( "h_" ), // several nearest neighbor techniques tried;
New Column( "l_" ), // the video uses HLS to find similar colors
New Column( "s_" ),
New Column( "r" ),
New Column( "g" ),
New Column( "b" ),
New Column( "sample", Expression, "None", Set Display Width( 85 ) ),
New Column( "swatch", Expression, "None", Set Display Width( 81 ) ),
New Column( "fold123", formula( Mod( Row(), nfolds ) ) ),
New Column( "nUsed", Numeric, "Nominal", <
<SET each="" value=""></SET>
<P>Para obtener una muestra de color de la imagen de muestra, utilice HLS en lugar de RGB para agrupar tonos similares y clasifíquelos en 32 tonos distintos. Cargue cada imagen y coloque la imagen de tamaño reducido en la columna de muestra (solo para depuración). En el siguiente código, `{hue, lite, sat} = Color To HLS( img << getpixels );` almacena una matriz de 512x512 en las variables `hue`, `sat` y `lite`. Realizaremos operaciones matriciales elemento a elemento con ellas.</P> <P>Uno de los problemas con HLS es que la tonalidad (H, lo que podríamos considerar como color) se ignora para los tonos de gris, incluidos el blanco y el negro. He optado por usar H=1.0 para cualquier color cercano al gris, de modo que se agrupen juntos. Si la mancha de color dominante es un amarillo oscuro, azul, etc., se clasificará como negro.</P> <P>Tras agrupar los valores H, L, S de cero a uno (multiplicando por nbuckets (32) y redondeando el resultado hacia abajo), se crea un array llamado `total`, lleno de ceros. A continuación, se inicializa usando el operador `++` y un subíndice complejo. Posteriormente, cuando `LocMax` recupera el índice del valor `total` más alto, este se puede invertir para obtener los valores H, L, S y determinar el color correspondiente. (Originalmente pensé que habría más de 32*32*32 contenedores y que la optimización del espacio sería más importante).</P> <P>El bucle while-keeplooking intenta encontrar un color que no sea de escala de grises con al menos 100 píxeles en la imagen; si no es posible, se utiliza el parche más grande de gris agrupado.</P> <CODE class=" language-jsl">nbuckets = 32; // the HLS values are binned into 32 ranges to find similar colors
nbucketsPlus = nbuckets + 1;
For Each Row(
dt,
path = imageDir || dt:filename;
img = New Image( path );
{dt:wide, dt:tall} = img << size;
{hue, lite, sat} = Color To HLS( img << getpixels );
img << scale( .125 );
dt:sample = img; // for debugging, mostly, keep a small copy in the datatable
If( Any( hue >= 1 ),
Show( Row(), Loc( hue >= 1 ) );
Throw( "bad assumptions that hue<1" );
); // use to flag gray scale colors...
// unsaturated is gray; force the hue to 1. Real hues are 0 to .9999
// similar for very low or high lightness
hue[Loc( sat < 0.4 )] = 1; // gray if unsaturated, may be redundant with one of...
hue[Loc( lite < 0.3 )] = 1; // black if low lightness
hue[Loc( lite > 0.7 )] = 1; // white if high lightness
// binning
hue = Floor( hue * nbuckets );//show(hue) 0..8 inclusive
lite = Floor( lite * nbuckets );//show(lite) 0..8 inclusive
sat = Floor( sat * nbuckets );//show(sat) 0..8 inclusive
total = J( nbucketsPlus * nbucketsPlus * nbucketsPlus, 1, 0 );
// HLS is encoded into the subscript for total[]... decoded further below...
total[((nbucketsPlus ^ 2) * hue + nbucketsPlus * lite + sat + 1)[0]]++;
// the HLS chosen for this picture is the largest patch of a
// binned color that is colorful, or gray if no color seems to stand out...
keeplooking = 1;
firstidx = 0;
While( keeplooking,
idxPlus = Loc Max( total ); // a large bin. Often the first one is not a colorful one.
idx = idxPlus - 1;
If( firstidx == 0,
firstidx = idx // But the first one is the gray level if all are non colorful
);
// arbitrarily, there must be at least 100 colorful pixels of the same binned color,
// or use gray...
If( total[idxPlus] < 100, // no color found, use the original gray and the hue will be 1
Write( "\!nOriginal gray" );
dt:h_ = (Floor( firstidx / (nbucketsPlus ^ 2) ) + .5) / nbuckets;
If( dt:h_ < 1,
Throw( "hue?" )
);
firstidx = Mod( firstidx, (nbucketsPlus ^ 2) );
dt:l_ = (Floor( firstidx / (nbucketsPlus) ) + .5) / nbuckets;
firstidx = Mod( firstidx, (nbucketsPlus) );
dt:s_ = (firstidx + .5) / nbuckets;
keeplooking = 0;//
, //
dt:h_ = Floor( idx / (nbucketsPlus ^ 2) ); // decode HLS uses 0-based idx
If( dt:h_ == nbuckets,
//Write( "\!nignore gray" );
total[idxPlus] = 0;// try another until get a color
, //
dt:h_ = (dt:h_ + .5) / nbuckets; // 0 to 1 recovered from binned index
Write( "\!ngot color" );
idx = Mod( idx, (nbucketsPlus ^ 2) );
dt:l_ = (Floor( idx / (nbucketsPlus) ) + .5) / nbuckets;
idx = Mod( idx, (nbucketsPlus) );
dt:s_ = (idx + .5) / nbuckets;
keeplooking = 0;
);
);
);
If( dt:h_ >= 1,
dt:h_ = 1;
dt:s_ = 0;
);
color = HLS Color( dt:h_, dt:l_, dt:s_ );
{dt:r, dt:g, dt:b} = Color To RGB( color ); // later use the RGB equivalent for kdtable nearest neighbor
dt:swatch = New Image( J( 16, 16, color ) ); // for debugging, need to see a swatch of the chosen color
If( Mod( Row(), 10 ) == 0,
dt << gotorow( Row() );
Wait( 0 );
);
);
For Each Row( dt, Row State( dt, Row() ) = Combine States( Marker State( 12 ), Color State( RGB Color( r, g, b ) ) ) );
Una KDTable encuentra los vecinos más cercanos en un espacio multidimensional. Aquí estoy usando 4 dimensiones; la dimensión adicional crea tres conjuntos de imágenes que casi siempre permanecen separados.
kdlookup = KDTable( dt[0, {r, g, b, fold123}] ); A continuación se muestran algunas constantes, algunas matrices de píxeles y una imagen inicial.
VideoRows = 1080;
VideoCols = 1920;
VideoSize = Eval List( List( videocols, videorows ) );
FudgeScale = 1.00001; // fix rounding error in <
<SCALE --="" fractional="" scales="" truncate="" big="" and="" small="" are="" both="" actually="" the="" size="" so="" they="" line="" up="">
1e-14, Throw( "xxx", traceback ) ); upscale = N Rows( BigBitmap ) / nrows; ptemp << scale( FudgeScale * upscale ); {ncols, nrows} = ptemp << size; If( ncols != N Cols( BigBitmap ) | nrows != N Rows( BigBitmap ), Throw( Char( ncols ) || " " || Char( nrows ), traceback ) ); // small is initialized to the start/final image which was just upscaled SmlBitmap[0, 0] = -(ptemp << getpixels); // the normally negative jmp colors are kept as positive in big and small ptemp = 0;//release
</SCALE> Esta es la función principal que encuentra mapas de bits similares. Originalmente, el mapa de bits "pequeño" era mucho más pequeño que el "grande", pero ahora ambos tienen el mismo tamaño para mantener los píxeles alineados. Esta función crea un nuevo mapa de bits grande con imágenes de 512x512 píxeles, seleccionadas para tener un color similar al color promedio de la misma área en el mapa de bits pequeño. Posteriormente, los mapas grande y pequeño se combinarán y escalarán para crear un fotograma de vídeo.
bigFromSml = Function( {fold}, // makes a new big from the 13K 512x512 choices that aligns with small
{irow, icol, patch, red, green, blue, rows, dists, path, img},
For( irow = 0, irow < N Rows( SmlBitmap ), irow += 512,
Show( irow );
Wait( 0 );
srcrow = Floor( irow ) + 1 :: Floor( irow ) + 512;
For( icol = 0, icol < N Cols( SmlBitmap ), icol += 512,
srccol = Floor( icol ) + 1 :: Floor( icol ) + 512;
patch = SmlBitmap[srcrow, srccol]; // carve out a patch of 512x512 pixels from small
blue = Mod( patch, 256 );
green = Mod( patch -= blue, 256 * 256 );
red = Mod( patch -= green, 256 * 256 * 256 );
// 0..255
blue = Mean( blue );
green = Mean( green ) / 256;
red = Mean( red ) / (256 * 256);
// lookup a bitmap with similar color
nchoices = 5; // pick one of these nearest choices
{rows, dists} = kdlookup << KNearestRows( nchoices, red / 255 || green / 255 || blue / 255 || fold );
If( Mean( dists ) > 1.1,
// if the lookup is returning far-away answers, recycle old pictures
Show( kdlookup << insertrows( 1 :: N Rows( dt ) ) )
);
choice = rows[Random Integer( 1, N Items( rows ) )]; // randomly pick one of the 5 best choices
dt:nUsed[choice] += 10 ^ (4 * fold);// debugging
kdlookup << removeRows( choice );// don't reuse this picture until recycled
path = imageDir || dt:filename[choice];
img = Try(
New Image( path )// during development I might delete a file while this is running
, //
Show( path, choice, exception_msg );
choice = 1;
kdlookup << insertrows( choice );
New Image( imageDir || dt:filename[choice] ); // see if row 1 can keep it going
);
{c, r} = img << size;
img << scale( FudgeScale * 512 / c, FudgeScale * 512 / r ); // a few images need fixups
BigBitmap[srcrow, srccol] = -(img << getpixels); // same location for small and big. store positive jmpcolor
);
)
);
fold = 0;
bigFromSml( fold ); // make the original big before the loop, using the small image Antes de ejecutar el bucle de generación de imágenes, se definieron un par de constantes adicionales, una de ellas calculada mediante el ajuste de algunos valores. Comprender el valor de 12.8 llevó un tiempo; este hace que la velocidad de zoom parezca constante. A medida que avanza el zoom en los mapas de bits combinados (grandes y pequeños), se eliminan cada vez menos píxeles en los bordes para obtener el mismo porcentaje de zoom. 12.8 es la relación entre los factores de zoom inicial y final.
outdir = "\\VBOXSVR\nfsc\deeppic\"; // the video uses a part1 and part2 sequence
// maxrows is the maximum rows that can be removed from the top and bottom without needing to stretch the middle to the VideoRows
maxrows = (N Rows( SmlBitmap ) - VideoRows) / 2;
countmaxrows = 0;
startrows = 48.705; // 48.705 hand picked for 360 frames...6 second transitions
endrows = startrows / (N Rows( SmlBitmap ) / VideoRows);// 12.8 is ratio of start image size to end image size
For( rowReduce = 1, rowReduce <= maxrows, rowReduce += Interpolate( rowReduce, 0, startrows, maxrows, endrows ),
countmaxrows += 1
);
Show( 361, countmaxrows, startrows, endrows );// targeting 180, no, lets use 360 at 60FPS, frames (181-1) (361-1)
Finalmente, el bucle que genera las imágenes. Se trata de un bucle anidado; el bucle externo llama a la función `bigFromSmall` para preparar el bucle interno y generar otras 360 imágenes. El bucle interno toma porciones de píxeles cada vez más pequeñas de los mapas de bits grandes y pequeños, las escala, las combina y guarda las imágenes resultantes en el disco.
// 1e12, etc for naming the output files, this loop is not intended to finish; interupt it when there are enough frames
For( iround = 1e12, iround <= 9e12, iround += 1e6,
Show( iround );
Wait( 0.01 );
pictureNumber = 0;
For( rowReduce = 0, rowReduce <= maxrows, rowReduce += Interpolate( rowReduce, 0, startrows, maxrows, endrows ),
If( rowReduce == 0 & iround != 1e12,
Continue()
);// keep first, later it is an expensive duplicate
colReduce = 16 * rowReduce / 9;
// each image is a blend of small and big; the slice from small and big is combined
// and scaled to the video size. the slices get smaller and smaller and need to be scaled
// down less and less until the middle slice needs a scale of 1:1
start = HP Time();
smlPatch = SmlBitmap[Round( rowReduce ) + 1 :: N Rows( SmlBitmap ) - Round( rowReduce ), Round( colReduce ) + 1 :: N Cols( SmlBitmap ) - Round( colReduce )];
smlImg = New Image( -smlPatch );
{ncols, nrows} = smlImg << size;
smlImg << scale( FudgeScale * VideoCols / ncols, FudgeScale * VideoRows / nrows );
If( smlImg << size != VideoSize,
Throw( Char( smlImg << size ), traceback )
);
{smlred, smlgreen, smlblue} = smlImg << getpixels( "rgb" );
bigPatch = BigBitmap[Round( rowReduce ) + 1 :: N Rows( SmlBitmap ) - Round( rowReduce ), Round( colReduce ) + 1 :: N Cols( SmlBitmap ) - Round( colReduce )];
bigImg = New Image( -bigPatch );
{ncols, nrows} = bigImg << size;
bigImg << scale( FudgeScale * VideoCols / ncols, FudgeScale * VideoRows / nrows );
If( bigImg << size != VideoSize,
Throw( Char( bigImg << size ), traceback )
);
{bigred, biggreen, bigblue} = bigImg << getpixels( "rgb" );
stop = HP Time();
// blend and save the scaled slices
alpha = 1 - (pictureNumber / countmaxrows);
beta = 1 - alpha;
combine = New Image( "rgb", {bigred * beta + smlred * alpha, biggreen * beta + smlgreen * alpha, bigblue * beta + smlblue * alpha} );
combine << savepicture( outdir || Char( iround + (pictureNumber += 1) ) || ".jpg", "jpg" );
Write( "\!n" || Char( (stop - start) / 1e6, 6 ) || " " || Char( iround - 1e12 ) || " " || Char( pictureNumber ) );
Wait( 0.01 );
);
// combine becomes the new smlBitmap
combine << scale( FudgeScale * upscale );
{ncols, nrows} = combine << size;
If( ncols != N Cols( BigBitmap ) | nrows != N Rows( BigBitmap ),
Throw( Char( ncols ) || " " || Char( nrows ), traceback )
);
SmlBitmap[0, 0] = -(combine << getpixels);
combine = 0;//release
// and make a new big bitmap from small
fold = Mod( fold + 1, nfolds );
bigFromSml( fold );
); Una vez creadas las imágenes, el editor no lineal de Blender puede cargarlas y generar un vídeo. Opté por 60 FPS en lugar de una mayor resolución espacial porque los mapas de bits "grandes" mencionados anteriormente ya están al límite de ser demasiado grandes.
Blender is open source; I use it on Linux. I think Win and Mac versions are available. It has a long learning curve.Blender is open source; I use it on Linux. I think Win and Mac versions are available. It has a long learning curve.Blender is open source; I use it on Linux. I think Win and Mac versions are available. It has a long learning curve.Blender es de código abierto; yo lo uso en Linux. Creo que también hay versiones para Windows y Mac. Tiene una curva de aprendizaje larga.
¡Listo! Me gustan las transiciones; la elección de los colores y la mezcla funcionan bien.
Si ya estás de vuelta en la oficina, quizá quieras bajar un poco el volumen antes de empezar.