J'ai commencé ce projet en créant une collection de 13 000 images IA de 512 × 512 pixels. L'étape suivante consiste à charger des métadonnées sur ces images dans un tableau de données. Plus tard, je sélectionnerai des images aux couleurs similaires pour remplacer des parties d'une image agrandie. Voici à quoi ressemble le tableau :
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.Les valeurs H, L, S et R, G, B correspondent à la couleur de l'échantillon. La couleur doit correspondre à une couleur présente dans l'échantillon.
Cela charge les noms de fichiers, mais pas encore les images.
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>Pour extraire une zone de couleur de l'image d'exemple, utilisez HLS plutôt que RGB afin de regrouper les teintes similaires et de les classer en 32 teintes distinctes. Chargez chaque image et placez l'image réduite dans la colonne « exemple » (à des fins de débogage uniquement). Dans le code suivant, `{hue, lite, sat} = Color To HLS( img << getpixels );` stocke une matrice 512x512 dans les variables `hue`, `sat` et `lite`. Nous effectuerons des calculs matriciels élément par élément sur ces matrices.</P> <P>L'un des problèmes du format HLS est que la teinte (H), que l'on pourrait assimiler à la couleur, est ignorée pour les nuances de gris, y compris le noir et le blanc. J'ai donc choisi d'utiliser H=1,0 pour toute couleur proche du gris afin de les regrouper. Si la couleur dominante est un jaune foncé, un bleu foncé, etc., elle sera classée comme noire.</P> <P>Après avoir regroupé les valeurs H, L, S comprises entre zéro et un (multipliées par nbuckets (32) et arrondies à l'entier inférieur), une instruction crée un tableau nommé total, initialement rempli de zéros. Ce tableau est ensuite initialisé à l'aide de l'opérateur ++ et d'un indice complexe. Plus tard, lorsque LocMax récupère l'indice du plus grand total, cet indice peut être reconverti en valeurs H, L, S pour déterminer la couleur correspondante. (À l'origine, je pensais qu'il y aurait plus de 32 × 32 × 32 classes et que l'optimisation de l'espace était peut-être plus importante.)</P> <P>La boucle while-keeplooking tente de trouver une couleur non grise avec au moins 100 pixels dans l'image ; si cela n'est pas possible, la plus grande zone grise regroupée est utilisée.</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 ) ) ) );
Une KDTable trouve les voisins les plus proches dans un espace multidimensionnel. J'utilise ici 4 dimensions ; la dimension supplémentaire crée trois ensembles d'images qui restent presque toujours séparés.
kdlookup = KDTable( dt[0, {r, g, b, fold123}] ); Viennent ensuite quelques constantes, des tableaux de pixels et une image de départ.
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> Voici la fonction principale qui trouve des images bitmap similaires. À l'origine, la « petite » image bitmap était beaucoup plus petite que la « grande », mais elles ont maintenant la même taille afin de conserver l'alignement des pixels. Cette fonction crée une nouvelle grande image bitmap composée d'images de 512 × 512 pixels dont la couleur est similaire à la couleur moyenne de la même zone dans la petite image bitmap. Les deux images seront ensuite fusionnées et mises à l'échelle pour créer une image vidéo.
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 Avant de lancer la boucle de génération d'images, il faut définir quelques constantes supplémentaires, dont une calculée en ajustant certains paramètres. J'ai mis un certain temps à comprendre la valeur de 12,8 : elle permet de maintenir une vitesse de zoom constante. À mesure que le zoom sur les images bitmap fusionnées (grande et petite) progresse, de moins en moins de pixels doivent être supprimés sur les bords pour obtenir le même facteur de zoom. 12,8 représente le rapport entre les facteurs de zoom initial et 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)
Enfin, la boucle qui génère les images. Il s'agit d'une boucle imbriquée ; la boucle externe appelle la fonction `bigFromSmall` pour préparer la boucle interne à créer 360 nouvelles images. La boucle interne extrait des zones de pixels de plus en plus petites des deux bitmaps (grande et petite), les redimensionne, les fusionne et enregistre les images fusionnées sur le disque.
// 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 );
); Une fois les images créées, l'éditeur non linéaire de Blender peut les charger et générer une vidéo. J'ai opté pour 60 images par seconde plutôt qu'une résolution spatiale plus élevée car les grandes images bitmap ci-dessus sont déjà à la limite de la taille excessive.
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 est un logiciel libre ; je l'utilise sous Linux. Il existe, je crois, des versions pour Windows et Mac. Son apprentissage est assez long.
C'est terminé. J'aime bien les transitions ; le choix des couleurs et leur mélange sont réussis.
Si vous êtes de retour au bureau, vous voudrez peut-être baisser un peu le volume avant de commencer.