Spaces:
Sleeping
Sleeping
Merge branch 'develop'
Browse files
pic_to_header/app.py
CHANGED
@@ -1,65 +1,111 @@
|
|
1 |
import streamlit as st
|
2 |
-
import
|
3 |
-
|
|
|
|
|
|
|
4 |
|
5 |
def main():
|
6 |
-
|
7 |
-
|
8 |
-
<div align="center">
|
9 |
-
|
10 |
-
# Pic-to-Header
|
11 |
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
[![GitHub issues](https://img.shields.io/github/issues/Sunwood-ai-labs/pic-to-header)](https://github.com/Sunwood-ai-labs/pic-to-header/issues)
|
17 |
|
18 |
-
|
19 |
-
![Streamlit](https://img.shields.io/badge/Streamlit-FF4B4B?style=for-the-badge&logo=Streamlit&logoColor=white)
|
20 |
-
![OpenCV](https://img.shields.io/badge/opencv-%23white.svg?style=for-the-badge&logo=opencv&logoColor=white)
|
21 |
|
22 |
-
|
|
|
|
|
23 |
|
24 |
-
|
|
|
|
|
|
|
|
|
25 |
|
26 |
-
|
27 |
st.write("マスク画像と入力画像をアップロードして、ヘッダー画像を生成します。")
|
28 |
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
)
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
if __name__ == "__main__":
|
65 |
main()
|
|
|
1 |
import streamlit as st
|
2 |
+
from PIL import Image
|
3 |
+
import numpy as np
|
4 |
+
from modules.image_processor import process_image, prepare_image, convert_to_pil
|
5 |
+
from modules.mask_manager import MaskManager
|
6 |
+
from modules.utils import create_download_button
|
7 |
|
8 |
def main():
|
9 |
+
# ワイドモードの設定
|
10 |
+
st.set_page_config(layout="wide")
|
|
|
|
|
|
|
11 |
|
12 |
+
st.markdown("""
|
13 |
+
<div align="center">
|
14 |
+
|
15 |
+
# Pic-to-Header
|
|
|
16 |
|
17 |
+
<img src="https://raw.githubusercontent.com/Sunwood-ai-labs/pic-to-header/refs/heads/main/assets/result.png" width="50%">
|
|
|
|
|
18 |
|
19 |
+
[![GitHub license](https://img.shields.io/github/license/Sunwood-ai-labs/pic-to-header)](https://github.com/Sunwood-ai-labs/pic-to-header/blob/main/LICENSE)
|
20 |
+
[![GitHub stars](https://img.shields.io/github/stars/Sunwood-ai-labs/pic-to-header)](https://github.com/Sunwood-ai-labs/pic-to-header/stargazers)
|
21 |
+
[![GitHub issues](https://img.shields.io/github/issues/Sunwood-ai-labs/pic-to-header)](https://github.com/Sunwood-ai-labs/pic-to-header/issues)
|
22 |
|
23 |
+
![Python](https://img.shields.io/badge/python-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54)
|
24 |
+
![Streamlit](https://img.shields.io/badge/Streamlit-FF4B4B?style=for-the-badge&logo=Streamlit&logoColor=white)
|
25 |
+
![OpenCV](https://img.shields.io/badge/opencv-%23white.svg?style=for-the-badge&logo=opencv&logoColor=white)
|
26 |
+
</div>
|
27 |
+
""", unsafe_allow_html=True)
|
28 |
|
29 |
+
st.write("Pic-to-Headerは、マスク画像と入力画像を使用してヘッダー画像を生成するPythonアプリケーションです。")
|
30 |
st.write("マスク画像と入力画像をアップロードして、ヘッダー画像を生成します。")
|
31 |
|
32 |
+
# 2段組レイアウトの作成(左側を狭く、右側を広く)
|
33 |
+
control_column, display_column = st.columns([1, 2])
|
34 |
+
|
35 |
+
with control_column:
|
36 |
+
# マスク管理インスタンスの作成
|
37 |
+
mask_manager = MaskManager()
|
38 |
+
|
39 |
+
# マスク画像の取得方法を選択
|
40 |
+
mask_source = st.radio(
|
41 |
+
"マスク画像の取得方法を選択",
|
42 |
+
["プリセットから選択", "URLから取得", "ファイルをアップロード"]
|
43 |
+
)
|
44 |
+
|
45 |
+
mask_image = None
|
46 |
+
if mask_source == "プリセットから選択":
|
47 |
+
preset_name = st.selectbox(
|
48 |
+
"プリセットを選択",
|
49 |
+
mask_manager.get_preset_names()
|
50 |
+
)
|
51 |
+
mask_image = mask_manager.get_preset_mask(preset_name)
|
52 |
+
|
53 |
+
elif mask_source == "URLから取得":
|
54 |
+
mask_url = st.text_input("マスク画像のURLを入力")
|
55 |
+
if mask_url:
|
56 |
+
mask_image = mask_manager.load_mask_from_url(mask_url)
|
57 |
+
|
58 |
+
else: # ファイルをアップロード
|
59 |
+
mask_file = st.file_uploader("マスク画像をアップロード", type=["png", "jpg", "jpeg"])
|
60 |
+
if mask_file is not None:
|
61 |
+
mask_image = Image.open(mask_file)
|
62 |
+
|
63 |
+
# 透明度の調整
|
64 |
+
alpha = st.slider("マスクの透明度", 0.0, 1.0, 1.0)
|
65 |
+
|
66 |
+
# ファイルアップロード(複数ファイル対応)
|
67 |
+
uploaded_files = st.file_uploader(
|
68 |
+
"画像をアップロードしてください",
|
69 |
+
type=["png", "jpg", "jpeg", "webp"],
|
70 |
+
accept_multiple_files=True
|
71 |
+
)
|
72 |
+
if mask_image:
|
73 |
+
# マスク画像のプレビュー表示
|
74 |
+
st.write("選択中のマスク画像:")
|
75 |
+
st.image(mask_image, caption="マスク画像", use_column_width=True)
|
76 |
|
77 |
+
with display_column:
|
78 |
+
|
79 |
+
if uploaded_files:
|
80 |
+
st.write("処理結果:")
|
81 |
+
# 各画像の処理と表示
|
82 |
+
for idx, uploaded_file in enumerate(uploaded_files):
|
83 |
+
# 水平に2列で表示するための設定
|
84 |
+
img_col1, img_col2 = st.columns(2)
|
85 |
+
|
86 |
+
# 元画像の表示
|
87 |
+
with img_col1:
|
88 |
+
st.write(f"元画像 {idx + 1}:")
|
89 |
+
st.image(uploaded_file, use_column_width=True)
|
90 |
+
|
91 |
+
# 処理後の画像の表示
|
92 |
+
with img_col2:
|
93 |
+
st.write(f"処理後 {idx + 1}:")
|
94 |
+
input_image = prepare_image(uploaded_file)
|
95 |
+
if input_image is not None:
|
96 |
+
result = process_image(input_image, np.array(mask_image), alpha)
|
97 |
+
result_pil = convert_to_pil(result)
|
98 |
+
st.image(result_pil, use_column_width=True)
|
99 |
+
|
100 |
+
# ダウンロードボタンの作成
|
101 |
+
create_download_button(
|
102 |
+
result_pil,
|
103 |
+
f"header_{uploaded_file.name}"
|
104 |
+
)
|
105 |
+
|
106 |
+
# 区切り線を追加(最後の画像以外)
|
107 |
+
if idx < len(uploaded_files) - 1:
|
108 |
+
st.markdown("---")
|
109 |
+
|
110 |
if __name__ == "__main__":
|
111 |
main()
|
pic_to_header/modules/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
|
pic_to_header/modules/image_processor.py
ADDED
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cv2
|
2 |
+
import numpy as np
|
3 |
+
from PIL import Image
|
4 |
+
import io
|
5 |
+
|
6 |
+
def process_image(image, mask_image, alpha=0.5):
|
7 |
+
"""画像処理を行う関数"""
|
8 |
+
# PILイメージをnumpy配列に変換
|
9 |
+
if isinstance(image, Image.Image):
|
10 |
+
image = np.array(image)
|
11 |
+
if isinstance(mask_image, Image.Image):
|
12 |
+
mask_image = np.array(mask_image)
|
13 |
+
|
14 |
+
# 画像のチャンネル数を確認し、必要に応じて変換
|
15 |
+
if len(image.shape) == 2: # グレースケール
|
16 |
+
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGRA)
|
17 |
+
elif image.shape[2] == 3: # BGR
|
18 |
+
image = cv2.cvtColor(image, cv2.COLOR_BGR2BGRA)
|
19 |
+
elif image.shape[2] == 4 and image.dtype == np.uint8:
|
20 |
+
pass # すでにBGRA形式
|
21 |
+
|
22 |
+
if len(mask_image.shape) == 2: # グレースケール
|
23 |
+
mask_image = cv2.cvtColor(mask_image, cv2.COLOR_GRAY2BGRA)
|
24 |
+
elif mask_image.shape[2] == 3: # BGR
|
25 |
+
mask_image = cv2.cvtColor(mask_image, cv2.COLOR_BGR2BGRA)
|
26 |
+
elif mask_image.shape[2] == 4 and mask_image.dtype == np.uint8:
|
27 |
+
pass # すでにBGRA形式
|
28 |
+
|
29 |
+
# マスク画像のリサイズ
|
30 |
+
mask_image = cv2.resize(mask_image, (image.shape[1], image.shape[0]))
|
31 |
+
|
32 |
+
# マスク画像のアルファチャンネルを取得
|
33 |
+
mask_alpha = mask_image[:, :, 3]
|
34 |
+
|
35 |
+
# 入力画像のアルファチャンネルを更新
|
36 |
+
# マスクのアルファ値を使って元の画像のアルファ値を減算
|
37 |
+
image[:, :, 3] = np.maximum(image[:, :, 3] - (mask_alpha * alpha).astype(np.uint8), 0)
|
38 |
+
|
39 |
+
return image
|
40 |
+
|
41 |
+
def convert_to_pil(image):
|
42 |
+
"""numpy配列をPIL Imageに変換"""
|
43 |
+
if isinstance(image, np.ndarray):
|
44 |
+
return Image.fromarray(image)
|
45 |
+
return image
|
46 |
+
|
47 |
+
def prepare_image(uploaded_file):
|
48 |
+
"""アップロードされたファイルを画像として準備"""
|
49 |
+
if uploaded_file is None:
|
50 |
+
return None
|
51 |
+
|
52 |
+
image_bytes = uploaded_file.read()
|
53 |
+
image = Image.open(io.BytesIO(image_bytes))
|
54 |
+
|
55 |
+
# RGBAモードに変換
|
56 |
+
if image.mode != 'RGBA':
|
57 |
+
image = image.convert('RGBA')
|
58 |
+
|
59 |
+
return image
|
pic_to_header/modules/mask_manager.py
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
from PIL import Image
|
3 |
+
import io
|
4 |
+
import streamlit as st
|
5 |
+
|
6 |
+
class MaskManager:
|
7 |
+
def __init__(self):
|
8 |
+
self.default_mask_url = "https://raw.githubusercontent.com/Sunwood-ai-labs/pic-to-header/refs/heads/main/assets/mask.png"
|
9 |
+
self.presets = {
|
10 |
+
"デフォルトマスク": self.default_mask_url,
|
11 |
+
}
|
12 |
+
|
13 |
+
@staticmethod
|
14 |
+
@st.cache_data
|
15 |
+
def load_mask_from_url(url):
|
16 |
+
"""URLから画像を読み込む"""
|
17 |
+
try:
|
18 |
+
response = requests.get(url)
|
19 |
+
response.raise_for_status()
|
20 |
+
image = Image.open(io.BytesIO(response.content))
|
21 |
+
return image
|
22 |
+
except Exception as e:
|
23 |
+
st.error(f"マスク画像の読み込みに失敗しました: {str(e)}")
|
24 |
+
return None
|
25 |
+
|
26 |
+
def get_preset_names(self):
|
27 |
+
"""プリセット名の一覧を取得"""
|
28 |
+
return list(self.presets.keys())
|
29 |
+
|
30 |
+
def get_preset_mask(self, preset_name):
|
31 |
+
"""プリセット名に対応するマスク画像を取得"""
|
32 |
+
if preset_name in self.presets:
|
33 |
+
return self.load_mask_from_url(self.presets[preset_name])
|
34 |
+
return None
|
35 |
+
|
36 |
+
def add_preset(self, name, url):
|
37 |
+
"""新しいプリセットを追加"""
|
38 |
+
self.presets[name] = url
|
pic_to_header/modules/utils.py
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
from PIL import Image
|
3 |
+
import io
|
4 |
+
|
5 |
+
def convert_image_to_bytes(image):
|
6 |
+
"""画像をバイト列に変換"""
|
7 |
+
if image is None:
|
8 |
+
return None
|
9 |
+
|
10 |
+
img_byte_arr = io.BytesIO()
|
11 |
+
image.save(img_byte_arr, format='PNG')
|
12 |
+
return img_byte_arr.getvalue()
|
13 |
+
|
14 |
+
def create_download_button(image, filename):
|
15 |
+
"""ダウンロードボタンを作成"""
|
16 |
+
if image is not None:
|
17 |
+
image_bytes = convert_image_to_bytes(image)
|
18 |
+
st.download_button(
|
19 |
+
label="画像をダウンロード",
|
20 |
+
data=image_bytes,
|
21 |
+
file_name=filename,
|
22 |
+
mime="image/png"
|
23 |
+
)
|