在平板上阅读双页扫描的电子书是一件很麻烦的事情,但是偏偏很多书都是双页扫描的,大概双页扫描比较方便吧。例如很多书籍的扫描出来的效果是这样的:

扫描页

我希望能将双页的扫描版转成单页的,但是在网上搜了一圈也没有找到合适的软件,只好自己寻找解决方案了。这问题的复杂性在于,我们不能简单地从中间剪开,因为由于扫描时候的操作,两页并不是正好各占一半的。但是我们可以注意到两页之间有一条细长的黑线,这条黑线可以很自然地成为分割两页的标准线。所以我们可以通过找这条黑线的位置来达到自动分割的功能。其实有很多书扫描的时候由于操作失误,扫出来的书是斜的,对于这种情况,问题就变得很复杂了。

结合自己的情况,我对要处理的扫描图像做出了以下假设:

假设1)扫描图像都是“正”的,也就是说图像没有倾斜或者只是有稍微的倾斜;
假设2)扫描图像都有一条作为分割标准的黑线,并处在图像中间附近;
假设3)扫描图像是黑白的。

黑色的RGB值为(0,0,0),而白色的RGB的值为(255,255,255),所以如果我们将图像每一列的所有像素点的颜色值求一个平均,在黑线附近会达到比较小的值。所以我们可以扫描所有列的平均值挑出最小的列,我们可以期待这一列就是黑线所在的位置。由假设2),我们可以大大提高算法的效率:只要扫描图像中心所在的一个小范围[width/2-range, width/2+range)就可以确定黑线的所在位置,其中width是图像的宽,而range是用来控制范围大小的值,单位是像素。我是用的range值为200。程序如下(使用CImg库):

/* 使用CImg库,以及其命名空间 */
#include "CImg.h"
using namespace cimg_library;

/* 处理扫描图片的核心程序,接受一个文件路径参数 */
int process(const char *filename);

int main(int argc, char **argv)
{
    /* 命令行接受处理的文件路径参数
     * 如果没有参数,则直接结束程序
     */
    if ( argc<2 )
    {
        return 0;
    }
    /* 处理图片 */
    process(argv[1]);
    return 0;
}

/* 核心程序 */
int process(const char *filename)
{
    /* 打开图像 */
    CImg<unsigned char> img(filename);
    /* 获得图像的宽、高参数 */
    int width = img.width();
    int height = img.height();
    /* 设置range值,控制扫描的范围 */
    int range = 200;
    /* 记录扫描范围内的最小平均值 */
    double min_mean = 255;
    /* 记录最小平均值的列 */
    int column = width/2;

    /* 在规定范围内逐列扫描 */
    for (int i=width/2-range; i<width/2+range; i++)
    {
        double mean = img.get_column(i).mean();
        if ( min_mean>mean )
        {
            min_mean = mean;
            column = i;
        }
    }

    /* 在平均值最小的列画出红线 */
    int line_size = 5;    // 控制线条的粗细
    const unsigned char red[] = {255, 0, 0};   // 红色
    for (int i=column-line_size; i<column+line_size; i++)
    {
        img.draw_line(i, 0, i, height, red);
    }

    /* 显示结果 */
    img.display();

    return 0;
}

下面是上面那个图像的处理结果:

处理结果

但是这个算法也并不是百分百成功的,下图就是一个反例:

反例

估计导致不准确的原因是中间那条黑线不明显。但是根据我对这本书的统计,170多个图像大概只有10多个图像是没有准确找出黑线的。不是100%的算法就无法做到自动地将所有扫描图像分割。

对于算法的改进,我有两个思路:

思路1)不只是将平均值最小的列拿出来,而是将平均值最小的若干列拿出来,再根据离中间的远近选择一列;
思路2)由于黑线都比较细,我们可以通过平均值两个突变之间的大小来推断黑线的位置。

等什么时候有时间了再回过头看看吧。