这个是我顺手写的玩具,因此有好些bug,例如图像步长、大小不能自适应等等的问题。不过按照这个思路你能拿到很好的结果。个人认为效果也可以,先上效果:
效果可以吧。
实现思路很简单,就是按照灰度查表。但是单纯这样是拿不到好结果的——因此我使用拉普拉斯算子的一个变种进行边缘检测,然后以此进行灰度加强,最后再输出图片。完整代码如下,图像编解码使用WIC接口,手写了一个二维卷积,一个图片转灰度图。组合在一起就是结果:
/*
| Img2Char
| 文件名称: img2chr.cpp
| 文件作用: 唯一的源文件
| 创建日期: 2020-04-16
| 更新日期: 2020-04-18
| 开发人员: JuYan
+----------------------------
Copyright (C) JuYan, all rights reserved.
该程序可以把你的图像变成字符画
WARNING: bug有好几个
*/
#pragma region include和define
#include
#include
#include
#include
#include
#include
#include
#include
#include
#pragma comment(lib, "Windowscodecs.lib")
#define memalloc(type, num) (type*)_memalloc(sizeof(type) * (num))
#define BUFF2(p, i, j) (*((p) + (j) * w + (i)))
#define KERNEL_W 7
#define KERNEL_H 7
#pragma endregion
#pragma region struct定义
enum ImageChannle
{
Channel_B, Channel_G, Channel_R, Channel_A
};
typedef struct tagImageData
{
BYTE *rgba;
int w, h;
} ImageData;
typedef struct tagGaryImageData
{
BYTE *dat; // 0xff是顶级
int *tmp; // 在各个处理过程中可能用到的temp数据
int w, h;
} GaryImageData;
#pragma endregion
#pragma region 全局变量
IWICImagingFactory *pFactory = NULL;
#pragma endregion
#pragma region 杂项函数
// 打印错误信息
void printmsg(const char *msg, ...)
{
va_list va;
va_start(va, msg);
vfprintf(stderr, msg, va);
putchar('\n');
va_end(va);
}
// 分配内存和释放内存
void* _memalloc(size_t sz)
{
void *p;
p = malloc(sz);
if (p == NULL)
{
printmsg("Can not allocate %d bytes on heap.", sz);
assert(0);
abort();
}
return p;
}
void memfree(void *p)
{
if (p != NULL)
{
free(p);
}
p = NULL;
}
#pragma endregion
#pragma region 图片读写
// 释放一个接口
template inline void SafeRelease(T * &p)
{
if (p)
{
p->Release();
}
p = NULL;
}
// 读取图片, 得到rgba, 失败返回false
bool LoadImageData(const wchar_t *file, ImageData *res)
{
bool ret;
BYTE *dat;
HRESULT hr;
WICRect rcLock;
UINT i, j, k, datSz, dataStride, imgW, imgH;
int dataStep;
BYTE *pImgData;
IWICBitmap *pBitmap = NULL;
IWICBitmapLock *pLock = NULL;
WICPixelFormatGUID formatGUID;
IWICBitmapDecoder *pDecoder = NULL; // 解码器要即时创建和释放
IWICBitmapFrameDecode *pDecFrame = NULL;
ret = true;
res->w = 0;
res->h = 0;
res->rgba = NULL;
hr = pFactory->CreateDecoderFromFilename(file, NULL, GENERIC_READ, WICDecodeMetadataCacheOnDemand, &pDecoder);
if (FAILED(hr))
{
printmsg("Can not load image : 0x%x", hr);
return false;
}
hr = pDecoder->GetFrame(0, &pDecFrame);
if (FAILED(hr))
{
ret = false;
goto err;
}
// 载入图像信息
pDecFrame->GetSize(&imgW, &imgH);
pDecFrame->GetPixelFormat(&formatGUID); // 取得图像编码
if (IsEqualGUID(formatGUID, GUID_WICPixelFormat32bppBGRA))
{
dataStep = 4;
}
else if (IsEqualGUID(formatGUID, GUID_WICPixelFormat24bppBGR))
{
dataStep = 3;
}
else {
ret = false;
printmsg("Unknow image format");
goto err;
}
// 拷贝图像数据
rcLock.X = 0;
rcLock.Y = 0;
rcLock.Width = imgW;
rcLock.Height = imgH; // 接下来我们创建位图以得到数据
hr = pFactory->CreateBitmapFromSource(pDecFrame, WICBitmapCacheOnDemand, &pBitmap);
if (FAILED(hr))
{
ret = false;
goto err;
}
pBitmap->Lock(&rcLock, WICBitmapLockWrite, &pLock);
pLock->GetStride(&dataStride); // 取得一行有多少字节
hr = pLock->GetDataPointer(&datSz, &dat); // 取得图像数据
if (FAILED(hr))
{
ret = false;
printmsg("Can not get image data from memory.");
goto err;
}
pImgData = memalloc(BYTE, imgW * imgH * sizeof(UINT)); // 最后, 拷贝像素数据
for (i = 0, j = 0; i < dataStride * imgH; i += dataStride)
{
for (k = 0; k < imgW * dataStep; k += dataStep, j += 4)
{
pImgData[j + 0] = dat[i + k + 0];
pImgData[j + 1] = dat[i + k + 1];
pImgData[j + 2] = dat[i + k + 2];
if (dataStep == 4)
{
pImgData[j + 3] = dat[i + k + 3];
}
else {
pImgData[j + 3] = 0xff;
}
}
}
res->w = (int)imgW;
res->h = (int)imgH;
res->rgba = pImgData;
err:
SafeRelease(pLock);
SafeRelease(pBitmap);
SafeRelease(pDecFrame);
SafeRelease(pDecoder);
return ret;
}
// 保存一段数据, 格式为32位的PNG
bool SaveImageData(const wchar_t *file, const ImageData *dat)
{
bool ret;
UINT w, h;
HRESULT hr;
IWICStream *pStream = NULL;
IWICBitmapEncoder *pEncoder = NULL;
IWICBitmapFrameEncode *pBitmapFrame = NULL;
WICPixelFormatGUID formatGUID = GUID_WICPixelFormat32bppBGRA;
ret = true;
w = (UINT)dat->w;
h = (UINT)dat->h;
pFactory->CreateStream(&pStream);
hr = pStream->InitializeFromFilename(file, GENERIC_WRITE);
if (FAILED(hr))
{
ret = false;
printmsg("Can not save file");
goto err;
}
hr = pFactory->CreateEncoder(GUID_ContainerFormatPng, NULL, &pEncoder); // png编码器
if (FAILED(hr))
{
ret = false;
printmsg("Can not create encoder");
goto err;
}
pEncoder->Initialize(pStream, WICBitmapEncoderNoCache);
hr = pEncoder->CreateNewFrame(&pBitmapFrame, NULL); // 创建一个帧
if (FAILED(hr))
{
ret = false;
goto err;
}
pBitmapFrame->Initialize(NULL);
pBitmapFrame->SetSize(w, h);
pBitmapFrame->SetPixelFormat(&formatGUID);
hr = pBitmapFrame->WritePixels(
h,
w * sizeof(UINT),
w * h * sizeof(UINT),
dat->rgba
);
if (FAILED(hr))
{
ret = false;
goto err;
}
pBitmapFrame->Commit(); // 提交更改
pEncoder->Commit();
pStream->Commit(STGC_DEFAULT);
err:
SafeRelease(pStream);
SafeRelease(pBitmapFrame);
SafeRelease(pEncoder);
return ret;
}
// 释放ImageData
void ReleaseImageData(ImageData *p)
{
memfree(p->rgba);
}
#pragma endregion
#pragma region 灰度处理
void CreateGaryImage(const ImageData *src, GaryImageData *res)
{
BYTE *pres;
int imgsize, r;
const BYTE *psrc;
assert(src->rgba);
res->w = src->w;
res->h = src->h;
imgsize = res->w * res->h;
res->dat = memalloc(BYTE, imgsize); // 分配内存空间
res->tmp = memalloc(int, imgsize);
pres = res->dat;
psrc = src->rgba;
while (imgsize-- > 0)
{
if (psrc[Channel_A] != 0)
{
r = (int)(psrc[Channel_R] * 0.3 + psrc[Channel_G] * 0.59 + psrc[Channel_B] * 0.11);
}
else {
r = 0xff; // 完全透明的地方设置为白色
}
*pres++ = r > 0xff ? 0xff : r;
psrc += 4; // R G B A 四个通道
}
}
// 把灰度图转到一个已有的RGBA数据上
void Gary2RGBA(const GaryImageData *src, ImageData *dst)
{
int imgsize;
BYTE * pdst;
const BYTE *psrc;
assert(src->dat);
pdst = dst->rgba;
psrc = src->dat;
imgsize = src->w * src->h;
while (imgsize-- > 0) // 这玩意说白了就是把rgb三个通道设置为灰度值
{
pdst[Channel_A] = 0xff;
pdst[Channel_R] = *psrc;
pdst[Channel_G] = *psrc;
pdst[Channel_B] = *psrc;
pdst += 4;
psrc++;
}
}
// 释放灰度图
void ReleaseGaryImage(GaryImageData *p)
{
if (p->dat != NULL)
{
memfree(p->tmp);
memfree(p->dat);
}
}
#pragma endregion
#pragma region 拉普拉斯
template int GetPixelValue(T *dat, int x, int y, int maxw, int maxh)
{
if (x < 0)
x = 0;
if (y < 0)
y = 0;
if (x >= maxw)
x = maxw - 1;
if (y >= maxh)
y = maxh - 1;
return (int)*(dat + y * maxw + x);
}
inline int GetPixelValue(const GaryImageData &img, int x, int y)
{
return GetPixelValue(img.dat, x, y, img.w, img.h);
}
// 对灰度图像进行卷积
void CreateGaryImageCov(const double kernel[KERNEL_W][KERNEL_H], const GaryImageData &src, GaryImageData *dst)
{
int x, y;
int m, n, w;
int imgsize, cx, cy;
double sum;
w = src.w;
dst->w = src.w;
dst->h = src.h;
imgsize = src.w * src.h;
dst->dat = memalloc(BYTE, imgsize); // 分配内存空间
dst->tmp = memalloc(int, imgsize);
cx = KERNEL_W / 2;
cy = KERNEL_H / 2;
for (x = 0; x < src.w; x++)
{
for (y = 0; y < src.h; y++)
{
int curx, cury;
sum = 0;
curx = x - cx;
cury = y - cy;
for (m = 0; m < KERNEL_W; m++)
{
for (n = 0; n < KERNEL_H; n++)
{
sum += kernel[m][n] * (GetPixelValue(src, curx + m, cury + n) / 255.0);
}
}
if (sum > 1)
{
sum = 1;
}
else if (sum < 0)
{
sum = 0;
}
BUFF2(dst->dat, x, y) = (BYTE)(sum * 0xff);
}
}
}
// 拉普拉斯算子变种
void ApplyLaplaceOper(const GaryImageData &src, GaryImageData *dst)
{
const double kernel[KERNEL_W][KERNEL_H] =
{
0, 1, 0, 0, 1, 0, 0,
1, 1, 0, 1, 0, 1, 1,
0, 0, 1, 0, 1, 0, 1,
1, 0, 0, -22.3, 0, 0, 1,
0, 0, 1, 0, 1, 0, 1,
1, 1, 0, 1, 0, 1, 1,
0, 0, 1, 0, 1, 0, 0,
};
CreateGaryImageCov(kernel, src, dst);
}
#pragma endregion
#pragma region 字符画输出
// 这个表是按照灰度对应的字符
const char str[] =
{
"@@&QNOBD%GmH8Abd$UwKXPZE#VShkC25eao3YnuTzxfL7vsc]|}1J?)(l=I+<>ri!*-~;:,... "
};
int wmain(int argc, wchar_t *argv[])
{
HRESULT hr;
ImageData img;
GaryImageData gary, bord;
constexpr int step = 2; // 图像转灰度的步长
CoInitialize(NULL);
hr = CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, IID_IWICImagingFactory, (LPVOID*)&pFactory);
if (FAILED(hr))
{
printmsg("Can not create factory : 0x%x", hr);
goto err;
}
if (argc == 1 || LoadImageData(argv[1], &img) == false)
{
printmsg("Can not open file");
goto err;
}
CreateGaryImage(&img, &gary); // 创建灰度图像
ApplyLaplaceOper(gary, &bord);
/**/
FILE *f;
fopen_s(&f, "out.txt", "wb");
for (int i = 0; i < gary.h; i += step)
{
const int maxlen = sizeof(str) - 1;
for (int j = 0; j < gary.w; j += step)
{
int k = (int)(GetPixelValue(gary, j, i) / 256.0 * maxlen);
double m = GetPixelValue(bord, j, i) / 255.0;
k = k - 20 * (m - 0.1);
if (k < 0)
{
k = 0;
}
fwrite(&str[k], 1, 1, f);
fwrite(&str[k], 1, 1, f);
}
fwrite("\r\n", 1, 2, f);
}
fclose(f);
Gary2RGBA(&bord, &img);
SaveImageData(L"out.png", &img);
sverr:
ReleaseImageData(&img);
ReleaseGaryImage(&gary);
ReleaseGaryImage(&bord);
err:
SafeRelease(pFactory);
CoUninitialize();
return 0;
}
#pragma endregion