web-dev-qa-db-ja.com

GLUT / OpenGLを使用してファイルにレンダリングする方法は?

時間の経過とともに変化する物理システムをシミュレートするプログラムがあります。あらかじめ決められた間隔(たとえば10秒ごと)で、シミュレーションの状態の視覚化をファイルに出力したいと思います。 「ビジュアライゼーションをオフ」にしやすく、ビジュアライゼーションをまったく出力しないようにしたいと思います。

私は、視覚化を行うためのグラフィックツールとしてOpenGLとGLUTを見ています。しかし、問題はまず第一に、ウィンドウに出力するだけでファイルに出力できないように見えることのようです。次に、視覚化を生成するには、GLUTMainLoopを呼び出す必要があります。これにより、メイン関数の実行が停止します。それ以降に呼び出される関数は、GUIからの呼び出しのみです。ただし、これをGUIベースのアプリケーションにする必要はありません。コマンドラインから実行するだけのアプリケーションにして、一連の画像を生成する必要があります。 GLUT/OpenGLでこれを行う方法はありますか?または、OpenGLはこれに対して完全に間違ったツールであり、他のものを使用する必要があります

20
Alex319

とにかく、ほぼ間違いなくGLUTは必要ありません。要件が意図したものに適合していません(要件doが意図した目的に適合している場合でも、通常は必要ありません)。

OpenGLを使用できます。ファイルに出力を生成するには、基本的にOpenGLを設定してテクスチャにレンダリングし、結果のテクスチャをメインメモリに読み込んでファイルに保存します。少なくとも一部のシステム(Windowsなど)では、おそらくおそらくでも、ウィンドウを作成してレンダリングコンテキストをウィンドウに関連付ける必要があると確信しています。ウィンドウが常に非表示になっている場合は問題ありません。

8
Jerry Coffin

glReadPixels実行可能なPBOの例

以下の例では、次のいずれかが生成されます。

  • 200FPSでフレームあたり1ppm、追加の依存関係なし、
  • libpngを使用して600FPSでフレームごとに1つのpng
  • fFmpegを使用した1200FPSでのすべてのフレームに対して1mpg

ramfsに。圧縮率が高いほどFPSが大きくなるため、メモリIOバウンドである必要があります。

60 FPS画面ではFPSが200を超えており、すべての画像が異なるため、画面のFPSに限定されないことは間違いありません。

この回答のGIFは、次の説明に従ってビデオから生成されました: https://askubuntu.com/questions/648603/how-to-create-an-animated-gif-from-mp4-video-via-コマンドライン/ 837574#837574

glReadPixelsは、画面からピクセルを読み取る主要なOpenGL関数です。 init()の下の設定もご覧ください。

glReadPixelsは、ほとんどの画像形式とは異なり、最初にピクセルの一番下の行を読み取るため、通常は変換が必要です。

#ifndef PPM
#define PPM 1
#endif
#ifndef LIBPNG
#define LIBPNG 1
#endif
#ifndef FFMPEG
#define FFMPEG 1
#endif

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

#define GL_GLEXT_PROTOTYPES 1
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glut.h>
#include <GL/glext.h>

#if LIBPNG
#include <png.h>
#endif

#if FFMPEG
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libavutil/opt.h>
#include <libswscale/swscale.h>
#endif

enum Constants { SCREENSHOT_MAX_FILENAME = 256 };
static GLubyte *pixels = NULL;
static GLuint fbo;
static GLuint rbo_color;
static GLuint rbo_depth;
static int offscreen = 1;
static unsigned int max_nframes = 128;
static unsigned int nframes = 0;
static unsigned int time0;
static unsigned int height = 128;
static unsigned int width = 128;
#define PPM_BIT (1 << 0)
#define LIBPNG_BIT (1 << 1)
#define FFMPEG_BIT (1 << 2)
static unsigned int output_formats = PPM_BIT | LIBPNG_BIT | FFMPEG_BIT;

/* Model. */
static double angle;
static double delta_angle;

#if PPM
/* Take screenshot with glReadPixels and save to a file in PPM format.
 *
 * -   filename: file path to save to, without extension
 * -   width: screen width in pixels
 * -   height: screen height in pixels
 * -   pixels: intermediate buffer to avoid repeated mallocs across multiple calls.
 *     Contents of this buffer do not matter. May be NULL, in which case it is initialized.
 *     You must `free` it when you won't be calling this function anymore.
 */
static void screenshot_ppm(const char *filename, unsigned int width,
        unsigned int height, GLubyte **pixels) {
    size_t i, j, cur;
    const size_t format_nchannels = 3;
    FILE *f = fopen(filename, "w");
    fprintf(f, "P3\n%d %d\n%d\n", width, height, 255);
    *pixels = realloc(*pixels, format_nchannels * sizeof(GLubyte) * width * height);
    glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, *pixels);
    for (i = 0; i < height; i++) {
        for (j = 0; j < width; j++) {
            cur = format_nchannels * ((height - i - 1) * width + j);
            fprintf(f, "%3d %3d %3d ", (*pixels)[cur], (*pixels)[cur + 1], (*pixels)[cur + 2]);
        }
        fprintf(f, "\n");
    }
    fclose(f);
}
#endif

#if LIBPNG
/* Adapted from https://github.com/cirosantilli/cpp-cheat/blob/19044698f91fefa9cb75328c44f7a487d336b541/png/open_manipulate_write.c */
static png_byte *png_bytes = NULL;
static png_byte **png_rows = NULL;
static void screenshot_png(const char *filename, unsigned int width, unsigned int height,
        GLubyte **pixels, png_byte **png_bytes, png_byte ***png_rows) {
    size_t i, nvals;
    const size_t format_nchannels = 4;
    FILE *f = fopen(filename, "wb");
    nvals = format_nchannels * width * height;
    *pixels = realloc(*pixels, nvals * sizeof(GLubyte));
    *png_bytes = realloc(*png_bytes, nvals * sizeof(png_byte));
    *png_rows = realloc(*png_rows, height * sizeof(png_byte*));
    glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, *pixels);
    for (i = 0; i < nvals; i++)
        (*png_bytes)[i] = (*pixels)[i];
    for (i = 0; i < height; i++)
        (*png_rows)[height - i - 1] = &(*png_bytes)[i * width * format_nchannels];
    png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    if (!png) abort();
    png_infop info = png_create_info_struct(png);
    if (!info) abort();
    if (setjmp(png_jmpbuf(png))) abort();
    png_init_io(png, f);
    png_set_IHDR(
        png,
        info,
        width,
        height,
        8,
        PNG_COLOR_TYPE_RGBA,
        PNG_INTERLACE_NONE,
        PNG_COMPRESSION_TYPE_DEFAULT,
        PNG_FILTER_TYPE_DEFAULT
    );
    png_write_info(png, info);
    png_write_image(png, *png_rows);
    png_write_end(png, NULL);
    png_destroy_write_struct(&png, &info);
    fclose(f);
}
#endif

#if FFMPEG
/* Adapted from: https://github.com/cirosantilli/cpp-cheat/blob/19044698f91fefa9cb75328c44f7a487d336b541/ffmpeg/encode.c */

static AVCodecContext *c = NULL;
static AVFrame *frame;
static AVPacket pkt;
static FILE *file;
static struct SwsContext *sws_context = NULL;
static uint8_t *rgb = NULL;

static void ffmpeg_encoder_set_frame_yuv_from_rgb(uint8_t *rgb) {
    const int in_linesize[1] = { 4 * c->width };
    sws_context = sws_getCachedContext(sws_context,
            c->width, c->height, AV_PIX_FMT_RGB32,
            c->width, c->height, AV_PIX_FMT_YUV420P,
            0, NULL, NULL, NULL);
    sws_scale(sws_context, (const uint8_t * const *)&rgb, in_linesize, 0,
            c->height, frame->data, frame->linesize);
}

void ffmpeg_encoder_start(const char *filename, int codec_id, int fps, int width, int height) {
    AVCodec *codec;
    int ret;
    avcodec_register_all();
    codec = avcodec_find_encoder(codec_id);
    if (!codec) {
        fprintf(stderr, "Codec not found\n");
        exit(1);
    }
    c = avcodec_alloc_context3(codec);
    if (!c) {
        fprintf(stderr, "Could not allocate video codec context\n");
        exit(1);
    }
    c->bit_rate = 400000;
    c->width = width;
    c->height = height;
    c->time_base.num = 1;
    c->time_base.den = fps;
    c->gop_size = 10;
    c->max_b_frames = 1;
    c->pix_fmt = AV_PIX_FMT_YUV420P;
    if (codec_id == AV_CODEC_ID_H264)
        av_opt_set(c->priv_data, "preset", "slow", 0);
    if (avcodec_open2(c, codec, NULL) < 0) {
        fprintf(stderr, "Could not open codec\n");
        exit(1);
    }
    file = fopen(filename, "wb");
    if (!file) {
        fprintf(stderr, "Could not open %s\n", filename);
        exit(1);
    }
    frame = av_frame_alloc();
    if (!frame) {
        fprintf(stderr, "Could not allocate video frame\n");
        exit(1);
    }
    frame->format = c->pix_fmt;
    frame->width  = c->width;
    frame->height = c->height;
    ret = av_image_alloc(frame->data, frame->linesize, c->width, c->height, c->pix_fmt, 32);
    if (ret < 0) {
        fprintf(stderr, "Could not allocate raw picture buffer\n");
        exit(1);
    }
}

void ffmpeg_encoder_finish(void) {
    uint8_t endcode[] = { 0, 0, 1, 0xb7 };
    int got_output, ret;
    do {
        fflush(stdout);
        ret = avcodec_encode_video2(c, &pkt, NULL, &got_output);
        if (ret < 0) {
            fprintf(stderr, "Error encoding frame\n");
            exit(1);
        }
        if (got_output) {
            fwrite(pkt.data, 1, pkt.size, file);
            av_packet_unref(&pkt);
        }
    } while (got_output);
    fwrite(endcode, 1, sizeof(endcode), file);
    fclose(file);
    avcodec_close(c);
    av_free(c);
    av_freep(&frame->data[0]);
    av_frame_free(&frame);
}

void ffmpeg_encoder_encode_frame(uint8_t *rgb) {
    int ret, got_output;
    ffmpeg_encoder_set_frame_yuv_from_rgb(rgb);
    av_init_packet(&pkt);
    pkt.data = NULL;
    pkt.size = 0;
    ret = avcodec_encode_video2(c, &pkt, frame, &got_output);
    if (ret < 0) {
        fprintf(stderr, "Error encoding frame\n");
        exit(1);
    }
    if (got_output) {
        fwrite(pkt.data, 1, pkt.size, file);
        av_packet_unref(&pkt);
    }
}

void ffmpeg_encoder_glread_rgb(uint8_t **rgb, GLubyte **pixels, unsigned int width, unsigned int height) {
    size_t i, j, k, cur_gl, cur_rgb, nvals;
    const size_t format_nchannels = 4;
    nvals = format_nchannels * width * height;
    *pixels = realloc(*pixels, nvals * sizeof(GLubyte));
    *rgb = realloc(*rgb, nvals * sizeof(uint8_t));
    /* Get RGBA to align to 32 bits instead of just 24 for RGB. May be faster for FFmpeg. */
    glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, *pixels);
    for (i = 0; i < height; i++) {
        for (j = 0; j < width; j++) {
            cur_gl  = format_nchannels * (width * (height - i - 1) + j);
            cur_rgb = format_nchannels * (width * i + j);
            for (k = 0; k < format_nchannels; k++)
                (*rgb)[cur_rgb + k] = (*pixels)[cur_gl + k];
        }
    }
}
#endif

static void model_init(void) {
    angle = 0;
    delta_angle = 1;
}

static int model_update(void) {
    angle += delta_angle;
    return 0;
}

static int model_finished(void) {
    return nframes >= max_nframes;
}

static void init(void)  {
    int glget;

    if (offscreen) {
        /*  Framebuffer */
        glGenFramebuffers(1, &fbo);
        glBindFramebuffer(GL_FRAMEBUFFER, fbo);

        /* Color renderbuffer. */
        glGenRenderbuffers(1, &rbo_color);
        glBindRenderbuffer(GL_RENDERBUFFER, rbo_color);
        /* Storage must be one of: */
        /* GL_RGBA4, GL_RGB565, GL_RGB5_A1, GL_DEPTH_COMPONENT16, GL_STENCIL_INDEX8. */
        glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB565, width, height);
        glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo_color);

        /* Depth renderbuffer. */
        glGenRenderbuffers(1, &rbo_depth);
        glBindRenderbuffer(GL_RENDERBUFFER, rbo_depth);
        glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height);
        glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rbo_depth);

        glReadBuffer(GL_COLOR_ATTACHMENT0);

        /* Sanity check. */
        assert(glCheckFramebufferStatus(GL_FRAMEBUFFER));
        glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &glget);
        assert(width < (unsigned int)glget);
        assert(height < (unsigned int)glget);
    } else {
        glReadBuffer(GL_BACK);
    }

    glClearColor(0.0, 0.0, 0.0, 0.0);
    glEnable(GL_DEPTH_TEST);
    glPixelStorei(GL_PACK_ALIGNMENT, 1);
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);

    time0 = glutGet(GLUT_ELAPSED_TIME);
    model_init();
#if FFMPEG
    ffmpeg_encoder_start("tmp.mpg", AV_CODEC_ID_MPEG1VIDEO, 25, width, height);
#endif
}

static void deinit(void)  {
    printf("FPS = %f\n", 1000.0 * nframes / (double)(glutGet(GLUT_ELAPSED_TIME) - time0));
    free(pixels);
#if LIBPNG
    if (output_formats & LIBPNG_BIT) {
        free(png_bytes);
        free(png_rows);
    }
#endif
#if FFMPEG
    if (output_formats & FFMPEG_BIT) {
        ffmpeg_encoder_finish();
        free(rgb);
    }
#endif
    if (offscreen) {
        glDeleteFramebuffers(1, &fbo);
        glDeleteRenderbuffers(1, &rbo_color);
        glDeleteRenderbuffers(1, &rbo_depth);
    }
}

static void draw_scene(void) {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
    glRotatef(angle, 0.0f, 0.0f, -1.0f);
    glBegin(GL_TRIANGLES);
    glColor3f(1.0f, 0.0f, 0.0f);
    glVertex3f( 0.0f,  0.5f, 0.0f);
    glColor3f(0.0f, 1.0f, 0.0f);
    glVertex3f(-0.5f, -0.5f, 0.0f);
    glColor3f(0.0f, 0.0f, 1.0f);
    glVertex3f( 0.5f, -0.5f, 0.0f);
    glEnd();
}

static void display(void) {
    char filename[SCREENSHOT_MAX_FILENAME];
    draw_scene();
    if (offscreen) {
        glFlush();
    } else {
        glutSwapBuffers();
    }
#if PPM
    if (output_formats & PPM_BIT) {
        snprintf(filename, SCREENSHOT_MAX_FILENAME, "tmp.%d.ppm", nframes);
        screenshot_ppm(filename, width, height, &pixels);
    }
#endif
#if LIBPNG
    if (output_formats & LIBPNG_BIT) {
        snprintf(filename, SCREENSHOT_MAX_FILENAME, "tmp.%d.png", nframes);
        screenshot_png(filename, width, height, &pixels, &png_bytes, &png_rows);
    }
#endif
# if FFMPEG
    if (output_formats & FFMPEG_BIT) {
        frame->pts = nframes;
        ffmpeg_encoder_glread_rgb(&rgb, &pixels, width, height);
        ffmpeg_encoder_encode_frame(rgb);
    }
#endif
    nframes++;
    if (model_finished())
        exit(EXIT_SUCCESS);
}

static void idle(void) {
    while (model_update());
    glutPostRedisplay();
}

int main(int argc, char **argv) {
    int arg;
    GLint glut_display;

    /* CLI args. */
    glutInit(&argc, argv);
    arg = 1;
    if (argc > arg) {
        offscreen = (argv[arg][0] == '1');
    } else {
        offscreen = 1;
    }
    arg++;
    if (argc > arg) {
        max_nframes = strtoumax(argv[arg], NULL, 10);
    }
    arg++;
    if (argc > arg) {
        width = strtoumax(argv[arg], NULL, 10);
    }
    arg++;
    if (argc > arg) {
        height = strtoumax(argv[arg], NULL, 10);
    }
    arg++;
    if (argc > arg) {
        output_formats = strtoumax(argv[arg], NULL, 10);
    }

    /* Work. */
    if (offscreen) {
        /* TODO: if we use anything smaller than the window, it only renders a smaller version of things. */
        /*glutInitWindowSize(50, 50);*/
        glutInitWindowSize(width, height);
        glut_display = GLUT_SINGLE;
    } else {
        glutInitWindowSize(width, height);
        glutInitWindowPosition(100, 100);
        glut_display = GLUT_DOUBLE;
    }
    glutInitDisplayMode(glut_display | GLUT_RGBA | GLUT_DEPTH);
    glutCreateWindow(argv[0]);
    if (offscreen) {
        /* TODO: if we hide the window the program blocks. */
        /*glutHideWindow();*/
    }
    init();
    glutDisplayFunc(display);
    glutIdleFunc(idle);
    atexit(deinit);
    glutMainLoop();
    return EXIT_SUCCESS;
}

GitHub上

コンパイル:

Sudo apt-get install libpng-dev libavcodec-dev libavutil-dev
gcc -DPPM=1 -DLIBPNG=1 -DFFMPEG=1 -ggdb3 -std=c99 -O0 -Wall -Wextra \
  -o offscreen offscreen.c -lGL -lGLU -lglut -lpng -lavcodec -lswscale -lavutil

サイズ200x 100およびすべての出力形式で10フレームを「オフスクリーン」で実行します(ほとんどはTODOで、機能しますが、利点はありません)。

./offscreen 1 10 200 100 7

CLI形式は次のとおりです。

./offscreen [offscreen [nframes [width [height [output_formats]]]]]

およびoutput_formatsはビットマスクです:

ppm >> 0 | png >> 1 | mpeg >> 2

画面上で実行します(FPSも制限しません):

./offscreen 0

Ubuntu 15.10、OpenGL 4.4.0 NVIDIA 352.63、Lenovo ThinkpadT430でベンチマーク。

Ubuntu 18.04、OpenGL 4.6.0 NVIDIA 390.77、Lenovo ThinkpadP51でもテスト済み。

TODO:GUIのない​​マシン(X11など)でそれを行う方法を見つけます。 OpenGLはオフスクリーンレンダリング用に作成されていないようで、GPUへのピクセルの読み取りはウィンドウシステムとのインターフェイスに実装されています(例: [〜#〜] glx [〜#〜] ) 。参照: LinuxのX.orgなしのOpenGL

TODO:1x1ウィンドウを使用し、サイズを変更できないようにし、非表示にして物事をより堅牢にします。これらのいずれかを実行すると、レンダリングが失敗します。コードコメントを参照してください。サイズ変更の防止は Glutでは不可能 のようですが、 GLFWはそれをサポートしています 。いずれにせよ、offscreenがオフの場合でも、FPSは画面の更新頻度によって制限されないため、これらはそれほど重要ではありません。

PBO以外のオプション

  • バックバッファにレンダリング(デフォルトのレンダリング場所)
  • テクスチャにレンダリングする
  • Pixelbufferオブジェクト(PBO)にレンダリングします

FramebufferPixelbufferは、データをCPUに読み戻すために作成されているため、バックバッファーとテクスチャよりも優れていますが、バックバッファーとテクスチャはGPUに残り、画面に表示されるように作成されています。

PBOは非同期転送用なので、必要ないと思います。以下を参照してください。 OpenGLのフレームバッファーオブジェクトとピクセルバッファーオブジェクトの違いは何ですか?

たぶん、画面外のMesaは調べる価値があります: http://www.mesa3d.org/osmesa.html

apiretrace

https://github.com/apitrace/apitrace

動作するだけで、コードを変更する必要はまったくありません。

git clone https://github.com/apitrace/apitrace
cd apitrace
git checkout 7.0
mkdir build
cd build
cmake ..
make
# Creates opengl_executable.out.trace
./apitrace trace /path/to/opengl_executable.out
./apitrace dump-images opengl_executable.out.trace

Ubuntu18.10でも利用できます。

 Sudo apt-get install apitrace

これで、次の名前のスクリーンショットがたくさんあります。

animation.out.<n>.png

TODO:動作原理。

ドキュメントはまた、ビデオのためにこれを提案します:

apitrace dump-images -o - application.trace \
  | ffmpeg -r 30 -f image2pipe -vcodec ppm -i pipe: -vcodec mpeg4 -y output.mp4

参照:

バルカン

Vulkanは、OpenGLよりも優れたオフスクリーンレンダリングをサポートするように設計されているようです。

これは、このNVIDIAの概要で言及されています: https://developer.nvidia.com/transitioning-opengl-vulkan

実行可能な例があります: https://github.com/SaschaWillems/Vulkan/blob/0616eeff4e697e4cd23cb9c97f5dd83afb79d908/offscreen/offscreen.cpp しかし、私はまだVulkanを実行することに成功していません。 1 kloc :-)

関連: VulkanでSurfaceなしでオフスクリーンレンダリングを行うことは可能ですか?

WebGL +キャンバス画像の保存

これは主にパフォーマンス上のおもちゃですが、実際に基本的なユースケースで機能します。

参考文献

ウィンドウより大きいFBO:

ウィンドウなし/ X11:

他の優れた答えを損なうことはありませんが、既存の例が必要な場合は、テストフレームワークのレンダリングの一部として、OpenSCADで数年間オフスクリーンGLレンダリングを行っています。コマンドラインからのpngファイル。関連ファイルは https://github.com/openscad/openscad/tree/master/src Offscreen * .ccの下にあります。

OSX(CGL)、Linux X11(GLX)、BSD(GLX)、およびWindows(WGL)で動作しますが、ドライバーの違いによるいくつかの癖があります。基本的なトリックは、窓を開けることを忘れることです(たとえば、ダグラスアダムズは、飛行の秘訣は地面にぶつかることを忘れることだと言っています)。 XvfbやXvncのように仮想X11サーバーを実行している場合は、「ヘッドレス」linux/bsdでも実行できます。プログラムを実行する前に環境変数LIBGL_ALWAYS_SOFTWARE = 1を設定することにより、Linux/BSDでソフトウェアレンダリングを使用する可能性もあります。これは状況によっては役立ちます。

これを行うシステムはこれだけではありません。VTKイメージングシステムも同様のことをしていると思います。

このコードは、特に新しいシステム(OSMesa、Mir、Wayland、EGL、Android、Vulkanなど)が登場するにつれて、そのメソッドでは少し古いです(GLXコードをBrian Paulのglxgearsからリッピングしました)が、OffscreenXXX.ccに注意してください。 XXXがGLコンテキストサブシステムであるファイル名。理論的には、さまざまなコンテキストジェネレーターに移植できます。

2
don bright

SFML http://www.sfml-dev.org/ を使用できます。 imageクラスを使用して、レンダリングされた出力を保存できます。

http://www.sfml-dev.org/documentation/1.6/classsf_1_1Image.htm

レンダリングされた出力を取得するには、テクスチャにレンダリングするか、画面をコピーします。

テクスチャへのレンダリング:

画面出力のコピー:

1
HyperCas

OpenGLが最適なソリューションかどうかはわかりません。
ただし、いつでもオフスクリーンバッファにレンダリングできます。

OpenGL出力をファイルに書き込む一般的な方法は、readPixelsを使用して、結果のシーンのピクセルピクセルを画像ファイルにコピーすることです。

1
Martin Beckett