这个是我顺手写的玩具,因此有好些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 <list> #include <math.h> #include <stdio.h> #include <stdlib.h> #include <stdarg.h> #include <assert.h> #include <Windows.h> #include <algorithm> #include <wincodec.h> #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<class T> 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<typename T> 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