7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

スキャンした書類画像を水平にする

Last updated at Posted at 2023-05-23

 こんにちは。ディマージシェアの技術担当です。皆さん、書類をスキャンする機会は多いと思います。そんなとき、「なんか傾いてるんだけど」となったことは多いのではないでしょうか。今回は、スキャンした書類画像を水平にするプログラムを作成しようと思います。言語はC++、使用するライブラリはOpenCVです。

入力画像の用意

image.png

盛大に傾いてます。結果をわかりやすくするために意図的に用意しました。これを、

image.png

こうします。余白が少しおかしいのは今回のテーマから外れるので無視してください。

画像の入出力、回転操作の実装

 画像を水平にするためには、「画像の入出力」「回転操作」が必要です。OpenCVを用いれば簡単に実装ができます。今回の環境はWindows + Visual Studioです。OpenCVのセットアップなどは各々行ってください。googleで調べればわかります。

#define  _USE_MATH_DEFINES 
#include  <iostream> 
#include  <stdio.h> 
#include  <stdint.h> 
#include  <opencv2/opencv.hpp> 
#include  <cmath>
#ifdef  _DEBUG 
#pragma  comment(lib, "opencv_world452d.lib") 
#else  
#pragma  comment(lib, "opencv_world452.lib")
#endif 

void rotate_img(float theta, cv::Mat* img) {
   float width = img->cols;
   float height = img->rows;
   cv::Point2f center = cv::Point2f((width / 2), (height / 2));//図形の中心
   float degree = theta * (180.0 / M_PI);  // 回転角度
   cv::Size size = cv::Size(width, height);
   cv::Mat change = cv::getRotationMatrix2D(center, degree, 1.0); //回転
   cv::warpAffine(*img, *img, change, size, cv::INTER_CUBIC, cv::BORDER_CONSTANT, cv::Scalar(255, 255, 255)); //画像の変換(アフィン変換)
}

int main()
{
   cv::Mat image;
   image = cv::imread("img001.png");
   if (image.empty() == true) {
       // 画像データが読み込めなかったときは終了する
       return 0;
   }
   rotate_img(1, &image);
   cv::imwrite("out.png", image);
   return 0;
}

 main関数のなかに、「img001.png」をロード、rotate_imgで画像を「1ラジアン」回転、「out.png」に書き込み、というコードを書きました。

image.png

 回転処理が実装できたことがわかります。ここで1つ問題があります。回転前後の画像サイズを考慮しなかったため、回転によって見切れてしまった部分が現れました。これを直します。

void rotate_img(float theta, cv::Mat* img) {
   float width = img->cols;
   float height = img->rows;
   cv::Point2f center = cv::Point2f((width / 2), (height / 2));//図形の中心
   float degree = theta * (180.0 / M_PI);  // 回転角度
   int32_t out_w, out_h; // 出力サイズ
   out_w = ceil(height * abs(sin(theta)) + width * abs(cos(theta)));
   out_h = ceil(height * abs(cos(theta)) + width * abs(sin(theta)));
   cv::Size size = cv::Size(out_w, out_h);
   cv::Mat change = cv::getRotationMatrix2D(center, degree, 1.0); //回転
   cv::Mat add = (cv::Mat_<float>(2, 3) << 0, 0, -width / 2 + out_w / 2, 0, 0, -height / 2 + out_h / 2); //平行移動
   change += add;
   cv::warpAffine(*img, *img, change, size, cv::INTER_CUBIC, cv::BORDER_CONSTANT, cv::Scalar(255, 255, 255)); //画像の変換(アフィン変換)
}

 OpenCVは便利です。回転後の解像度、平行移動の量を計算することにより、回転させた後の画像が見切れることを防ぐことができます。修正後の出力は次のようになります。

image.png

あとは、main関数でおもむろに1を入力していたところに、「修正したい角度」を入れることができれば今回の目的は達成できます。

画像の傾きを検出する

 今回のメインテーマです。画像の傾きを検出するということは、コンピュータに対して「水平な状態とは何ぞ」を教えてあげる必要があります。今回は「印刷物の多くに共通する特徴」を考えてみます。まず、水平な印刷物の「水平方向に積みあがっている有効画素数」をグラフにしてみます。

image.png

 次に、傾いた印刷物で同様にします。

image.png

 印刷物の多くは文字を水平方向にレイアウトします。タイプライターの頃からのお決まりです。そして、水平方向にレイアウトされた印刷物の横方向に注目すると、なにも印字されていない水平方向、何かしら文字がある水平方向、に分かれます。上のグラフでもその傾向はよくわかります。今回は、画像を横方向にパースして、有効画素を積み上げた結果、1つ目のグラフのような傾向がみられる場合を水平と定義することにします。

 先の記述だけだと、まだ計算機が扱うには不便です。もう少し式を具体的にします。上記のように「グラフの線が上下に大きくブレるほど大きな値を取る」ようになる計算式を考えればよいので、「横方向に積みあがった有効画素数の偏差値の合計」としました。この計算方法が良い結果を示すかどうか観察してみます。

 画像を0.5度ずつ回転させながら、上記の式の値を計算し、グラフにプロットしてみます。

image.png

 グラフの形を見る限り良い傾向と解釈できます。87度回転させると良いようです。式の値が最大となる角度を求めるプログラムを書いてみます。

float __inclination_detection(uint8_t* data,
   int32_t src_h, int32_t src_w,
   int32_t out_h, int32_t out_w,
   float t1, float t2, int32_t split
) {
   int32_t x;
   float *score_arr;
   float** horizontal_sum;
   horizontal_sum = (float**)malloc(sizeof(float*) * (split + 1));
   horizontal_sum[0] = (float*)malloc(sizeof(float) * out_h * (split + 1));
   for (x = 1; x < (split + 1); x++) {
       horizontal_sum[x] = horizontal_sum[x - 1] + out_h;
   }
   score_arr = (float*)malloc(sizeof(float) * (split + 1));
   for (x = 0; x <= split; x++) {
       float c, s;
       int32_t i;
       float t = t1 + (t2 - t1) * x / split;
       c = cos(t);
       s = sin(t);
       float const_y = -((float)out_w * 0.5) * s - ((float)out_h * 0.5 * c) + ((float)src_h * 0.5);
       float const_x = -((float)out_w * 0.5) * c + ((float)out_h * 0.5 * s) + ((float)src_w * 0.5);
       float all_sum = 0;
       float avg;
       int32_t nz_s = 0, nz_e = out_h;
       for (i = 0; i < out_h; i++) {
           float dsi = s * i, dci = c * i;
           int32_t j;
           horizontal_sum[x][i] = 0;
           for (j = 0; j < out_w; j++) {
               float dsj = s * j, dcj = c * j;
               int32_t sy = dsj + dci + const_y;
               int32_t sx = dcj - dsi + const_x;
               if (sx >= 0 && sx < src_w && sy >= 0 && sy < src_h) {
                   int32_t d_y_pad = src_w * sy;
                   horizontal_sum[x][i] += (255 - data[d_y_pad + sx]);
               }
           }
       }
       for (i = 0; i < out_h; i++) {
           all_sum += horizontal_sum[x][i];
       }
       for (i = 0; i < out_h; i++) {
           if (horizontal_sum[x][i]) {
               nz_s = i;
               break;
           }
       }
       for (i = out_h - 1; i >= 0; i--) {
           if (horizontal_sum[x][i]) {
               nz_e = i;
               break;
           }
       }
       avg = all_sum / (float)(nz_e - nz_s);
       float score = 0;
       for (i = nz_s; i <= nz_e; i++) {
           score += (avg - horizontal_sum[x][i]) * (avg - horizontal_sum[x][i]);
       }
       score /= (float)(nz_e - nz_s);
       score_arr[x] = score;
   }
   float max_score = 0;
   float max_t = 0;
   for (x = 0; x <= split; x++) {
       float t = t1 + (t2 - t1) * x / split;
       if (max_score < score_arr[x]) {
           max_score = score_arr[x];
           max_t = t;
       }
   }
   free(horizontal_sum[0]);
   free(horizontal_sum);
   free(score_arr);
   return max_t;
}

float inclination_detection(cv::Mat* src) {
   uint8_t* data;
   int32_t w = src->cols;
   int32_t h = src->rows;
   int32_t i, j;
   int32_t out_h, out_w;
   // 回転後に必要なサイズ計算
   out_w = out_h = (int)ceil(sqrt((w * w) + (h * h)));

   // グレースケール化
   data = (uint8_t*)malloc(w * h);
   for (i = 0; i < h; i++) {
       int32_t d_y_pad = w * i;
       cv::Vec3b* ptr = src->ptr<cv::Vec3b>(i);
       for (j = 0; j < w; j++) {
           cv::Vec3b bgr = ptr[j];
           uint8_t v = (uint8_t)(0.2126 * bgr[2] +
               0.7152 * bgr[1] +
               0.0722 * bgr[0]);
           data[d_y_pad + j] = v;
       }
   }
   // 傾き検出
   float t = __inclination_detection(data, h, w, out_h, out_w, -M_PI / 2, M_PI / 2, 360);
   float tt = __inclination_detection(data, h, w, out_h, out_w, t - M_PI / 360, t + M_PI / 360, 100);
   free(data);
   return tt;
}

 __inclination_detectionは角度をt1~t2までを、split刻みで実際に画像を回転させながら、先述の偏差値がmaxになる角度を返します。この際、画像がRGBのカラー画像だと有効画素の解釈が難しいので、輝度でグレースケール化してから処理しています。まず-90度から90度を360で刻んで0.5度単位の結果を算出、得られたmax周辺を更に100分割してmaxとなる角度を求めています。この関数で得られた値で画像を回転させてみましょう。

int main()
{
   cv::Mat image;
   image = cv::imread("img001.png");
   if (image.empty() == true) {
       // 画像データが読み込めなかったときは終了する
       return 0;
   }
   float t = inclination_detection(&image);
   rotate_img(t, &image);
   cv::imwrite("out.png", image);
}

得られた画像がこちらです。

image.png

画像が水平になりました。

全体のソースコード

#define  _USE_MATH_DEFINES 
#include  <iostream> 
#include  <stdio.h> 
#include  <stdint.h> 
#include  <opencv2/opencv.hpp> 
#include  <cmath>

#ifdef  _DEBUG 
#pragma  comment(lib, "opencv_world452d.lib") 
#else  
#pragma  comment(lib, "opencv_world452.lib") 
#endif 


float __inclination_detection(uint8_t* data,
   int32_t src_h, int32_t src_w,
   int32_t out_h, int32_t out_w,
   float t1, float t2, int32_t split
) {
   int32_t x;
   float *score_arr;
   float** horizontal_sum;
   horizontal_sum = (float**)malloc(sizeof(float*) * (split + 1));
   horizontal_sum[0] = (float*)malloc(sizeof(float) * out_h * (split + 1));
   for (x = 1; x < (split + 1); x++) {
       horizontal_sum[x] = horizontal_sum[x - 1] + out_h;
   }
   score_arr = (float*)malloc(sizeof(float) * (split + 1));
   for (x = 0; x <= split; x++) {
       float c, s;
       int32_t i;
       float t = t1 + (t2 - t1) * x / split;
       c = cos(t);
       s = sin(t);
       float const_y = -((float)out_w * 0.5) * s - ((float)out_h * 0.5 * c) + ((float)src_h * 0.5);
       float const_x = -((float)out_w * 0.5) * c + ((float)out_h * 0.5 * s) + ((float)src_w * 0.5);
       float all_sum = 0;
       float avg;
       int32_t nz_s = 0, nz_e = out_h;
       for (i = 0; i < out_h; i++) {
           float dsi = s * i, dci = c * i;
           int32_t j;
           horizontal_sum[x][i] = 0;
           for (j = 0; j < out_w; j++) {
               float dsj = s * j, dcj = c * j;
               int32_t sy = dsj + dci + const_y;
               int32_t sx = dcj - dsi + const_x;
               if (sx >= 0 && sx < src_w && sy >= 0 && sy < src_h) {
                   int32_t d_y_pad = src_w * sy;
                   horizontal_sum[x][i] += (255 - data[d_y_pad + sx]);
               }
           }
       }
       for (i = 0; i < out_h; i++) {
           all_sum += horizontal_sum[x][i];
       }
       for (i = 0; i < out_h; i++) {
           if (horizontal_sum[x][i]) {
               nz_s = i;
               break;
           }
       }
       for (i = out_h - 1; i >= 0; i--) {
           if (horizontal_sum[x][i]) {
               nz_e = i;
               break;
           }
       }
       avg = all_sum / (float)(nz_e - nz_s);
       float score = 0;
       for (i = nz_s; i <= nz_e; i++) {
           score += (avg - horizontal_sum[x][i]) * (avg - horizontal_sum[x][i]);
       }
       score /= (float)(nz_e - nz_s);
       score_arr[x] = score;
   }
   float max_score = 0;
   float max_t = 0;
   for (x = 0; x <= split; x++) {
       float t = t1 + (t2 - t1) * x / split;
       if (max_score < score_arr[x]) {
           max_score = score_arr[x];
           max_t = t;
       }
   }
   free(horizontal_sum[0]);
   free(horizontal_sum);
   free(score_arr);
   return max_t;
}

float inclination_detection(cv::Mat* src) {
   uint8_t* data;
   int32_t w = src->cols;
   int32_t h = src->rows;
   int32_t i, j;
   int32_t out_h, out_w;
   // 回転後に必要なサイズ計算
   out_w = out_h = (int)ceil(sqrt((w * w) + (h * h)));

   // グレースケール化
   data = (uint8_t*)malloc(w * h);
   for (i = 0; i < h; i++) {
       int32_t d_y_pad = w * i;
       cv::Vec3b* ptr = src->ptr<cv::Vec3b>(i);
       for (j = 0; j < w; j++) {
           cv::Vec3b bgr = ptr[j];
           uint8_t v = (uint8_t)(0.2126 * bgr[2] +
               0.7152 * bgr[1] +
               0.0722 * bgr[0]);
           data[d_y_pad + j] = v;
       }
   }
   // 傾き検出
   float t = __inclination_detection(data, h, w, out_h, out_w, -M_PI / 2, M_PI / 2, 360);
   float tt = __inclination_detection(data, h, w, out_h, out_w, t - M_PI / 360, t + M_PI / 360, 100);
   free(data);
   return tt;
}

void rotate_img(float theta, cv::Mat* img) {
   float width = img->cols;
   float height = img->rows;
   cv::Point2f center = cv::Point2f((width / 2), (height / 2));//図形の中心
   float degree = theta * (180.0 / M_PI);  // 回転角度
   int32_t out_w, out_h; // 出力サイズ
   out_w = ceil(height * abs(sin(theta)) + width * abs(cos(theta)));
   out_h = ceil(height * abs(cos(theta)) + width * abs(sin(theta)));
   cv::Size size = cv::Size(out_w, out_h);
   cv::Mat change = cv::getRotationMatrix2D(center, degree, 1.0); //回転
   cv::Mat add = (cv::Mat_<float>(2, 3) << 0, 0, -width / 2 + out_w / 2, 0, 0, -height / 2 + out_h / 2); //平行移動
   change += add;
   cv::warpAffine(*img, *img, change, size, cv::INTER_CUBIC, cv::BORDER_CONSTANT, cv::Scalar(255, 255, 255)); //画像の変換(アフィン変換)
}

int main()
{
   cv::Mat image;
   image = cv::imread("img001.png");
   if (image.empty() == true) {
       // 画像データが読み込めなかったときは終了する
       return 0;
   }
   float t = inclination_detection(&image);
   rotate_img(t, &image);
   cv::imwrite("out.png", image);
}

当社に興味を持たれた方はHPもご覧ください。

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?