AlphaViewer は、透過 PNG(RGBA 画像)の アルファ(透明度) を確認しながら、半透明の整理・輪郭のクリーニング・透明部分の色にじみ対策などを行い、結果を PNG として保存するアプリです。
イメージ

はじめに
AlphaViewer_New.py は、透過 PNG(RGBA 画像)の アルファ(透明度) を確認しながら、半透明の整理・輪郭のクリーニング・透明部分の色にじみ対策などを行い、結果を PNG として保存するデスクトップアプリです。
処理はすべて「プレビュー → 確認 → 保存」の流れで行い、元画像を直接書き換えるのは「不透明化」だけです(確認ダイアログあり)。処理の組み合わせと実行順を変えられるので、画像に合わせて調整できます。
概要
主な機能
- 画像を読み込み、元画像と 8 種類の処理結果を 3×3 の 9 パネルで同時に確認
- 処理ごとに ON / OFF と 実行順 を設定(別ウィンドウ)
- 半透明(α が 0 より大きく閾値以下)を 透明にする / 不透明にする を選択
- α 分布(α ごとのピクセル数)を別ウィンドウの表で確認
- 1 枚ずつ PNG 保存、またはフォルダ内の画像を 一括処理
- 背景色・背景画像・市松模様との合成表示(保存内容にも反映可)
- 透明部分の RGB だけを変更(α は変えない)して色にじみを軽減
- パラメータ・実行順・ON/OFF を JSON ファイルに保存 / 読み込み
- 全 UI に ツールチップ(マウスを乗せると説明を表示)
- 画像ファイルの ドラッグ&ドロップ読み込み
動作環境
- Python 3(tkinter を含むもの)
- 必須ライブラリ:
numpy,Pillow - 任意ライブラリ:
scipy… 端部小物体除去・ロングフェザーが高速かつ高精度になります。無い場合は簡易処理で動作します。tkinterdnd2… ドラッグ&ドロップ読み込みが有効になります。無い場合はメニューから読み込みます。
起動方法
python AlphaViewer_New.py
同じフォルダに画像を置く必要はありません。起動後に「フォルダ読み込み」で選ぶか、画像ファイルをウィンドウへドロップしてください。
対応画像形式
.png / .jpg / .jpeg / .bmp / .tif / .tiff / .webp
読み込み時に RGBA へ変換されます(α が無い形式は α=255 として扱われます)。
基本の流れ
- 「フォルダ読み込み」でフォルダを選ぶ(または画像をドロップ)
- 左のサムネイルをクリックして画像を読み込む
- 右側で処理の ON/OFF・実行順・パラメータを調整する
- 9 パネルと「α分布」で結果を確認する
- 各パネルの「保存」または「フォルダ一括」で PNG を書き出す
初期設定
| 項目 | 初期値 |
|---|---|
| 半透明の扱い | 半透明 → 透明 |
| 処理の ON/OFF | 6 処理すべて ON |
| 実行順 | 半透明 → 膨張/縮小 → 行・列クリーン → 色クリア → 端部小物体除去 → ロングフェザー |
| 背景 | 無(保存は α を保持) |
| 透明部 RGB 変更 | OFF(α≤10、色は黒) |
画面の全体構成
┌──────────────────────────────────────────────────────────────────────┐
│ タイトル行: AlphaViewer New | 幅W × 高さH px | ファイルパス │
├────────────┬──────────────────────────────────────┬──────────────────┤
│ 左側 │ 中央 3×3 パネル │ 右側 │
│ ・フォルダ │ 1 元画像 2 透明検出 3 半透明 │ ・実行順ボタン │
│ ・背景合成 │ 4 膨張縮小 5 行列入り 6 色クリア │ ・一括処理 │
│ ・サムネイル │ 7 端部除去 8 フェザー 9 最終 │ ・各パラメータ │
│ │ │ ・下部固定バー │
└────────────┴──────────────────────────────────────┴──────────────────┘
メインウィンドウ
タイトル行
ウィンドウ最上部に、次の内容を常に表示します。
AlphaViewer New | 幅240 × 高さ200 px | C:\...\sample.png
- 画像を読み込むと 幅・高さ・フルパス が更新されます。
- パスが長い場合は行内では末尾を残して省略されます。カーソルを乗せると全文がツールチップで表示されます。
- ウィンドウのタイトルバーにも同じ内容(フルパス)が入ります。
左側
フォルダ読み込み
| ボタン | 説明 |
|---|---|
| フォルダ読み込み | 画像フォルダを選択し、その中の画像をサムネイル一覧に表示します。選択したフォルダは「フォルダ一括」の処理対象にもなります。 |
背景合成・色
プレビューおよび保存時の背景の扱いを設定します。
| 操作 | 説明 |
|---|---|
| 無 | 背景を合成しません。保存は α 付きのまま(透過 PNG)です。画面では透明部分が市松模様で表示されます。 |
| 色 | 選んだ単色と合成します。保存にも背景色が焼き込まれます。 |
| 画 | 読み込んだ背景画像と合成します。保存にも焼き込まれます。 |
| 市 | 市松模様と合成します。保存にも市松模様が焼き込まれます(透過を残したいときは「無」を使います)。 |
| 白 / 黒 | 背景色を白 / 黒に設定し、自動的に「色」に切り替えます。 |
| 選択 | カラーピッカーで任意の背景色を選びます。 |
| 背景画像読込 | 背景として使う画像ファイルを読み込み、「画」に切り替えます。 |
| 解除 | 背景画像を破棄し、「市」に切り替えます。 |
サムネイル一覧
- フォルダ内の画像をファイル名順に並べます。
- 透明部分は 市松模様 で合成して表示されるため、透過の状態が分かりやすくなっています。
- クリックするとその画像を読み込みます。
- カーソルを乗せると フルパスと画像サイズ が表示されます。
- 現在読み込んでいる画像の強調表示はありません。
中央 3×3 パネル
9 枚のパネルは「どこまで処理を適用したか」が異なります。処理の ON/OFF によって内容が変わります。
| No. | パネル名 | 内容 |
|---|---|---|
| 1 | 元画像 | 読み込んだままの画像。透明部 RGB 変更が ON ならその変更も反映されます。 |
| 2 | 透明検出(α) | α だけを白黒(グレースケール)で表示します。白いほど不透明、黒いほど透明です。α チャンネルを持たない 3 チャンネル画像です。 |
| 3 | 半透明→透明 / 不透明 | 「3. 半透明」だけを適用した結果です。 |
| 4 | 膨張 / 縮小 | 「3. 半透明」→「4. 膨張/縮小」までを適用した結果です。 |
| 5 | 行・列クリーン | 上記のベースに「5. 行・列クリーン」だけを適用した結果です。他の 6〜8 は含みません。 |
| 6 | 色クリア | ベースに「6. 色クリア」だけを適用した結果です。 |
| 7 | 端部小物体除去 | ベースに「7. 端部小物体除去」だけを適用した結果です。 |
| 8 | ロングフェザー | ベースに「8. ロングフェザー」だけを適用した結果です。 |
| 9 | 最終(全適用) | ON になっているすべての処理を 実行順どおり に適用した結果です。 |
補足:
- 「ベース」は「3. 半透明」と「4. 膨張/縮小」までを適用した状態です。
- パネル 4〜8 は それぞれ単独の効果を確認するため のもので、他のクリーン処理は含みません。
- パネル 9 が実際に保存したい最終結果です(一括処理の「最終(全適用)」と同じ)。
パネルのクリック操作
- パネル 1(元画像) または 6(色クリア) をクリックすると、その位置の画素の RGB 色を取得します。
- 色を取得すると、色クリアの「左上 / 右上 / 左下 / 右下」チェックはすべて外れます。
- 角のチェックが 1 つも無い状態では、取得した色に近い画素(色差の範囲内)を画像全体から透明にします。
右側(実行順とパラメータ)
右側は縦スクロールできます。下部の「不透明化/設定/RGB 変更」バーだけは固定です。
上部のボタン
| ボタン | 説明 |
|---|---|
| 実行順を設定… | 別ウィンドウを開き、6 処理の ON/OFF と実行順を変更します。 |
| ⑨ 最終を保存 | パネル 9(最終)の画像を PNG で保存します。 |
| 初期化 | 6 処理の ON/OFF と実行順を初期状態に戻します(パラメータは変わりません)。 |
| フォルダ一括 | 選択中のフォルダの画像をまとめて処理し、batch_output に保存します。 |
| 対象メニュー | 一括処理で保存するパネルを選びます。最終(全適用) / ロングフェザー / 端部小物体除去 / 色クリア / 行列クリーン / 膨張/縮小 / 半透明 / 透明検出 / 元画像 / 全9種 |
ボタンの下には「ON になっている処理の数」と「実行順の要約」が表示されます。
下部固定バー(不透明化・設定・RGB 変更)
| 操作 | 説明 |
|---|---|
| 不透明化 | 読み込んだ元画像の α をすべて 255(完全な不透明)にします。元に戻せません(確認ダイアログが出ます)。 |
| 設定保存 | 現在のパラメータ・実行順・ON/OFF を JSON ファイルに保存します。成功するとボタンが一瞬「保存✓」になります。 |
| 設定読込 | 保存した JSON を読み込み、設定を復元します。成功すると「読込✓」になります。壊れたファイルはエラーダイアログを出し、アプリは継続します。 |
| α≤ スライダー | 透明部 RGB 変更の対象となる α の上限です(0〜255)。 |
| 透明部 RGB 変更 | ON にすると、α が閾値以下の画素の RGB だけを変更します。α は変えません(透明のまま)。 |
| 表示 | ON にすると、変更した RGB を目視できるよう、画面上だけ対象画素を塗りつぶして表示します。保存内容は変わりません。 |
| 色選択 | 透明部に設定する RGB 色をカラーピッカーで選びます。 |
| 黒 / 白 / 紫 | 透明部の RGB を黒・白・紫に設定します。 |
| 状態表示 | 対象ピクセル数を 1234px のように表示します。OFF のときは OFF。 |
1. 元画像 / 2. 透明検出
| 操作 | 説明 |
|---|---|
| 保存 | そのパネルの画像を PNG で保存します(初期ファイル名は 元ファイル名_original.png / _alpha_detect.png)。 |
| α分布 | 「α分布」ウィンドウを開きます(下記参照)。 |
3. 半透明→透明 / 不透明
| 操作 | 説明 |
|---|---|
| 透明 | α が「0 より大きく α≤ 閾値」の画素を α=0 にします(初期設定)。 |
| 不透明 | 同じ画素を α=255 にします。 |
| α≤ スライダー | 半透明とみなす α の上限です(0〜255、初期値 128)。 |
| 保存 | パネル 3 を保存します(_semi_transparent.png)。 |
選んだモードに応じて、パネル名・見出し・実行順ウィンドウの表示が「半透明→透明」または「半透明→不透明」に切り替わります。
4. 膨張 / 縮小
| 操作 | 説明 |
|---|---|
| 膨張 | 不透明部分の輪郭を外側へ太らせます。 |
| 縮小 | 不透明部分の輪郭を内側へ痩せさせます。 |
| カーネル | 処理する範囲の大きさです(1〜9)。偶数のときは内部で +1 されます。 |
| 回数 | 同じ処理を繰り返す回数です(1〜10)。 |
| 保存 | パネル 4 を保存します(_morph.png)。 |
5. 行・列クリーン
| 操作 | 説明 |
|---|---|
| α閾値 | これ以下の α を透明とみなして行・列を判定します(0〜255)。 |
| 行 割合% | 行内の透明画素の割合がこの値以上なら、その行を透明にします(0〜100)。 |
| 行 連続数 | 行内の「不透明部分の塊(ラン)の個数」がこの値以下なら対象にします(1〜50)。 |
| 列 割合% / 列 連続数 | 列について同じ判定を行います。 |
| 保存 | パネル 5 を保存します(_rowcol_clean.png)。 |
処理結果として「クリア: 行n / 列m」が表示されます。膨張で生成した画素は判定時に透明として扱われます。
6. 色クリア
| 操作 | 説明 |
|---|---|
| 左上 / 右上 / 左下 / 右下 | チェックした角から連続する同色を透明にします(複数指定可)。 |
| 色差 | 同じ色とみなす許容差です(0〜100。大きいほど広い色が対象)。 |
| 保存 | パネル 6 を保存します(_color_clear.png)。 |
角のチェックがすべて OFF のときは、パネル 1 / 6 のクリックで取得した色に近い画素を画像全体から透明にします。
7. 端部小物体除去
画面の端に接している小さなゴミ(ノイズ)を消します。
| 操作 | 説明 |
|---|---|
| 上 / 下 / 左 / 右 | チェックした端に接する塊を除去対象にします。 |
| 面積≤% | 画像全体の面積に対する上限割合です(0〜20%、0.1 刻み、初期値 0.9%)。例: 0.5% なら全画素の 0.5% 以下の大きさの塊を消します。画像サイズが変わっても同じ意味になります。 |
| α閾値 | これ以下の α を透明とみなして塊の大きさを判定します(0〜255)。 |
| 保存 | パネル 7 を保存します(_edge_clean.png)。 |
結果は「除去: n個 (m px)」と表示されます。
8. ロングフェザー
輪郭の α をなめらかなグラデーションにします。
| 操作 | 説明 |
|---|---|
| S字滑らか | α の変化を S 字カーブにして自然なグラデーションにします。 |
| 境界色保護 | 輪郭付近の色を内側からにじませ、色の欠けを防ぎます。 |
| 内側侵入 | 不透明部分の内側へグラデーションを入れる幅です(0〜50)。 |
| 外側広がり | 輪郭の外側へ透明グラデーションを広げる幅です(0〜50)。 |
| 広がり方 | カーブの強さです(10〜300、100 が標準)。 |
| 保存 | パネル 8 を保存します(_long_feather.png)。 |
実行順の設定ウィンドウ
「実行順を設定…」で開きます。チェックした処理を 上から順に 適用します。
| 操作 | 説明 |
|---|---|
| 左の番号 | 現在の適用順です。 |
| チェックボックス | その処理を適用する / しない。 |
| ↑ / ↓ | その処理を 1 つ上 / 下へ移動します。 |
| 全ON | すべての処理を ON にします。 |
| 全OFF | すべての処理を OFF にします(⑨ 最終は元画像と同じになります)。 |
| 初期化 | ON/OFF と実行順を初期状態に戻します。 |
| 閉じる | ウィンドウを閉じます(設定は保持されます)。 |
この順序は パネル 9(最終) と、一括処理の「最終(全適用)」に反映されます。
α分布ウィンドウ
「2. 透明検出」の行にある α分布 ボタンで開きます。α(0〜255)ごとのピクセル数を一覧表示します。
| 列 | 内容 |
|---|---|
| α | 0〜255 のアルファ値(全 256 行)。 |
| 元画像 px / % | 元画像でその α を持つ画素の数と割合。 |
| 最終 px / % | パネル 9(最終)でその α を持つ画素の数と割合。 |
- 半透明(1〜254)の行は色付きで表示されます。
- 上部の集計は
合計 / α=0 / 半透明(1-254) / α=255をそれぞれ表示します。 - ウィンドウを開いたままパラメータを変えると、約 0.4 秒後に自動で再計算されます。
| ボタン | 説明 |
|---|---|
| 再計算 | 現在の設定で表と集計を作り直します。 |
| コピー | 表の内容をタブ区切りテキストとしてクリップボードにコピーします(Excel などに貼り付け可能)。 |
| 閉じる | ウィンドウを閉じます。 |
保存の仕組み
パネル単体の保存
- 各セクションの 保存 ボタン、または上部の ⑨ 最終を保存 で PNG を書き出します。
- 初期ファイル名は
元ファイル名_パネル名.pngです。 - 背景が「無」なら RGBA(透過を保持)、「色 / 画 / 市」ならその背景を合成した画像になります。
パネル名と保存ファイル名の対応:
| パネル | 保存名の接尾辞 |
|---|---|
| 1 元画像 | _original.png |
| 2 透明検出 | _alpha_detect.png |
| 3 半透明 | _semi_transparent.png |
| 4 膨張/縮小 | _morph.png |
| 5 行・列クリーン | _rowcol_clean.png |
| 6 色クリア | _color_clear.png |
| 7 端部小物体除去 | _edge_clean.png |
| 8 ロングフェザー | _long_feather.png |
| 9 最終 | _final.png |
フォルダ一括処理
- 処理対象のフォルダを「フォルダ読み込み」で選ぶ
- 対象メニューから保存したいパネルを選ぶ(「全9種」ですべて)
- 「フォルダ一括」を押す
- 確認ダイアログ(対象件数・内容・保存モード・出力先)で実行
- 完了後、成功件数と失敗件数、出力先が表示される
- 出力先は 選択フォルダ内の
batch_outputに固定です(自動作成)。 - ファイル名は
元ファイル名_パネル名.pngです。 - 通常保存と一括処理は同じ書き出し処理を使うため、同じ設定なら 同じ内容の PNG になります。
- 処理中はボタンが無効になり、
3/10のような進捗が表示されます。 - 失敗したファイルは最大 10 件までエラー内容が表示されます。
背景合成と保存の関係
| 背景 | 画面表示 | 保存 |
|---|---|---|
| 無 | 透明部は市松模様で表示 | α 付きのまま(透過保持) |
| 色 | 指定色と合成 | 指定色を焼き込み |
| 画 | 背景画像と合成 | 背景画像を焼き込み |
| 市 | 市松模様と合成 | 市松模様を焼き込み |
設定ファイル(設定保存 / 設定読込)
保存される内容
- 半透明モード(透明 / 不透明)と α 閾値
- 膨張/縮小(種類・カーネル・回数)
- 行・列クリーン(α 閾値・割合・連続数)
- 色クリア(色差・角のチェック)
- 端部小物体除去(面積 %・α 閾値・上下左右)
- ロングフェザー(内側・外側・広がり方・S 字・境界色保護)
- 透明部 RGB 変更(ON/OFF・α 閾値・変更色)
- 6 処理の 実行順 と ON/OFF
動作
- 形式は JSON(
version: 1)。既定のファイル名はalphaviewer_settings.jsonです。 - 読み込み時、未知の項目や古い項目は無視されます。
- 値は現在の型(真偽値・整数・小数・文字列)に合わせて変換されます。
- 実行順ウィンドウと画面表示は読み込み直後に更新されます。
- 壊れた JSON を読ませてもアプリは終了せず、エラーダイアログを表示します。
ツールチップとドラッグ&ドロップ
- 主要なボタン・スライダー・チェックボックス・パネル・サムネイルにツールチップがあります。約 0.45 秒カーソルを乗せると表示されます。
- タイトル行とサムネイルのツールチップではフルパスを確認できます。
- 画面外にはみ出さないよう、ツールチップの位置は自動調整されます。
tkinterdnd2が入っている場合、画像ファイルをウィンドウへドロップすると読み込めます(フォルダのドロップには対応していません)。
処理の考え方(パイプライン)
- パネル 1 は「元画像」、パネル 2 は「α の可視化」です。
- パネル 3 は「半透明」、パネル 4 は「半透明 + 膨張/縮小」までのベースです。
- パネル 5〜8 は、そのベースに各処理を 1 つだけ 適用した確認用の画像です。
- パネル 9 は、ON の処理を実行順にすべて適用した最終結果です。
- 透明部 RGB 変更が ON のときは、α チャンネルを持たないパネル 2 を除く各 RGBA パネルに反映されます。
- 表示用の合成(背景・市松)と、保存用の画像は別々に管理されています。「表示」チェック(RGB 変更の目視用)は保存内容に影響しません。
注意事項・既知の制限
- 「不透明化」は元画像の α を書き換えるため 元に戻せません。必要な場合は画像を再読み込みしてください。
- 背景を「市」にすると、保存した PNG に市松模様が焼き込まれます。透過を残したい場合は「無」を選んでください。
- パネル 2(透明検出)は 3 チャンネルのグレースケール画像として保存されるため、α を持ちません。
- 透明部 RGB 変更は α=0 の画素の RGB も書き換えます。一部のビューアでは α=0 の RGB が見えないため、「表示」で確認してください。
- 行・列クリーンの「連続数」は、不透明部分の 塊(ラン)の個数 です(長さではありません)。
- 端部小物体除去の「面積≤%」は、画像全体の面積に対する割合です。画像サイズが大きいほど、消える塊の画素数も大きくなります。
scipyが無い場合、端部小物体除去は簡易的な探索処理になり、ロングフェザーも簡易的なぼかしになります。処理速度と精度のためscipyの導入を推奨します。tkinterdnd2が無い場合、ドラッグ&ドロップは使えません。「フォルダ読み込み」から読み込んでください。- サムネイルはフォルダ読み込み時に生成します。ファイルを追加・削除した場合は、もう一度フォルダを読み込んでください。
- 一括処理の出力先は選択フォルダ内の
batch_output固定です。既存ファイルは同じ名前で上書きされます。 - 設定の「初期化」は ON/OFF と実行順のみを戻します。パラメータは初期化されません。
- 画像処理はメモリ上で行われます。非常に大きな画像を多数一括処理する場合は時間とメモリに注意してください。
付録: 用語
| 用語 | 意味 |
|---|---|
| α(アルファ) | 画素の不透明度。0=完全な透明、255=完全な不透明。 |
| 半透明 | α が 0 より大きく 255 未満の画素。本アプリでは「0 より大きく閾値以下」を処理対象にします。 |
| ベース | 「半透明」と「膨張/縮小」までを適用した状態。パネル 4〜8 の出発点です。 |
| パネル 9 / 最終 | ON の処理を実行順にすべて適用した結果。 |
| 背景合成 | 透過画像を色・画像・市松模様の上に重ねて表示・保存すること。 |
| 色にじみ | 透過 PNG の透明部分に残った RGB が、縮小表示などで縁に色として現れる現象。透明部 RGB 変更で軽減できます。 |
プログラム
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
AlphaViewer New
- 左: サムネイル一覧(透明部分は市松模様)
- 中央: 3x3 グリッド
1 元画像 / 2 透明検出 / 3 半透明→透明(または不透明)
4 膨張・縮小 / 5 行列クリーン / 6 色クリア
7 端部小物体除去 / 8 ロングフェザー / 9 最終(全適用)
- 右: 実行順序(有効/無効 + 並べ替え)と各パラメータ
- 上部: 画像サイズとファイルパス
"""
import os
import json
import tkinter as tk
from tkinter import filedialog, messagebox, colorchooser, ttk
from PIL import Image, ImageTk, ImageFilter
import numpy as np
from collections import deque
try:
from tkinterdnd2 import DND_FILES, TkinterDnD
DND_AVAILABLE = True
except Exception:
DND_AVAILABLE = False
try:
from scipy import ndimage
HAS_SCIPY = True
except Exception:
HAS_SCIPY = False
F6 = ("Meiryo", 6)
F7 = ("Meiryo", 7)
F7B = ("Meiryo", 7, "bold")
F8 = ("Meiryo", 8)
F8B = ("Meiryo", 8, "bold")
F9 = ("Meiryo", 9)
class ToolTip:
"""マウスを乗せると説明を表示する簡易ツールチップ。"""
DELAY = 450
def __init__(self, widget, text):
self.widget = widget
self.text = str(text or "")
self._after = None
self._win = None
try:
widget.bind("<Enter>", self._schedule, add="+")
widget.bind("<Leave>", self._hide, add="+")
widget.bind("<ButtonPress>", self._hide, add="+")
widget.bind("<Destroy>", self._hide, add="+")
except Exception:
pass
def set_text(self, text):
self.text = str(text or "")
def _schedule(self, event=None):
self._cancel()
if not self.text:
return
try:
self._after = self.widget.after(self.DELAY, self._show)
except Exception:
self._after = None
def _cancel(self):
if self._after is not None:
try:
self.widget.after_cancel(self._after)
except Exception:
pass
self._after = None
def _show(self):
self._after = None
if self._win is not None or not self.text:
return
try:
if not self.widget.winfo_exists():
return
x = self.widget.winfo_rootx() + 12
y = self.widget.winfo_rooty() + self.widget.winfo_height() + 6
except Exception:
return
try:
win = tk.Toplevel(self.widget)
win.wm_overrideredirect(True)
try:
win.attributes("-topmost", True)
except Exception:
pass
tk.Label(win, text=self.text, justify=tk.LEFT,
bg="#ffffe0", fg="#111111", relief=tk.SOLID, bd=1,
font=("Meiryo", 8), padx=5, pady=3,
wraplength=430).pack()
win.update_idletasks()
# 画面内に収まるように位置を調整
tw, th = win.winfo_reqwidth(), win.winfo_reqheight()
scr_w = win.winfo_screenwidth()
scr_h = win.winfo_screenheight()
x = max(0, min(x, scr_w - tw - 4))
y = max(0, min(y, scr_h - th - 4))
win.wm_geometry("+{}+{}".format(x, y))
self._win = win
except Exception:
self._win = None
def _hide(self, event=None):
self._cancel()
win = self._win
self._win = None
if win is not None:
try:
win.destroy()
except Exception:
pass
def attach_tip(widget, text):
"""ウィジェットにツールチップを付ける(重複時は文言を更新)。"""
if widget is None or not text:
return widget
try:
old = getattr(widget, "_av_tooltip", None)
if old is not None:
old.set_text(text)
return widget
widget._av_tooltip = ToolTip(widget, text)
except Exception:
pass
return widget
def tip(widget, text):
"""attach_tip の短縮形(戻り値は widget)。"""
attach_tip(widget, text)
return widget
class AlphaViewerNew:
IMAGE_EXTS = ('.png', '.jpg', '.jpeg', '.bmp', '.tif', '.tiff', '.webp')
# パネル番号 0-8
PANEL_TITLES = [
"1. 元画像",
"2. 透明検出(α)",
"3. 半透明→透明",
"4. 膨張 / 縮小",
"5. 行列クリーン",
"6. 色クリア",
"7. 端部小物体除去",
"8. ロングフェザー",
"9. 最終(全適用)",
]
PANEL_NAMES = [
"original",
"alpha_detect",
"semi_transparent",
"morph",
"rowcol_clean",
"color_clear",
"edge_clean",
"long_feather",
"final",
]
PANEL_TIPS = [
"1. 元画像:読み込んだままの画像です。\nクリックするとその画素の色を取得します。",
"2. 透明検出:α(透明度)だけをグレースケールで表示します。\n白いほど不透明、黒いほど透明です。\n上の「α分布」でαごとの画素数を確認できます。",
"3. 半透明:αが0より大きく閾値以下の画素を\n「透明」または「不透明」にまとめます。",
"4. 膨張/縮小:不透明部分の輪郭を太らせる(膨張)か\n痩せさせる(縮小)処理です。",
"5. 行列クリーン:ほぼ透明な行・列をまとめて透明にします。",
"6. 色クリア:指定した角から連続する同色を透明にします。\nパネルをクリックすると色を取得できます。",
"7. 端部小物体除去:画面の端に接する小さなゴミを消します。\nサイズは画像全体の面積に対する割合(%)で指定します。",
"8. ロングフェザー:輪郭のαをなめらかなグラデーションにします。",
"9. 最終:ON の処理を実行順にすべて適用した結果です。",
]
# 適用順を入れ替えられる処理
STAGE_DEFS = [
("semi", "3 半透明→透明", 2),
("morph", "4 膨張/縮小", 3),
("rowcol", "5 行列クリーン", 4),
("color", "6 色クリア", 5),
("edge_clean", "7 端部小物体除去", 6),
("long_feather", "8 ロングフェザー", 7),
]
# 既定でONにする処理と、その初期順序
DEFAULT_ON = ("semi", "morph", "rowcol", "color", "edge_clean", "long_feather")
DEFAULT_ORDER = ("semi", "morph", "rowcol", "color", "edge_clean", "long_feather")
# 設定ファイルに保存するパラメータ
PARAM_KEYS = (
"semi_mode", "semi_thresh", "morph_op", "morph_k", "morph_iter",
"rowcol_alpha", "row_pct", "row_cnt", "col_pct", "col_cnt",
"color_thresh", "corner_tl", "corner_tr", "corner_bl", "corner_br",
"edge_pct", "edge_alpha", "edge_top", "edge_bottom",
"edge_left", "edge_right",
"lf_inner", "lf_outer", "lf_curve", "lf_smooth", "lf_bleed",
"rgb_enable", "rgb_alpha_th",
)
SETTINGS_VERSION = 1
BATCH_TARGETS = {
"最終(全適用)": 8,
"ロングフェザー": 7,
"端部小物体除去": 6,
"色クリア": 5,
"行列クリーン": 4,
"膨張/縮小": 3,
"半透明": 2,
"透明検出": 1,
"元画像": 0,
}
def __init__(self, root):
self.root = root
self.root.title("AlphaViewer New")
self.root.geometry("1040x750")
self.root.minsize(920, 680)
self.folder = ""
self.image_path = ""
self.original_image = None # PIL RGBA
self.original_array = None # numpy RGBA
# 結果画像(9枚)
self.img_orig = None
self.img_trans_detect = None
self.img_semi = None
self.img_morph = None
self.img_rowcol = None
self.img_color = None
self.img_edge = None
self.img_alpha = None
self.img_final = None
self.panels_raw = None # 保存用の生パネル配列(9枚)
self._update_job = None
self._resize_job = None
self.batch_running = False
self._generated = None # 膨張で生成した画素(行列クリーンで透明扱い)
self._rc_info = (0, 0) # 行列クリーンで消した行数・列数
self._edge_info = (0, 0) # 端部小物体除去で消した個数・画素数
# 実行順の別ウィンドウ
self.order_win = None
self.order_frames = {}
# α分布の別ウィンドウ
self.alpha_win = None
self.alpha_tree = None
self.alpha_summary = None
self.alpha_summary2 = None
self.alpha_rows = None
self._alpha_job = None
# --- パラメータ ---
# 半透明("transparent" = 半透明→透明 / "opaque" = 半透明→不透明)
self.semi_mode = tk.StringVar(value="transparent")
self.semi_thresh = tk.IntVar(value=128)
# 膨張/縮小
self.morph_op = tk.StringVar(value="dilate")
self.morph_k = tk.IntVar(value=3)
self.morph_iter = tk.IntVar(value=1)
# 行列クリーン
self.rowcol_alpha = tk.IntVar(value=10)
self.row_pct = tk.IntVar(value=95)
self.row_cnt = tk.IntVar(value=3)
self.col_pct = tk.IntVar(value=95)
self.col_cnt = tk.IntVar(value=3)
# 色クリア
self.color_thresh = tk.IntVar(value=30)
self.corner_tl = tk.BooleanVar(value=True)
self.corner_tr = tk.BooleanVar(value=False)
self.corner_bl = tk.BooleanVar(value=False)
self.corner_br = tk.BooleanVar(value=False)
self.picked_color = None
# 端部小物体除去(面積割合: 画像全体に対する %)
self.edge_pct = tk.DoubleVar(value=0.9)
self.edge_alpha = tk.IntVar(value=10)
self.edge_top = tk.BooleanVar(value=True)
self.edge_bottom = tk.BooleanVar(value=True)
self.edge_left = tk.BooleanVar(value=True)
self.edge_right = tk.BooleanVar(value=True)
# ロングフェザー(透明グラデーション)
self.lf_inner = tk.IntVar(value=5)
self.lf_outer = tk.IntVar(value=5)
self.lf_curve = tk.IntVar(value=100)
self.lf_smooth = tk.BooleanVar(value=True)
self.lf_bleed = tk.BooleanVar(value=True)
# バッチ
self.batch_target = tk.StringVar(value="最終(全適用)")
# 背景合成・色設定
self.bg_mode = tk.StringVar(value="raw") # "checker", "color", "image"
self.bg_color = (255, 255, 255) # デフォルト白
self.bg_image_path = ""
self.bg_image_pil = None
# 透明部のRGB変更設定(背景透明のまま)
self.rgb_enable = tk.BooleanVar(value=False)
self.rgb_target_color = (0, 0, 0) # デフォルト黒 (0, 0, 0)
self.rgb_alpha_th = tk.IntVar(value=10) # α <= 閾値の透明画素を対象
# 表示確認用: ONにすると透明部を塗りつぶしてRGB変更を目視できる(保存内容は変わらない)
self.rgb_preview = tk.BooleanVar(value=True)
# --- 処理ステージ ---
self.stages = []
for i, (key, label, panel) in enumerate(self.STAGE_DEFS):
self.stages.append({
"key": key,
"label": label,
"panel": panel,
"var": tk.BooleanVar(value=key in self.DEFAULT_ON),
})
self.setup_ui()
def _on_root_config(e):
if e.widget is not self.root:
return
if self._resize_job is not None:
try:
self.root.after_cancel(self._resize_job)
except Exception:
pass
self._resize_job = self.root.after(120, self._redraw_views_now)
self.root.bind("<Configure>", _on_root_config)
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
def on_close(self):
"""終了時に保留中の after ジョブを止める。"""
for job in (self._update_job, self._resize_job):
if job is not None:
try:
self.root.after_cancel(job)
except Exception:
pass
self._update_job = None
self._resize_job = None
try:
self.close_order_window()
except Exception:
pass
try:
self.close_alpha_window()
except Exception:
pass
try:
self.root.destroy()
except Exception:
pass
# ------------------------------------------------------------
# UI
# ------------------------------------------------------------
def _redraw_views_now(self):
self._resize_job = None
self.update_all()
def setup_ui(self):
# 最上部: タイトル行(サイズとファイルパス)
header = tk.Frame(self.root, bg="#e8e8e8", bd=1, relief=tk.SOLID)
header.pack(side=tk.TOP, fill=tk.X)
self.title_label = tk.Label(header, text="AlphaViewer New", font=F7,
bg="#e8e8e8", anchor="w", padx=4, pady=1)
self.title_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
# 左 → 右 → 中央 の順に配置
left = tk.Frame(self.root, width=140, bg="#e8e8e8")
left.pack(side=tk.LEFT, fill=tk.Y)
left.pack_propagate(False)
b = tk.Button(left, text="フォルダ読み込み", command=self.load_folder,
font=F7, padx=2, pady=1)
b.pack(fill=tk.X, padx=2, pady=2)
tip(b, "画像フォルダを選択し、左側にサムネイル一覧を表示します。\n"
"画像ファイルをウィンドウへドラッグ&ドロップしても読み込めます。")
# 背景合成・色設定枠
bg_box = tk.LabelFrame(left, text="背景合成・色", font=F7B, bg="#e8e8e8", padx=2, pady=2)
bg_box.pack(fill=tk.X, padx=2, pady=(0, 2))
bm_frame = tk.Frame(bg_box, bg="#e8e8e8")
bm_frame.pack(fill=tk.X, pady=1)
for txt, val, tip_text in (
("無", "raw", "背景を合成せず、透明部分をそのまま表示します。"),
("色", "color", "単色の背景と合成して表示します。"),
("画", "image", "読み込んだ背景画像と合成して表示します。"),
("市", "checker",
"市松模様(チェッカーボード)の背景で表示します。\n透明部分が分かりやすくなります。")):
rb = tk.Radiobutton(bm_frame, text=txt, variable=self.bg_mode,
value=val, command=self.update_all, font=F6,
bg="#e8e8e8", padx=0, pady=0)
rb.pack(side=tk.LEFT, padx=0)
tip(rb, tip_text)
bc_frame = tk.Frame(bg_box, bg="#e8e8e8")
bc_frame.pack(fill=tk.X, pady=1)
b = tk.Button(bc_frame, text="白", font=F6, padx=2, pady=0,
command=lambda: self.set_bg_color(255, 255, 255))
b.pack(side=tk.LEFT, padx=1)
tip(b, "背景色を白にします。")
b = tk.Button(bc_frame, text="黒", font=F6, padx=2, pady=0,
command=lambda: self.set_bg_color(0, 0, 0))
b.pack(side=tk.LEFT, padx=1)
tip(b, "背景色を黒にします。")
b = tk.Button(bc_frame, text="選択", font=F6, padx=2, pady=0,
command=self.choose_bg_color)
b.pack(side=tk.LEFT, padx=1)
tip(b, "任意の背景色を選択します。")
self.bg_swatch = tk.Label(bc_frame, text=" ", bg="#ffffff", relief=tk.SUNKEN, width=2)
self.bg_swatch.pack(side=tk.LEFT, padx=1)
tip(self.bg_swatch, "現在の背景色の見本です。")
bi_frame = tk.Frame(bg_box, bg="#e8e8e8")
bi_frame.pack(fill=tk.X, pady=1)
b = tk.Button(bi_frame, text="背景画像読込", font=F6, padx=2, pady=0,
command=self.load_bg_image)
b.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=1)
tip(b, "背景として合成する画像を読み込みます。")
b = tk.Button(bi_frame, text="解除", font=F6, padx=2, pady=0,
command=self.clear_bg_image)
b.pack(side=tk.LEFT, padx=1)
tip(b, "背景画像を解除して市松模様に戻します。")
thumb_c = tk.Frame(left, bg="#e8e8e8")
thumb_c.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
self.tcanvas = tk.Canvas(thumb_c, bg="#d0d0d0", width=110,
highlightthickness=0)
ts = tk.Scrollbar(thumb_c, orient=tk.VERTICAL, command=self.tcanvas.yview)
self.tcanvas.configure(yscrollcommand=ts.set)
ts.pack(side=tk.RIGHT, fill=tk.Y)
self.tcanvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.tinner = tk.Frame(self.tcanvas, bg="#d0d0d0")
self.tcanvas.create_window((0, 0), window=self.tinner, anchor=tk.NW)
self.tinner.bind("<Configure>",
lambda e: self.tcanvas.configure(
scrollregion=self.tcanvas.bbox("all")))
tip(self.tcanvas, "サムネイル一覧(透明部分は市松模様)\n"
"クリックでその画像を読み込みます。")
# 右(縦スクロール)
right = tk.Frame(self.root, width=310, bg="#f0f0f0")
right.pack(side=tk.RIGHT, fill=tk.Y)
right.pack_propagate(False)
r_canvas = tk.Canvas(right, width=290, highlightthickness=0, bg="#f0f0f0")
r_scroll = tk.Scrollbar(right, orient=tk.VERTICAL, command=r_canvas.yview)
r_canvas.configure(yscrollcommand=r_scroll.set)
self.rc_canvas = r_canvas
# 下部固定バー(不透明化 & 色クリア & 透明部RGB変更)
bottom = tk.Frame(right, bg="#dcdcdc", bd=1, relief=tk.SOLID)
bottom.pack(side=tk.BOTTOM, fill=tk.X)
# 1行目: 不透明化 / 設定の保存・読込 / α閾値
bf2 = tk.Frame(bottom, bg="#dcdcdc")
bf2.pack(fill=tk.X, padx=2, pady=(2, 1))
b = tk.Button(bf2, text="不透明化", font=F7, padx=3, pady=1,
command=self.clear_alpha_all)
b.pack(side=tk.LEFT, padx=(1, 3))
tip(b, "元画像のαをすべて255(完全な不透明)にします。\n"
"元に戻せないため、確認ダイアログが出ます。")
self.cfg_save_btn = tk.Button(bf2, text="設定保存", font=F6,
padx=2, pady=0,
command=self.save_settings)
self.cfg_save_btn.pack(side=tk.LEFT, padx=(0, 1))
tip(self.cfg_save_btn, "現在のパラメータ・実行順・ON/OFF を\n"
"JSONファイルに保存します。")
self.cfg_load_btn = tk.Button(bf2, text="設定読込", font=F6,
padx=2, pady=0,
command=self.load_settings)
self.cfg_load_btn.pack(side=tk.LEFT, padx=(0, 2))
tip(self.cfg_load_btn, "保存したJSONファイルを読み込み、\n"
"パラメータ・実行順・ON/OFF を復元します。")
self.rgb_alpha_th_label = tk.Label(
bf2, text="α≤{}".format(self.rgb_alpha_th.get()), font=F6,
bg="#dcdcdc", width=5)
self.rgb_alpha_th_label.pack(side=tk.RIGHT, padx=(1, 2))
sc = tk.Scale(bf2, from_=0, to=255, orient=tk.HORIZONTAL,
variable=self.rgb_alpha_th, font=F6, length=70, width=7,
sliderlength=10, showvalue=False, bd=0, highlightthickness=0,
bg="#dcdcdc",
command=lambda e: (
self.rgb_alpha_th_label.config(
text="α≤{}".format(self.rgb_alpha_th.get())),
self.update_all()
))
sc.pack(side=tk.RIGHT)
self.rgb_alpha_th_scale = sc
tip(sc, "透明部RGB変更の対象となるαの上限です。\n"
"この値以下の画素のRGBを変更します(αは変わりません)。")
# 2行目: 透明部RGB変更(背景透明のまま)
bf3 = tk.Frame(bottom, bg="#dcdcdc")
bf3.pack(fill=tk.X, padx=2, pady=(0, 2))
c = tk.Checkbutton(bf3, text="透明部RGB変更", variable=self.rgb_enable,
command=self.update_all, font=F7, bg="#dcdcdc",
padx=0, pady=0)
c.pack(side=tk.LEFT)
tip(c, "透明な画素のRGB値だけを変更します(αは透明のまま)。\n"
"透過PNGの色にじみ対策などに使います。")
c = tk.Checkbutton(bf3, text="表示", variable=self.rgb_preview,
command=self.update_all, font=F6, bg="#dcdcdc",
padx=0, pady=0)
c.pack(side=tk.LEFT, padx=(1, 2))
tip(c, "変更したRGBを確認しやすいよう、\n"
"画面上だけ透明部を塗りつぶして表示します。\n"
"保存される画像は変わりません。")
b = tk.Button(bf3, text="色選択", command=self.choose_rgb_target,
font=F6, padx=2, pady=0)
b.pack(side=tk.LEFT)
tip(b, "透明部に設定するRGB色を選択します。")
self.rgb_swatch = tk.Label(bf3, text=" ", bg="#000000", relief=tk.SUNKEN, width=2)
self.rgb_swatch.pack(side=tk.LEFT, padx=(2, 0))
tip(self.rgb_swatch, "現在の変更色の見本です。")
b = tk.Button(bf3, text="黒", font=F6, padx=2, pady=0,
command=lambda: self.set_rgb_target(0, 0, 0))
b.pack(side=tk.LEFT, padx=(2, 1))
tip(b, "透明部のRGBを黒 (0,0,0) にします。")
b = tk.Button(bf3, text="白", font=F6, padx=2, pady=0,
command=lambda: self.set_rgb_target(255, 255, 255))
b.pack(side=tk.LEFT, padx=1)
tip(b, "透明部のRGBを白 (255,255,255) にします。")
b = tk.Button(bf3, text="紫", font=F6, padx=2, pady=0,
command=lambda: self.set_rgb_target(255, 0, 255))
b.pack(side=tk.LEFT, padx=1)
tip(b, "透明部のRGBを紫 (255,0,255) にします。\n"
"にじみの確認用です。")
self.rgb_status = tk.Label(bf3, text="", font=F6, fg="#666666",
bg="#dcdcdc", anchor="e")
self.rgb_status.pack(side=tk.RIGHT, padx=(2, 2))
r_scroll.pack(side=tk.RIGHT, fill=tk.Y)
r_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
rc = tk.Frame(r_canvas, bg="#f0f0f0")
r_win = r_canvas.create_window((0, 0), window=rc, anchor=tk.NW)
rc.bind("<Configure>",
lambda e: r_canvas.configure(scrollregion=r_canvas.bbox("all")))
r_canvas.bind("<Configure>",
lambda e: r_canvas.itemconfig(r_win, width=e.width))
def _on_rwheel(e):
try:
r_canvas.yview_scroll(int(-1 * (e.delta / 120)), "units")
except Exception:
pass
r_canvas.bind("<Enter>",
lambda e: r_canvas.bind_all("<MouseWheel>", _on_rwheel))
r_canvas.bind("<Leave>", lambda e: r_canvas.unbind_all("<MouseWheel>"))
# 中央
center = tk.Frame(self.root)
center.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
for rr in range(3):
center.grid_rowconfigure(rr, weight=1, uniform="row")
for cc in range(3):
center.grid_columnconfigure(cc, weight=1, uniform="col")
self.cvs = []
self.panel_title_labels = []
for i, title in enumerate(self.PANEL_TITLES):
rr = i // 3
cc = i % 3
f = tk.Frame(center, bd=1, relief=tk.SOLID)
f.grid(row=rr, column=cc, sticky="nsew", padx=1, pady=1)
f.grid_rowconfigure(1, weight=1)
f.grid_columnconfigure(0, weight=1)
tl = tk.Label(f, text=title, font=F7)
tl.grid(row=0, column=0, sticky="w")
self.panel_title_labels.append(tl)
cv = tk.Canvas(f, bg="#808080", highlightthickness=0,
width=110, height=85)
cv.grid(row=1, column=0, sticky="nsew")
if i in (0, 5):
cv.bind("<Button-1>", self.click_pick_color)
tip(cv, self.PANEL_TIPS[i])
self.cvs.append(cv)
# ---- 右パネル内容 ----
rc_ref = tk.Frame(rc, padx=2, pady=2)
rc_ref.pack(fill=tk.BOTH, expand=True)
rc_ref.grid_columnconfigure(1, weight=1)
def section(row, title, save_idx=None, enable_var=None, buttons=(),
label_sink=None):
bar = tk.Frame(rc_ref)
bar.grid(row=row, column=0, columnspan=3, sticky="ew", pady=(3, 0))
if enable_var is not None:
w = tk.Checkbutton(bar, text=title, variable=enable_var,
command=self.update_all, font=F8B,
padx=0, pady=0)
else:
w = tk.Label(bar, text=title, font=F8B)
w.pack(side=tk.LEFT)
if label_sink is not None:
label_sink.append(w)
if save_idx is not None:
b = tk.Button(bar, text="保存", width=4, font=F7, padx=2, pady=0,
command=lambda i=save_idx: self.save_panel(i))
b.pack(side=tk.RIGHT)
tip(b, "このパネルの画像をPNGで保存します。")
for text, cmd, tip_text in buttons:
b = tk.Button(bar, text=text, font=F7, padx=2, pady=0,
command=cmd)
b.pack(side=tk.RIGHT, padx=(0, 2))
tip(b, tip_text)
return row + 1
def prow(row, label, var, from_, to, width=8, stage_key=None,
resolution=None, tip_text=None):
lb = tk.Label(rc_ref, text=label, font=F6, width=width, anchor="w")
lb.grid(row=row, column=0, sticky="w")
kw = {}
if resolution:
kw["resolution"] = resolution
sc = tk.Scale(rc_ref, from_=from_, to=to, orient=tk.HORIZONTAL,
variable=var, font=F6, length=84, width=7,
sliderlength=12, showvalue=False, bd=0,
highlightthickness=0, **kw,
command=lambda e: self._param_changed(stage_key))
sc.grid(row=row, column=1, sticky="ew")
vl = tk.Label(rc_ref, textvariable=var, font=F6, width=3,
anchor="e")
vl.grid(row=row, column=2, sticky="e", padx=(0, 4))
if tip_text is None:
tip_text = "{}({}〜{})".format(label, from_, to)
for w in (lb, sc, vl):
tip(w, tip_text)
return row + 1
r = 0
# 実行順序(上部横並び配置)
bar1 = tk.Frame(rc_ref)
bar1.grid(row=r, column=0, columnspan=3, sticky="ew", pady=(2, 2))
b = tk.Button(bar1, text="実行順を設定…", font=F8B, padx=3, pady=1,
command=self.open_order_window)
b.pack(side=tk.LEFT)
tip(b, "適用する処理のON/OFFと実行順を別ウィンドウで設定します。\n"
"上から順に適用され、⑨ 最終(全適用)に反映されます。")
b = tk.Button(bar1, text="⑨ 最終を保存", font=F8B, padx=3, pady=1,
command=lambda: self.save_panel(8))
b.pack(side=tk.LEFT, padx=2)
tip(b, "⑨ 最終(全適用)の画像をPNGで保存します。")
b = tk.Button(bar1, text="初期化", font=F7, padx=3, pady=1,
command=self.reset_stages)
b.pack(side=tk.RIGHT)
tip(b, "処理のON/OFFと実行順を初期状態に戻します。")
r += 1
# フォルダ一括処理(実行順ボタンの場所に移動、横並び)
bar2 = tk.Frame(rc_ref)
bar2.grid(row=r, column=0, columnspan=3, sticky="ew", pady=(1, 3))
self.batch_btn = tk.Button(bar2, text="フォルダ一括",
command=self.batch_process, font=F8B,
padx=3, pady=1)
self.batch_btn.pack(side=tk.LEFT)
tip(self.batch_btn, "選択したフォルダ内の画像をまとめて処理し、\n"
"別フォルダに保存します。")
self.batch_menu = tk.OptionMenu(
bar2, self.batch_target,
"最終(全適用)", "ロングフェザー", "端部小物体除去", "色クリア", "行列クリーン",
"膨張/縮小", "半透明", "透明検出", "元画像", "全9種")
self.batch_menu.config(font=F7, padx=1, pady=0, highlightthickness=0,
width=10, anchor="w")
self.batch_menu.pack(side=tk.LEFT, padx=2)
tip(self.batch_menu, "一括処理で保存するパネルを選びます。\n"
"「全9種」は9枚すべてを保存します。")
self.batch_status = tk.Label(bar2, text="", font=F6, fg="#555555",
anchor="w")
self.batch_status.pack(side=tk.LEFT, padx=1)
r += 1
self.order_summary = tk.Label(rc_ref, text="", font=F6, fg="#444444",
anchor="w", justify=tk.LEFT,
wraplength=285)
self.order_summary.grid(row=r, column=0, columnspan=3, sticky="ew",
pady=(0, 2))
r += 1
# 1, 2
r = section(r, "1. 元画像", 0)
r = section(r, "2. 透明検出", 1,
buttons=(("α分布", self.show_alpha_histogram,
"α(0〜255)ごとのピクセル数を\n"
"別ウィンドウの一覧で表示します。"),))
# 3
semi_labels = []
r = section(r, self._semi_title(), 2, label_sink=semi_labels)
self.semi_section_label = semi_labels[0]
tip(self.semi_section_label,
"αが0より大きく閾値以下の画素をどう扱うかを選びます。")
sm = tk.Frame(rc_ref)
sm.grid(row=r, column=0, columnspan=3, sticky="w")
rb = tk.Radiobutton(sm, text="透明", variable=self.semi_mode,
value="transparent", command=self._set_semi_mode,
font=F7)
rb.pack(side=tk.LEFT)
tip(rb, "半透明の画素を完全な透明(α=0)にします。\n(初期設定)")
rb = tk.Radiobutton(sm, text="不透明", variable=self.semi_mode,
value="opaque", command=self._set_semi_mode,
font=F7)
rb.pack(side=tk.LEFT, padx=(6, 0))
tip(rb, "半透明の画素を完全な不透明(α=255)にします。")
r += 1
r = prow(r, "α≤", self.semi_thresh, 0, 255, stage_key="semi",
tip_text="この値以下のαを半透明として扱います。\n"
"0 より大きく、この値以下の画素が対象です。")
# 4
r = section(r, "4. 膨張/縮小", 3)
mf = tk.Frame(rc_ref)
mf.grid(row=r, column=0, columnspan=3, sticky="w")
rb = tk.Radiobutton(mf, text="膨張", variable=self.morph_op, value="dilate",
command=self.update_all, font=F8)
rb.pack(side=tk.LEFT)
tip(rb, "不透明部分を太らせます(輪郭が広がります)。")
rb = tk.Radiobutton(mf, text="縮小", variable=self.morph_op, value="erode",
command=self.update_all, font=F8)
rb.pack(side=tk.LEFT)
tip(rb, "不透明部分を痩せさせます(輪郭が内側に縮みます)。")
r += 1
r = prow(r, "カーネル", self.morph_k, 1, 9, stage_key="morph",
tip_text="処理する範囲(カーネル)の大きさです。\n大きいほど強く効きます。")
r = prow(r, "回数", self.morph_iter, 1, 10, stage_key="morph",
tip_text="同じ膨張/縮小を繰り返す回数です。")
# 5
r = section(r, "5. 行・列クリーン", 4)
r = prow(r, "α閾値", self.rowcol_alpha, 0, 255, stage_key="rowcol",
tip_text="これ以下のαを透明とみなして行・列を判定します。")
r = prow(r, "行 割合%", self.row_pct, 0, 100, stage_key="rowcol",
tip_text="行のうち透明な画素がこの割合以上なら、その行を消します。")
r = prow(r, "行 連続数", self.row_cnt, 1, 50, stage_key="rowcol",
tip_text="行内で許容する不透明の連続数です。\n少ないほど消えやすくなります。")
r = prow(r, "列 割合%", self.col_pct, 0, 100, stage_key="rowcol",
tip_text="列のうち透明な画素がこの割合以上なら、その列を消します。")
r = prow(r, "列 連続数", self.col_cnt, 1, 50, stage_key="rowcol",
tip_text="列内で許容する不透明の連続数です。\n少ないほど消えやすくなります。")
self.rc_status = tk.Label(rc_ref, text="クリア: 行0 / 列0", font=F6,
fg="#777777", anchor="w")
self.rc_status.grid(row=r, column=0, columnspan=3, sticky="w")
r += 1
# 6
r = section(r, "6. 色クリア", 5)
cf0 = tk.Frame(rc_ref)
cf0.grid(row=r, column=0, columnspan=3, sticky="w")
tk.Label(cf0, text="角から連続する同色を透明化", font=F6).pack(anchor="w")
cf1 = tk.Frame(cf0)
cf1.pack(anchor="w")
for txt, var, tip_text in (
("左上", self.corner_tl, "左上から連続する同色を透明にします。"),
("右上", self.corner_tr, "右上から連続する同色を透明にします。"),
("左下", self.corner_bl, "左下から連続する同色を透明にします。"),
("右下", self.corner_br, "右下から連続する同色を透明にします。")):
cb = tk.Checkbutton(cf1, text=txt, variable=var,
command=self.update_all, font=F7, padx=0, pady=0,
takefocus=0)
cb.pack(side=tk.LEFT, padx=(0, 2))
tip(cb, tip_text)
r += 1
r = prow(r, "色差", self.color_thresh, 0, 100, stage_key="color",
tip_text="同じ色とみなす許容差です。\n大きいほど広い色を透明にします。")
# 7 端部小物体除去
r = section(r, "7. 端部小物体除去", 6)
ef0 = tk.Frame(rc_ref)
ef0.grid(row=r, column=0, columnspan=3, sticky="w")
for txt, var, tip_text in (
("上", self.edge_top, "上端に接する小物体を除去します。"),
("下", self.edge_bottom, "下端に接する小物体を除去します。"),
("左", self.edge_left, "左端に接する小物体を除去します。"),
("右", self.edge_right, "右端に接する小物体を除去します。")):
cb = tk.Checkbutton(ef0, text=txt, variable=var,
command=self.update_all, font=F7, padx=0, pady=0,
takefocus=0)
cb.pack(side=tk.LEFT, padx=(0, 2))
tip(cb, tip_text)
r += 1
r = prow(r, "面積≤%", self.edge_pct, 0, 20, stage_key="edge_clean",
resolution=0.1,
tip_text="画像全体の面積に対する上限割合です。\n"
"この割合より小さい塊を除去します(例: 0.5%)。")
r = prow(r, "α閾値", self.edge_alpha, 0, 255, stage_key="edge_clean",
tip_text="これ以下のαを透明とみなして塊の大きさを判定します。")
self.edge_status = tk.Label(rc_ref, text="除去: 0個 (0 px)", font=F6,
fg="#777777", anchor="w")
self.edge_status.grid(row=r, column=0, columnspan=3, sticky="w")
r += 1
# 8 ロングフェザー(透明グラデーション)
r = section(r, "8. ロングフェザー", 7)
lff = tk.Frame(rc_ref)
lff.grid(row=r, column=0, columnspan=3, sticky="w")
cb = tk.Checkbutton(lff, text="S字滑らか", variable=self.lf_smooth,
command=self.update_all, font=F7, padx=0, pady=0,
takefocus=0)
cb.pack(side=tk.LEFT, padx=(0, 4))
tip(cb, "αの変化をS字カーブにして、より自然なグラデーションにします。")
cb = tk.Checkbutton(lff, text="境界色保護", variable=self.lf_bleed,
command=self.update_all, font=F7, padx=0, pady=0,
takefocus=0)
cb.pack(side=tk.LEFT)
tip(cb, "輪郭付近の色を内側からにじませて、色の欠けを防ぎます。")
r += 1
r = prow(r, "内側侵入", self.lf_inner, 0, 50, stage_key="long_feather",
tip_text="不透明部分の内側へグラデーションを入れる幅です。")
r = prow(r, "外側広がり", self.lf_outer, 0, 50, stage_key="long_feather",
tip_text="輪郭の外側へ透明グラデーションを広げる幅です。")
r = prow(r, "広がり方", self.lf_curve, 10, 300, stage_key="long_feather",
tip_text="グラデーションのカーブの強さです。\n100 が標準です。")
self._update_title()
if DND_AVAILABLE:
try:
DragDropHandler(self.root, self.load_file)
except Exception:
pass
# ------------------------------------------------------------
# タイトル行(サイズ・ファイルパス)
# ------------------------------------------------------------
def _update_title(self):
"""タイトル行とウィンドウタイトルに サイズ・パス を表示する。"""
if self.original_image is None:
label = "AlphaViewer New"
full = "AlphaViewer New"
else:
try:
w, h = self.original_image.size
except Exception:
w, h = 0, 0
path = self.image_path or "(未読み込み)"
size_txt = "幅{} × 高さ{} px".format(w, h)
full = "AlphaViewer New | {} | {}".format(size_txt, path)
label = full
# 幅に収まらない場合はパスを末尾優先で省略(全文はツールチップ)
max_len = 110
if len(label) > max_len:
tail = path[-(max_len - 30):]
label = "AlphaViewer New | {} | …{}".format(size_txt, tail)
try:
self.root.title(full)
except Exception:
pass
try:
self.title_label.config(text=label)
tip(self.title_label, full)
except Exception:
pass
# ------------------------------------------------------------
# 半透明モード(透明 / 不透明)
# ------------------------------------------------------------
def _semi_target(self):
try:
return "不透明" if self.semi_mode.get() == "opaque" else "透明"
except Exception:
return "透明"
def _semi_title(self, short=False):
prefix = "3 " if short else "3. "
return prefix + "半透明→" + self._semi_target()
def _update_semi_labels(self):
"""半透明モードに合わせてパネル名・見出し・実行順の表示を更新する。"""
for st in self.stages:
if st.get("key") == "semi":
st["label"] = self._semi_title(short=True)
try:
self.semi_section_label.config(text=self._semi_title())
except Exception:
pass
try:
self.panel_title_labels[2].config(text=self._semi_title())
except Exception:
pass
tip(self.cvs[2], self.PANEL_TIPS[2].replace(
"「透明」または「不透明」", "「" + self._semi_target() + "」"))
def _set_semi_mode(self):
self._update_semi_labels()
self._refresh_order_window()
self.update_all()
# ------------------------------------------------------------
# α分布(別画面)
# ------------------------------------------------------------
def show_alpha_histogram(self):
if self.original_array is None or self.panels_raw is None:
messagebox.showinfo("α分布", "画像が読み込まれていません。")
return
if self.alpha_win is not None:
try:
if self.alpha_win.winfo_exists():
self._fill_alpha_window()
self.alpha_win.deiconify()
self.alpha_win.lift()
self.alpha_win.focus_set()
return
except Exception:
pass
self.alpha_win = None
w = tk.Toplevel(self.root)
self.alpha_win = w
w.title("α分布(アルファごとのピクセル数)")
w.geometry("460x600")
w.minsize(420, 400)
try:
w.transient(self.root)
except Exception:
pass
w.protocol("WM_DELETE_WINDOW", self.close_alpha_window)
head = tk.Frame(w)
head.pack(fill=tk.X, padx=8, pady=(6, 2))
tk.Label(head, text="αごとのピクセル数", font=F8B).pack(anchor="w")
tk.Label(head, text="元画像と ⑨ 最終(全適用)のα分布を並べて表示します。",
font=F6, fg="#666666").pack(anchor="w")
self.alpha_summary = tk.Label(w, text="", font=F7, anchor="w",
justify=tk.LEFT, wraplength=430)
self.alpha_summary.pack(fill=tk.X, padx=8)
self.alpha_summary2 = tk.Label(w, text="", font=F7, anchor="w",
justify=tk.LEFT, fg="#555555",
wraplength=430)
self.alpha_summary2.pack(fill=tk.X, padx=8, pady=(0, 4))
wrap = tk.Frame(w)
wrap.pack(fill=tk.BOTH, expand=True, padx=8, pady=2)
cols = ("a", "o", "op", "f", "fp")
tree = ttk.Treeview(wrap, columns=cols, show="headings", height=16,
selectmode="browse")
for c, text, width, anchor in (
("a", "α", 52, "center"),
("o", "元画像 px", 86, "e"),
("op", "元画像 %", 76, "e"),
("f", "最終 px", 86, "e"),
("fp", "最終 %", 76, "e")):
tree.heading(c, text=text)
tree.column(c, width=width, anchor=anchor, stretch=False)
vs = tk.Scrollbar(wrap, orient=tk.VERTICAL, command=tree.yview)
tree.configure(yscrollcommand=vs.set)
vs.pack(side=tk.RIGHT, fill=tk.Y)
tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
try:
st = ttk.Style(w)
st.configure("Alpha.Treeview", font=("Meiryo", 7), rowheight=15)
st.configure("Alpha.Treeview.Heading", font=("Meiryo", 7))
tree.configure(style="Alpha.Treeview")
except Exception:
pass
tree.tag_configure("semi", background="#fff3c4")
self.alpha_tree = tree
foot = tk.Frame(w)
foot.pack(fill=tk.X, padx=8, pady=(2, 8))
b = tk.Button(foot, text="再計算", font=F7, padx=4, pady=1,
command=self._fill_alpha_window)
b.pack(side=tk.LEFT)
tip(b, "現在の設定で α分布 を計算し直します。")
b = tk.Button(foot, text="コピー", font=F7, padx=4, pady=1,
command=self.copy_alpha_table)
b.pack(side=tk.LEFT, padx=3)
tip(b, "表の内容をタブ区切りテキストとして\nクリップボードにコピーします。")
b = tk.Button(foot, text="閉じる", font=F8B, padx=8, pady=1,
command=self.close_alpha_window)
b.pack(side=tk.RIGHT)
tip(b, "このウィンドウを閉じます。")
self._fill_alpha_window()
def _fill_alpha_window(self):
"""α分布の表と集計を最新の内容で埋め直す。"""
tree = getattr(self, "alpha_tree", None)
if tree is None or self.original_array is None or self.panels_raw is None:
return
try:
if not tree.winfo_exists():
return
except Exception:
return
orig = np.asarray(self.original_array[..., 3]).ravel()
fin = np.asarray(self.panels_raw[8][..., 3]).ravel()
co = np.bincount(orig, minlength=256).astype(np.int64)
cf = np.bincount(fin, minlength=256).astype(np.int64)
tot_o = int(co.sum()) or 1
tot_f = int(cf.sum()) or 1
try:
tree.delete(*tree.get_children())
except Exception:
pass
rows = []
for a in range(256):
o = int(co[a])
f = int(cf[a])
po = 100.0 * o / tot_o
pf = 100.0 * f / tot_f
tags = ("semi",) if (0 < a < 255 and (o or f)) else ()
try:
tree.insert("", "end", values=(
a, o, "{:.2f}".format(po), f, "{:.2f}".format(pf)),
tags=tags)
except Exception:
break
rows.append((a, o, po, f, pf))
self.alpha_rows = rows
def summary(c):
z = int(c[0])
semi = int(c[1:255].sum())
full = int(c[255])
return dict(z=z, semi=semi, full=full)
so = summary(co)
sf = summary(cf)
try:
self.alpha_summary.config(
text="元画像: 合計 {:,} px / α=0: {:,} ({:.2f}%) / "
"半透明(1-254): {:,} ({:.2f}%) / α=255: {:,} ({:.2f}%)".format(
tot_o, so["z"], 100.0 * so["z"] / tot_o,
so["semi"], 100.0 * so["semi"] / tot_o,
so["full"], 100.0 * so["full"] / tot_o))
self.alpha_summary2.config(
text="最終 : 合計 {:,} px / α=0: {:,} ({:.2f}%) / "
"半透明(1-254): {:,} ({:.2f}%) / α=255: {:,} ({:.2f}%)".format(
tot_f, sf["z"], 100.0 * sf["z"] / tot_f,
sf["semi"], 100.0 * sf["semi"] / tot_f,
sf["full"], 100.0 * sf["full"] / tot_f))
except Exception:
pass
def copy_alpha_table(self):
rows = getattr(self, "alpha_rows", None)
if not rows:
return
lines = ["α\t元画像 px\t元画像 %\t最終 px\t最終 %"]
for a, o, po, f, pf in rows:
lines.append("{}\t{}\t{:.2f}\t{}\t{:.2f}".format(a, o, po, f, pf))
try:
self.root.clipboard_clear()
self.root.clipboard_append("\n".join(lines))
except Exception:
pass
def _schedule_alpha_refresh(self):
"""α分布ウィンドウが開いているときだけ、少し遅らせて再計算する。"""
if self.alpha_win is None or self._alpha_job is not None:
return
try:
self._alpha_job = self.root.after(400, self._run_alpha_refresh)
except Exception:
self._alpha_job = None
def _run_alpha_refresh(self):
self._alpha_job = None
win = self.alpha_win
if win is None:
return
try:
if not win.winfo_exists():
self.alpha_win = None
return
except Exception:
self.alpha_win = None
return
self._fill_alpha_window()
def _cancel_alpha_job(self):
job = self._alpha_job
self._alpha_job = None
if job is not None:
try:
self.root.after_cancel(job)
except Exception:
pass
def close_alpha_window(self):
self._cancel_alpha_job()
win = self.alpha_win
self.alpha_win = None
self.alpha_tree = None
self.alpha_rows = None
if win is not None:
try:
win.destroy()
except Exception:
pass
# ------------------------------------------------------------
# 実行順ウィンドウ(別画面)
# ------------------------------------------------------------
def open_order_window(self):
if self.order_win is not None:
try:
if self.order_win.winfo_exists():
self.order_win.deiconify()
self.order_win.lift()
self.order_win.focus_set()
return
except Exception:
pass
self.order_win = None
w = tk.Toplevel(self.root)
self.order_win = w
w.title("実行順の設定(上から順に適用)")
w.geometry("500x680")
w.minsize(460, 480)
try:
w.transient(self.root)
except Exception:
pass
w.protocol("WM_DELETE_WINDOW", self.close_order_window)
head = tk.Frame(w)
head.pack(fill=tk.X, padx=10, pady=(8, 2))
tk.Label(head, text="チェックした処理を上から順に適用します",
font=F8B).pack(anchor="w")
tk.Label(head,
text="↑↓ で順番を入れ替え / 各処理と ⑨ 最終(全適用)に反映",
font=F6, fg="#666666").pack(anchor="w")
listwrap = tk.Frame(w)
listwrap.pack(fill=tk.BOTH, expand=True, padx=8, pady=2)
canvas = tk.Canvas(listwrap, highlightthickness=0)
sb = tk.Scrollbar(listwrap, orient=tk.VERTICAL, command=canvas.yview)
canvas.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
inner = tk.Frame(canvas)
win_id = canvas.create_window((0, 0), window=inner, anchor=tk.NW)
inner.bind("<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.bind("<Configure>",
lambda e: canvas.itemconfig(win_id, width=e.width))
def _on_wheel(e):
try:
canvas.yview_scroll(int(-1 * (e.delta / 120)), "units")
except Exception:
pass
canvas.bind("<Enter>",
lambda e: canvas.bind_all("<MouseWheel>", _on_wheel))
canvas.bind("<Leave>", lambda e: canvas.unbind_all("<MouseWheel>"))
self.order_canvas = canvas
self.order_list = inner
foot = tk.Frame(w)
foot.pack(fill=tk.X, padx=8, pady=(2, 8))
b = tk.Button(foot, text="全ON", font=F7, padx=4, pady=1,
command=lambda: self.set_all_stages(True))
b.pack(side=tk.LEFT)
tip(b, "すべての処理をONにします。")
b = tk.Button(foot, text="全OFF", font=F7, padx=4, pady=1,
command=lambda: self.set_all_stages(False))
b.pack(side=tk.LEFT, padx=3)
tip(b, "すべての処理をOFFにします(⑨ 最終は元画像と同じになります)。")
b = tk.Button(foot, text="初期化", font=F7, padx=4, pady=1,
command=self.reset_stages)
b.pack(side=tk.LEFT)
tip(b, "初期のON/OFFと実行順に戻します。")
b = tk.Button(foot, text="閉じる", font=F8B, padx=8, pady=1,
command=self.close_order_window)
b.pack(side=tk.RIGHT)
tip(b, "このウィンドウを閉じます(設定は保持されます)。")
self._refresh_order_window()
def close_order_window(self):
win = self.order_win
self.order_win = None
self.order_frames = {}
try:
self.root.unbind_all("<MouseWheel>")
except Exception:
pass
if win is not None:
try:
win.destroy()
except Exception:
pass
def _refresh_order_window(self):
"""順序リストを作り直す(番号と並びを常に最新にする)"""
win = self.order_win
if win is None:
return
try:
if not win.winfo_exists():
self.order_win = None
return
if not hasattr(self, "order_list") or not self.order_list.winfo_exists():
return
except Exception:
self.order_win = None
return
for ch in self.order_list.winfo_children():
ch.destroy()
self.order_frames = {}
for i, st in enumerate(self.stages):
f = tk.Frame(self.order_list, bd=1, relief=tk.GROOVE)
f.pack(fill=tk.X, pady=1)
tk.Label(f, text="{:>2}".format(i + 1), font=F8, width=3,
anchor="e").pack(side=tk.LEFT, padx=(4, 2))
cb = tk.Checkbutton(f, text=st["label"], variable=st["var"],
command=self.update_all, font=F8, anchor="w",
padx=0, pady=1)
cb.pack(side=tk.LEFT)
tip(cb, "チェックした処理が上から順に適用されます。")
b = tk.Button(f, text="↓", font=F7, width=2, pady=0,
command=lambda k=st["key"]: self.move_stage(k, 1))
b.pack(side=tk.RIGHT, padx=(1, 4))
tip(b, "この処理を1つ下へ移動します。")
b = tk.Button(f, text="↑", font=F7, width=2, pady=0,
command=lambda k=st["key"]: self.move_stage(k, -1))
b.pack(side=tk.RIGHT, padx=1)
tip(b, "この処理を1つ上へ移動します。")
self.order_frames[st["key"]] = f
def move_stage(self, key, delta):
keys = [s["key"] for s in self.stages]
try:
i = keys.index(key)
except ValueError:
return
j = i + delta
if 0 <= j < len(self.stages):
self.stages[i], self.stages[j] = self.stages[j], self.stages[i]
self._refresh_order_window()
self.update_all()
def reset_stages(self):
order = list(self.DEFAULT_ORDER)
self.stages.sort(key=lambda s: order.index(s["key"]))
for st in self.stages:
st["var"].set(st["key"] in self.DEFAULT_ON)
self._refresh_order_window()
self.update_all()
def set_all_stages(self, value):
for st in self.stages:
st["var"].set(value)
self._refresh_order_window()
self.update_all()
# ------------------------------------------------------------
# 実行順の要約表示(右パネル)
# ------------------------------------------------------------
def _enabled_stage_names(self):
names = []
for st in self.stages:
if st["var"].get():
parts = st["label"].split(" ", 1)
names.append(parts[1] if len(parts) > 1 else st["label"])
return names
def _update_order_summary(self):
if not getattr(self, "order_summary", None):
return
names = self._enabled_stage_names()
total = len(self.stages)
if names:
txt = "ON {} / {} 件\n{}".format(
len(names), total, " → ".join(names))
else:
txt = "ON 0 / {} 件(処理なし)".format(total)
try:
self.order_summary.config(text=txt)
except Exception:
pass
# ------------------------------------------------------------
# 読み込み
# ------------------------------------------------------------
def thumbnail_image(self, im):
"""サムネイル用画像(透明部分を市松模様に合成)"""
im = im.copy()
im.thumbnail((96, 96))
bg = self.checker_bg(im.width, im.height, cell=6)
return Image.alpha_composite(bg, im)
def load_folder(self):
folder = filedialog.askdirectory()
if not folder:
return
self.folder = folder
for w in self.tinner.winfo_children():
w.destroy()
for fn in sorted(os.listdir(folder)):
if fn.lower().endswith(self.IMAGE_EXTS):
path = os.path.join(folder, fn)
try:
im = Image.open(path).convert('RGBA')
iw, ih = im.size
# 透明部分が分かるよう市松模様に合成する
tkim = ImageTk.PhotoImage(self.thumbnail_image(im))
b = tk.Button(self.tinner, image=tkim,
command=lambda p=path: self.load_file(p))
b.image = tkim
b.pack(pady=2)
tip(b, "{}\n{} × {} px".format(path, iw, ih))
except Exception:
pass
def load_file(self, path):
try:
self.image_path = path
self.original_image = Image.open(path).convert('RGBA')
self.original_array = np.array(self.original_image).copy()
self.picked_color = None
self._update_title()
self.update_all()
except Exception as e:
messagebox.showerror("エラー", str(e))
# ------------------------------------------------------------
# 表示・背景合成
# ------------------------------------------------------------
def checker_bg(self, w, h, cell=8):
yy, xx = np.indices((h, w))
mask = ((xx // cell + yy // cell) % 2 == 0)
arr = np.where(mask[..., None],
np.array([240, 240, 240], dtype=np.uint8),
np.array([200, 200, 200], dtype=np.uint8))
return Image.fromarray(arr).convert('RGBA')
def get_background(self, nw, nh):
mode = self.bg_mode.get()
if mode == "color":
r, g, b = self.bg_color
arr = np.zeros((nh, nw, 4), dtype=np.uint8)
arr[..., 0] = r
arr[..., 1] = g
arr[..., 2] = b
arr[..., 3] = 255
return Image.fromarray(arr, 'RGBA')
elif mode == "image" and self.bg_image_pil is not None:
bg = self.bg_image_pil
bg_w, bg_h = bg.size
if bg_w > 0 and bg_h > 0:
sc = max(nw / bg_w, nh / bg_h)
rw, rh = max(1, int(bg_w * sc)), max(1, int(bg_h * sc))
bg_resized = bg.resize((rw, rh), Image.LANCZOS)
left_x = (rw - nw) // 2
top_y = (rh - nh) // 2
bg_cropped = bg_resized.crop((left_x, top_y, left_x + nw, top_y + nh))
return bg_cropped.convert('RGBA')
return self.checker_bg(nw, nh)
else:
return self.checker_bg(nw, nh)
def set_bg_color(self, r, g, b):
self.bg_color = (r, g, b)
self.bg_mode.set("color")
try:
self.bg_swatch.config(bg='#{:02x}{:02x}{:02x}'.format(r, g, b))
except Exception:
pass
self.update_all()
def choose_bg_color(self):
c = colorchooser.askcolor(title="背景色の選択")
if c[0]:
r, g, b = map(int, c[0])
self.set_bg_color(r, g, b)
def load_bg_image(self):
fn = filedialog.askopenfilename(
title="背景画像を選択",
filetypes=[("画像ファイル", "*.png;*.jpg;*.jpeg;*.bmp;*.webp;*.tif;*.tiff"),
("すべてのファイル", "*.*")]
)
if fn:
try:
self.bg_image_path = fn
self.bg_image_pil = Image.open(fn).convert('RGBA')
self.bg_mode.set("image")
self.update_all()
except Exception as e:
messagebox.showerror("背景画像エラー", str(e))
def clear_bg_image(self):
self.bg_image_path = ""
self.bg_image_pil = None
self.bg_mode.set("checker")
self.update_all()
def set_rgb_target(self, r, g, b):
self.rgb_target_color = (r, g, b)
try:
self.rgb_swatch.config(bg='#{:02x}{:02x}{:02x}'.format(r, g, b))
except Exception:
pass
self.update_all()
def choose_rgb_target(self):
c = colorchooser.askcolor(title="透明部分のRGB色を選択")
if c[0]:
r, g, b = map(int, c[0])
self.set_rgb_target(r, g, b)
# ------------------------------------------------------------
# 設定ファイル(パラメータの保存・読込)
# ------------------------------------------------------------
def collect_settings(self):
"""現在のパラメータ・実行順・ON/OFF を dict にまとめる"""
params = {}
for key in self.PARAM_KEYS:
var = getattr(self, key, None)
if var is None:
continue
try:
params[key] = var.get()
except Exception:
pass
params["rgb_target_color"] = [int(c) for c in self.rgb_target_color]
return {
"version": self.SETTINGS_VERSION,
"stages": [{"key": s["key"], "on": bool(s["var"].get())}
for s in self.stages],
"params": params,
}
def apply_settings(self, data):
"""collect_settings() の内容を適用する(未知の項目は無視)"""
if not isinstance(data, dict):
raise ValueError("設定ファイルの形式が不正です")
# 実行順と ON/OFF
known = {s["key"]: s for s in self.stages}
ordered = []
for item in data.get("stages") or []:
if not isinstance(item, dict):
continue
st = known.pop(item.get("key"), None)
if st is None:
continue
try:
st["var"].set(bool(item.get("on", True)))
except Exception:
pass
ordered.append(st)
for key in self.DEFAULT_ORDER:
st = known.pop(key, None)
if st is not None:
ordered.append(st)
ordered.extend(known.values())
if ordered:
self.stages = ordered
# パラメータ
params = data.get("params") or {}
for key, value in params.items():
var = getattr(self, key, None)
if not isinstance(var, tk.Variable):
continue
try:
cur = var.get()
if isinstance(cur, bool):
var.set(bool(value))
elif isinstance(cur, int):
var.set(int(round(float(value))))
elif isinstance(cur, float):
var.set(float(value))
else:
var.set(str(value))
except Exception:
pass
col = params.get("rgb_target_color")
if isinstance(col, (list, tuple)) and len(col) == 3:
try:
self.set_rgb_target(*[int(c) for c in col])
except Exception:
pass
if self.semi_mode.get() not in ("transparent", "opaque"):
self.semi_mode.set("transparent")
self._update_semi_labels()
self._refresh_order_window()
self.update_all()
def _flash_button(self, btn, text):
try:
orig = btn.cget("text")
btn.config(text=text)
def _restore():
try:
if btn.winfo_exists():
btn.config(text=orig)
except Exception:
pass
self.root.after(1200, _restore)
except Exception:
pass
def save_settings(self):
fn = filedialog.asksaveasfilename(
title="設定の保存",
defaultextension=".json",
filetypes=[("設定ファイル", "*.json"), ("すべてのファイル", "*.*")],
initialfile="alphaviewer_settings.json")
if not fn:
return
try:
with open(fn, "w", encoding="utf-8") as fh:
json.dump(self.collect_settings(), fh,
ensure_ascii=False, indent=2)
self._flash_button(self.cfg_save_btn, "保存✓")
except Exception as e:
messagebox.showerror("設定の保存", str(e))
def load_settings(self):
fn = filedialog.askopenfilename(
title="設定の読込",
filetypes=[("設定ファイル", "*.json"), ("すべてのファイル", "*.*")])
if not fn:
return
try:
with open(fn, "r", encoding="utf-8") as fh:
data = json.load(fh)
self.apply_settings(data)
self._flash_button(self.cfg_load_btn, "読込✓")
except Exception as e:
messagebox.showerror("設定の読込", "読み込めません:\n" + str(e))
def _apply_rgb_change(self, arr):
"""本来は透明な部分(α<=閾値)のRGB値を、α(透明)はそのまま維持して変更する"""
if not self.rgb_enable.get():
return arr
out = arr.copy()
th = int(self.rgb_alpha_th.get())
mask = (out[:, :, 3] <= th)
if not mask.any():
return out
r, g, b = self.rgb_target_color
out[mask, 0] = r
out[mask, 1] = g
out[mask, 2] = b
# out[:, :, 3] は一切変更しない!透明のまま!
return out
def show(self, cv, pil_img):
"""キャンバスへ描画(表示専用。保存用画像には一切影響しない)"""
if pil_img is None:
cv.delete("all")
return
cv.delete("all")
cw = cv.winfo_width()
ch = cv.winfo_height()
if cw < 5 or ch < 5:
return
iw, ih = pil_img.size
sc = min(cw / iw, ch / ih) if iw > 0 and ih > 0 else 1
nw, nh = max(1, int(iw * sc)), max(1, int(ih * sc))
src = pil_img
if self._rgb_preview_active():
# 縮小前に塗る(PILのresizeはα=0のRGBを0にしてしまうため)
src = self._rgb_preview_image(src)
r = src.resize((nw, nh), Image.LANCZOS).convert('RGBA')
bg = self.get_background(nw, nh)
comp = Image.alpha_composite(bg, r)
tkimg = ImageTk.PhotoImage(comp)
cv.create_image(cw // 2, ch // 2, image=tkimg)
cv.image = tkimg
def _rgb_preview_active(self):
try:
return bool(self.rgb_preview.get() and self.rgb_enable.get())
except Exception:
return False
def _rgb_preview_image(self, img):
"""透明画素(α<=閾値)のαを表示用に255へ。RGBは既に変更済みのものを使う。"""
try:
if img.mode != 'RGBA':
return img
th = int(self.rgb_alpha_th.get())
a = np.array(img.getchannel('A'))
m = a <= th
if not m.any():
return img
out = img.copy()
out.putalpha(Image.fromarray(np.where(m, 255, a).astype(np.uint8)))
return out
except Exception:
return img
# ------------------------------------------------------------
# 更新(スライダー連続操作はスロットル)
# ------------------------------------------------------------
def _param_changed(self, stage_key=None):
if stage_key:
for st in self.stages:
if st["key"] == stage_key:
if not st["var"].get():
st["var"].set(True)
break
self.schedule_update()
def schedule_update(self, *args):
if self._update_job is not None:
return
self._update_job = self.root.after(16, self._run_scheduled_update)
def _run_scheduled_update(self):
self._update_job = None
self.update_all()
# ------------------------------------------------------------
# 各処理(入力配列は変更せず、常に新しい配列を返す)
# ------------------------------------------------------------
def _stage_semi(self, arr):
out = arr.copy()
st = int(self.semi_thresh.get())
m = (out[:, :, 3] > 0) & (out[:, :, 3] <= st)
if self.semi_mode.get() == "opaque":
out[m, 3] = 255
else:
out[m, 3] = 0
return out
def _stage_morph(self, arr):
out = arr.copy()
h, w = out.shape[:2]
mask = out[:, :, 3] > 0
binm = mask.astype(np.uint8)
k = int(self.morph_k.get())
if k % 2 == 0:
k += 1
it = int(self.morph_iter.get())
dilate = self.morph_op.get() == "dilate"
if HAS_SCIPY:
stc = np.ones((k, k))
if dilate:
bm = ndimage.binary_dilation(binm, structure=stc, iterations=it)
else:
bm = ndimage.binary_erosion(binm, structure=stc, iterations=it)
bm = bm.astype(bool)
else:
bm = mask.copy()
rad = k // 2
for _ in range(it):
pad = np.pad(bm, rad, mode="constant", constant_values=False)
if dilate:
res = np.zeros_like(bm)
for dy in range(k):
for dx in range(k):
res |= pad[dy:dy + h, dx:dx + w]
else:
res = np.ones_like(bm)
for dy in range(k):
for dx in range(k):
res &= pad[dy:dy + h, dx:dx + w]
bm = res
if dilate:
# 膨張で新たに覆われる画素は最寄りの不透明画素の色をコピー
added = bm & ~mask
if added.any() and mask.any():
if HAS_SCIPY:
try:
_, (iy, ix) = ndimage.distance_transform_edt(
~mask, return_indices=True)
out[added, :3] = out[iy[added], ix[added], :3]
out[added, 3] = out[iy[added], ix[added], 3]
except Exception:
out[added, 3] = 255
else:
out[added, 3] = 255
if self._generated is None or self._generated.shape != added.shape:
self._generated = added.copy()
else:
self._generated |= added
out[~bm, 3] = 0
return out
def _stage_rowcol(self, arr):
out = arr.copy()
h, w = out.shape[:2]
if h == 0 or w == 0:
return out
th = int(self.rowcol_alpha.get())
rp = int(self.row_pct.get())
rcnt = int(self.row_cnt.get())
cp = int(self.col_pct.get())
ccnt = int(self.col_cnt.get())
def eff_alpha():
"""判定用のα。膨張で生成した画素は「透明」として扱う。"""
a = out[:, :, 3]
gen = self._generated
if gen is not None and gen.shape == a.shape:
a = a.copy()
a[gen] = 0
return a
# 行判定
a = eff_alpha()
z = (a <= th) # 透明(背景)判定
op = ~z # 不透明(物体・ノイズ)判定
pct = z.sum(axis=1) / w * 100.0
# 不透明画素の連続している数(塊/ランの個数)
runs_r = op[:, 0].astype(np.int32)
if w > 1:
runs_r += (np.diff(op.astype(np.int8), axis=1) == 1).sum(axis=1)
clear_r = (pct >= rp) & (runs_r <= rcnt)
nrow = int(np.count_nonzero(clear_r))
out[clear_r, :, 3] = 0
# 行を消した後のαで列を判定
a = eff_alpha()
z = (a <= th)
op = ~z
pct = z.sum(axis=0) / h * 100.0
# 列の不透明画素の連続している数(塊/ランの個数)
runs_c = op[0, :].astype(np.int32)
if h > 1:
runs_c += (np.diff(op.astype(np.int8), axis=0) == 1).sum(axis=0)
clear_c = (pct >= cp) & (runs_c <= ccnt)
ncol = int(np.count_nonzero(clear_c))
out[:, clear_c, 3] = 0
self._rc_info = (nrow, ncol)
return out
def _stage_color(self, arr):
out = arr.copy()
return self._apply_color_clear(out)
def _stage_edge_clean(self, arr):
out = arr.copy()
h, w = out.shape[:2]
if h == 0 or w == 0:
return out
th = int(self.edge_alpha.get())
# 画像全体の面積に対する割合(%)で上限を決める
pct = max(0.0, float(self.edge_pct.get()))
max_size = pct / 100.0 * (h * w)
a = out[:, :, 3]
mask = (a > th)
if not mask.any():
self._edge_info = (0, 0)
return out
if HAS_SCIPY:
stc = np.ones((3, 3), bool)
lbl, num = ndimage.label(mask, structure=stc)
sizes = ndimage.sum(mask, lbl, range(1, num + 1))
else:
lbl = np.zeros((h, w), dtype=np.int32)
cur_lbl = 0
sizes_list = []
for y in range(h):
for x in range(w):
if mask[y, x] and lbl[y, x] == 0:
cur_lbl += 1
q = deque([(y, x)])
lbl[y, x] = cur_lbl
sz = 0
while q:
cy, cx = q.popleft()
sz += 1
for dy in (-1, 0, 1):
for dx in (-1, 0, 1):
ny, nx = cy + dy, cx + dx
if 0 <= ny < h and 0 <= nx < w:
if mask[ny, nx] and lbl[ny, nx] == 0:
lbl[ny, nx] = cur_lbl
q.append((ny, nx))
sizes_list.append(sz)
num = cur_lbl
sizes = np.array(sizes_list)
edge_labels = set()
if self.edge_top.get():
edge_labels.update(lbl[0, :])
if self.edge_bottom.get():
edge_labels.update(lbl[-1, :])
if self.edge_left.get():
edge_labels.update(lbl[:, 0])
if self.edge_right.get():
edge_labels.update(lbl[:, -1])
edge_labels.discard(0)
rem_cnt = 0
rem_px = 0
for comp_id in edge_labels:
sz = sizes[comp_id - 1]
if sz <= max_size:
rem_cnt += 1
rem_px += int(sz)
out[lbl == comp_id, 3] = 0
self._edge_info = (rem_cnt, rem_px)
return out
def _stage_long_feather(self, arr):
out = arr.copy()
h, w = out.shape[:2]
if h == 0 or w == 0:
return out
inner = int(self.lf_inner.get())
outer = int(self.lf_outer.get())
curve = max(10, int(self.lf_curve.get())) / 100.0
smooth = self.lf_smooth.get()
bleed = self.lf_bleed.get()
if inner <= 0 and outer <= 0:
return out
a = out[:, :, 3]
mask = a > 0
if not mask.any() or mask.all():
return out
if HAS_SCIPY:
d_in = ndimage.distance_transform_edt(mask)
d_out = ndimage.distance_transform_edt(~mask)
signed_dist = d_in - d_out
if outer > 0 and bleed:
bleed_region = (~mask) & (d_out <= outer + 2)
if bleed_region.any():
try:
_, (iy, ix) = ndimage.distance_transform_edt(~mask, return_indices=True)
out[bleed_region, :3] = out[iy[bleed_region], ix[bleed_region], :3]
except Exception:
pass
total_range = inner + outer
if total_range <= 0:
return out
t = np.clip((signed_dist + outer) / total_range, 0.0, 1.0)
if smooth:
t = t * t * (3.0 - 2.0 * t)
if abs(curve - 1.0) > 1e-3:
t = np.power(t, curve)
new_a = np.clip(t * 255.0, 0, 255).astype(np.uint8)
feather_zone = (signed_dist >= -outer) & (signed_dist <= inner)
out[signed_dist < -outer, 3] = 0
out[signed_dist > inner, 3] = np.maximum(out[signed_dist > inner, 3], new_a[signed_dist > inner])
out[feather_zone, 3] = new_a[feather_zone]
else:
rad = max(1, (inner + outer) // 2)
pil_a = Image.fromarray(out[:, :, 3]).filter(ImageFilter.GaussianBlur(rad))
out[:, :, 3] = np.array(pil_a)
return out
def _apply_stage(self, key, arr):
fn = getattr(self, "_stage_" + key, None)
if fn is None:
return arr
try:
return fn(arr)
except Exception:
return arr
# ------------------------------------------------------------
# 色クリア(角フラッド / 取得色)
# ------------------------------------------------------------
def _corner_flood_clear(self, arr, sy, sx, th):
h, w = arr.shape[:2]
if not (0 <= sy < h and 0 <= sx < w):
return
tr = int(arr[sy, sx, 0])
tg = int(arr[sy, sx, 1])
tb = int(arr[sy, sx, 2])
rgb = arr[..., :3].astype(np.int16)
mask = ((np.abs(rgb[..., 0] - tr) <= th) &
(np.abs(rgb[..., 1] - tg) <= th) &
(np.abs(rgb[..., 2] - tb) <= th))
if not mask[sy, sx]:
return
if HAS_SCIPY:
lbl, _ = ndimage.label(mask)
tgt = lbl[sy, sx]
if tgt:
arr[lbl == tgt, 3] = 0
return
vis = np.zeros((h, w), dtype=bool)
dq = deque([(sy, sx)])
vis[sy, sx] = True
while dq:
cy, cx = dq.popleft()
if not mask[cy, cx]:
continue
arr[cy, cx, 3] = 0
for ny, nx in ((cy + 1, cx), (cy - 1, cx), (cy, cx + 1), (cy, cx - 1)):
if 0 <= ny < h and 0 <= nx < w and not vis[ny, nx]:
vis[ny, nx] = True
dq.append((ny, nx))
def _apply_color_clear(self, arr):
h, w = arr.shape[:2]
if h == 0 or w == 0:
return arr
th = int(self.color_thresh.get())
seeds = []
if self.corner_tl.get():
seeds.append((0, 0))
if self.corner_tr.get():
seeds.append((0, w - 1))
if self.corner_bl.get():
seeds.append((h - 1, 0))
if self.corner_br.get():
seeds.append((h - 1, w - 1))
if seeds:
done = set()
for sy, sx in seeds:
if (sy, sx) in done:
continue
done.add((sy, sx))
self._corner_flood_clear(arr, sy, sx, th)
elif self.picked_color is not None:
pr, pg, pb = self.picked_color
rgb = arr[..., :3].astype(np.int16)
mask = ((np.abs(rgb[..., 0] - pr) <= th) &
(np.abs(rgb[..., 1] - pg) <= th) &
(np.abs(rgb[..., 2] - pb) <= th))
arr[mask, 3] = 0
return arr
# ------------------------------------------------------------
# パイプライン(9枚の ndarray を返す)
# ------------------------------------------------------------
def _run_stages(self, arr, skip=()):
"""arr から出発して、ON の処理を順に適用する。
戻り値: (最終状態, {キー: その処理を終えた時点の状態})
"""
self._generated = None
state = arr
snaps = {}
for st in self.stages:
key = st["key"]
if st["var"].get() and key not in skip:
state = self._apply_stage(key, state)
snaps[key] = state
return state, snaps
def _is_stage_on(self, key):
for st in self.stages:
if st["key"] == key:
return st["var"].get()
return False
def compute_pipeline(self, arr_in):
arr = np.array(arr_in, copy=True)
panels = [None] * 9
# パネル 0: 元画像
panels[0] = arr.copy()
panels[0] = self._apply_rgb_change(panels[0])
# パネル 1: 透明検出(α)
a0 = arr[:, :, 3]
panels[1] = np.dstack([a0, a0, a0]).astype(np.uint8)
# 基礎ベース処理 (半透明 -> 膨張/縮小)
base = arr.copy()
if self._is_stage_on("semi"):
base = self._apply_stage("semi", base)
panels[2] = base.copy() # パネル 2: 3. 半透明→透明
panels[2] = self._apply_rgb_change(panels[2])
if self._is_stage_on("morph"):
base = self._apply_stage("morph", base)
panels[3] = base.copy() # パネル 3: 4. 膨張/縮小
panels[3] = self._apply_rgb_change(panels[3])
# --- 各処理の分離・非反映化プレビュー ---
# パネル 4: 5. 行列クリーン (baseに対して適用。端部小物体除去・ロングフェザー・色クリアは反映しない)
self._rc_info = (0, 0)
if self._is_stage_on("rowcol"):
panels[4] = self._apply_stage("rowcol", base.copy())
else:
panels[4] = base.copy()
rc_info = self._rc_info
panels[4] = self._apply_rgb_change(panels[4])
# パネル 5: 6. 色クリア (baseに対して適用。行列クリーン・端部小物体除去・ロングフェザーは反映しない)
if self._is_stage_on("color"):
panels[5] = self._apply_stage("color", base.copy())
else:
panels[5] = base.copy()
panels[5] = self._apply_rgb_change(panels[5])
# パネル 6: 7. 端部小物体除去 (行列クリーンは反映しない!baseに対して適用)
self._edge_info = (0, 0)
if self._is_stage_on("edge_clean"):
panels[6] = self._apply_stage("edge_clean", base.copy())
else:
panels[6] = base.copy()
edge_info = self._edge_info
panels[6] = self._apply_rgb_change(panels[6])
# パネル 7: 8. ロングフェザー
if self._is_stage_on("long_feather"):
panels[7] = self._apply_stage("long_feather", base.copy())
else:
panels[7] = base.copy()
panels[7] = self._apply_rgb_change(panels[7])
# パネル 8: 9. 最終 (全適用: 実行順にすべてのON処理を適用)
self._rc_info = (0, 0)
self._edge_info = (0, 0)
final_state, _ = self._run_stages(arr)
# 背景透明のままRGB変更(する/しない)
final_state = self._apply_rgb_change(final_state)
panels[8] = final_state
# ステータス情報を行列クリーン・端部小物体除去から正しく保持
self._rc_info = rc_info
self._edge_info = edge_info
self._generated = None
return panels
def update_all(self, *args):
self._update_order_summary()
if self.original_array is None:
return
panels = self.compute_pipeline(self.original_array)
# 保存・一括処理の元データ(表示合成とは無関係に保持する)
self.panels_raw = panels
self.img_orig = Image.fromarray(panels[0])
self.img_trans_detect = Image.fromarray(panels[1])
self.img_semi = Image.fromarray(panels[2])
self.img_morph = Image.fromarray(panels[3])
self.img_rowcol = Image.fromarray(panels[4])
self.img_color = Image.fromarray(panels[5])
self.img_edge = Image.fromarray(panels[6])
self.img_alpha = Image.fromarray(panels[7])
self.img_final = Image.fromarray(panels[8])
for i, img in enumerate(self._panel_images()):
self.show(self.cvs[i], img)
try:
self.rc_status.config(
text="クリア: 行{} / 列{}".format(self._rc_info[0],
self._rc_info[1]))
except Exception:
pass
try:
self.edge_status.config(
text="除去: {}個 ({} px)".format(self._edge_info[0],
self._edge_info[1]))
except Exception:
pass
try:
if self.rgb_enable.get():
th = int(self.rgb_alpha_th.get())
n = int((self.panels_raw[8][..., 3] <= th).sum())
self.rgb_status.config(text="{}px".format(n))
else:
self.rgb_status.config(text="OFF")
except Exception:
pass
self._schedule_alpha_refresh()
# ------------------------------------------------------------
# 色の取得
# ------------------------------------------------------------
def click_pick_color(self, event):
if self.original_array is None:
return
cv = event.widget
cw = cv.winfo_width()
ch = cv.winfo_height()
if cw < 5 or ch < 5:
return
img = self.original_image
iw, ih = img.size
sc = min(cw / iw, ch / ih) if iw > 0 and ih > 0 else 1
nw, nh = max(1, int(iw * sc)), max(1, int(ih * sc))
cx = event.x - (cw - nw) // 2
cy = event.y - (ch - nh) // 2
ix = int(cx / sc) if sc else 0
iy = int(cy / sc) if sc else 0
if 0 <= ix < iw and 0 <= iy < ih:
r, g, b, a = self.original_array[iy, ix]
self.picked_color = (int(r), int(g), int(b))
self._clear_corners()
self.update_all()
def _clear_corners(self):
for v in (self.corner_tl, self.corner_tr, self.corner_bl, self.corner_br):
v.set(False)
def clear_alpha_all(self):
if self.original_array is None:
return
if not messagebox.askyesno(
"不透明化",
"読み込んだ元画像のαをすべて255にします。\n"
"この操作は元に戻せません(再読み込みしてください)。\n\n"
"実行しますか?"):
return
arr = self.original_array.copy()
arr[:, :, 3] = 255
self.original_array = arr
self.original_image = Image.fromarray(arr)
self.picked_color = None
self.update_all()
# ------------------------------------------------------------
# 保存
# ------------------------------------------------------------
def _panel_images(self):
return [
self.img_orig,
self.img_trans_detect,
self.img_semi,
self.img_morph,
self.img_rowcol,
self.img_color,
self.img_edge,
self.img_alpha,
self.img_final,
]
def _export_image(self, panels, idx):
"""保存用画像。通常保存・一括処理で共通(内容が完全に一致する)。"""
if panels is None or idx < 0 or idx >= len(panels):
return None
arr = np.ascontiguousarray(panels[idx])
img = Image.fromarray(arr)
if self.bg_mode.get() != "raw":
# 表示と同じ背景合成を「原寸」で行う(縮小しない)
bg = self.get_background(img.size[0], img.size[1])
img = Image.alpha_composite(bg, img.convert('RGBA'))
return img
def save_panel(self, idx):
img = None
try:
img = self._export_image(self.panels_raw, idx)
except Exception:
img = None
if img is None:
messagebox.showinfo("保存", "画像が読み込まれていません。")
return
if self.image_path:
base = os.path.splitext(os.path.basename(self.image_path))[0]
else:
base = "image"
name = self.PANEL_NAMES[idx]
fn = filedialog.asksaveasfilename(
defaultextension=".png",
filetypes=[("PNG", "*.png")],
initialfile="{}_{}.png".format(base, name),
)
if not fn:
return
try:
img.save(fn)
# messagebox.showinfo("保存", "保存しました:\n" + fn)
except Exception as e:
messagebox.showerror("保存", str(e))
# ------------------------------------------------------------
# フォルダ一括処理
# ------------------------------------------------------------
def batch_process(self):
if self.batch_running:
return
if not self.folder or not os.path.isdir(self.folder):
messagebox.showinfo(
"バッチ処理",
"先に「フォルダ読み込み」でフォルダを指定してください。")
return
files = [fn for fn in sorted(os.listdir(self.folder))
if fn.lower().endswith(self.IMAGE_EXTS)]
if not files:
messagebox.showinfo("バッチ処理", "対象画像がありません。")
return
target = self.batch_target.get()
if target == "全9種":
targets = list(range(9))
else:
targets = [self.BATCH_TARGETS.get(target, 8)]
outdir = os.path.join(self.folder, "batch_output")
try:
os.makedirs(outdir, exist_ok=True)
except Exception as e:
messagebox.showerror("バッチ処理",
"出力フォルダを作成できません:\n" + str(e))
return
mode_name = {
"raw": "αを保持(背景透明のまま)",
"color": "背景色と合成",
"image": "背景画像と合成",
"checker": "市松模様と合成",
}.get(self.bg_mode.get(), "αを保持")
if not messagebox.askyesno(
"バッチ処理",
"対象: {} ファイル\n内容: {}\n保存: {}\n出力: {}\n\n"
"実行しますか?".format(len(files), target, mode_name, outdir)):
return
self.batch_running = True
self.batch_btn.config(state=tk.DISABLED)
ok = 0
errors = []
try:
for i, fn in enumerate(files, 1):
self.batch_status.config(text="{}/{}".format(i, len(files)))
self.root.update_idletasks()
try:
im = Image.open(os.path.join(self.folder, fn)).convert("RGBA")
panels = self.compute_pipeline(np.array(im))
base = os.path.splitext(fn)[0]
for idx in targets:
out = os.path.join(
outdir, "{}_{}.png".format(base, self.PANEL_NAMES[idx]))
exp = self._export_image(panels, idx)
if exp is None:
continue
exp.save(out)
ok += 1
except Exception as e:
errors.append("{}: {}".format(fn, e))
finally:
self.batch_running = False
self.batch_btn.config(state=tk.NORMAL)
self.batch_status.config(
text="成功{} / 失敗{}".format(ok, len(errors)))
try:
self.update_all()
except Exception:
pass
msg = "完了しました。\n成功: {} / 失敗: {}\n出力先:\n{}".format(
ok, len(errors), outdir)
if errors:
msg += "\n\n--- エラー ---\n" + "\n".join(errors[:10])
if len(errors) > 10:
msg += "\n... 他 {} 件".format(len(errors) - 10)
messagebox.showwarning("バッチ処理", msg)
else:
messagebox.showinfo("バッチ処理", msg)
class DragDropHandler:
def __init__(self, widget, callback):
self.widget = widget
self.callback = callback
if not DND_AVAILABLE:
return
try:
if hasattr(widget, 'drop_target_register'):
widget.drop_target_register(DND_FILES)
widget.dnd_bind('<<Drop>>', self.drop)
except Exception:
pass
def drop(self, event):
try:
items = self.widget.tk.splitlist(event.data)
except Exception:
items = [str(getattr(event, "data", ""))]
for fn in items:
fn = str(fn).strip()
if os.path.isfile(fn):
try:
self.callback(fn)
except Exception:
pass
break
def main():
if DND_AVAILABLE:
try:
root = TkinterDnD.Tk()
except Exception:
root = tk.Tk()
else:
root = tk.Tk()
AlphaViewerNew(root)
root.mainloop()
if __name__ == '__main__':
main()









ディスカッション
コメント一覧
まだ、コメントがありません