C言語で絵を動かしたい1

目標

ペンライトを横に触れたりできたらいいな.


方法

調べてみた限り,画像の移動は平行移動と回転移動くらいしか簡単にできそうではなかったので,今回はグリップの先を軸として回転移動させることでペンライトの横揺れを再現したい.


環境

macOS Big Sur ver.11.5.1
MacBook Air (M1, 2020)


参考にするサイト

パッと見,結構あれなサイトだけど,まぁ三角関数すら知らないよって人には優しいと言えるかもしれない.ここでは三角関数や行列による回転操作の話はわかってるものとして飛ばしていく.


実践

使うコード

メインとなるコードは次のrot_mov.c

#include "myJpeg.h"
#include <math.h>
#include <string.h>

int main(int argc, char *argv[]){

  BITMAPDATA_t bitmap, rotatedBitmap;
  int m, n, c;
  int angle;
  int m0, n0;
  double originalm, originaln;
  double rad;

  char outname[256];

  if(argc != 3){
    printf("ファイル名と回転角度[int](0 - 359)を引数に指定してください\n");
    return -1;
  }

  angle = atoi(argv[2]);
  if(angle > 359 || angle < 0){
    printf("ファイル名と回転角度[int](0 - 359)を引数に指定してください\n");
    return -1;
  }

  /* 単位をラジアンへ変換 */
  rad = (double)angle * M_PI / (double)180;

  if(jpegFileReadDecode(&bitmap, argv[1]) == -1){
    printf("jpegFileReadDecode error\n");
    return -1;
  }

  /* 回転後画像の情報設定 */
  rotatedBitmap.width = bitmap.width;
  rotatedBitmap.height = bitmap.height;
  rotatedBitmap.ch = bitmap.ch;

  if(rotatedBitmap.width == 0 || rotatedBitmap.height == 0){
    printf("回転後の幅もしくは高さが0です\n");
    freeBitmapData(&bitmap);
    return -1;
  }

  /* 回転後画像用のメモリ確保 */
  rotatedBitmap.data = (unsigned char*)malloc(sizeof(unsigned char) * rotatedBitmap.width * rotatedBitmap.height * rotatedBitmap.ch);
  if(rotatedBitmap.data == NULL){
    printf("malloc rotatedBitmap error\n");
    freeBitmapData(&bitmap);
    return -1;
  }

   /* 事前に回転後画像の前画素を白色にしておく */
   memset(rotatedBitmap.data, 0xFF, rotatedBitmap.width * rotatedBitmap.height * rotatedBitmap.ch);

   /* ここから画像の回転 */
  for(n = 0; n < rotatedBitmap.height; n++){
    for(m = 0; m < rotatedBitmap.width; m++){
      /* 回転前画像の座標を算出 */
      originalm =
        (m - (int)rotatedBitmap.width / 2) * cos(rad) +
        (n - (int)rotatedBitmap.height / 2) * sin(rad)  + bitmap.width/ 2;
      originaln =
        - (m - (int)rotatedBitmap.width / 2) * sin(rad) +
        (n - (int)rotatedBitmap.height / 2) * cos(rad)  + bitmap.height / 2;

      /* 一番近い座標を四捨五入で算出 */
      m0 = originalm + 0.5;
      n0 = originaln + 0.5;

      /* 画像外にはみ出ている場合はコピーしない */
      if(m0 >= bitmap.width || m0 < 0) continue;
      if(n0 >= bitmap.height || n0 < 0) continue;

      /* 最近傍補間した画素の輝度値をコピー */
      for(c = 0; c < rotatedBitmap.ch; c++){
        rotatedBitmap.data[rotatedBitmap.ch * (m + n * rotatedBitmap.width) + c]
          = bitmap.data[bitmap.ch * (m0 + n0 * bitmap.width) + c];
      }
    }
  }
  /* ここまで画像の回転 */

  sprintf(outname, "%s", "rotated.jpeg");

  if(jpegFileEncodeWrite(&rotatedBitmap, outname) == -1){
    printf("jpegFileEncodeWrite error\n");
    freeBitmapData(&bitmap);
    return -1;
  }

  freeBitmapData(&bitmap);

  return 0;
}


回転に指定する角度(rad)はintになっている.特に問題ないのでこのままでいく.

最上行でincludeしてるmyJpeg.hと必要なmyJpeg.cは次のサイトにある.


一応ここにも貼っておこう.


myJpeg.h

#include <stdio.h>
#include <stdlib.h>

#include "jpeglib.h"

typedef struct{
  unsigned char *data;
  unsigned int width;
  unsigned int height;
  unsigned int ch;
} BITMAPDATA_t;

int jpegFileReadDecode(BITMAPDATA_t *, const char*);
int jpegFileEncodeWrite(BITMAPDATA_t *, const char*);
int freeBitmapData(BITMAPDATA_t *);


myJpeg.c

#include "myJpeg.h"

int jpegFileReadDecode(BITMAPDATA_t *bitmapData, const char* filename){
  struct jpeg_decompress_struct jpeg;
  struct jpeg_error_mgr err;

  FILE *fi;
  int j;
  JSAMPLE *tmp;

  jpeg.err = jpeg_std_error(&err);

  fi = fopen(filename, "rb");
  if(fi == NULL){
    printf("%sは開けません\n", filename);
    return -1;
  }

  jpeg_create_decompress(&jpeg);
  jpeg_stdio_src(&jpeg, fi);
  jpeg_read_header(&jpeg, TRUE);
  jpeg_start_decompress(&jpeg);

  printf("width = %d, height = %d, ch = %d\n", jpeg.output_width, jpeg.output_height, jpeg.out_color_components);

  bitmapData->data =
    (unsigned char*)malloc(sizeof(unsigned char) * jpeg.output_width * jpeg.output_height * jpeg.out_color_components);
  if(bitmapData->data == NULL){
    printf("data malloc error\n");
    fclose(fi);
    jpeg_destroy_decompress(&jpeg);
    return -1;
  }

  for(j = 0; j < jpeg.output_height; j++){
    tmp = bitmapData->data + j * jpeg.out_color_components * jpeg.output_width;
    jpeg_read_scanlines(&jpeg, &tmp, 1);
  }

  bitmapData->height = jpeg.output_height;
  bitmapData->width = jpeg.output_width;
  bitmapData->ch = jpeg.out_color_components;

  jpeg_finish_decompress(&jpeg);
  jpeg_destroy_decompress(&jpeg);

  fclose(fi);

  return 0;
}

int jpegFileEncodeWrite(BITMAPDATA_t *bitmapData, const char *filename){
  struct jpeg_compress_struct jpeg;
  struct jpeg_error_mgr err;
  FILE *fo;
  JSAMPLE *address;
  int j;

  jpeg.err = jpeg_std_error(&err);
  jpeg_create_compress(&jpeg);

  fo = fopen(filename, "wb");
  if(fo == NULL){
    printf("%sは開けません\n", filename);
    jpeg_destroy_compress(&jpeg);
    return -1;
  }

  jpeg_stdio_dest(&jpeg, fo);

  jpeg.image_width = bitmapData->width;
  jpeg.image_height = bitmapData->height;
  jpeg.input_components = bitmapData->ch;
  jpeg.in_color_space = JCS_RGB;
  jpeg_set_defaults(&jpeg);

  jpeg_set_quality(&jpeg, 50, TRUE);

  jpeg_start_compress(&jpeg, TRUE);

  for (j = 0; j < jpeg.image_height; j++ ) {
    address = bitmapData->data + (j * bitmapData->width * bitmapData->ch);
    jpeg_write_scanlines(&jpeg, &address, 1);
  }

  jpeg_finish_compress(&jpeg);

  jpeg_destroy_compress(&jpeg);

  return 0;
}

int freeBitmapData(BITMAPDATA_t *bitmap){
  free(bitmap->data);
  return 0;
}



libjpegのインストール


この記事にやったことはまとめておいた.


実行

イラストはここからフリーのペンライトをjpegの形で適当に取ってきた.


コマンドは以下のようになります.


MacBook-Air Cpract % gcc myJpeg.c -c
MacBook-Air Cpract % gcc rot_mov.c -c
MacBook-Air Cpract % gcc myJpeg.o rot_mov.o 
-ljpeg -o main.exe
 $MacBook-Air Cpract % ./main.exe 13291.jpg 12
0
width = 276, height = 576, ch = 3


フォルダは最終的にこうなった.



rotated.jpegが回転させた後の画像であろう.見てみよう.



回転はできてるが,途切れてる…。



元の画像がこれなので,回転させた後に元のサイズに合わせようとした結果このようになってのであろう.

じゃあ,回転させるときにサイズを変えちゃおう.具体的には,縦と横のサイズを画像の中心から四隅のどこかまでの2倍距離とする.まぁ中心を求めるのも面倒なので対角線の長さを使う.

例えば,縦:3,横:4の長方形サイズだとする.このとき対角線の長さは5なので回転後の画像のサイズは縦:5,横;5の正方形にし,その上に回転させた情報を載せる.


具体的にrot_mov.cで変える場所は

/* 画像外にはみ出ている場合はコピーしない */
      if(m0 >= bitmap.width || m0 < 0) continue;
      if(n0 >= bitmap.height || n0 < 0) continue;


このbitmapというのは回転前の元画像のことで,このサイズ外のところはコピーしないように記述されている.この部分をさっきの対角線(diagonal line)に置き換えてやれば良いだろう.

まずは対角線を宣言して三平方の定理で初期化,それを回転後の画像のサイズとする.

  int diagLine = (int)sqrt(bitmap.width*bitmap.width + bitmap.height*bitmap.height) + 1;
  printf("diagLine:%d, bit.width:%d, bit.height:%d\n", diagLine, bitmap.width, bitmap.height);
  
  /* 回転後画像の情報設定 */
  rotatedBitmap.width = diagLine;
  rotatedBitmap.height = diagLine;
  rotatedBitmap.ch = bitmap.ch;

chは画像の色数(カラーの時は 3、グレーの時は 1)を表すらしい.


その後,さっきのif文を書き換える.

      /* 画像外にはみ出ている場合はコピーしない */
      if(m0 >= diagLine || m0 < 0) continue;
      if(n0 >= diagLine || n0 < 0) continue;


一旦これでどんなものかやってみる.


MacBook-Air Cpract % gcc myJpeg.c -c 
MacBook-Air Cpract % gcc rot_mov.c -c
MacBook-Air Cpract % gcc myJpeg.o rot_mov.o -ljpeg -o main.exe
MacBook-Air Cpract % ./main.exe 13291.jpg 30

width = 276, height = 576, ch = 3
diagLine:639, bit.width:276, bit.height:576

なんか増殖してる….

黒い線は何…???

回転もしてるしサイズの拡張もできているので,コピーしているところの動作を見直せば右側の見切れてる二本目は消せるはず.

一旦黒い線のことがよくわからないので,サイズを拡張したときに白紙状態にできているかを,コピー部分をコメントアウトすることで確認する.

 /* 最近傍補間した画素の輝度値をコピー */
      /*
      for(c = 0; c < rotatedBitmap.ch; c++){
        rotatedBitmap.data[rotatedBitmap.ch * (m + n * rotatedBitmap.width) + c]
          = bitmap.data[bitmap.ch * (m0 + n0 * bitmap.width) + c];
      }
      */


真っ白だ.

じゃぁやっぱりコメントアウトしたあたりを直せば良さそうだ.
でも特に直せそうなところはこには見当たらない.


原因になりそうなのは別のところにあった.
さっき直したif文だ,あそこで範囲を広げちゃったから増殖したのでは???

      if(m0 >= bitmap.width || m0 < 0) continue;
      if(n0 >= bitmap.height || n0 < 0) continue;

に戻してやってみよう.



できた,ビンゴだった.
そしたら書いてあったコメントは誤解を招くな.
「画面外にはみ出ている場合はコピーしない」という書き方だと「回転後のイラストで元の画像のサイズからはみ出していたらコピーしない」のようにしてしまう.(元のコードでは回転後もサイズを変えていないのでよかったが)

黒い線ができていたのは,データは一次元配列で格納されていて,先程のif文の範囲を広げてしまったことでデータが入っているよりも先の添字のところに行ってしまったのが原因だろう.


最終結果

#include "myJpeg.h"
#include <math.h>
#include <string.h>

int main(int argc, char *argv[]){

  BITMAPDATA_t bitmap, rotatedBitmap;
  int m, n, c;
  int angle;
  int m0, n0;
  double originalm, originaln;
  double rad;

  char outname[256];

  if(argc != 3){
    printf("ファイル名と回転角度[int](0 - 359)を引数に指定してください\n");
    return -1;
  }

  angle = atoi(argv[2]);
  if(angle > 359 || angle < 0){
    printf("ファイル名と回転角度[int](0 - 359)を引数に指定してください\n");
    return -1;
  }

  /* 単位をラジアンへ変換 */
  rad = (double)angle * M_PI / (double)180;

  if(jpegFileReadDecode(&bitmap, argv[1]) == -1){
    printf("jpegFileReadDecode error\n");
    return -1;
  }

  int diagLine = (int)sqrt(bitmap.width*bitmap.width + bitmap.height*bitmap.height) + 1;
  printf("diagLine:%d, bit.width:%d, bit.height:%d\n", diagLine, bitmap.width, bitmap.height);
  /* 回転後画像の情報設定 */
  rotatedBitmap.width = diagLine;
  rotatedBitmap.height = diagLine;
  rotatedBitmap.ch = bitmap.ch;

  if(rotatedBitmap.width == 0 || rotatedBitmap.height == 0){
    printf("回転後の幅もしくは高さが0です\n");
    freeBitmapData(&bitmap);
    return -1;
  }

  /* 回転後画像用のメモリ確保 */
  rotatedBitmap.data = (unsigned char*)malloc(sizeof(unsigned char) * rotatedBitmap.width * rotatedBitmap.height * rotatedBitmap.ch);
  if(rotatedBitmap.data == NULL){
    printf("malloc rotatedBitmap error\n");
    freeBitmapData(&bitmap);
    return -1;
  }

   /* 事前に回転後画像の前画素を白色にしておく */
   memset(rotatedBitmap.data, 0xFF, rotatedBitmap.width * rotatedBitmap.height * rotatedBitmap.ch);

   /* ここから画像の回転 */
  for(n = 0; n < rotatedBitmap.height; n++){
    for(m = 0; m < rotatedBitmap.width; m++){
      /* 回転前画像の座標を算出 */
      originalm =
        (m - (int)rotatedBitmap.width / 2) * cos(rad) +
        (n - (int)rotatedBitmap.height / 2) * sin(rad)  + bitmap.width/ 2;
      originaln =
        - (m - (int)rotatedBitmap.width / 2) * sin(rad) +
        (n - (int)rotatedBitmap.height / 2) * cos(rad)  + bitmap.height / 2;

      /* 一番近い座標を四捨五入で算出 */
      m0 = originalm + 0.5;
      n0 = originaln + 0.5;

      
      if(m0 >= bitmap.width || m0 < 0) continue;
      if(n0 >= bitmap.height || n0 < 0) continue;

      /* 最近傍補間した画素の輝度値をコピー */
      
      for(c = 0; c < rotatedBitmap.ch; c++){
        rotatedBitmap.data[rotatedBitmap.ch * (m + n * rotatedBitmap.width) + c]
          = bitmap.data[bitmap.ch * (m0 + n0 * bitmap.width) + c];
      }
      
    }
  }
  /* ここまで画像の回転 */

  sprintf(outname, "%s", "rotated.jpeg");

  if(jpegFileEncodeWrite(&rotatedBitmap, outname) == -1){
    printf("jpegFileEncodeWrite error\n");
    freeBitmapData(&bitmap);
    return -1;
  }

  freeBitmapData(&bitmap);

  return 0;
}


まとめ

画像を回転させることはできた.

でも,まだ想定している「動かす」の一歩でしかない.想定では滑らかでなくてもよいのできちんと左右運動させることになっている.ただ,以上の手順を用いて回転させたものを点々と表示させていけばそれは実現できそうだ.

長くなって来たのでこの記事は一旦ここで締める.
続きは別の記事で書こう.

この記事が気に入ったらサポートをしてみませんか?