[ImageMagick 学习] ImageMagick学习笔记

龚睿
2023-12-01

IM 用C写了一系列的command,然后由这些command 配以各种参数,搭配出种类繁多的图像处理特效。


这样做的好处是:1)层次分明;2)上层逻辑实现简单快速。

坏处是:1)用shell脚本写的特效,不方便由C代码调用;2)Shell 脚本执行效率没有C原生的快


如果对某种特效,需要频繁调用,这时就要考虑将shell 脚本由C/C++ 重写了。以下将记录这方面的学习笔记。


曾经听说过Lua 脚本和C 代码关系比较密切,考虑将shell 脚本转成Lua脚本,然后将Lua脚本“自动转成C 代码”,后来发现,这是不可能的,太天真了!并且,如果由C 调用Lua 脚本,并且频繁跨越C/Lua 边界的话,效率也很低下。于是放弃Lua 思路,只有一条路:手工将Shell 脚本转成C 代码。于是要搞清楚以下问题:

  1. shell脚本中大量使用的convert 命令是怎么生成的,代码和Makefile 在哪?我们自己的C 代码该怎样模拟调用conert 命令?
  2. 对于众多的shell 脚本,怎样用一个统一的方法实现Shell 到C 的转换?
  3. 这些Shell 脚本是否具备“Linux 命令通道”的功能,即几个脚本串联,前一个的输出是后一个的输入?这样的串联用代码怎样高效实现?
  4. 怎样实现跨平台?

观察IM 的代码和安装后的文件,有以下发现:

1)貌似convert 命令是由wand/convert.h, wand/convert.c 生成的

2)安装了IM 之后,在 /usr/local/lib 中会有以下和IM 相关的库文件:

-rw-r--r--. 1 root root  4561412 Apr 21 20:00 libMagick++-6.Q16.a
-rwxr-xr-x. 1 root root     1123 Apr 21 20:00 libMagick++-6.Q16.la
lrwxrwxrwx. 1 root root       26 Apr 21 20:00 libMagick++-6.Q16.so -> libMagick++-6.Q16.so.1.0.0
lrwxrwxrwx. 1 root root       26 Apr 21 20:00 libMagick++-6.Q16.so.1 -> libMagick++-6.Q16.so.1.0.0
-rwxr-xr-x. 1 root root  2134069 Apr 21 20:00 libMagick++-6.Q16.so.1.0.0
-rw-r--r--. 1 root root 11540026 Apr 21 20:00 libMagickCore-6.Q16.a
-rwxr-xr-x. 1 root root     1075 Apr 21 20:00 libMagickCore-6.Q16.la
lrwxrwxrwx. 1 root root       28 Apr 21 20:00 libMagickCore-6.Q16.so -> libMagickCore-6.Q16.so.1.0.0
lrwxrwxrwx. 1 root root       28 Apr 21 20:00 libMagickCore-6.Q16.so.1 -> libMagickCore-6.Q16.so.1.0.0
-rwxr-xr-x. 1 root root  6309290 Apr 21 20:00 libMagickCore-6.Q16.so.1.0.0
-rw-r--r--. 1 root root  4043444 Apr 21 20:00 libMagickWand-6.Q16.a
-rwxr-xr-x. 1 root root     1113 Apr 21 20:00 libMagickWand-6.Q16.la
lrwxrwxrwx. 1 root root       28 Apr 21 20:00 libMagickWand-6.Q16.so -> libMagickWand-6.Q16.so.1.0.0
lrwxrwxrwx. 1 root root       28 Apr 21 20:00 libMagickWand-6.Q16.so.1 -> libMagickWand-6.Q16.so.1.0.0
-rwxr-xr-x. 1 root root  2254380 Apr 21 20:00 libMagickWand-6.Q16.so.1.0.0

3)安装了IM 之后,在 /usr/local/include/ImageMagick-6 中会有IM 的头文件,其中,/usr/local/include/ImageMagick-6/wand/convert.h 中有如下函数定义:

#if defined(__cplusplus) || defined(c_plusplus)
extern "C" {
#endif

extern WandExport MagickBooleanType
 ConvertImageCommand(ImageInfo *,int,char **,char **,ExceptionInfo *);

#if defined(__cplusplus) || defined(c_plusplus)
}
#endif

而这个ConvertImageCommand() 函数正是在 IM 解压根目录/utilities/convert.c 中真正干活的函数,也就是说,通过#include "/usr/local/include/ImageMagick-6/wand/convert.h" 和其他相关的头文件,然后link /usr/local/lib 下的某些库文件,我们就能够方便地模拟调用IM 生成的命令了。在此基础上,加上合适的上层逻辑,传入合适的参数,就能模拟IM 提供的Shell 脚本的功能了。


参照这里给出的简单例子和 /home/yasi/ImageMagick-6.8.4-10/utilities/convert.c,下面是一个调用ConvertImageCommand()的简单实现:test.cpp。

#include <wand/MagickWand.h>

int main(int argc, char* argv[])
{
	ExceptionInfo   *exception;
	ImageInfo   *image_info;
	MagickBooleanType   status;

	MagickCoreGenesis(*argv,MagickTrue);
	exception=AcquireExceptionInfo();
	image_info=AcquireImageInfo();
	status=MagickCommandGenesis(image_info,ConvertImageCommand,argc,argv,(char **) NULL,exception);
	image_info=DestroyImageInfo(image_info);
	exception=DestroyExceptionInfo(exception);
	MagickCoreTerminus();
	return(status);
}

编译方式:

g++ -o test `pkg-config --cflags --libs MagickWand` test.cpp

其实,pkg-config --cflags --libs MagickWand 的执行结果是这样的:

-fopenmp -DMAGICKCORE_HDRI_ENABLE=0 -DMAGICKCORE_QUANTUM_DEPTH=16 -I/usr/local/include/ImageMagick-6  -L/usr/local/lib -lMagickWand-6.Q16 -lMagickCore-6.Q16

生成的test 可执行程序的用法和 convert 命令式一样的,如:

./test -negate ./lena_noise.jpg ./lena_converted.jpg


但是,这样是不够的,IM 的脚本中常常会多次用到某个函数,如ConvertImageCommand,或者先后用到多个函数;对于这种情况,就要搞清楚下面一些问题:

  1. 前一个函数执行完的结果能否直接通过内存对象传递给后一个函数,而不需要通过 “前一个函数保存结果为文件,后一个函数读取文件” 的方式?(如果做不到的话,就和调用脚本没多大区别了)
  2. 图像处理函数,如ConvertImageCommand 中的第一个参数,ImageInfo 结构,是否足以充当这里的 ”内存对象“ ?

这里有IM 的主要函数的详细说明,理论上可以通过这些函数实现convert 等工具的所有功能,但工作量巨大。能否找到这样一种简单的做法:

尽量多地利用IM 提供的封装好的代码,fully handle 所有命令参数,同时又不失灵活性,可以通过内存对象传递临时图像对象,从shell 脚本转译成C++代码工作量降至最低。

之所以希望这样,是因为Fred's ImageMagick Scripts 已经对IM 的命令做了大量的调优,做成了大量的效果很不错的图像处理脚本,我们不想对这些工作再从头做起,而是简单地临摹这些脚本,翻译成C++代码。


如果不考虑临摹所有的脚本,只针对个别的特性,如文字图像的二值化,可以从TEXTCLEANER 脚本API 说明中抠出最核心的代码,从底层做起。这样的代码更紧凑,效率更高。


其实,TEXTCLEANER 脚本 效果去除背景图像、将图像调整角度(水平放置)效果挺好的,并且在其页面最后给出了脚本对应的 convert 命令格式:

convert \( $infile -colorspace gray -type grayscale -contrast-stretch 0 \) \
\( -clone 0 -colorspace gray -negate -lat ${filtersize}x${filtersize}+${offset}% -contrast-stretch 0 \) \
-compose copy_opacity -composite -fill "$bgcolor" -opaque none +matte \
-deskew 40% -sharpen 0x1 \ $outfile
对于这个TEXTCLEANER 脚本,需要增强的效果是:既然已经去除背景了,只关注文字线条轮廓本身,那么可以进一步二值化,缩小图像文件尺寸。


============== 杂项,待整理 =================

ConvertImageCommand() 是在 wand/convert.c 中实现的

MagicCommandGenesis() 是在 wand/mogrify.c 中实现的

struct _ImageInfo 是在 magick/image.h 中定义的

AcquireImageInfo() 是在 magick/image.c 中实现的

magick/magick-type.h:typedef struct _Image Image; 而_Image 是magick/image.h 中定义的

FireImageStack、PushImageStack 和 PopImageStack是在wand/mogrify-private.h 中定义的宏


这里有个问题,MagickCommandGenesis(image_info,ConvertImageCommand,argc,argv,(char **) NULL,exception) 中传入的argc, argv是 int, char**类型的,所以对于字符串类型的参数,如 "./test -negate ./lena_noise.jpg ./lena_converted.jpg",要先转成 int, char** 类型的。方法如下:

#include <iostream>
#include <sstream>
#include <string>
#include <vector>
using namespace std;

void split_begin(const string &sCmd, vector<char*> &args) {
        istringstream iss(sCmd);
        string token;
        args.clear();
        while(iss >> token) {
                char *arg = new char[token.size() + 1];
                copy(token.begin(), token.end(), arg);
                arg[token.size()] = '\0';
                args.push_back(arg);
        }
}

void split_end(vector<char*> &args) {
        for(int i = 0; i < args.size(); ++i) {
                delete[] args[i];
        }
        args.clear();
}

int main(int argc, char* argv[]) {
        string sCmd = "/opt/usr/bin/app -p 12300 -d 200";
        vector<char*> args;

        split_begin(sCmd, args);
        for(int i = 0; i < args.size(); ++i) {
                cout << args[i] << "~" << endl;
        }
        split_end(args);

        return 0;
}


IM 的各种类型定义是在 /usr/local/include/ImageMagick-6/magick/magick-type.h 中,如MagickRealType、Quantum、SignedQuantum、MagickSizeType、MagickBooleanType 等。

对于编译出现的下面的提示

In file included from /usr/local/include/ImageMagick-6/magick/magick-type.h:25, from main.cpp:2:
/usr/local/include/ImageMagick-6/magick/magick-config.h:29:3: warning: #warning "you should set MAGICKCORE_QUANTUM_DEPTH to sensible default set it to configure time default"
/usr/local/include/ImageMagick-6/magick/magick-config.h:30:3: warning: #warning "this is an obsolete behavior please fix your makefile"
/usr/local/include/ImageMagick-6/magick/magick-config.h:52:3: warning: #warning "you should set MAGICKCORE_HDRI_ENABLE to sensible default set it to configure time default"
/usr/local/include/ImageMagick-6/magick/magick-config.h:53:3: warning: #warning "this is an obsolete behavior please fix yours makefile"
在编译选项中加入“ -DMAGICKCORE_QUANTUM_DEPTH=16 -DMAGICKCORE_HDRI_ENABLE=1”(当然也可以设置除了16和1以外的其他值),如:
$(OBJ_DIR)/%.o : $(SRC_DIR)/%.cpp
        $(CXX) $(INC_OPT) -c -DMAGICKCORE_QUANTUM_DEPTH=16 -DMAGICKCORE_HDRI_ENABLE=1 -o $@ $<

MagickExport 是在 /usr/local/include/ImageMagick-6/magick/method-attribute.h 中定义的。


这里有简单的使用IM 代码的例子



 类似资料: