aws-sdk-go-v2でS3のNoSuchKeyを処理する
pkg.go.dev docs.aws.amazon.com
これの捕捉。
エラー関係の型はここ。
s3.GetObjectなどのAPIの戻り値のエラー型の定義は smithy パッケージの方にある。
smithy.OperationError がそれ。なので GetObject の場合のエラー処理はこういう感じになる。
import (
"errors"
"github.com/aws/smithy-go"
)
// (略)
out, err := s3Client.GetObject(ctx, input)
if err != nil {
var opErr *smithy.OperationError
var noSuchKey *types.NoSuchKey
if errors.As(err, &opErr) && errors.As(opErr.Err, &noSuchKey) {
// Keyが無い場合の処理
} else {
// その他のエラー処理
}
}
ここまではいい。
キーの位置にオブジェクトが無い、つまりNoSuchKeyの場合だけを検出したい場合はGetObject.Output.Bodyが不要なので s3.HeadObject を使いたくなる。でも何故か s3.HeadObject は NoSuchKey エラーに対応していないのでちょっと面倒くさい。
s3.HeadObjectでオブジェクトが無い事を検出するためにはHTTPのステータスコードを確認する必要がある。HTTPのステータスコードは smithy.OperationErrorのErrフィールド、Errはsmithy/transport/http.ResponseError型になっていて、その中のResponseフィールドがGo標準のnet/http.Response型になっているので、そこのStatusCodeフィールドを見る。つまりこう。
import (
"errors"
"github.com/aws/smithy-go"
"github.com/aws/smithy-go/transport/http"
)
// (略)
out, err := s3Client.HeadObject(ctx, input)
if err != nil {
var opErr *smithy.OperationError
var httpErr *http.ResponseError
if errors.As(err, &opErr) &&
errors.As(opErr.Err, &httpError) &&
httpError.Response.StatusCode == 404 {
// Keyが無い場合の処理
} else {
// その他のエラー処理
}
}
ただし、s3.GetObjectもs3.HeadObjectも、バケットに対してs3:ListBucketの権限がついていない場合は NoSuchError (HTTP 404) ではなく InvalidObjectState (HTTP 403)が返るようになるので、このあたりはIAMの権限とセットで考えて設定する必要がある。
『型システムのしくみ』を読みました
型システムのしくみ ― TypeScriptで実装しながら学ぶ型とプログラミング言語www.lambdanote.com
TypeScriptを使ってTypeScriptっぽいプログラミング言語の型チェッカーを実装しながら型システムの仕組みについて学ぶ本です。せっかくなので手を動かしながら、ライブラリのバグ修正しながら読んでました(読んでる途中で修正されてました)。
内容はさほど難しくはなく、分量としてはだいたい実装しながらでも1週間程度で終わるくらいじゃないかなというところ。
パーサはあらかじめ用意されていて、そこで使う構文木を定義しつつ、パーサが吐いた構文木の型をチェックする関数を実装していくという雰囲気で、「なんでTypeScriptなんだろう?」と思ったけど、実装しているファイルの中にテスト用のプログラミング言語を文字列で埋め込んで動作確認しなきゃいけないのが気持ち悪かったけど、そういうことなら確かにTypeScriptかもなあと思った。パーサを別に用意しつつ構文木の定義を見ながら型チェッカーを書くというのを1ファイルで簡単にやろうとするとこうなる。対象言語がTypeScript風なのも、記述量が少なくて型を明示できるやつとなると確かにあまり無いかも。
やってみて思ったのは、オブジェクト型って意外と面倒じゃないんだなというのと、再帰型はそりゃ面倒だよなというの。あとたぶん同じような理由で相互再帰関数や後で定義する変数を先に使うやつの実装なんかも面倒なんだろう。相互再帰関数については演習問題の補足でなんとかなるというような事が書いてあったけど。
ジェネリクスは、やっぱり想像通り面倒だったし、説明のしかたが面倒くさくなっていたのもまあ仕方ないくらい実際入り組んだものではあった。それは、そうか。
スコープの話もちょっと出てきたし、部分型のところなんかで何をもって型検査をOKとするかという話なんかも出てきて、今まで見てきた多数のプログラミング言語ではあれをああやってたんだろうとかそういう事情があったんだろうなとかいう想像ができて楽しかった。
そもそも型はエラーを出すためのものなのでこれをもって何ができるというものでもないというのはなんか冒頭にも書いてあったような気はするけど、何がわかったのっていうとちょっと説明が難しいところは実際あるとは思う。
これをもって実際のプログラミング言語の実装を見てどういう風にやってるのかを調べてみるのも楽しそうだし、思い切って型推論の方にいってみるのもいいし、『Types and Programming Languages』をあらためて読んでみるのも楽しそうな気はするんだけど、日本語訳版の『型システム入門』は古本で2万くらいするのでなかなか手が出ないところではある。というか本当はそっちが読みたかった……。
JPEGを読む(11)(終) 画像の成分を組み立てる
最終回です。
RGBとYCbCr
JPEGの色空間はYCbCrだけどコンピュータの画面はRGBなので変換する必要がある。YCbCrをRGBに変換するには、単純な行列のかけ算で計算できる。
このうち、Goのimage/colorモジュールではITU-R BT.601を採用している。
color package - image/color - Go Packages
これに従って変換してもいいけれど、GoのimageモジュールにはYCbCrというデータ構造があるので、レンダリングまで自分でやるわけでなければ単に成分ごとにYCbCr構造体に値を入れればよい。
サンプリング係数に基づいた画素の復元
サンプリング係数については前に出てきたとおり。
JPEGを読む(7) コンポーネント、サンプリング係数、MCUなど - yunomuのブログ
Y成分に関しては画素の数だけサンプル値があるので問題無いが、Cb成分とCr成分については間引かれていることが多い。今回利用した画像でも、それぞれ縦と横に1/2ずつ圧縮されていた。これを復元するためには単に縦と横に2倍にしてやればよい。つまり1つのサンプル値を4画素の値として使うということになる。
けれどこれもGoのimageモジュールのYCbCr構造体を使うとそんな面倒なことは必要なく、NewYCbCrの時に適切なSubsampleRatioを設定してあげれば、それぞれの成分値のスライスを適切なサイズで作っておいてくれるので、あとは値を設定するだけでよい。
おわり
自分でレンダリングする、またはその気分を味わいたい場合はYCbCr構造体ではなくNRGBA構造体などを使って書いてみても結構楽しい。
というわけで後半は面倒で図もコードもなくなったけど、ひとまずおわりです。以下は今回手をつけなかったところ。やってもいいけど、普通に仕様を読めばわかるようになったのでもうやる気が無い。
- 階層構造
- 算術符号化
- 可逆変換
- 画像の縦横のサイズが16×n+a(nは整数, 1≦a≦8)の時どうするの?(これはたぶん仕様に書いてなかった)
おまけ: テストに使った画像とデバッグの日々
テスト用の画像は最初はダウンロードフォルダにあった適当な画像を使っていたのだけど、ちょっとテストとしてよくなかった(真っ白な部分が多かった)ので、有名なlenna.jpgを再エンコードして単純化したものを使ったんですが(Goのimageモジュールでデコードしてエンコードすると構造が単純になる)、こうやって遊んでいるうちに「サンプル画像としてLennaを使った論文は受理しない」という声明が出たりしてましたね。
途中の工程を飛ばしたり間違えたりしてめちゃくちゃになった画像が面白かったので紹介。



実際はこの画像ではなくて輝度(Y)成分だけの黄色い画像で確認してたんですけど、間違いが目に見える分だけわかりやすくて良かったですね。最終的な画像として出てくるまでのDCTだのハフマン符号テーブルだのが合っているのかどうかなんてわかりにくすぎて、何通りも実装してテストするしか無くて面倒だったし、結局のところそのあたりには全くバグは無かった。yとvを書き間違えていたのは3枚目の画像の時でした。これを見つけるのに1ヶ月かかった…
JPEGを読む(10) DC/AC成分のおさらいとレベルシフト
サンプル値の復元は今回で終わり。
DC成分とAC成分のおさらい
離散コサイン変換で出力された行列の(0,0)成分は周波数が0なのでDC(直流)成分、他は周波数が1以上なのでAC(交流)成分と呼ばれている。
なぜDCとACを分けているのかというと、DC成分は周波数0、つまりユニット(8×8の画像)を一様にベタ塗りした時の値を示していて、全体のサンプル値の傾向を決定していると見ることもできる。8×8の平均値みたいなものだ。そう考えるとDC成分が0になることは少なそうだし、画像は隣合う値がなだらかに変化することが多いので連続するユニットのDC成分の間で差分エンコードするとうまく0付近に偏ったデータを作ることができそう。偏ったデータはハフマン符号の圧縮効果が出やすくなる。
AC成分はほとんどが0なのでランレングス符号で圧縮しやすい。これがDC/AC成分で符号化手順を変えられている理由のようだ。
レベルシフト
A.3.1より、逆DCT変換、逆量子化で復元された値はレベルシフトする必要がある。レベルシフトというのは、サンプル値に 2P-1 を足して 0〜2P-1の値に納めるということらしい。
具体的には、大抵はP=8なので128を足して0〜255の値にするということになる。このとき、0以下の値は0に、255以上の値は255に丸める。この処理で思った以上に丸めなければいけない値がたくさん出てきてちょっと驚いた。
レベルシフトは、エンコードの時に0〜255の値を-128〜127にすることで絶対値を0に近づけてハフマン符号の圧縮率を高める効果があるようだ。EXTEND関数の仕様とも関わってくる。
成分ごとに並べかえる
(7)で書いた通りにユニットが並んでいるので、それぞれY成分、Cb成分、Cr成分ごとに並べなおす。ついでにユニットもバラして左上から右下まで順に並べておくと後の処理が楽になる。
JPEGを読む(7) コンポーネント、サンプリング係数、MCUなど - yunomuのブログ
ここまでで一応全てのサンプル値を復元することはできているんだけど、計算間違いがあってデバッグするのに2ヶ月かかった。余談だけど、原因は変数vとyの書き間違いだった。1文字変数には気をつけよう。
JPEGを読む(9) AC係数のデコード(DCT、量子化)
AC係数のデコード
8×8のDCT係数のうち先頭を除く63個をAC係数と呼ぶ。(3)で後回しにしたAC係数の部分を見る。
JPEGを読む(3) ハフマン符号の復号準備 - yunomuのブログ

この中のDecode_ZZ(K)の部分はDCと同じで、RECEIVEしてEXTENDしている。DCと異なるのはDECODEで取得したハフマン符号値の扱いになる。
RS = DECODE SSSS = RS modulo 16 RRRR = SRL RS 4 R = RRRR
とあるように、DECODEでハフマン符号を読み取り、取得した値を上位と下位の4ビットずつに分けている。SSSS = 0の時は一旦無視して、上位4ビットの表す数RだけZZのインデックスKをスキップしている。そしてDecode_ZZ(K)でSSSSの示す値をZZ(K)に格納する。Rは4ビットしかなく15までの値しか表現できないため、ZZの中で0が15以上連続する場合はSSSS = 0とする。
つまりAC係数は0についてのみのランレングス符号化されている。おそらくほとんどの値が0なんだろう。
DCTとZZと量子化の関係
ちょっと一旦わかったことまとめ。
DCT(離散コサイン変換)
ここでちょっとDCTの話。定義はここで紹介されているのがわかりやすい。
離散コサイン変換 - MATLAB & Simulink - MathWorks 日本
要するに、8×8の値を8×8=64種類の正弦波の和として表現し、それぞれの重みを8×8のDCT係数に変換するということ。8×8行列の値を8×8行列に変換しても情報量は変わっていない気がするけど、画像の場合は8×8の中でそれほど複雑に値が変化しないので、DCT係数のほとんどが0になる。この0が続く部分をランレングス符号で圧縮することができるので、画像の圧縮にはよくDCTが使われるらしい。この段階では情報の欠落は発生していない。
DCTの定義を見ると、DC成分は周波数0を表していて、他の値のベースになるので確かに必ず値が入っていそうではある。かつ隣の8×8ブロックとの連続性もありそうなので、DCだけを差分符号化するのは理に適っている。コンポーネントのH, Vの値で近隣の8×8ブロックをグループ化していたのも、差分の絶対値を小さくする効果がありそう。絶対値が小さいと値を表現するビット数が少なくなる。ビット長が短い方に偏るので、ビット長を表現するハフマン符号の圧縮効果も高くなる。
ところでもしかしてDCとかACって直流と交流ってこと?
Zig-zag sequence
8×8=64個のDCT係数はFigure A.6のジグザグ順序で配列ZZに格納する。

これもどうしてかと思っていたんだけど、DCTの周波数が少なく単純なデータから順に並べることで0が連続する部分を増やし、ランレングス符号の圧縮効果を高めるためなんだろう。
量子化
最初の方で飛ばしちゃったけど、JPEGファイルの最初の方に量子化表(DQT)という64個の値がある。

Figure A.5の符号化/復号化の流れを見ると、これは単純に符号化時にDCT係数を量子化表の値で割り、復号化の時に掛けている。
これにより、DCT係数ぞれぞれの絶対値が小さくなる。
ただしRoundがかかっているので、情報の欠落がそれなりに発生する。元の成分値が105で量子化係数が50だった場合、符号化後は2、復号後は100になり、元の値と5だけずれてしまう。つまり量子化表が品質とファイルサイズの関係を決定付けているのだろう。
手元のJPEGファイルをいくつか見たところ、おおむね高周波数の量子化係数が大きくなる傾向があった。量子化係数50の場合は50未満は0になるので、複雑な変化は多少つぶしてしまってもいいということだろう。ジグザグ順序との関連もあるし、0が多いとランレングス符号の効果も高くなる。
一方で[1, 1, 1, ..., 1]となっているものもあった。最高品質にするとそうなるんだろう。
JPEGを読む(8) RECEIVEとEXTEND
ハフマン符号の復号はいいけど、その後のデータを読む部分を忘れていた。(3)のRECEIVEとEXTENDについて。
JPEGを読む(3) ハフマン符号の復号準備 - yunomuのブログ
RECEIVE(SSSS)
SSSSビットを読み取る処理。自明かと思ったけどちゃんと説明されていたので一応読む。SSSSというのは4ビットの数値というだけで特に意味があるわけではない。

1ビット読み取る関数NEXTBITがあって、特筆することはない。コードは以下。
func (d *Decoder) receive(l int) (uint8, error) {
var ret uint8
for i := 0; i < l; i++ {
b, err := d.nextBit()
if err != nil {
return 0, err
}
ret = ret<<1 + uint8(b)
}
return ret, nil
}
NEXTBIT
NEXTBITも細かい解説がある。

変数CNTとBに状態を保存してある。基本的には1バイトずつ読み取ってBに保存しておき、そこからNEXTBITを呼び出される度に1ビットずつシフトして取り出していく。8回読み終えると次の1バイトを読む。読んだ1バイトがDNLマーカーだった場合はスキャンを終了する。ついでなのでDNLの読み取りも実装するとこうなる。
type Decoder struct {
// ...
bitMask uint8
bits uint8
numLine uint16
}
import "errors"
var (
EOS = errors.New("end of scan")
)
func (d *Decoder) readDNL() (uint16, error) {
ld, err := d.readUint16()
if err != nil {
return 0, err
}
if ld != 4 {
return 0, errors.New("Unexpected NDL length")
}
return d.readUint16()
}
func (d *Decoder) nextBit() (uint16, error) {
if d.bitMask == 0 {
b, err := d.readUint8()
if err == ErrUnexpectedMarker {
d.unread()
m, err := d.readMarker()
if err != nil {
return 0, err
}
if m == Marker_DNL {
l, err := d.readDNL()
if err != nil {
return 0, err
}
d.numLine = l
return 0, EOS
}
return 0, ErrUnexpectedMarker
} else if err != nil {
return 0, err
}
d.bits = b
d.bitMask = 0b1000_0000
}
bitMask := d.bitMask
d.bitMask >>= 1
if d.bits&bitMask == 0 {
return 0b0, nil
} else {
return 0b1, nil
}
}
CNTだとピンとこなかったのでビットマスクで表現した。スキャンの終了はエラー値を定義して表現することにした。あとは以前に実装したDecoder#readUint8とDecoder#readMarker, Decoder#unreadで簡単に実装できる。
DNLの意味は一旦置いておく。
EXTEND(V, T)
EXTENDは、RECEIVEで読み取った値を符号つき整数に変換する処理となる。VはRECEIVEの戻り値で、TはRECEIVEの引数となっている。

フローチャートの通りに実装することは簡単だけど、このフローチャートの意味がよくわからない。というのはつまり正負の符号を含む値の表現方法を理解していないということになる。説明はF.1.2.1節とTable F.1にある。

ここでSSSSはRECEIVEの引数で、読み取ったビット数Tのこと。DIFFはRECEIVEで読み取った値Vのこととなる。
SSSS = 0の場合、ストリームを読み取ることなくそのまま0となる。
SSSS = 1 の場合、つまり1ビット読み取った場合、1ビットには -1, 1 という2つの値が割り当てられている。これを小さい順に符号に割り当てる。
| T | V | value |
|---|---|---|
| 1 | 02 | -1 |
| 1 | 12 | 1 |
SSSS = 2 の場合、2ビットには -3, -2, 2, 3 という4つの値が割り当てられているので、これを小さい順に符号に割り当てる。
| T | V | value |
|---|---|---|
| 2 | 002 | -3 |
| 2 | 012 | -2 |
| 2 | 102 | 2 |
| 2 | 112 | 3 |
以下同じ。
これをアルゴリズムで表すとFigure F.12のフローチャートになる。表を見ると正の数の時 = 最上位ビットが1の時 = V < 2T-1でない時、戻り値はVの値そのままになっていることがわかる。
このように数値をコード化している理由は、0もしくは絶対値が小さい値の出現確率が極端に多く、かつ表現する値域が広いためハフマン符号化するとテーブルが大きくなりすぎるためだと思われる。差分符号化なので絶対値が小さい値が多いのだろう。
実装は以下のようにした。ほぼフローチャートのまま。
func extend(v_ uint8, t int) int16 {
var v int16 = int16(v_)
var vt int16 = 1 << (t - 1)
if v < vt {
return v + (-1 << t) + 1
}
return v
}
JPEGを読む(7) コンポーネント、サンプリング係数、MCUなど
Annex Aの数学的定義を読んで実際のところ何をやっているのかを掴む。
コンポーネント(component)
まずコンポーネントについて。コンポーネントは仕様のどこを読んでも「画像に含まれる2次元配列」という以上の情報が出てこない。
実のところJPEG規格は2次元配列のリストを符号化する方法を規定しているだけであり、その2次元配列のデータが何を表現しているのか、画像なのかどうかすらも問題にしていない。なので原理的にはこの2次元配列のリストというデータはRGBのビットマップ値でもいいし、YCbCrでもいいし、CMYKでもいいし、他の何かでもいい。ただしJPEGには「コンポーネントのリストが何を表現しているのか」を表現するフィールドは存在しないので、JPEG形式のデータをデコードできたとしても元の画像を再現することはできない。
そのためJPEGファイルの仕様を定めたJFIF(JPEG File Interchange Format)という仕様がある。JFIF形式では、JPEGに色空間などの情報を付加して元画像を復元できるようにしたものらしいけど、妙に複雑になってしまったし、なんやかんやあってJPEGにも規格が取り込まれていて、MIMEタイプも"image/jpeg"のままだし、だいたいJPEGと似たようなものだよ。みたいな事がWikipediaに書かれている。
JPEG File Interchange Format - Wikipedia
JFIFでは色空間は標準ではY(グレースケール)またはYCbCrと定義されていて、このあたりが取り込まれてJPEGの色空間もYCbCrで表現するということになっているらしい。
つまり、テスト用の画像に含まれていた3つのコンポーネントはそれぞれY, Cb, Crの成分を表現した2次元配列だったということになる。これでコンポーネントの事は完全に理解した。
サンプルと精度(sample and sample precision)
コンポーネントが成分のことなので、サンプルというのはそのままその標本値、つまりYCbCrの値ということになる。ということは精度Pはビット数で、前回使ったファイルではP=8, X=400, Y=400だったので、このファイルはそれぞれの成分が0〜255の値をとる大きさ400x400の画像だということになる。
水平/垂直サンプリング係数(Horizontal/Vertical sampling factor)
いろいろ書いてあるのを読み解くと、元画像の要素数はX×Yだけど、コンポーネントの要素数はそこから割り引いていくということらしい。その比率はフレームヘッダのコンポーネントパラメータで示されている。
例として、X = 512, Y = 512 の画像で、3つのコンポーネントのサンプリング係数が以下の場合、
- Component 0: H0 = 4, V0 = 1
- Component 1: H1 = 2, V1 = 2
- Component 2: H2 = 1, V2 = 1
それぞれのコンポーネントCiの縦横のサンプル数xi, yiは以下のようになる。
- Componennt 0: x0 = 512, y0 = 256
- Componennt 1: x1 = 256, y1 = 512
- Componennt 2: x2 = 128, y2 = 256
つまりこの場合、コンポーネント0は水平方向の圧縮は無し、垂直方向は1/2になる。コンポーネント1は水平方向に1/2、垂直方向はそのまま。コンポーネント2は水平方向に1/4、垂直方向に1/2に圧縮されている。
前回利用したデータでは以下のようになっていた。
- C0: H0 = 2, V0 = 2
- C1: H1 = 1, V1 = 1
- C2: H1 = 1, V1 = 1
この場合は、C0は圧縮せず、C1, C2では縦横に1/2ずつ圧縮されているということになる。C0はYなので、輝度に敏感な人の目の特性に合わせてYだけは細かく表現できるようにしようということだろう。手持ちの画像のほとんどのサンプリング係数はこのようになっていた。
データユニット(unit)
可逆変換では1つの値を1単位(ユニット)とする。DCTを利用する場合は8×8で64個の値を1単位とする。このユニットは次のMCU(Minimum coded unit)と関わってくる。
MCU(Minimum coded unit)
最初に出てきたFigure B.2で、Entropy-coded segmentの中に並んでいたデータ。だんだん近付いてきた。
非インターリーブ
スキャンに含まれるコンポーネント数(Ns)が1の時は1ユニットがそのまま1MCUになる。コンポーネントC0のユニットuj, i (0 ≦ i ≦ x0, 0 ≦ j ≦ y0)に対して、x0×y0個のMCUが含まれ、
となる。一般化するとMCUi+j×x0 = uj, i となる。
例として、DCT利用で x = 512, y = 256 だった場合、ユニットは 8 × 8 なので、512 × 256 ÷ (8 × 8) = 2048 となり、このスキャンには2048個のMCUが含まれることになる。
インターリーブ
Ns > 1 の時は、インターリーブする。つまりコンポーネントをまたいだデータのブロックを1つのMCUとする。コンポーネントCiの横Hiユニット、縦Viユニットをひとつの塊として扱い、ちょっと説明が大変なのでFigure A.3のように並べる。

水平/垂直サンプリング係数の定義により、この塊の数は各コンポーネントで同一になる。この数=MCUの数になる。そしてインターリーブの場合は1つのMCUの中にΣNsi=0Hi×Vi個のユニットが含まれている。
例として、Ns = 3の時(スキャンに全てのコンポーネントが含まれる時)、サンプリング係数の節のコンポーネントの場合をそれぞれ計算してみると、こうなる。
- Component 0: H0 = 4, V0 = 1, x0 = 512, y0 = 256 → (512 ÷ 8 ÷ 4) × (256 ÷ 8 ÷ 1) = 512
- Componennt 1: H1 = 2, V1 = 2, x1 = 256, y1 = 512 → (256 ÷ 8 ÷ 2) × (512 ÷ 8 ÷ 2) = 512
- Component 2: H2 = 1, V2 = 1, x2 = 128, y2 = 256 → (128 ÷ 8 ÷ 1) × (256 ÷ 8 ÷ 1) = 512
つまりスキャンに512個のMCUが含まれることになる。それぞれのMCUに含まれるユニットは 4 × 1 + 2 × 2 + 1 × 1 = 9個になる。
restart interval
DRI(Define restart interval)マーカーが存在する場合、MCUの列を定期的に区切ってrestart intervalを置くことができる。これがあることによりinterval毎に並列処理したり、ランダムアクセスしたり、差分符号化のエラー伝播を防いだりすることができる。というようなことが4.9節に書いてある。
実際のところは私が使っているサンプルデータにDRIが無いのでよくわからない。なので一旦置いておくことにする。
ともあれこれで今のところわからない部分は全部解決したはず。