Pythonの仮想環境、venvとuvの違いは「箱」と「記録」の分担にある

Python を書き始めてしばらくすると、どこかで「仮想環境を作りましょう」という一文に出会う。入門書の多くは標準の venv を使う手順を載せていて、ここ数年の解説記事やプロジェクトの README には uv という名前もよく出てくる。どちらも「仮想環境を作る」ための道具として紹介されるので、最初のうちは何が違うのか分かりにくい。 私自身、両方を使い分けるようになってから気づいたのは、この二つは同じ仕事を奪い合っているのではなく、受け持つ範囲の広さが違うということだった。venv は「部品を入れる箱」を作るところまでを担当し、uv は箱を作ったうえで「何をどの版で入れたか」の記録や、Python 本体の用意まで引き受ける。この記事では、そもそも仮想環境が何を解決しているのかを確かめたうえで、venv と uv の作り方、記録の残し方、速さの理由を順に見ていき、最初の1本をどちらで作るかの目安を示したい。 仮想環境は、プロジェクトごとに部品の置き場を分ける仕組み エディターの下端には、そのファイルを動かしている Python の版が表示される。仮想環境を作ると、ここで選ぶ相手が増えるPhoto: MikeRun / CC BY-SA 4.0 Python で何かを作るときは、表計算のデータを扱う部品やグラフを描く部品のように、ほかの人が公開しているパッケージを取り込んで使うことが多い。これらは pip install で入れるのが基本だが、何も考えずに入れると、パソコンに入っている Python 本体の置き場にすべてが積み上がっていく。 困るのは、プロジェクトが二つ、三つと増えたときだ。古いプロジェクトでは部品の版1を前提に書いたのに、新しいプロジェクトのために版2へ上げた途端、古いほうが動かなくなる。版を戻せば今度は新しいほうが壊れる。どちらのプロジェクトがどの部品を必要としていたのかも、時間がたつと分からなくなる。 仮想環境は、この置き場をプロジェクトごとに分ける仕組みだ。プロジェクトのフォルダーの中に .venv という専用の置き場を作り、そこに Python の実行ファイルへの参照と、部品を入れる site-packages というフォルダーを用意する。その環境を使っているあいだは、pip install した部品が .venv の中だけに入り、ほかのプロジェクトには影響しない。不要になったら、.venv のフォルダーを丸ごと消せば元どおりになる。 要するに、仮想環境の本体はただのフォルダーで、特別な仮想マシンのようなものではない。この点を押さえておくと、venv と uv の違いも見通しやすくなる。 venvは標準の道具で、箱を作るところまでを担当する venv は Python 3.3 から標準で付いてくる仕組みで、追加で何かを入れなくても使える。作り方は、プロジェクトのフォルダーで次の1行を実行するだけだ。 python -m venv .venv これで .venv フォルダーができる。中には環境の設定を書いた pyvenv.cfg と、実行ファイルを置く場所(Windows では Scripts、macOS と Linux では bin)、部品を入れる site-packages が並ぶ。Python 3.4 以降は、作った時点で pip も環境の中に入っている。 ...

2026年10月25日 · 2 分 · 402 文字 · kingsworksub-jpg

画像の知覚ハッシュをPythonで比べた記録、縮小では動かず左右反転で30ビット変わる

このブログでは、記事に載せる写真を取り込むたびに、保存したファイルが出典の画像と同じものかを機械的に確かめている。出典から画像を取り直し、手元のファイルと並べて比べるのだが、保存のときに縮小と再圧縮をかけているので、ファイルの中身は1バイト単位ではまず一致しない。そこで使っているのが、画像の見た目から短い数字を作る「知覚ハッシュ」だ。検査のスクリプトには64ビットのdHashが入っていて、2つのハッシュで食い違うビットが10個以下なら同じ画像とみなしている。 この「10」という数字は、目安として決めたもので、どこまで加工に耐え、どこから別の画像を取り違えるのかをきちんと測ったことはなかった。そこで、Pythonでよく使われる知覚ハッシュの実装を並べ、写真1枚にさまざまな加工をかけたときの距離と、手元にある画像同士を総当たりで比べたときの距離を記録しておくことにした。使ったのはPython 3.12.10、ImageHash 4.3.2、Pillow 13.0.0、NumPy 2.5.4、SciPy 1.18.1で、作業用の仮想環境にまとめて入れている。 ファイルのハッシュと、見た目のハッシュ 左が元の写真、右が強く圧縮した写真。ファイルとしては別物でも、知覚ハッシュでは距離0になったPhoto: JJ Harrison, side-by-side comparison by Ibrahim.ID / CC BY-SA 3.0 MD5やSHA-256のようなハッシュは、入力が1ビットでも変われば出力がまったく別の値になるように作られている。改ざんを見つけるにはこの性質が欠かせないが、画像の比較には向かない。JPEGで保存し直しただけ、幅を半分にしただけでも、ハッシュ値は似ても似つかない値になり、「同じ写真かどうか」という問いには答えてくれない。 知覚ハッシュは、その逆を狙っている。画像を小さく縮めて細部を捨て、明るさの大まかな並びだけから数十ビットの値を作るので、人の目に同じに見える画像なら、ほぼ同じビット列になる。2つの値がどれだけ違うかは、食い違うビットの数、つまりハミング距離で測る。64ビットなら距離は0から64までで、0なら見た目がほぼ同じ、32前後なら無関係な画像同士と同じくらい離れている、と読む。 左の図は、1枚の鳥の写真を左右に並べ、右側だけ強く圧縮したものだ。この画像を真ん中で2つに切って比べると、aHash・dHash・pHashのいずれも距離は0、このブログの検査に入っているdHashでも1だった。背景のぼけた緑がくすみ、羽の色の境目が粗くなっていても、明るさの並びは変わっていないということだ。ファイルのハッシュが改ざんの有無を問うのに対し、知覚ハッシュは「人が見て同じ絵か」を問う道具だと考えると分かりやすい。 aHashとdHashは、8×8の明るさを並べるだけ 同じ写真を色の成分(上)と明るさ(中)に分けた例。知覚ハッシュが使うのは中段の明るさだけだPhoto: Tom Axford 1 / CC BY-SA 4.0 ImageHashに入っている代表的な3種類は、どれも最初にカラー画像をグレースケールに変える。Pillowでconvert("L")を呼ぶと、赤・緑・青の値に1000分の299、587、114を掛けて足した値が明るさになる。緑の重みが大きいのは、人の目が緑の明るさに敏感だからだ。色の違いはここで捨てられるので、写真をモノクロにしても3種類とも距離は0のままだった。 aHash(平均ハッシュ)は、いちばん素直な作り方をしている。画像を8×8の64画素まで縮め、64個の明るさの平均を出し、各画素が平均より明るければ1、暗ければ0とする。64画素を左上から順に並べれば、そのまま64ビットの値になる。計算は軽いが、画像全体の明暗の配置しか見ていないので、白い背景の真ん中に暗い物が立っている写真は、写っている物が何であれ似た値になりやすい。 dHash(差分ハッシュ)は、横に9画素、縦に8画素まで縮め、各行で隣り合う画素の明るさを比べる。1行に比べる組が8つ、8行で64ビットになる。平均と比べるのではなく、隣との差の向きを記録するので、画像全体が明るくなったり暗くなったりしても、差の向きはあまり変わらない。このブログの検査に入っているのもこの方式で、Pillowだけで書ける短さも選んだ理由だった。 from PIL import Image def dhash(im): g = im.convert("L").resize((9, 8)) px = list(g.get_flattened_data()) return sum(1 << i for i in range(64) if px[(i // 8) * 9 + i % 8] > px[(i // 8) * 9 + i % 8 + 1]) def distance(a, b): return bin(dhash(a) ^ dhash(b)).count("1") 2つの値の排他的論理和を取ると、食い違うビットだけが1になる。その1の数を数えればハミング距離だ。aHashとpHashは2011年、dHashは2013年に、画像の解析を仕事にしているニール・クラウェッツが自身のブログで紹介した方式で、ImageHashの関数の説明にもその記事が挙がっている。 pHashは32×32に縮めてから、周波数の低い成分を見る 8×8の離散コサイン変換で使う64通りの模様。pHashは左上に近い、粗い模様の成分だけを残すPhoto: Devcore / Public domain pHashは、画像を一度周波数の成分に分解してから判定する。ImageHashのphashは、まず画像を32×32まで縮めてグレースケールにし、縦と横に1回ずつ離散コサイン変換(DCT)をかける。DCTは、画像を左の図のような縞模様の重ね合わせとして表し、それぞれの模様がどれだけ含まれているかを数字にする変換で、JPEG圧縮の中でも使われている。左上ほどゆるやかな明暗の変化、右下ほど細かい模様に当たる。 pHashが残すのは、32×32の結果のうち左上の8×8、つまり周波数の低い64個の成分だけだ。細かい模様はノイズや圧縮のむらに左右されやすいので捨て、画像の骨格に当たる粗い明暗の分布だけで判定する。64個の値の中央値より大きければ1、そうでなければ0としてビットを並べる。ソースを読むと、中央値を求める範囲には画像全体の平均の明るさを表す左上の1個も含まれていた。平均値で比べるphash_simpleという別の関数もあり、こちらは左上の1列目を除いた8×8を平均と比べる。 ほかにも、DCTの代わりにウェーブレット変換を使うwhash、色の分布を見るcolorhash、画像を領域に分けて部分ごとにハッシュを取るcrop_resistant_hashがある。いずれも結果はImageHashという型で返り、2つを引き算するとハミング距離が整数で返ってくる。 import imagehash from PIL import Image a = Image.open("a.jpg") b = Image.open("b.jpg") print(imagehash.phash(a) - imagehash.phash(b)) # 0〜64 1枚あたりの計算時間も測った。手元の画像549枚を読み込み済みの状態で、aHashとdHashが1枚あたり約3.4ミリ秒、pHashが約5.0ミリ秒だった。大半は元の大きさから縮小する処理にかかっていて、DCTの分はわずかだ。数万枚程度なら、どれを選んでも速度で困ることはないと思う。 写真1枚を14通りに加工して、距離を測る 加工の元にした写真。左は2009年にマイクロソフトとPhotoDNAを作ったハニー・ファリドPhoto: John Manard / CC BY-SA 4.0 元にしたのは、右の幅1000画素の写真だ。これに縮小、再圧縮、明るさとコントラストの変更、トリミング、回転、左右反転などを1つずつかけ、元の写真とのハミング距離を4つのハッシュで測った。「ブログ」の列は、このブログの検査に入っているPillowだけのdHashで、ImageHashのdHashとは縮小に使うフィルターが違う。ImageHashはランチョス、Pillowのresizeは既定でバイキュービックを使う。 ...

2026年10月21日 · 2 分 · 262 文字 · kingsworksub-jpg