Moiz's journal

プログラミングやFPGAなどの技術系の趣味に関するブログです

RISCVエミュレータ-ELFファイルの実行

はじめに

前回前々回のエントリーの続きです。

ゆっくりとRISCVエミュレータを作っています。

命令の追加

RV32Iの命令の殆ど(SRETとWFI以外)を処理できるようになりました。 ただし、システムレジスタ系の命令(CSRR*)は、読み書きはできるものの内部状態は実装されていません。 同じ理由でMRETも、権限の変更は行いません。

システムコールエミュレーション

ECALL命令ではシステムコールのエミュレーションを実行します。今実行できるのは以下の2つです。

システムコール 番号 処理
exit 93 終了
write 64 ファイルへの書き出し

fstat等の実装がまだなので、writeが出力するのは標準出力だけです。

ELFファイルの読み込み

簡単なELFファイルを読みこんで実行できるようになりました。 これで普通のC言語でRISCVプログラムを書いて、GCCでクロスコンパイルして、RISCVエミュレータで実行する事ができます。

RISCV用のクロスコンパイル環境はriscv-gnu-toolchainを使用しました。

github.com

実行できる命令が限られているので、このエミュレータ用のコンフィギュレーションにするには、toolのビルドのときに、

./configure --prefix=/opt/riscv --with-arch=rv32i --with-abi=ilp32

を指定して、I命令とILP32のABIを指定する必要があります。

これで、例えばこんなCプログラムが実行できます。

#include <unistd.h>

int main()
{
  write(1, "Hello, RISCV\n", 13);
  return 0;
}

コンパイルするときはriscv用コンパイラを使って、RV32IとILP32を指定して、ライブラリを静的にリンクする必要があります。 例えばこうなります。(gccにパスが通っている前提です。)

$ riscv32-unknown-elf-gcc hello.c -o hello -Wall -march=rv32i -mabi=ilp32 -static

これでhelloという名前のELF形式実行ファイルが作られます。

ちなみに-Sオプションをつけてコンパイルするとこんなアセンブラコードになります。

        .file   "hello.c"
        .option nopic
        .attribute arch, "rv32i2p0"
        .attribute unaligned_access, 0
        .attribute stack_align, 16
        .text
        .section        .rodata
        .align  2
.LC0:
        .string "Hello, RISCV\n"
        .text
        .align  2
        .globl  main
        .type   main, @function
main:
        addi    sp,sp,-16
        sw      ra,12(sp)
        sw      s0,8(sp)
        addi    s0,sp,16
        li      a2,13
        lui     a5,%hi(.LC0)
        addi    a1,a5,%lo(.LC0)
        li      a0,1
        call    write
        li      a5,0
        mv      a0,a5
        lw      ra,12(sp)
        lw      s0,8(sp)
        addi    sp,sp,16
        jr      ra
        .size   main, .-main
        .ident  "GCC: (GNU) 9.2.0"

こちらが実行結果です。

$ cmake-build-debug/RISCV_Emulator hello
Elf file name: hello
This is an Elf file
Program Header 0:Type: LOAD. Copy to 0x10000 from 0x0, size 5270. 
Memory size extended to 100000
Loaded
Program Header 1:Type: LOAD. Copy to 0x12498 from 0x1498, size 2132. Loaded
Section .bss found at 0x01cec.
Secure BSS.
Entry point is 0x10090
Section .symtab(2) found at 0x01d1c.
Number of symbols = 92, (1472 bytes)
Section .strtab found at 0x022dc.
Symbol "__global_pointer$" found at index 34.
Global Pointer Value = 0x12ca8.

Memory size extended to 800000
Execution start
Hello, RISCV
Return value: 0.

いろいろエミュレータが生成したメッセージに紛れていますが、最後に"Hello, RISCV"と表示されています。成功です。

この例ではwrite以外のシステムコールを使わずに済むように、write()を使って出力させましたが、printf()でも動作するようです。

RISCV-TESTSの実行。

前回のエントリでも触れましたが、RISCVにはRISCV-TESTSという名前の標準テストがあります。

github.com

このテストのうち、rv32ui(RV32ユーザーレベル、整数命令のみ)を実行してみたところ、LH命令にサイン拡張のバグが見つかりました。

テスト重要ですね。

他にも何個か見つかったバグを修正し、すべてのテストがパスするようになりました。

まとめ

RISCVのエミュレータC++で書いて、簡単なプログラムのELF形式実行ファイルを処理できるようになりました。

また、RISCV-TESTSのユーザーレベル整数命令テストをパスすることを確認しました。

このエミュレータのコードは私のgitリポジトリで公開しています。

github.com

RISCVエミュレータの途中経過

はじめに

前回のエントリーの続きです。

ちょっと今週来週雑用で作業ができなそうなので、忘れないように自分用の進捗メモです。 またしても読者の方々の事はあまり考えていません。

わざわざページ開いてくださった方には、もうしわけありません。いつかまとめてエントリーかきます。

このプロジェクトについて

RISC-Vのエミュレータをゆっくり作っています。最終目標はFPGA上で動作させることです。

github.com

すでに同様のものが多数あるのは知っていますが、自分の趣味と学習が目的ですので特に気にしていません。

命令の追加

命令を多数追加しました。いま実装されている命令は次のとおりです。

命令 タイプ
add rd, rs1, rs2 R
addi rd, rs1, im12 I
and rd, rs1, rs2 R
andi rd, rs1, imm12 I
beq rs1, rs2, offset B
bge rs1, rs2, offset B
bltu rs1, rs2, offset B
bne B
jal rd, offset21 J
jalr rd, offset(rs1) I
lw rd, offset(rs1) I
lui rd, imm24 U
or rd, rs1, rs2 R
ori rd, rs1, imm12 I
sll rd, rs1, rs2 R
slli rd, rs1, shamt I
slt rd, rs1, rs2 R
slti rd, rs1, imm12 I
sltiu rd, rs1, imm12 I
sltu rd, rs1, imm12 R
sra rd, rs1, imm12 R
srai rd, rs1, imm12 I
srl rd, rs1, imm12 R
srli rd, rs1, imm12 I
sub rd, rs1, rs2 R
xor rd, rs1, rs2 R
xori rd, rs1, imm12 I

結構増えました! RV32Iの50命令中、22個実装しています。

実行できるようになったもの。

命令を増やしたので、実行できるアルゴリズムが増えました。 例えばRISC-V原典にのっているインサートソートが実行できます。

// A1 is n and A3 points to array[0]
// A4 is i, A5 is j, A6 is x
addi A3, A0, 4
addi A4, ZERO, 1
// Outer Loop
bltu A4, A1, 8
jalr ZERO, RA, 0
lw A6, A3, 0
addi A2, A3, 0
addi A5, A4, 0
// Inner Loop
lw A7, A2, -4
bgte A6, A7, 20
sw A2, A7, 0
addi A5, A5, -1
addi A2, A2, -4
bne A5, ZERO, -20
// End of Inner Loop
slli A5, A5, 2
add A5, A0, A5
sw A5, A6, 0
addi A4, A4, 1
addi A3, A3, 4
jal ZERO, -64
// End of Outer Loop

レジスターA3にソートする配列へのポインター、A1にソートする要素の数を代入して読み出すとソートしてくれます。 こちら動作結果です。

Before: 116 211 664 486 472 777 429 528 290 433 136 777 502 357 160 481 731 675 802 671 427 21 949 350 997 985 960 483 836 987 126 305 551 790 791 375 919 220 255 209 6 392 339 508 101 499 989 832 174 791 856 953 164 805 655 161 142 616 644 331 955 122 636 506 265 427 233 184 999 489 746 357 881 85 865 982 584 207 167 110 998 23 415 515 180 71 676 675 39 673 6 994 795 642 853 412 421 86 949 772

After: 6 6 21 23 39 71 85 86 101 110 116 122 126 136 142 160 161 164 167 174 180 184 207 209 211 220 233 255 265 290 305 331 339 350 357 357 375 392 412 415 421 427 427 429 433 472 481 483 486 489 499 502 506 508 515 528 551 584 616 636 642 644 655 664 671 673 675 675 676 731 746 772 777 777 790 791 791 795 802 805 832 836 853 856 865 881 919 949 949 953 955 960 982 985 987 989 994 997 998 999

なんだかCPUの動作っぽくなってきました。

テストの追加

複雑になってきたのでテストを追加しました。

今回のコードではテストは主に、アセンブラ作成部分のテストと、CPUの動作のテストに分かれています。

アセンブラ部分のテスト

アセンブラ作成部分では一度符号拡張で見つけにくいエラーをだしてしまったのでカバレッジを上げることにしました。 具体的には以下の部分をランダム化したテストを各命令毎に作成しました。

  • rd
  • rs1
  • rs2
  • immediate

命令タイプによって存在しないものは除きます。 テストの内容は、

  1. ランダム化したパラメータを作成
  2. ランダム化したパラメータからバイナリ命令を作成
  3. アセンブラ機能を使ってパラーメータからバイナリ命令を作成
  4. 2で作成したものと3で作成したものを比較。

というものです。 3でテストを作成したのも自分自身なので、同じ誤解を2回している可能性はありますが、そうではないテクニカルな失敗はかなり防げます。

このテストは実装した全命令に対してそれぞれ100回ずつ行っています。

CPU部分のテスト

CPU側では上記のような命令ごとのユニットテストは難しいのですが、なるべくそれに近いものを作っています。 たとえば、add rd, rs1, rs2のテストでは、次のようなバイナリー・コードを生成します。

addi rs1, ZERO, value1 & 0xFFF
lui rs1, (value1 >> 12)
addi rs2, ZERO, value2 & 0xFFF
lui rs2, (value2 >> 12)
add rd, rs1, rs2
addi A0, rd, 0
jalr ZERO, RA, 0

ここで、value1, value2, rd, rs1, rs2はランダム化されたテストパラメータです。

これでadd rd, rs1, rs2に与えるパラメータ(レジスタの組み合わせ、レジスタの内容)のカバレージはかなり上がると思います。

ランダム化したテストはそれぞれ100回ずつ行っています。

また、同様のテストは他のR Typeの命令(cmd rd, rs1, rs2形式のもの)や、addiなどI Typeの命令(cmd rd, rs1, imm12形式のもの)でも行いました。

これらのユニットテストの他に、前回の1から10まで足し合わせるプログラムと、前述のソートプログラムをテストとして、コードを変更するたびに実行しています。

次のステップ

  • Bタイプ命令(blt等)へのランダム化テストの追加
  • LD/SW命令等の追加(+テスト)
  • 外部ファイル読み取り
  • 最小限のシステムコールのエミュレーション(テキストの表示くらいしたい)
  • システム系命令の対応
  • テストの自動化
  • MMUとか割り込みとかどうしよう(最終目標はハードウェア実装なので)

先は長いですね。

追記

記事のアップロード後早速こんな有用なコメントをいただきました。ありがとうございます。

当該のテストはこちらです。今後のTODOにいれておきます。

https://twitter.com/LDScell/status/1175905081050329088?s=20 github.com

RISCVエミュレータを作り始めた

はじめに

RISCVエミュレータを作り始めました。このエントリーはそのメモです。 個人的なメモなので、読者の方の事はまだあまり考えていません。

ある程度めどがたったらもうちょっと読みやすいエントリーを書きます。

目標

(完全な車輪の再発明ですが、個人の趣味ですので。)

参考図書

リポジトリ

https://github.com/moizumi99/m99_riscv_emulator

作業

  • 「動かしてわかるCPUの作り方10講」(以下CPU10講)のエミュレータを元に、雛形を作成。

    • ターゲット言語をC++に変更。
    • 拡張に備えてファイルを分割

      • RISCV_Emulator.cc (メイン)
      • RISCV_cpu.cc (エミュレータ本体)
      • load_assembler.cc (アセンブラ・コードの作成・ロード)
      • instruction_encdec.cc (機械語を32bitにパックするためのコード)
      • bit_tools.cc (ビットの切り出し等用のユーティリティ)
      • load_assembler_test.cc (load_assembler.cc用のテスト)
    • 拡張のための変更

      • 32bit命令のために配列をshortからuint32_tに変更
      • メモリ配列を大きくして、romとramを共通化
      • レジスタを32個に拡張
      • その他いろいろ(Makefileの作成など)
  • RISC-V 命令対応への変更

最初に使用する命令の選定

まずは「動かしてわかるCPUの作り方10講」で使われている1から10まで足し合わせるアセンブリコードを再現したい。

  • 「動かしてわかるCPUの作り方10講」のCPUの命令と、RISCVの命令(RV32I)の対応
CPU10講CPU CPU10講動作 対応するRISC-V命令
mov RegA <- RegB add rd, rs1, zeroで代用。(zeroはゼロレジスタ)
add RegA <- RegA + RegB add rd, rs1, rs2 (x[rd] <- x[rs1] + x[rs2])
ldl RegA(Low) <- Data (8bit) addi rd, zero, immediate (x[rd] <- zero + immeidate)で代用
ldh RegA(High) <- Data (8bit) 今回はスキップ。必要ならlui (Load Upper Immediateを実装)
cmp flag <- (RegA == RegB) 今回はスキップ(次のbeqで代用)
je pc <- Addr if flag==1 beq rs1, rs2, offset (pc += offset if rs1 == rs2)で代用
jmp pc <- Addr jal x0, offset (pc += offset)
ld RegA <- M[Addr] 今回はスキップ(レジスタを使うので)
st M[Addr] <- RegA 今回はスキップ(レジスタを使うので)
hlt 停止 ret (実体はjalr x0, x1, 0)で代用。x1は戻りアドレス

とりあえず、このエミュレータはret命令で停止するものとします。

それぞれの命令のラベルとタイプ、ファンクションのリストです。

命令 タイプ ラベル funct7 or 6 funct3
add R 0110011 0000000 000
addi I 0010011 NA 000
sub R 0110011 0100000 000
and R 0110011 0000000 111
or R 0110011 0000000 110
slli I 0010011 000000 001
srli I 0010011 000000 101
srai I 0010011 010000 101
beq B 1100011 NA 000
jal J 1101111 NA NA
jalr I 1100111 NA NA

funct6という名称は実際にはありませんが、immediateの上位6ビットが固定になっている部分を便宜上このように表記しました。

アセンブリコードの変更

まずは「動かしてわかるCPUの作り方10講」のサンプルコードを変換してみます。

# t0: カウンタ(初期値0)
addi t0, zero, 0
# t1; 和(初期値0)
addi t1, zero, 0
# t2: 上限(10)
addi t2, zero, 10
# ループ先頭
# t0 = t0 + 1
addi t0, t0, 1
# t1 = t1 + t0
add t1, t1, t0
# t0 = t2 なら+ 8ジャンプ
beq t0, t2, 8
# -16 (4命令バック)ジャンプ
jal x0, -16
# A0 (返り値を保存するレジスタに結果を移動)
add a0, t1, x0
# リターン(a0は戻り値)
jalr x0, ra, 0

実際に使われているのは、addi, add, beq, jal, jalだけなのでこれらの命令を優先して実装します。

命令のエンコーダー

まずはCPUに渡すバイナリを作る必要があります。

RISC-V原典を眺めながら、各命令のopcode、funct3、funct7、レジスタ番号、即値を32bitに埋め込んでいくコードを書きます。

RISC-V(RV32I)の命令は次の6つのタイプにわかれます。

  • R-type
  • I-type
  • S-type
  • B-type
  • U-type
  • J-type

それぞれフォーマットが異なり、タイプごとに引数の種類も異なります。

そこで必要な命令毎に

  1. タイプを判断しフォーマットを選択
  2. opcodeとfunct3、funct7を32bit内に埋め込む
  3. 必要な引数(rd, rs1, rs2, immediate )を判断
  4. 引数の情報を32bit内の指定された位置に埋め込む

というコードを書きます。

RISC-Vでは、何故か一部の即値のビット位置が入れ替わっていて、これを入れ替えるのが大変でした。C/C++でのビット演算に苦労していると、こんなアドバイスをいただきました。

喜んで使わせていただきます。これでいちいちマスクする苦労から開放されました。

ビットフィールドを使って、それぞれの要素(レジスタ番号、即値、opcode等)をクラスメンバーとして定義します。

例えばこんな感じです。

class bitfield {
  public:
    virtual uint32_t value() { return 0; };
    virtual void set_value(uint32_t value) {};

    uint8_t opcode: 7;
    uint8_t rd: 5;
    uint8_t rs2: 5;
    uint8_t rs1: 5;
    uint8_t funct3: 3;
};

class r_type : public bitfield {
  public:
    uint8_t funct7: 7;
    uint32_t value();
    void set_value(uint32_t value);
};

最初はunionとstructを使って、こんな感じで書いていたのですが、

union r_type {
    uint32_t value;
    struct {
        uint8_t funct7 : 7;
        uint8_t rs2 : 5;
        uint8_t rs1 : 5;
        uint8_t funct3 : 3;
        uint8_t rd : 5;
        uint8_t opcode : 7;
    };
};

パッキングやパディングが不安なのでとりあえずクラスにしています。 アトリビュートを使ってうまくいくようならこの方法にもどそうと思います。

機械語を生成する部分は、CPU10講にならって決め打ちのバイナリを生成するコードを書きました。

void load_assembler(uint32_t *mem) {
  mem[0] = asm_addi(T0, ZERO, 0);
  mem[1] = asm_addi(T1, ZERO, 0);
  mem[2] = asm_addi(T2, ZERO, 10);
  mem[3] = asm_addi(T0, T0, 1);
  mem[4] = asm_add(T1, T1, T0);
  mem[5] = asm_beq(T0, T2, 8);
  mem[6] = asm_jal(ZERO, -16);
  mem[7] = asm_add(A0, T1, ZERO);
  mem[8] = asm_jalr(ZERO, RA, 0);
}

これで実行対象機械語コードが用意できました。次はいよいよCPUエミュレータ側のコードです。

命令のデコード

命令のデコードはCPU10講の内容を参考に、RISC-V用に次の用なものにしました。

  1. 下7bitのオペコード(opcode)を読み込む
  2. 必要があれば、14:12ビットのfunct3を読み込む
  3. 必要があれば、31:25ビットのfunct7を読み込む(今回は未実装)
  4. opcode、funct3とfunct7から命令の種類を判定
  5. 必要に応じてrd(ディスティネーションレジスタ)、rs1(対象レジスタ1)、rs2(対象レジスタ2)をデコード
  6. 必要に応じて即値(12bit, 13bit, 21bit, 24bitの4種類がある)をデコード

さきほどシャッフルした即値のビットを戻す作業が面倒ですが、それ以外は単に対応する位置にあるビットを切り出すだけです。

命令の実行

今回選んだ命令に関しては、命令の実行はCPU10講と似た形式です。大きな違いは

  • 命令種類が減った
  • ゼロ番目のレジスタはゼロレジスタ
  • ジャンプが相対ジャンプになった
  • ret命令で動作終了

程度です

その他

実は今回一番複雑だったのは機械語を作成する部分、次がデコーダーです。複雑なので最初からテストを(割と)ちゃんと書きました。 各アセンブラの命令毎に、2〜3の命令例が正しいバイナリになっているか、またバイナリが正しくデコードされているのかを確認しました。コードはload_assembler_test.ccにあります。実は今回一番時間がかかっている部分かもしれません。

ファイルが増えたのでMakefileを作成しておきました。

実行結果

では実行してみます。

$ ./RISCV_Emulator 
Assembler set.
Execution start
   PC    Binary     T0     T1     T2     T3     A0
    0  00000293      0      0      0      0      0
    4  00000313      0      0      0      0      0
    8  00a00393      0      0      0      0      0
   12  00128293      0      0     10      0      0
   16  00530333      1      0     10      0      0
   20  00728463      1      1     10      0      0
   24  ff1ff06f      1      1     10      0      0
    8  00a00393      1      1     10      0      0
   12  00128293      1      1     10      0      0
   16  00530333      2      1     10      0      0
   20  00728463      2      3     10      0      0
   24  ff1ff06f      2      3     10      0      0
    8  00a00393      2      3     10      0      0
   12  00128293      2      3     10      0      0
   16  00530333      3      3     10      0      0
   20  00728463      3      6     10      0      0
   24  ff1ff06f      3      6     10      0      0
    8  00a00393      3      6     10      0      0
   12  00128293      3      6     10      0      0
   16  00530333      4      6     10      0      0
   20  00728463      4     10     10      0      0
   24  ff1ff06f      4     10     10      0      0
    8  00a00393      4     10     10      0      0
   12  00128293      4     10     10      0      0
   16  00530333      5     10     10      0      0
   20  00728463      5     15     10      0      0
   24  ff1ff06f      5     15     10      0      0
    8  00a00393      5     15     10      0      0
   12  00128293      5     15     10      0      0
   16  00530333      6     15     10      0      0
   20  00728463      6     21     10      0      0
   24  ff1ff06f      6     21     10      0      0
    8  00a00393      6     21     10      0      0
   12  00128293      6     21     10      0      0
   16  00530333      7     21     10      0      0
   20  00728463      7     28     10      0      0
   24  ff1ff06f      7     28     10      0      0
    8  00a00393      7     28     10      0      0
   12  00128293      7     28     10      0      0
   16  00530333      8     28     10      0      0
   20  00728463      8     36     10      0      0
   24  ff1ff06f      8     36     10      0      0
    8  00a00393      8     36     10      0      0
   12  00128293      8     36     10      0      0
   16  00530333      9     36     10      0      0
   20  00728463      9     45     10      0      0
   24  ff1ff06f      9     45     10      0      0
    8  00a00393      9     45     10      0      0
   12  00128293      9     45     10      0      0
   16  00530333     10     45     10      0      0
   20  00728463     10     55     10      0      0
   28  00030533     10     55     10      0      0
   32  00008067     10     55     10      0     55
Return value: 55

実行できました!

今後の課題

次のステップですが、

  • テスト用コードのリファクタリング(今は手作業なので命令が増えると大変)
  • ロードストア等、対応する命令を増やす
  • メモリ領域を増やす
  • 外部プログラムファイルの読み込み(できればelf形式で)

というあたりを考えています。

最終目標はFPGAで動作するCPUをRTLで書くことなのですが、これは年単位の目標です。

【技術書典6】Colabから印刷所に出せる原稿を作るワークフロー

Colabから印刷所に出せる原稿を作るワークフロー

はじめに

先日行われた技術書典6で「PythonとColabでつくる-ゼロから作るRAW現像」をid:uchan_nosさんのブースで頒布していただき、用意していた全100部を完売することができました。 ひと月前に部数を相談していたときには50部売れるかなと心配していたのが嘘のようです。 id:uchan_nosさんと@akachochinさんにはいくら感謝しても足りません。

さてイベントも終わり一段落したところですが、今回の原稿作成はちょっと変わったワークフローだったのでまとめておこうかと思います。

Colabから原稿へ移す際の問題点

今回の本はタイトルからもわかるとおり、Colab上で実行できるコードを使う事により、読者のハードルを下げるというのが特徴の一つです。 書籍の方ではColabで行った処理を、結果含めてそのまま再現しなくてはなりません。

当然コードは全部実行可能、出力結果はColabの物と同一でなくてはなりませんが、これが意外と問題になります。

もともとこの同人誌は当ブログで書いた連続記事が元になっています。ブログの方ではJupyter Notebookを使いましたが、実行可能なコードと解説という組み合わせは同人誌版とほぼ同じです。 しかしブログ版を書いたときにはコードはJupyterからのコピペに頼っていたために、

  • コピーミス
  • バージョンが違うコードが混在する
  • 後からの編集で原稿とのミスマッチがおきる

という問題がおきました。特に最初の2つはコードの実行ができなくなる可能性があるので大問題です。

また今回は終盤結構な数の数式が出てくるので、編集の手間を考えるとできるだけ手戻りは防ぎたいところです。

Colabから原稿へのワークフロー

そこで今回はこのようなワークフローを選択しました。

  1. ブラウザ上でColabでプログラム作成。GitHubに保存。
  2. 同Colabで原稿作成(図およびLaTex形式の数式入り)。GitHubに保存。
  3. ローカル環境で、nbconvertによりColabのノートブックからPDFに変換。
  4. 一次校正
  5. nbconvertによりColabからマークダウン形式への変換
  6. 自作スクリプトPython)による画像リンクの修正
  7. マークダウンからPDFに変換
  8. 二次校正
  9. md2reviewでMark DownからRe:VIEW形式に変換
  10. 壊れてしまったLaTex形式数式を修正
  11. レイアウトの調整
  12. Re:VIEWによるPDF原稿の出力
  13. 最終校正
  14. GitHub上の原稿の調整(公開するため)
  15. 完成

また各ステップでMakefileをつくり、できるだけ単純作業は自動化するようにしました。 具体的にはColabで変更を加えると、Make一発で9のRe:VIEW形式への変換まで実行されるようにしました。

ここで4の校正は数回、8のニ時校正も2〜3回行っていますが、Colabで変更してから校正用PDFにするまでは自動なので負担はそれほど大きくありませんでした。

一次と二次の校正の回数が多いのは、Re:VIEW形式にしたあとでは手作業が多いため、これ以降はColabには戻りたくないので、事前にできるだけ完成度を上げておく必用があったからです。

解説と振り返り

最初の目論見では、3のPDFの完成度が高いようならそのまま本にしてしまおうと思っていたのですが、実際に出力されたPDFは間延びしたもので、また、印刷原稿に必用なトンボやノンブルもなく、印刷所にだす原稿には使えなさそうなクオリティーでした。ただ、テキストやColabのコードの確認には十分だったので何回かPDF化してその上で校正しました。

次の5のマークダウン形式への変換と7のPDFの変換では、GitHub上に用意した画像ファイルがPDF化されないという問題がありました。これは6にて、ローカルのリポジトリーのパスに置き換えるスクリプトを実行することで回避しました。

9の「壊れてしまったLaTex形式数式」の修正というのは、md2reviewがうまく認識できなかったLaTex数式を通常のマークダウンと認識してしまう部分が何箇所かあったので、それを修正する作業です。これは自動化したかったのですがうまくいきませんでした。また、md2reviewの調整でうまくいくのかもしれません。1

10以降は通常のRe:VIEWによる技術書執筆とあまり変わらないと思います。今回このあたりは基本的に手作業でした。23

原稿ができたあとでみてみると、Colabノートブックから、マークダウン形式、Re:VIEW形式、と2段階の返還をしているのが気になります。 nbconvertはLaTex形式への変換もできるので、そうしておけば1回の変換ですんだのではないかと思いますが、私自信がLaTexについてあまり知らないのでこのようなフローになってしまいました。

また、ステップ10以降が手作業がメインになってしまっている点も気になります。理想はColabから最終Re:VIEWへ完全自動化だったのですが、なかなかそううまくは行かないようです。 このあたりは次回への課題です。

環境に関しては、今回は原稿部分は図の作成含めてすべてUbuntu18.04上で行いました。表紙の作成はWindows上で行っています。

校正には印刷の他にiPad Proを使用しましたが非常に便利でした。

まとめ

以上、今回「ゼロから作るRAW現像」の原稿を作成するにあたって利用したワークフローを紹介しました。

ColabやJupyterといったPythonインタラクティブノートブックで実行したものを原稿にする際にしか参考にならないニッチな情報ですが、もともと技術書典というのはそういうニッチな物を扱う点に価値があるのかもしれませんので、ここで関連記事として公開させていただきます。

最後に

【技術書典6】で頒布した「PythonとColabでできる - ゼロから作るRAW現像」の書籍版・PDF版ををBOOTHにて販売しています。 書籍+PDF版は2200円プラス送料、PDF版は1200円です。

moiz.booth.pm

【技術書典6】キヤノンD70のRAW画像のPythonによる現像

キヤノンD70のRAW画像のPythonによる現像

このページについて

このページでは「技術書典6」にて配布する「PythonとColabでできる - ゼロからできるRAW現像」のデモとして、書籍ではあつわかないキヤノンのRAWファイルの現像を行ってみます。

あくまでデモという目的なのでパラメータは最適化していませんし、一部機能はオフにしています。

このデモの内容をColabで実際に実行するには次のリンクからアクセスしてみて下さい。

colab.research.google.com

準備

まずライブラリーのインストールと、モジュールのインポート、画像の読み込みを行います。内容については書籍を参照ください。

# rawpyとimageioのインストール
!pip install rawpy;
!pip install imageio;

# rawpy, imageio, numpuy, pyplot, imshowのインポート
import rawpy, imageio
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.pyplot import imshow

# 前節までに作成したモジュールのダウンロードとインポート
!if [ ! -f raw_process.py ]; then wget https://github.com/moizumi99/camera_raw_processing/raw/master/raw_process.py; fi
from raw_process import simple_demosaic, white_balance, black_level_correction, gamma_correction
from raw_process import demosaic, defect_correction, color_correction_matrix, lens_shading_correction
from raw_process import noise_filter, apply_matrix, edge_enhancement, tone_curve_correction, advanced_demosaic

# 日本語フォントの設定
!apt -y install fonts-ipafont-gothic
plt.rcParams['font.family'] = 'IPAPGothic'

日本語が文字化けしている場合は、以下のセルから冒頭の#を削除した上で実行して、Rntime->Restart Runtimeを選択し、その後ページはじめから実行してみて下さい。

# もし日本語が文字化けしている場合以下の3行の行頭の#を削除して実行後、
#import matplotlib
#target_dir = matplotlib.get_cachedir()
#! rm {target_dir}/*.json
# その後Runtime->Restart Runtime選択してページ全体を再実行

今回はフリーのRAWファイルサンプルを公開しているRAWSMAPLES.CHからデータをダウンロードします。

使うのはCANON EOS70D のデータで、ファイル名はRAW_CANON_EOS70D.CR2です。

# 画像をダウンロードします。
! wget http://www.rawsamples.ch/raws/canon/RAW_CANON_EOS70D.CR2

自分で撮影した画像を使用する場合は以下のセルからコメントを取り除きアップロードします。

#from google.colab import files
#uploaded = files.upload()
# RAWファイルの名前。
# アップロードしたファイルを使う場合はその名前に変更。
raw_file  = "RAW_CANON_EOS70D.CR2"
raw = rawpy.imread(raw_file)

exiftoolを使ってRAWファイルの情報を見てみましょう。

! apt install exiftool
! exiftool RAW_CANON_EOS70D.CR2
ExifTool Version Number         : 10.80
File Name                       : RAW_CANON_EOS70D.CR2
Directory                       : .
File Size                       : 23 MB
File Modification Date/Time     : 2019:04:07 15:52:05+00:00
File Access Date/Time           : 2019:04:07 15:52:33+00:00
File Inode Change Date/Time     : 2019:04:07 15:52:05+00:00
File Permissions                : rw-r--r--
File Type                       : CR2
File Type Extension             : cr2
MIME Type                       : image/x-canon-cr2
Exif Byte Order                 : Little-endian (Intel, II)
Image Width                     : 5472
Image Height                    : 3648
Bits Per Sample                 : 8 8 8

 以下略

沢山の情報が得られます。

まず、ブラックレベルは2049です。

Per Channel Black Level : 2049 2049 2049 2049

また、次の行からホワイトレベル(白をしめす基準値)11765と思われます。

Normal White Level : 11765

Bayerのパターンは「赤・緑」「緑・青」のようです。

CR2 CFA Pattern : [Red,Green][Green,Blue]

残念ながらカラーマトリクスの値がわかりません。もしかするとこのカメラでは単純なカラーマトリクスではない方式で色補正を行っているのかもしれません。 今回は適当な値を設定して使う事にします。

まずはRAWPYの機能を使って最終的な画像を見ておきましょう。

# RAWPYの機能で現像処理。
# use_camera_wbはホワイトバランスにカメラの値を使う指定。
rgb = raw.postprocess(use_camera_wb=True)
plt.title("RAWPYによる現像")
plt.imshow(rgb)
plt.axis('off')
plt.show()

f:id:uzusayuu:20190408035709p:plain

これは、鳥の餌かなにかでしょうか?

ちょっと面食らいましたが気を取り直して、RAWデータの情報を見てみましょう。

print(raw.sizes)
ImageSizes(raw_height=3708, raw_width=5568, height=3670, width=5496, top_margin=38, left_margin=72, iheight=3670, iwidth=5496, pixel_aspect=1.0, flip=6)

画像サイズは5568x3670のようです。約20Mです。

Bayerパターンを確認しましょう。

print(raw.raw_pattern)
[[0 1]
 [1 2]]

左上が赤、右下が青、というそれ以外が緑というパターンで、先程確認したEXIFの情報と一致しています。

ホワイトバランスを確認します。

print(raw.camera_whitebalance)
[2036.0, 1024.0, 1739.0, 1024.0]

これはEXIFの「WB RGGB Levels As Shot」に対応しているようです。

それではRAW現像に入りましょう。

RAW画像データをNumpy配列に読み込みます。

raw_array = raw.raw_image
h, w = raw_array.shape

それでは処理を行いましょう。

今回はデモなのでシェーディング補正や欠陥画素補正、ノイズリダクション、トーンカーブ処理は省略しています。

EXIFでわからなかったカラーマトリクスの値として(1.5, -0.25, -0.25, -0.25, 1.5, -0.25, -0.25, -0.25, 1.5)という物を仮の値としていれています。

また、ホワイトレベルで正規化した場合暗い画像になってしまうので、x2のデジタルゲインを与える事にしました。

処理の詳細については技術書典6で配布する書籍「PythonとColabでできる - ゼロからできるRAW現像」をご覧ください。

blc, pattern = raw.black_level_per_channel, raw.raw_pattern
blc_raw = black_level_correction(raw_array, blc, pattern)
gains, colors = raw.camera_whitebalance, raw.raw_colors
wb_raw = white_balance(blc_raw, gains, colors)
dms_img = advanced_demosaic(wb_raw, pattern)
# カラーマトリクス。EXIFから得られたもの。
color_matrix = np.array(
    [1.5, -0.25, -0.25, -0.25, 1.5, -0.25, -0.25, 0.25, 1.5])
ccm_img = color_correction_matrix(dms_img, color_matrix)
# ホワイトレベル。EXIFから得られた値。
white_level = 11765.0
# デジタルゲイン
digital_gain = 2
gmm_img = gamma_correction(ccm_img / white_level * digital_gain, 2.2)
sigma, edge_intensity = 2, 0.25
shp_img = edge_enhancement(gmm_img, sigma, edge_intensity)

表示してみます。

# 最終画像表示
plt.figure(figsize=(8, 8))
plt.imshow(shp_img)
plt.axis('off')
plt.title(u"最終画像")
plt.show()

f:id:uzusayuu:20190408035610p:plain

先程のRAWPYによる現像と似た画像が得られました。

今回はパラメータを簡易的なものにしたので色合いがことなる、コントラストが違う、などの点がみられます。

このあたりはカラーマトリクスの値の変更、トーンカーブ処理などで改善できると思われます。

まとめ

今回は技術書典6で配布する書籍「PythonとColabでできる-ゼロから作るRAW現像」のデモとして、キヤノンのカメラEOS70DのRAW画像ファイルを現像してみました。

f:id:uzusayuu:20190331124210j:plain:w200

書籍ではRAW画像とは何か?という疑問に答えるところから始まり、各ステップで行われる処理を基本的なところから説明し、実際に実行可能なコードを使って処理を行ってみます。

技術書典について

公式ウェブページによる紹介は以下の通りです。

新しい技術に出会えるお祭りです。

技術書典は、いろんな技術の普及を手伝いたいとの想いではじまりました。

技術書を中心として出展者はノウハウを詰め込み、来場者はこの場にしかないおもしろい技術書をさがし求める、技術に関わる人のための場として『技術書典』を開催します。

  • 日時: 2019/04/14 (日) 11:00〜17:00 (一般参加は11時~13時のみ有料)
  • 開催場所: 池袋サンシャインシティ2F 展示ホールD(文化会館ビル2F)

配布情報

  • ブース: う38「bitnos
  • 書名:「PythonとColabでできる-ゼロから作るRAW現像」
  • フォーマット
    • 書籍版:164ページ(内12ページフルカラー+表紙含む)、予価2000円
    • PDF版:フルカラー164ページ(表紙含む)、予価1000円

書籍版の方はカラーページを含むために若干高めになってしまいましたので、特典としてPDF版ダウンロード権を添付します。

またPDF版は、同等品をイベント後BOOTHでの販売を予定しています(値段など変わる可能性があります)。

追記

現在はBOOTHにて入手可能です。書籍+PDF版は2200円プラス送料、PDF版は1200円です。

moiz.booth.pm

サークルbitnosについて

「う38」のサークルbitnosは低レイヤーを中心としたソフトウェアのサークルです。

今回は新刊「Local APICタイマー入門」を配布します。 以下、著者の@uchan_nosさんによる紹介です。

現代のPCにはいくつものタイマーが搭載されています。本書は現代のPCタイマーの主役であるLocal APICタイマーを中心にしつつも、ACPI PMタイマー、TSCUEFI RTCの使い方も紹介します。また、x86-64における割り込みの設定についても詳しく解説します。

他に以下の既巻も頒布します。

  • Linuxカーネルモジュール自作入門」
  • システムプログラミングハンドブック」
  • 「USB 3.0 ホストドライバ自作入門」
  • C++でできる!OS自作入門」