J'ai une application console qui gère les images. Maintenant, j'ai besoin de quelque chose comme un aperçu des images dans l'application console. Est-il possible de les afficher dans la console?
Voici une comparaison des réponses actuelles basées sur les caractères:
Contribution:
Sortie:
J'ai ensuite joué avec le code de @DieterMeemken. J'ai réduit de moitié la résolution verticale et ajouté le dithering via °. Le résultat de Dieter Meemken est à gauche, à droite ma. En bas, l’image originale est redimensionnée pour correspondre grossièrement à la sortie. Bien que la fonction de conversion de Malwyns soit impressionnante, elle n’utilise pas toutes les couleurs grises, ce qui est dommage.
static int[] cColors = { 0x000000, 0x000080, 0x008000, 0x008080, 0x800000, 0x800080, 0x808000, 0xC0C0C0, 0x808080, 0x0000FF, 0x00FF00, 0x00FFFF, 0xFF0000, 0xFF00FF, 0xFFFF00, 0xFFFFFF };
public static void ConsoleWritePixel(Color cValue)
{
Color[] cTable = cColors.Select(x => Color.FromArgb(x)).ToArray();
char[] rList = new char[] { (char)9617, (char)9618, (char)9619, (char)9608 }; // 1/4, 2/4, 3/4, 4/4
int[] bestHit = new int[] { 0, 0, 4, int.MaxValue }; //ForeColor, BackColor, Symbol, Score
for (int rChar = rList.Length; rChar > 0; rChar--)
{
for (int cFore = 0; cFore < cTable.Length; cFore++)
{
for (int cBack = 0; cBack < cTable.Length; cBack++)
{
int R = (cTable[cFore].R * rChar + cTable[cBack].R * (rList.Length - rChar)) / rList.Length;
int G = (cTable[cFore].G * rChar + cTable[cBack].G * (rList.Length - rChar)) / rList.Length;
int B = (cTable[cFore].B * rChar + cTable[cBack].B * (rList.Length - rChar)) / rList.Length;
int iScore = (cValue.R - R) * (cValue.R - R) + (cValue.G - G) * (cValue.G - G) + (cValue.B - B) * (cValue.B - B);
if (!(rChar > 1 && rChar < 4 && iScore > 50000)) // rule out too weird combinations
{
if (iScore < bestHit[3])
{
bestHit[3] = iScore; //Score
bestHit[0] = cFore; //ForeColor
bestHit[1] = cBack; //BackColor
bestHit[2] = rChar; //Symbol
}
}
}
}
}
Console.ForegroundColor = (ConsoleColor)bestHit[0];
Console.BackgroundColor = (ConsoleColor)bestHit[1];
Console.Write(rList[bestHit[2] - 1]);
}
public static void ConsoleWriteImage(Bitmap source)
{
int sMax = 39;
decimal percent = Math.Min(decimal.Divide(sMax, source.Width), decimal.Divide(sMax, source.Height));
Size dSize = new Size((int)(source.Width * percent), (int)(source.Height * percent));
Bitmap bmpMax = new Bitmap(source, dSize.Width * 2, dSize.Height);
for (int i = 0; i < dSize.Height; i++)
{
for (int j = 0; j < dSize.Width; j++)
{
ConsoleWritePixel(bmpMax.GetPixel(j * 2, i));
ConsoleWritePixel(bmpMax.GetPixel(j * 2 + 1, i));
}
System.Console.WriteLine();
}
Console.ResetColor();
}
usage:
Bitmap bmpSrc = new Bitmap(@"HuwnC.gif", true);
ConsoleWriteImage(bmpSrc);
[~ # ~] éditer [~ # ~]
La distance de couleur est un sujet complexe ( ici , ici et des liens sur ces pages ...). J'ai essayé de calculer la distance en YUV et les résultats étaient plutôt pires qu'en RVB. Ils pourraient être meilleurs avec Lab et DeltaE, mais je n’ai pas essayé cela. La distance en RVB semble être suffisante. En fait, les résultats sont très similaires pour l’espace colorimétrique RVB euclidien et manhattan. Je suppose donc qu’il ya trop peu de couleurs à choisir.
Le reste consiste simplement à comparer la force brute de la couleur à toutes les combinaisons de couleurs et de motifs (= symboles). J'ai indiqué que le taux de remplissage pour ▒▓█ = 1/4, 2/4, 3/4 et 4/4. Dans ce cas, le troisième symbole est en fait redondant au premier. Mais si les ratios n'étaient pas aussi uniformes (dépend de la police de caractères), les résultats pourraient changer, alors je les ai laissés là pour des améliorations futures. La couleur moyenne du symbole est calculée comme la moyenne pondérée de foregroudColor et backgroundColor en fonction du taux de remplissage. Il suppose des couleurs linéaires, ce qui est également une grande simplification. Il y a donc encore place à amélioration.
Bien que l'affichage d'une image dans une console ne soit pas l'utilisation prévue de la console, vous pouvez sûrement pirater les choses, car la fenêtre de la console n'est qu'une fenêtre, comme toute autre fenêtre.
En fait, une fois que j'ai commencé à développer une bibliothèque de contrôles de texte pour les applications console avec support graphique. Je n'ai jamais fini cela, bien que ma démonstration de démonstration de principe fonctionne:
Et si vous obtenez la taille de police de la console, vous pouvez placer l’image très précisément.
Voici comment vous pouvez le faire:
static void Main(string[] args)
{
Console.WriteLine("Graphics in console window!");
Point location = new Point(10, 10);
Size imageSize = new Size(20, 10); // desired image size in characters
// draw some placeholders
Console.SetCursorPosition(location.X - 1, location.Y);
Console.Write(">");
Console.SetCursorPosition(location.X + imageSize.Width, location.Y);
Console.Write("<");
Console.SetCursorPosition(location.X - 1, location.Y + imageSize.Height - 1);
Console.Write(">");
Console.SetCursorPosition(location.X + imageSize.Width, location.Y + imageSize.Height - 1);
Console.WriteLine("<");
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPictures), @"Sample Pictures\tulips.jpg");
using (Graphics g = Graphics.FromHwnd(GetConsoleWindow()))
{
using (Image image = Image.FromFile(path))
{
Size fontSize = GetConsoleFontSize();
// translating the character positions to pixels
Rectangle imageRect = new Rectangle(
location.X * fontSize.Width,
location.Y * fontSize.Height,
imageSize.Width * fontSize.Width,
imageSize.Height * fontSize.Height);
g.DrawImage(image, imageRect);
}
}
}
Voici comment vous pouvez obtenir la taille de police actuelle de la console:
private static Size GetConsoleFontSize()
{
// getting the console out buffer handle
IntPtr outHandle = CreateFile("CONOUT$", GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
IntPtr.Zero,
OPEN_EXISTING,
0,
IntPtr.Zero);
int errorCode = Marshal.GetLastWin32Error();
if (outHandle.ToInt32() == INVALID_HANDLE_VALUE)
{
throw new IOException("Unable to open CONOUT$", errorCode);
}
ConsoleFontInfo cfi = new ConsoleFontInfo();
if (!GetCurrentConsoleFont(outHandle, false, cfi))
{
throw new InvalidOperationException("Unable to get font information.");
}
return new Size(cfi.dwFontSize.X, cfi.dwFontSize.Y);
}
Et les appels, constantes et types WinApi supplémentaires requis:
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetConsoleWindow();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr CreateFile(
string lpFileName,
int dwDesiredAccess,
int dwShareMode,
IntPtr lpSecurityAttributes,
int dwCreationDisposition,
int dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetCurrentConsoleFont(
IntPtr hConsoleOutput,
bool bMaximumWindow,
[Out][MarshalAs(UnmanagedType.LPStruct)]ConsoleFontInfo lpConsoleCurrentFont);
[StructLayout(LayoutKind.Sequential)]
internal class ConsoleFontInfo
{
internal int nFont;
internal Coord dwFontSize;
}
[StructLayout(LayoutKind.Explicit)]
internal struct Coord
{
[FieldOffset(0)]
internal short X;
[FieldOffset(2)]
internal short Y;
}
private const int GENERIC_READ = unchecked((int)0x80000000);
private const int GENERIC_WRITE = 0x40000000;
private const int FILE_SHARE_READ = 1;
private const int FILE_SHARE_WRITE = 2;
private const int INVALID_HANDLE_VALUE = -1;
private const int OPEN_EXISTING = 3;
Et le résultat:
[
Si vous utilisez deux fois ASCII 219 (), vous obtenez un pixel (). Vous êtes maintenant limité par le nombre de pixels et le nombre de couleurs de votre application console.
si vous conservez les paramètres par défaut, vous avez environ 39 x 39 pixels. Si vous en voulez plus, vous pouvez redimensionner votre console avec Console.WindowHeight = resSize.Height + 1;
et Console.WindowWidth = resultSize.Width * 2;
vous devez garder le format de l'image le plus loin possible, pour ne pas avoir 39x39 dans la plupart des cas
Malwyn a publié une méthode totalement sous-estimée pour convertir System.Drawing.Color
à System.ConsoleColor
donc mon approche serait
using System.Drawing;
public static void ConsoleWriteImage(Bitmap bmpSrc)
{
int sMax = 39;
decimal percent = Math.Min(decimal.Divide(sMax, bmpSrc.Width), decimal.Divide(sMax, bmpSrc.Height));
Size resSize = new Size((int)(bmpSrc.Width * percent), (int)(bmpSrc.Height * percent));
Func<System.Drawing.Color, int> ToConsoleColor = c =>
{
int index = (c.R > 128 | c.G > 128 | c.B > 128) ? 8 : 0;
index |= (c.R > 64) ? 4 : 0;
index |= (c.G > 64) ? 2 : 0;
index |= (c.B > 64) ? 1 : 0;
return index;
};
Bitmap bmpMin = new Bitmap(bmpSrc, resSize);
for (int i = 0; i < resSize.Height; i++)
{
for (int j = 0; j < resSize.Width; j++)
{
Console.ForegroundColor = (ConsoleColor)ToConsoleColor(bmpMin.GetPixel(j, i));
Console.Write("██");
}
System.Console.WriteLine();
}
}
afin que vous puissiez
ConsoleWriteImage(new Bitmap(@"C:\image.gif"));
échantillon d'entrée:
exemple de sortie:
c'était amusant. Merci fubo , j’ai essayé votre solution et j’ai pu augmenter la résolution de l’aperçu de 4 (2x2).
J'ai trouvé que vous pouvez définir la couleur d'arrière-plan pour chaque personnage. Ainsi, au lieu d’utiliser deux ASCII 219 caractères (), j’ai utilisé ASCII 223 () deux fois avec des couleurs d’avant-plan et d’arrière-plan différentes. Cela divise le gros pixel (██) en 4 sous-pixels comme celui-ci ().
Dans cet exemple, je mets les deux images l'une à côté de l'autre pour que vous puissiez voir facilement la différence:
Voici le code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
namespace ConsoleWithImage
{
class Program
{
public static void ConsoleWriteImage(Bitmap bmpSrc)
{
int sMax = 39;
decimal percent = Math.Min(decimal.Divide(sMax, bmpSrc.Width), decimal.Divide(sMax, bmpSrc.Height));
Size resSize = new Size((int)(bmpSrc.Width * percent), (int)(bmpSrc.Height * percent));
Func<System.Drawing.Color, int> ToConsoleColor = c =>
{
int index = (c.R > 128 | c.G > 128 | c.B > 128) ? 8 : 0;
index |= (c.R > 64) ? 4 : 0;
index |= (c.G > 64) ? 2 : 0;
index |= (c.B > 64) ? 1 : 0;
return index;
};
Bitmap bmpMin = new Bitmap(bmpSrc, resSize.Width, resSize.Height);
Bitmap bmpMax = new Bitmap(bmpSrc, resSize.Width * 2, resSize.Height * 2);
for (int i = 0; i < resSize.Height; i++)
{
for (int j = 0; j < resSize.Width; j++)
{
Console.ForegroundColor = (ConsoleColor)ToConsoleColor(bmpMin.GetPixel(j, i));
Console.Write("██");
}
Console.BackgroundColor = ConsoleColor.Black;
Console.Write(" ");
for (int j = 0; j < resSize.Width; j++)
{
Console.ForegroundColor = (ConsoleColor)ToConsoleColor(bmpMax.GetPixel(j * 2, i * 2));
Console.BackgroundColor = (ConsoleColor)ToConsoleColor(bmpMax.GetPixel(j * 2, i * 2 + 1));
Console.Write("▀");
Console.ForegroundColor = (ConsoleColor)ToConsoleColor(bmpMax.GetPixel(j * 2 + 1, i * 2));
Console.BackgroundColor = (ConsoleColor)ToConsoleColor(bmpMax.GetPixel(j * 2 + 1, i * 2 + 1));
Console.Write("▀");
}
System.Console.WriteLine();
}
}
static void Main(string[] args)
{
System.Console.WindowWidth = 170;
System.Console.WindowHeight = 40;
Bitmap bmpSrc = new Bitmap(@"image.bmp", true);
ConsoleWriteImage(bmpSrc);
System.Console.ReadLine();
}
}
}
Pour exécuter l'exemple, le bitmap "image.bmp" doit se trouver dans le même répertoire que l'exécutable. J'ai augmenté la taille de la console, la taille de l'aperçu est toujours de 39 et peut être modifiée à int sMax = 39;
.
La solution de taffer est également très cool. Vous deux avez mon upvote ...
Je lisais à propos de espaces de couleur et [~ # ~] lab [~ # ~] l'espace semble être une bonne option pour vous (voir cette question: recherche d'une "distance" précise entre les couleurs et algorithme de vérification de la similarité des couleurs )
Citant Wikipedia CIELAB page, les avantages de cet espace colorimétrique sont les suivants:
Contrairement aux modèles de couleurs RVB et CMJN, Lab Colour est conçu pour rapprocher la vision humaine. Il aspire à l'uniformité perceptuelle et sa composante L correspond étroitement à la perception humaine de la légèreté. Ainsi, il peut être utilisé pour effectuer des corrections précises de la balance des couleurs en modifiant les courbes de sortie dans les composantes a et b.
Pour mesurer la distance entre les couleurs, vous pouvez utiliser Delta E distance.
Avec cela, vous pouvez approximer mieux de Color
à ConsoleColor
:
Tout d'abord, vous pouvez définir une classe CieLab
pour représenter les couleurs dans cet espace:
public class CieLab
{
public double L { get; set; }
public double A { get; set; }
public double B { get; set; }
public static double DeltaE(CieLab l1, CieLab l2)
{
return Math.Pow(l1.L - l2.L, 2) + Math.Pow(l1.A - l2.A, 2) + Math.Pow(l1.B - l2.B, 2);
}
public static CieLab Combine(CieLab l1, CieLab l2, double amount)
{
var l = l1.L * amount + l2.L * (1 - amount);
var a = l1.A * amount + l2.A * (1 - amount);
var b = l1.B * amount + l2.B * (1 - amount);
return new CieLab { L = l, A = a, B = b };
}
}
Il existe deux méthodes statiques, l’une pour mesurer la distance à l’aide de Delta E (DeltaE
) et l’autre pour combiner deux couleurs spécifiant la quantité de chaque couleur (Combine
).
Et pour transformer de RGB
à LAB
, vous pouvez utiliser la méthode suivante (de here ):
public static CieLab RGBtoLab(int red, int green, int blue)
{
var rLinear = red / 255.0;
var gLinear = green / 255.0;
var bLinear = blue / 255.0;
double r = rLinear > 0.04045 ? Math.Pow((rLinear + 0.055) / (1 + 0.055), 2.2) : (rLinear / 12.92);
double g = gLinear > 0.04045 ? Math.Pow((gLinear + 0.055) / (1 + 0.055), 2.2) : (gLinear / 12.92);
double b = bLinear > 0.04045 ? Math.Pow((bLinear + 0.055) / (1 + 0.055), 2.2) : (bLinear / 12.92);
var x = r * 0.4124 + g * 0.3576 + b * 0.1805;
var y = r * 0.2126 + g * 0.7152 + b * 0.0722;
var z = r * 0.0193 + g * 0.1192 + b * 0.9505;
Func<double, double> Fxyz = t => ((t > 0.008856) ? Math.Pow(t, (1.0 / 3.0)) : (7.787 * t + 16.0 / 116.0));
return new CieLab
{
L = 116.0 * Fxyz(y / 1.0) - 16,
A = 500.0 * (Fxyz(x / 0.9505) - Fxyz(y / 1.0)),
B = 200.0 * (Fxyz(y / 1.0) - Fxyz(z / 1.0890))
};
}
L'idée est d'utiliser des caractères d'ombre tels que @AntoninLejsek do ('█', '▓', '▒', '░'), cela vous permet d'obtenir plus de 16 couleurs en combinant les couleurs de la console (en utilisant la méthode Combine
).
Ici, nous pouvons faire quelques améliorations en pré-calculant les couleurs à utiliser:
class ConsolePixel
{
public char Char { get; set; }
public ConsoleColor Forecolor { get; set; }
public ConsoleColor Backcolor { get; set; }
public CieLab Lab { get; set; }
}
static List<ConsolePixel> pixels;
private static void ComputeColors()
{
pixels = new List<ConsolePixel>();
char[] chars = { '█', '▓', '▒', '░' };
int[] rs = { 0, 0, 0, 0, 128, 128, 128, 192, 128, 0, 0, 0, 255, 255, 255, 255 };
int[] gs = { 0, 0, 128, 128, 0, 0, 128, 192, 128, 0, 255, 255, 0, 0, 255, 255 };
int[] bs = { 0, 128, 0, 128, 0, 128, 0, 192, 128, 255, 0, 255, 0, 255, 0, 255 };
for (int i = 0; i < 16; i++)
for (int j = i + 1; j < 16; j++)
{
var l1 = RGBtoLab(rs[i], gs[i], bs[i]);
var l2 = RGBtoLab(rs[j], gs[j], bs[j]);
for (int k = 0; k < 4; k++)
{
var l = CieLab.Combine(l1, l2, (4 - k) / 4.0);
pixels.Add(new ConsolePixel
{
Char = chars[k],
Forecolor = (ConsoleColor)i,
Backcolor = (ConsoleColor)j,
Lab = l
});
}
}
}
Une autre amélioration pourrait consister à accéder directement aux données d’image en utilisant LockBits
au lieu d’utiliser GetPixel
.
[~ # ~] mise à jour [~ # ~] : si l'image comporte des parties de même couleur, vous pouvez accélérer considérablement le processus de dessin de la portion de caractères. ayant les mêmes couleurs, au lieu de caractères individuels:
public static void DrawImage(Bitmap source)
{
int width = Console.WindowWidth - 1;
int height = (int)(width * source.Height / 2.0 / source.Width);
using (var bmp = new Bitmap(source, width, height))
{
var unit = GraphicsUnit.Pixel;
using (var src = bmp.Clone(bmp.GetBounds(ref unit), PixelFormat.Format24bppRgb))
{
var bits = src.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, src.PixelFormat);
byte[] data = new byte[bits.Stride * bits.Height];
Marshal.Copy(bits.Scan0, data, 0, data.Length);
for (int j = 0; j < height; j++)
{
StringBuilder builder = new StringBuilder();
var fore = ConsoleColor.White;
var back = ConsoleColor.Black;
for (int i = 0; i < width; i++)
{
int idx = j * bits.Stride + i * 3;
var pixel = DrawPixel(data[idx + 2], data[idx + 1], data[idx + 0]);
if (pixel.Forecolor != fore || pixel.Backcolor != back)
{
Console.ForegroundColor = fore;
Console.BackgroundColor = back;
Console.Write(builder);
builder.Clear();
}
fore = pixel.Forecolor;
back = pixel.Backcolor;
builder.Append(pixel.Char);
}
Console.ForegroundColor = fore;
Console.BackgroundColor = back;
Console.WriteLine(builder);
}
Console.ResetColor();
}
}
}
private static ConsolePixel DrawPixel(int r, int g, int b)
{
var l = RGBtoLab(r, g, b);
double diff = double.MaxValue;
var pixel = pixels[0];
foreach (var item in pixels)
{
var delta = CieLab.DeltaE(l, item.Lab);
if (delta < diff)
{
diff = delta;
pixel = item;
}
}
return pixel;
}
Enfin, appelez DrawImage
comme suit:
static void Main(string[] args)
{
ComputeColors();
Bitmap image = new Bitmap("image.jpg", true);
DrawImage(image);
}
Images de résultat:
Les solutions suivantes ne sont pas basées sur des caractères, mais fournissent des images détaillées complètes.
Vous pouvez dessiner sur n'importe quelle fenêtre en utilisant son gestionnaire pour créer un objet Graphics
. Pour obtenir le gestionnaire d'une application console, vous pouvez le faire en important GetConsoleWindow
:
[DllImport("kernel32.dll", EntryPoint = "GetConsoleWindow", SetLastError = true)]
private static extern IntPtr GetConsoleHandle();
Ensuite, créez un graphique avec le gestionnaire (en utilisant Graphics.FromHwnd
) et dessinez l'image en utilisant les méthodes de l'objet Graphics
, par exemple:
static void Main(string[] args)
{
var handler = GetConsoleHandle();
using (var graphics = Graphics.FromHwnd(handler))
using (var image = Image.FromFile("img101.png"))
graphics.DrawImage(image, 50, 50, 250, 200);
}
Cela a l'air bien, mais si la console est redimensionnée ou défilée, l'image disparaît car la fenêtre est rafraîchie (peut-être qu'un mécanisme quelconque pour redessiner l'image est possible dans votre cas).
Une autre solution consiste à incorporer une fenêtre (Form
) dans l'application console. Pour ce faire, vous devez importer SetParent
(et MoveWindow
pour déplacer la fenêtre dans la console):
[DllImport("user32.dll")]
public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
Ensuite, il vous suffit de créer une propriété Form
et de définir la propriété BackgroundImage
sur l’image souhaitée (faites-le sur un Thread
ou Task
pour éviter de bloquer la console). :
static void Main(string[] args)
{
Task.Factory.StartNew(ShowImage);
Console.ReadLine();
}
static void ShowImage()
{
var form = new Form
{
BackgroundImage = Image.FromFile("img101.png"),
BackgroundImageLayout = ImageLayout.Stretch
};
var parent = GetConsoleHandle();
var child = form.Handle;
SetParent(child, parent);
MoveWindow(child, 50, 50, 250, 200, true);
Application.Run(form);
}
Bien sûr, vous pouvez définir FormBorderStyle = FormBorderStyle.None
pour masquer les bordures de la fenêtre (image de droite)
Dans ce cas, vous pouvez redimensionner la console et l’image/fenêtre sera toujours là.
Un avantage de cette approche est que vous pouvez localiser la fenêtre où vous voulez et changer l’image à tout moment en modifiant simplement la propriété BackgroundImage
.
Il n'y a pas de moyen direct. Mais vous pouvez essayer d’utiliser un convertisseur d’image en ascii-art comme celui-ci
Oui, vous pouvez le faire si vous élargissez un peu la question en ouvrant un Form
à partir de l'application Console.
Voici comment vous pouvez demander à votre application console d’ouvrir un formulaire et d’afficher une image:
System.Drawing
et System.Windows.Forms
using System.Windows.Forms;
using System.Drawing;
Voir ce post sur la façon de le faire !
Maintenant, tout ce dont vous avez besoin pour ajouter quelque chose comme ça:
Form form1 = new Form();
form1.BackgroundImage = bmp;
form1.ShowDialog();
Bien sûr, vous pouvez aussi utiliser un PictureBox
..
Et vous pouvez utiliser form1.Show();
pour maintenir la console active pendant que l'aperçu s'affiche.
Message original: Bien sûr, vous ne pouvez pas afficher correctement une image à l'intérieur une fenêtre 25x80; Même si vous utilisez une fenêtre plus grande et que vous bloquez des graphiques, ce ne serait pas un aperçu, mais un désordre!
Mise à jour: On dirait que vous pouvez après tout dessiner GDI une image sur le formulaire de la console; voir la réponse de taffer!