myscent 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- myscent-0.1.0/PKG-INFO +99 -0
- myscent-0.1.0/README.md +70 -0
- myscent-0.1.0/myscent/__init__.py +80 -0
- myscent-0.1.0/myscent/configs/base_scents.yaml +32 -0
- myscent-0.1.0/myscent/configs/odor_sources.yaml +34 -0
- myscent-0.1.0/myscent/configs/scenes.yaml +35 -0
- myscent-0.1.0/myscent/logger.py +35 -0
- myscent-0.1.0/myscent/model_utils.py +28 -0
- myscent-0.1.0/myscent/scent_pipeline.py +209 -0
- myscent-0.1.0/myscent/visualizer.py +124 -0
- myscent-0.1.0/myscent.egg-info/PKG-INFO +99 -0
- myscent-0.1.0/myscent.egg-info/SOURCES.txt +16 -0
- myscent-0.1.0/myscent.egg-info/dependency_links.txt +1 -0
- myscent-0.1.0/myscent.egg-info/requires.txt +9 -0
- myscent-0.1.0/myscent.egg-info/top_level.txt +1 -0
- myscent-0.1.0/pyproject.toml +48 -0
- myscent-0.1.0/setup.cfg +4 -0
- myscent-0.1.0/tests/test_scent.py +53 -0
myscent-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: myscent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 图像 -> 10种香料配比,CLIP零样本分类的轻量香调视觉识别库
|
|
5
|
+
Author-email: jeffchen1995 <10977437+jeffchen1995@user.noreply.gitee.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitee.com/jeffchen1995/myscent
|
|
8
|
+
Project-URL: Bug Reports, https://gitee.com/jeffchen1995/myscent/issues
|
|
9
|
+
Keywords: clip,scent,fragrance,vision,zero-shot,perfume
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: open_clip_torch>=2.24.0
|
|
21
|
+
Requires-Dist: torch>=2.0.0
|
|
22
|
+
Requires-Dist: torchvision>=0.15.0
|
|
23
|
+
Requires-Dist: Pillow>=9.0.0
|
|
24
|
+
Requires-Dist: opencv-python>=4.5.0
|
|
25
|
+
Requires-Dist: numpy<2,>=1.21.0
|
|
26
|
+
Requires-Dist: PyYAML>=6.0
|
|
27
|
+
Requires-Dist: huggingface_hub>=0.20.0
|
|
28
|
+
Requires-Dist: timm>=0.9.0
|
|
29
|
+
|
|
30
|
+
# Scent Vision Pipeline
|
|
31
|
+
图像输入 → CLIP 零样本分类 → 10种基础香料配比输出 的轻量算法。
|
|
32
|
+
|
|
33
|
+
## 核心特性
|
|
34
|
+
- 单模型 CLIP 一步输出 10 维香调配比,架构极简
|
|
35
|
+
- 零样本分类,无需训练,可快速扩展香调类别
|
|
36
|
+
- 可视化输出:主调标识 + 配比柱状图(支持中文)
|
|
37
|
+
- 支持国内 huggingface 镜像(hf-mirror.com),开箱即用
|
|
38
|
+
- 内置帧间平滑,支持视频流/实时摄像头输入
|
|
39
|
+
- 配置与代码分离,模块化设计,易于扩展
|
|
40
|
+
|
|
41
|
+
## 性能指标
|
|
42
|
+
| 设备 | 模型 | 速度 | 显存占用 |
|
|
43
|
+
|------|------|------|----------|
|
|
44
|
+
| GTX 1050 Ti (4GB) | ViT-B-32 | ~50 FPS | ~590 MB |
|
|
45
|
+
| CPU (i7) | ViT-B-32 | ~5 FPS | - |
|
|
46
|
+
|
|
47
|
+
## 环境要求
|
|
48
|
+
- Python: 3.10.x
|
|
49
|
+
- GPU: NVIDIA 显卡,显存 3GB+
|
|
50
|
+
- CUDA: 11.8
|
|
51
|
+
- 系统: Ubuntu 20.04+ / Windows 10+
|
|
52
|
+
|
|
53
|
+
## 快速开始
|
|
54
|
+
安装依赖:
|
|
55
|
+
pip install -r requirements.txt
|
|
56
|
+
|
|
57
|
+
运行测试:
|
|
58
|
+
python tests/test_scent.py --image data/input/test1.png --output data/output/result.jpg
|
|
59
|
+
|
|
60
|
+
自定义镜像(可选):
|
|
61
|
+
export HF_ENDPOINT=https://你的镜像地址
|
|
62
|
+
python tests/test_scent.py --image data/input/test1.png
|
|
63
|
+
|
|
64
|
+
## 10 种基础香料
|
|
65
|
+
柑橘调 / 木质调 / 草本调 / 花香调 / 海洋调 / 焦甜调 / 肉烤调 / 奶脂调 / 鲜腥调 / 酸酪调
|
|
66
|
+
注:鲜腥调、酸酪调默认有浓度上限(50%),可在 configs/base_scents.yaml 中调整。
|
|
67
|
+
|
|
68
|
+
## API 用法
|
|
69
|
+
from src.scent_pipeline import ScentPipeline
|
|
70
|
+
from src.visualizer import ResultVisualizer
|
|
71
|
+
import cv2
|
|
72
|
+
|
|
73
|
+
pipeline = ScentPipeline(config_dir="configs")
|
|
74
|
+
viz = ResultVisualizer()
|
|
75
|
+
|
|
76
|
+
img = cv2.imread("test.jpg")
|
|
77
|
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
|
78
|
+
|
|
79
|
+
ratio = pipeline.predict(img_rgb) # 10维数组,0~100
|
|
80
|
+
top_name, top_val = pipeline.get_top_scent(ratio)
|
|
81
|
+
|
|
82
|
+
# 可视化
|
|
83
|
+
result_img = viz.draw(img, f"主调: {top_name} ({top_val:.1f}%)", ratio, pipeline.scent_names)
|
|
84
|
+
cv2.imwrite("result.jpg", result_img)
|
|
85
|
+
|
|
86
|
+
## 目录结构
|
|
87
|
+
data/input/ 输入图片
|
|
88
|
+
data/output/ 可视化输出
|
|
89
|
+
src/ 源代码
|
|
90
|
+
models/ 模型缓存(自动下载)
|
|
91
|
+
configs/ 配置文件(香调定义、场景、气味源)
|
|
92
|
+
tests/ 测试脚本
|
|
93
|
+
logs/ 运行日志
|
|
94
|
+
|
|
95
|
+
## 配置说明
|
|
96
|
+
所有可配置项都在 configs/ 目录下:
|
|
97
|
+
- base_scents.yaml:10种基础香料定义(名称、浓度上限)
|
|
98
|
+
- odor_sources.yaml:常见气味源物体的香调特征向量
|
|
99
|
+
- scenes.yaml:不同场景的背景香调配比
|
myscent-0.1.0/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Scent Vision Pipeline
|
|
2
|
+
图像输入 → CLIP 零样本分类 → 10种基础香料配比输出 的轻量算法。
|
|
3
|
+
|
|
4
|
+
## 核心特性
|
|
5
|
+
- 单模型 CLIP 一步输出 10 维香调配比,架构极简
|
|
6
|
+
- 零样本分类,无需训练,可快速扩展香调类别
|
|
7
|
+
- 可视化输出:主调标识 + 配比柱状图(支持中文)
|
|
8
|
+
- 支持国内 huggingface 镜像(hf-mirror.com),开箱即用
|
|
9
|
+
- 内置帧间平滑,支持视频流/实时摄像头输入
|
|
10
|
+
- 配置与代码分离,模块化设计,易于扩展
|
|
11
|
+
|
|
12
|
+
## 性能指标
|
|
13
|
+
| 设备 | 模型 | 速度 | 显存占用 |
|
|
14
|
+
|------|------|------|----------|
|
|
15
|
+
| GTX 1050 Ti (4GB) | ViT-B-32 | ~50 FPS | ~590 MB |
|
|
16
|
+
| CPU (i7) | ViT-B-32 | ~5 FPS | - |
|
|
17
|
+
|
|
18
|
+
## 环境要求
|
|
19
|
+
- Python: 3.10.x
|
|
20
|
+
- GPU: NVIDIA 显卡,显存 3GB+
|
|
21
|
+
- CUDA: 11.8
|
|
22
|
+
- 系统: Ubuntu 20.04+ / Windows 10+
|
|
23
|
+
|
|
24
|
+
## 快速开始
|
|
25
|
+
安装依赖:
|
|
26
|
+
pip install -r requirements.txt
|
|
27
|
+
|
|
28
|
+
运行测试:
|
|
29
|
+
python tests/test_scent.py --image data/input/test1.png --output data/output/result.jpg
|
|
30
|
+
|
|
31
|
+
自定义镜像(可选):
|
|
32
|
+
export HF_ENDPOINT=https://你的镜像地址
|
|
33
|
+
python tests/test_scent.py --image data/input/test1.png
|
|
34
|
+
|
|
35
|
+
## 10 种基础香料
|
|
36
|
+
柑橘调 / 木质调 / 草本调 / 花香调 / 海洋调 / 焦甜调 / 肉烤调 / 奶脂调 / 鲜腥调 / 酸酪调
|
|
37
|
+
注:鲜腥调、酸酪调默认有浓度上限(50%),可在 configs/base_scents.yaml 中调整。
|
|
38
|
+
|
|
39
|
+
## API 用法
|
|
40
|
+
from src.scent_pipeline import ScentPipeline
|
|
41
|
+
from src.visualizer import ResultVisualizer
|
|
42
|
+
import cv2
|
|
43
|
+
|
|
44
|
+
pipeline = ScentPipeline(config_dir="configs")
|
|
45
|
+
viz = ResultVisualizer()
|
|
46
|
+
|
|
47
|
+
img = cv2.imread("test.jpg")
|
|
48
|
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
|
49
|
+
|
|
50
|
+
ratio = pipeline.predict(img_rgb) # 10维数组,0~100
|
|
51
|
+
top_name, top_val = pipeline.get_top_scent(ratio)
|
|
52
|
+
|
|
53
|
+
# 可视化
|
|
54
|
+
result_img = viz.draw(img, f"主调: {top_name} ({top_val:.1f}%)", ratio, pipeline.scent_names)
|
|
55
|
+
cv2.imwrite("result.jpg", result_img)
|
|
56
|
+
|
|
57
|
+
## 目录结构
|
|
58
|
+
data/input/ 输入图片
|
|
59
|
+
data/output/ 可视化输出
|
|
60
|
+
src/ 源代码
|
|
61
|
+
models/ 模型缓存(自动下载)
|
|
62
|
+
configs/ 配置文件(香调定义、场景、气味源)
|
|
63
|
+
tests/ 测试脚本
|
|
64
|
+
logs/ 运行日志
|
|
65
|
+
|
|
66
|
+
## 配置说明
|
|
67
|
+
所有可配置项都在 configs/ 目录下:
|
|
68
|
+
- base_scents.yaml:10种基础香料定义(名称、浓度上限)
|
|
69
|
+
- odor_sources.yaml:常见气味源物体的香调特征向量
|
|
70
|
+
- scenes.yaml:不同场景的背景香调配比
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
myscent: 图像 -> 10种香料配比的轻量香调视觉识别库
|
|
3
|
+
|
|
4
|
+
使用示例:
|
|
5
|
+
import myscent
|
|
6
|
+
model = myscent.load_model()
|
|
7
|
+
ratio = model.predict_image("test.jpg") # 10维数组
|
|
8
|
+
print(myscent.SCENT_NAMES_CN) # 对应每一位的香调名称
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .scent_pipeline import ScentPipeline
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
|
|
15
|
+
# 香调名称(英文)- 与输出向量维度一一对应
|
|
16
|
+
SCENT_NAMES = [
|
|
17
|
+
"citrus", # 0: 柑橘调
|
|
18
|
+
"woody", # 1: 木质调
|
|
19
|
+
"herbal", # 2: 草本调
|
|
20
|
+
"floral", # 3: 花香调
|
|
21
|
+
"marine", # 4: 海洋调
|
|
22
|
+
"caramel", # 5: 焦甜调
|
|
23
|
+
"roasted", # 6: 肉烤调
|
|
24
|
+
"milky", # 7: 奶脂调
|
|
25
|
+
"fishy", # 8: 鲜腥调
|
|
26
|
+
"sour", # 9: 酸酪调
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# 香调名称(中文)
|
|
30
|
+
SCENT_NAMES_CN = [
|
|
31
|
+
"柑橘调",
|
|
32
|
+
"木质调",
|
|
33
|
+
"草本调",
|
|
34
|
+
"花香调",
|
|
35
|
+
"海洋调",
|
|
36
|
+
"焦甜调",
|
|
37
|
+
"肉烤调",
|
|
38
|
+
"奶脂调",
|
|
39
|
+
"鲜腥调",
|
|
40
|
+
"酸酪调",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
# 每一位的详细说明
|
|
44
|
+
SCENT_DESCRIPTIONS = {
|
|
45
|
+
0: "柑橘调 - 清新柑橘、橙子、柠檬、阳光果香味",
|
|
46
|
+
1: "木质调 - 温暖雪松、檀香、森林、大地气息",
|
|
47
|
+
2: "草本调 - 青草、草本植物、新鲜自然花园感",
|
|
48
|
+
3: "花香调 - 浪漫花朵、玫瑰、茉莉、百合花香",
|
|
49
|
+
4: "海洋调 - 海洋、海水、沙滩、清新海风咸味",
|
|
50
|
+
5: "焦甜调 - 焦糖、烘焙糕点、蛋糕、香草甜点",
|
|
51
|
+
6: "肉烤调 - 烤肉、烧烤、牛排、烟熏烟火味",
|
|
52
|
+
7: "奶脂调 - 奶油、牛奶、黄油、奶酪浓郁乳香",
|
|
53
|
+
8: "鲜腥调 - 海鲜、鱼虾、生鲜市场腥鲜感(上限50%)",
|
|
54
|
+
9: "酸酪调 - 酸奶、发酵奶酪、酸香乳味(上限50%)",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_model(device=None):
|
|
59
|
+
"""
|
|
60
|
+
加载香调识别模型
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
device: 设备,None 自动选择 (cuda/cpu)
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
ScentPipeline 实例,可调用 predict_image() 进行预测
|
|
67
|
+
"""
|
|
68
|
+
import os
|
|
69
|
+
config_dir = os.path.join(os.path.dirname(__file__), "configs")
|
|
70
|
+
return ScentPipeline(config_dir=config_dir, device=device)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
"load_model",
|
|
75
|
+
"ScentPipeline",
|
|
76
|
+
"__version__",
|
|
77
|
+
"SCENT_NAMES",
|
|
78
|
+
"SCENT_NAMES_CN",
|
|
79
|
+
"SCENT_DESCRIPTIONS",
|
|
80
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# 10种基础香料定义,顺序即输出向量维度顺序
|
|
2
|
+
base_scents:
|
|
3
|
+
- name: citrus
|
|
4
|
+
desc: 柑橘调
|
|
5
|
+
max_concentration: 100
|
|
6
|
+
- name: woody
|
|
7
|
+
desc: 木质调
|
|
8
|
+
max_concentration: 100
|
|
9
|
+
- name: herbal
|
|
10
|
+
desc: 草本调
|
|
11
|
+
max_concentration: 100
|
|
12
|
+
- name: floral
|
|
13
|
+
desc: 花香调
|
|
14
|
+
max_concentration: 100
|
|
15
|
+
- name: marine
|
|
16
|
+
desc: 海洋调
|
|
17
|
+
max_concentration: 100
|
|
18
|
+
- name: caramel
|
|
19
|
+
desc: 焦甜调
|
|
20
|
+
max_concentration: 100
|
|
21
|
+
- name: roasted
|
|
22
|
+
desc: 肉烤调
|
|
23
|
+
max_concentration: 100
|
|
24
|
+
- name: milky
|
|
25
|
+
desc: 奶脂调
|
|
26
|
+
max_concentration: 100
|
|
27
|
+
- name: fishy
|
|
28
|
+
desc: 鲜腥调
|
|
29
|
+
max_concentration: 50 # 安全浓度上限
|
|
30
|
+
- name: sour
|
|
31
|
+
desc: 酸酪调
|
|
32
|
+
max_concentration: 50 # 安全浓度上限
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# 前景气味源物体 → 10维气味特征向量(强度0~100)
|
|
2
|
+
odor_sources:
|
|
3
|
+
hotpot:
|
|
4
|
+
desc: 火锅
|
|
5
|
+
feature: [5, 10, 5, 0, 0, 30, 40, 10, 5, 5]
|
|
6
|
+
priority: 90
|
|
7
|
+
barbecue:
|
|
8
|
+
desc: 烤肉
|
|
9
|
+
feature: [5, 5, 0, 0, 0, 20, 50, 5, 3, 2]
|
|
10
|
+
priority: 85
|
|
11
|
+
milk_tea:
|
|
12
|
+
desc: 奶茶
|
|
13
|
+
feature: [10, 0, 0, 5, 0, 25, 0, 30, 0, 2]
|
|
14
|
+
priority: 70
|
|
15
|
+
coffee:
|
|
16
|
+
desc: 咖啡
|
|
17
|
+
feature: [5, 10, 0, 0, 0, 35, 10, 10, 0, 0]
|
|
18
|
+
priority: 75
|
|
19
|
+
rose:
|
|
20
|
+
desc: 玫瑰
|
|
21
|
+
feature: [5, 0, 0, 60, 5, 5, 0, 0, 0, 0]
|
|
22
|
+
priority: 80
|
|
23
|
+
sea:
|
|
24
|
+
desc: 海水
|
|
25
|
+
feature: [10, 0, 0, 0, 70, 0, 0, 0, 5, 0]
|
|
26
|
+
priority: 75
|
|
27
|
+
garbage:
|
|
28
|
+
desc: 垃圾
|
|
29
|
+
feature: [0, 10, 0, 0, 0, 5, 0, 0, 10, 40]
|
|
30
|
+
priority: 85
|
|
31
|
+
seafood:
|
|
32
|
+
desc: 海鲜
|
|
33
|
+
feature: [5, 0, 5, 0, 10, 5, 0, 0, 35, 5]
|
|
34
|
+
priority: 80
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# 8大类场景 → 背景香调配比(10维,低浓度基底)
|
|
2
|
+
scenes:
|
|
3
|
+
restaurant:
|
|
4
|
+
desc: 餐厅美食
|
|
5
|
+
base_ratio: [5, 10, 0, 5, 0, 25, 25, 10, 3, 7]
|
|
6
|
+
barbecue:
|
|
7
|
+
desc: 烧烤
|
|
8
|
+
base_ratio: [3, 8, 0, 3, 0, 20, 40, 5, 5, 5]
|
|
9
|
+
dessert_shop:
|
|
10
|
+
desc: 甜品店
|
|
11
|
+
base_ratio: [10, 5, 0, 10, 0, 40, 5, 25, 0, 5]
|
|
12
|
+
outdoor_nature:
|
|
13
|
+
desc: 户外自然
|
|
14
|
+
base_ratio: [20, 10, 25, 5, 10, 0, 0, 0, 0, 0]
|
|
15
|
+
city_street:
|
|
16
|
+
desc: 城市街道
|
|
17
|
+
base_ratio: [15, 15, 5, 5, 10, 5, 0, 0, 0, 0]
|
|
18
|
+
indoor_home:
|
|
19
|
+
desc: 室内居家
|
|
20
|
+
base_ratio: [5, 20, 0, 10, 0, 10, 0, 5, 0, 0]
|
|
21
|
+
rainy:
|
|
22
|
+
desc: 雨天
|
|
23
|
+
base_ratio: [5, 25, 10, 0, 20, 0, 0, 0, 0, 0]
|
|
24
|
+
cafe:
|
|
25
|
+
desc: 咖啡馆
|
|
26
|
+
base_ratio: [5, 15, 0, 5, 0, 20, 5, 10, 0, 0]
|
|
27
|
+
night:
|
|
28
|
+
desc: 夜晚
|
|
29
|
+
base_ratio: [0, 20, 0, 10, 5, 5, 0, 5, 0, 0]
|
|
30
|
+
seaside:
|
|
31
|
+
desc: 海边
|
|
32
|
+
base_ratio: [20, 5, 5, 5, 30, 0, 0, 0, 0, 0]
|
|
33
|
+
forest:
|
|
34
|
+
desc: 森林
|
|
35
|
+
base_ratio: [10, 20, 30, 5, 5, 0, 0, 0, 0, 0]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
日志配置:同时输出到终端和文件
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def setup_logger(name="scent", log_file="logs/pipeline.log", level=logging.INFO):
|
|
10
|
+
"""初始化 logger,同时输出到终端和文件"""
|
|
11
|
+
logger = logging.getLogger(name)
|
|
12
|
+
logger.setLevel(level)
|
|
13
|
+
logger.propagate = False
|
|
14
|
+
|
|
15
|
+
if logger.handlers:
|
|
16
|
+
return logger
|
|
17
|
+
|
|
18
|
+
formatter = logging.Formatter(
|
|
19
|
+
"%(asctime)s [%(levelname)s] %(message)s",
|
|
20
|
+
datefmt="%Y-%m-%d %H:%M:%S"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# 终端输出
|
|
24
|
+
sh = logging.StreamHandler(sys.stdout)
|
|
25
|
+
sh.setFormatter(formatter)
|
|
26
|
+
logger.addHandler(sh)
|
|
27
|
+
|
|
28
|
+
# 文件输出
|
|
29
|
+
log_path = Path(log_file)
|
|
30
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
fh = logging.FileHandler(log_path, encoding="utf-8")
|
|
32
|
+
fh.setFormatter(formatter)
|
|
33
|
+
logger.addHandler(fh)
|
|
34
|
+
|
|
35
|
+
return logger
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
模型权重下载工具 - 支持 huggingface 镜像站 hf-mirror.com
|
|
3
|
+
|
|
4
|
+
如果环境变量 HF_ENDPOINT 已设置,则使用用户指定的镜像;
|
|
5
|
+
否则默认使用 hf-mirror.com 国内镜像。
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
# 尊重用户已设置的 HF_ENDPOINT,未设置时默认用国内镜像
|
|
11
|
+
if not os.environ.get("HF_ENDPOINT"):
|
|
12
|
+
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
|
|
13
|
+
|
|
14
|
+
# 模型缓存目录(项目内)
|
|
15
|
+
CACHE_DIR = Path(__file__).parent.parent / "models"
|
|
16
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def download_clip_vitb32():
|
|
20
|
+
"""获取 CLIP ViT-B-32 (LAION 2B 预训练) 权重路径"""
|
|
21
|
+
from huggingface_hub import hf_hub_download
|
|
22
|
+
|
|
23
|
+
path = hf_hub_download(
|
|
24
|
+
"laion/CLIP-ViT-B-32-laion2B-s34B-b79K",
|
|
25
|
+
"open_clip_pytorch_model.bin",
|
|
26
|
+
cache_dir=str(CACHE_DIR),
|
|
27
|
+
)
|
|
28
|
+
return path
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scent Pipeline: 图像 → 10 种香料配比(CLIP 零样本分类)
|
|
3
|
+
"""
|
|
4
|
+
import warnings
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import cv2
|
|
8
|
+
import numpy as np
|
|
9
|
+
import torch
|
|
10
|
+
import open_clip
|
|
11
|
+
import yaml
|
|
12
|
+
from PIL import Image
|
|
13
|
+
|
|
14
|
+
from .model_utils import download_clip_vitb32
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# 过滤 TypedStorage 废弃警告(open_clip 内部触发,不影响功能)
|
|
18
|
+
warnings.filterwarnings("ignore", message="TypedStorage is deprecated")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# 10 种香调的 CLIP 文本 prompt(描述气味场景)
|
|
22
|
+
SCENT_PROMPTS = {
|
|
23
|
+
"柑橘调": "bright fresh citrus orange lemon fruit sunny refreshing",
|
|
24
|
+
"木质调": "warm wood cedar sandalwood forest trees earthy cozy",
|
|
25
|
+
"草本调": "green grass herbal leafy plants fresh natural garden",
|
|
26
|
+
"花香调": "romantic flowers rose jasmine lily sweet floral bouquet",
|
|
27
|
+
"海洋调": "ocean sea water beach waves salty fresh breeze blue",
|
|
28
|
+
"焦甜调": "sweet caramel baked pastry cake sugar warm vanilla dessert",
|
|
29
|
+
"肉烤调": "grilled meat barbecue steak smoky savory cooking fire",
|
|
30
|
+
"奶脂调": "creamy milk butter cheese rich dairy vanilla sweet",
|
|
31
|
+
"鲜腥调": "seafood fish shrimp ocean raw fresh fishy market",
|
|
32
|
+
"酸酪调": "sour yogurt cheese fermented tangy milky creamy",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_device():
|
|
37
|
+
"""检测可用设备,GPU 不兼容时自动回退 CPU"""
|
|
38
|
+
if not torch.cuda.is_available():
|
|
39
|
+
return "cpu"
|
|
40
|
+
try:
|
|
41
|
+
# 实际跑一个小算子验证 GPU 是否真的可用
|
|
42
|
+
_ = torch.zeros(1, device="cuda")
|
|
43
|
+
return "cuda"
|
|
44
|
+
except Exception:
|
|
45
|
+
print("⚠️ GPU 不可用(PyTorch 与显卡算力不兼容),回退到 CPU 模式")
|
|
46
|
+
return "cpu"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _setup_torch():
|
|
50
|
+
"""
|
|
51
|
+
优化 PyTorch 运行时配置
|
|
52
|
+
|
|
53
|
+
- 关闭 cuDNN:避免 Pascal 架构老显卡 (GTX 10xx) 触发 cuDNN workaround 警告
|
|
54
|
+
- 允许 TF32:Ampere 及以上显卡自动启用,老卡忽略
|
|
55
|
+
"""
|
|
56
|
+
# Pascal 架构(算力 6.x)的 cuDNN 有已知兼容问题,直接关闭更稳定
|
|
57
|
+
# 如果是 newer 显卡,可以设为 True 获得更快速度
|
|
58
|
+
if torch.cuda.is_available():
|
|
59
|
+
capability = torch.cuda.get_device_capability()
|
|
60
|
+
if capability[0] <= 6: # Pascal 及更早
|
|
61
|
+
torch.backends.cudnn.enabled = False
|
|
62
|
+
else:
|
|
63
|
+
torch.backends.cudnn.benchmark = True
|
|
64
|
+
torch.backends.cudnn.allow_tf32 = True
|
|
65
|
+
torch.backends.cuda.matmul.allow_tf32 = True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ScentPipeline:
|
|
69
|
+
"""
|
|
70
|
+
图像 → 10 种香料配比(一步到位,CLIP 零样本分类)
|
|
71
|
+
|
|
72
|
+
支持单帧预测和多帧平滑(视频流用)。
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
# softmax 温度系数,越小分布越尖锐(主次香调更分明)
|
|
76
|
+
TEMPERATURE = 0.15
|
|
77
|
+
# 帧间平滑系数:当前帧占比,值越大响应越快
|
|
78
|
+
SMOOTH_ALPHA = 0.8
|
|
79
|
+
|
|
80
|
+
def __init__(self, config_dir: str = "configs", device: str = None):
|
|
81
|
+
config_dir = Path(config_dir)
|
|
82
|
+
|
|
83
|
+
# 优化 PyTorch 配置
|
|
84
|
+
_setup_torch()
|
|
85
|
+
|
|
86
|
+
# 加载基础香料定义
|
|
87
|
+
with open(config_dir / "base_scents.yaml", "r", encoding="utf-8") as f:
|
|
88
|
+
self.base_scents = yaml.safe_load(f)["base_scents"]
|
|
89
|
+
self.scent_names = [s["desc"] for s in self.base_scents]
|
|
90
|
+
self.num_scents = len(self.base_scents)
|
|
91
|
+
self.max_concentrations = np.array(
|
|
92
|
+
[s["max_concentration"] for s in self.base_scents],
|
|
93
|
+
dtype=np.float32
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# 设备选择
|
|
97
|
+
self.device = device if device else _get_device()
|
|
98
|
+
print(f"加载 CLIP ViT-B-32 模型({self.device})...")
|
|
99
|
+
|
|
100
|
+
# 初始化 CLIP
|
|
101
|
+
clip_weight = download_clip_vitb32()
|
|
102
|
+
self.model, _, self.preprocess = open_clip.create_model_and_transforms(
|
|
103
|
+
"ViT-B-32", pretrained=clip_weight, device=self.device
|
|
104
|
+
)
|
|
105
|
+
self.tokenizer = open_clip.get_tokenizer("ViT-B-32")
|
|
106
|
+
self.model.eval()
|
|
107
|
+
|
|
108
|
+
# 预计算 10 种香调的文本特征
|
|
109
|
+
prompts = [SCENT_PROMPTS[name] for name in self.scent_names]
|
|
110
|
+
tokens = self.tokenizer(prompts).to(self.device)
|
|
111
|
+
with torch.no_grad():
|
|
112
|
+
self.text_features = self.model.encode_text(tokens)
|
|
113
|
+
self.text_features /= self.text_features.norm(dim=-1, keepdim=True)
|
|
114
|
+
|
|
115
|
+
# 帧间平滑状态
|
|
116
|
+
self.last_ratio = np.zeros(self.num_scents, dtype=np.float32)
|
|
117
|
+
self._initialized = False # 第一帧不做平滑
|
|
118
|
+
|
|
119
|
+
print(f"Pipeline 初始化完成,{self.num_scents} 种基础香料")
|
|
120
|
+
|
|
121
|
+
def reset(self):
|
|
122
|
+
"""重置平滑状态(视频流切换场景时调用)"""
|
|
123
|
+
self.last_ratio = np.zeros(self.num_scents, dtype=np.float32)
|
|
124
|
+
self._initialized = False
|
|
125
|
+
|
|
126
|
+
def predict(self, image_rgb: np.ndarray) -> np.ndarray:
|
|
127
|
+
"""
|
|
128
|
+
输入 RGB 图像,输出 10 维配比(0~100%)
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
image_rgb: HxWx3 的 RGB 图像数组
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
10 维 float32 数组,对应 base_scents 顺序的百分比配比
|
|
135
|
+
"""
|
|
136
|
+
# 图像预处理 + CLIP 编码
|
|
137
|
+
image = Image.fromarray(image_rgb)
|
|
138
|
+
image = self.preprocess(image).unsqueeze(0).to(self.device)
|
|
139
|
+
|
|
140
|
+
with torch.no_grad():
|
|
141
|
+
img_feat = self.model.encode_image(image)
|
|
142
|
+
img_feat /= img_feat.norm(dim=-1, keepdim=True)
|
|
143
|
+
|
|
144
|
+
# 余弦相似度 → temperature 缩放 softmax → 概率分布
|
|
145
|
+
similarity = (img_feat @ self.text_features.T).squeeze(0)
|
|
146
|
+
probs = (similarity / self.TEMPERATURE).softmax(dim=0).cpu().numpy()
|
|
147
|
+
|
|
148
|
+
# 映射到 0~100 范围
|
|
149
|
+
ratio = probs * 100.0
|
|
150
|
+
|
|
151
|
+
# 帧间平滑(第一帧直接输出,不做平滑)
|
|
152
|
+
if self._initialized:
|
|
153
|
+
final_ratio = self.SMOOTH_ALPHA * ratio + (1 - self.SMOOTH_ALPHA) * self.last_ratio
|
|
154
|
+
else:
|
|
155
|
+
final_ratio = ratio
|
|
156
|
+
self._initialized = True
|
|
157
|
+
|
|
158
|
+
self.last_ratio = final_ratio
|
|
159
|
+
|
|
160
|
+
# 浓度上限 + 裁剪到合法范围(一次 clip 搞定)
|
|
161
|
+
return np.clip(final_ratio, 0, self.max_concentrations).astype(np.float32)
|
|
162
|
+
|
|
163
|
+
def predict_file(self, image_path: str) -> np.ndarray:
|
|
164
|
+
"""从文件读取并预测"""
|
|
165
|
+
img = cv2.imread(image_path)
|
|
166
|
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
|
167
|
+
return self.predict(img_rgb)
|
|
168
|
+
|
|
169
|
+
def predict_image(self, image) -> np.ndarray:
|
|
170
|
+
"""
|
|
171
|
+
通用预测接口,支持多种输入格式
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
image: 支持以下类型:
|
|
175
|
+
- str: 图片文件路径
|
|
176
|
+
- PIL.Image.Image: PIL 图像
|
|
177
|
+
- np.ndarray: numpy 数组 (HxWx3, RGB)
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
10 维 float32 数组,对应 SCENT_NAMES 顺序的百分比配比
|
|
181
|
+
"""
|
|
182
|
+
if isinstance(image, str):
|
|
183
|
+
return self.predict_file(image)
|
|
184
|
+
elif isinstance(image, Image.Image):
|
|
185
|
+
return self.predict(np.array(image.convert("RGB")))
|
|
186
|
+
elif isinstance(image, np.ndarray):
|
|
187
|
+
if image.ndim == 3 and image.shape[2] == 3:
|
|
188
|
+
return self.predict(image)
|
|
189
|
+
else:
|
|
190
|
+
raise ValueError(f"不支持的图像形状: {image.shape},需要 HxWx3")
|
|
191
|
+
else:
|
|
192
|
+
raise TypeError(f"不支持的输入类型: {type(image)}")
|
|
193
|
+
|
|
194
|
+
def get_top_scent(self, ratio: np.ndarray):
|
|
195
|
+
"""获取最强香调 (名称, 百分比)"""
|
|
196
|
+
idx = ratio.argmax()
|
|
197
|
+
return self.scent_names[idx], float(ratio[idx])
|
|
198
|
+
|
|
199
|
+
def print_result(self, ratio: np.ndarray):
|
|
200
|
+
"""打印配比结果(带可视化进度条)"""
|
|
201
|
+
print("=" * 55)
|
|
202
|
+
print("基础香料配比输出(0~100%):")
|
|
203
|
+
for i, s in enumerate(self.base_scents):
|
|
204
|
+
bar_len = int(ratio[i] / 2)
|
|
205
|
+
bar = "█" * bar_len
|
|
206
|
+
print(" {:<6}: {:>5.1f}% {}".format(s["desc"], ratio[i], bar))
|
|
207
|
+
top_name, top_val = self.get_top_scent(ratio)
|
|
208
|
+
print(f"\n主调: {top_name} ({top_val:.1f}%)")
|
|
209
|
+
print("=" * 55)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
结果可视化:在图片上绘制香料配比柱状图 + 主调标题
|
|
3
|
+
使用 PIL 渲染中文,解决 OpenCV 不支持中文的问题
|
|
4
|
+
"""
|
|
5
|
+
import cv2
|
|
6
|
+
import numpy as np
|
|
7
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# 中文字体候选路径(按优先级排序)
|
|
11
|
+
_FONT_CANDIDATES = [
|
|
12
|
+
"/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc",
|
|
13
|
+
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
|
14
|
+
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
|
|
15
|
+
"/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
|
|
16
|
+
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
|
|
17
|
+
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _find_font_path():
|
|
22
|
+
"""查找可用的中文字体,找不到返回 None 用默认字体兜底"""
|
|
23
|
+
import os
|
|
24
|
+
for path in _FONT_CANDIDATES:
|
|
25
|
+
if os.path.exists(path):
|
|
26
|
+
return path
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ResultVisualizer:
|
|
31
|
+
"""香调配比可视化"""
|
|
32
|
+
|
|
33
|
+
# 10 种香调的专属配色(与香调含义对应,顺序与 base_scents 一致)
|
|
34
|
+
COLORS = [
|
|
35
|
+
(255, 170, 50), # 柑橘调 - 橙黄
|
|
36
|
+
(140, 85, 40), # 木质调 - 棕色
|
|
37
|
+
(70, 175, 70), # 草本调 - 草绿
|
|
38
|
+
(255, 120, 175), # 花香调 - 粉红
|
|
39
|
+
(70, 160, 255), # 海洋调 - 蓝色
|
|
40
|
+
(200, 120, 50), # 焦甜调 - 焦糖色
|
|
41
|
+
(220, 55, 55), # 肉烤调 - 红色
|
|
42
|
+
(255, 230, 205), # 奶脂调 - 奶白
|
|
43
|
+
(100, 180, 190), # 鲜腥调 - 青灰
|
|
44
|
+
(255, 220, 75), # 酸酪调 - 嫩黄
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
def __init__(self):
|
|
48
|
+
font_path = _find_font_path()
|
|
49
|
+
self.small_font = self._load_font(font_path, 20)
|
|
50
|
+
self.medium_font = self._load_font(font_path, 28)
|
|
51
|
+
self.big_font = self._load_font(font_path, 36)
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def _load_font(path, size):
|
|
55
|
+
"""加载字体,失败则用默认字体兜底"""
|
|
56
|
+
if path:
|
|
57
|
+
try:
|
|
58
|
+
return ImageFont.truetype(path, size)
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
return ImageFont.load_default()
|
|
62
|
+
|
|
63
|
+
def draw(self, image_bgr, title_text, scent_ratio, scent_names):
|
|
64
|
+
"""
|
|
65
|
+
在图片上绘制顶部标题 + 底部香料配比柱状图
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
image_bgr: BGR 图像(cv2 格式)
|
|
69
|
+
title_text: 顶部标题文字
|
|
70
|
+
scent_ratio: 10 维配比数组 (0~100)
|
|
71
|
+
scent_names: 10 种香调名称列表
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
带可视化结果的 BGR 图像
|
|
75
|
+
"""
|
|
76
|
+
img_pil = Image.fromarray(cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB))
|
|
77
|
+
draw = ImageDraw.Draw(img_pil)
|
|
78
|
+
w, h = img_pil.size
|
|
79
|
+
|
|
80
|
+
# ---------- 顶部标题条 ----------
|
|
81
|
+
top_h = 70
|
|
82
|
+
draw.rectangle([0, 0, w, top_h], fill=(0, 0, 0))
|
|
83
|
+
draw.text((20, 15), title_text, font=self.big_font, fill=(0, 255, 128))
|
|
84
|
+
|
|
85
|
+
# ---------- 底部香料配比区域 ----------
|
|
86
|
+
bar_area_h = 320
|
|
87
|
+
bar_y = h - bar_area_h
|
|
88
|
+
draw.rectangle([0, bar_y, w, h], fill=(15, 15, 25))
|
|
89
|
+
|
|
90
|
+
n = len(scent_names)
|
|
91
|
+
bar_w = int((w - 80) / n)
|
|
92
|
+
max_h = bar_area_h - 130
|
|
93
|
+
start_x = 50
|
|
94
|
+
|
|
95
|
+
# 区域标题
|
|
96
|
+
draw.text((start_x, bar_y + 15), "香 料 配 比",
|
|
97
|
+
font=self.medium_font, fill=(255, 255, 255))
|
|
98
|
+
|
|
99
|
+
for i in range(n):
|
|
100
|
+
name = scent_names[i]
|
|
101
|
+
val = float(scent_ratio[i])
|
|
102
|
+
x = start_x + i * bar_w
|
|
103
|
+
bh = int(val / 100.0 * max_h)
|
|
104
|
+
y1 = h - 80 - bh
|
|
105
|
+
y2 = h - 80
|
|
106
|
+
|
|
107
|
+
color = self.COLORS[i % len(self.COLORS)]
|
|
108
|
+
|
|
109
|
+
# 柱状图
|
|
110
|
+
draw.rectangle([x + 8, y1, x + bar_w - 8, y2], fill=color)
|
|
111
|
+
|
|
112
|
+
# 柱顶百分比(居中)
|
|
113
|
+
pct = "{:.0f}%".format(val)
|
|
114
|
+
tw = draw.textlength(pct, font=self.small_font)
|
|
115
|
+
draw.text((x + (bar_w - tw) / 2, y1 - 28),
|
|
116
|
+
pct, font=self.small_font, fill=(255, 255, 255))
|
|
117
|
+
|
|
118
|
+
# 名称(居中)
|
|
119
|
+
tw = draw.textlength(name, font=self.small_font)
|
|
120
|
+
draw.text((x + (bar_w - tw) / 2, h - 50),
|
|
121
|
+
name, font=self.small_font, fill=(220, 220, 220))
|
|
122
|
+
|
|
123
|
+
# ---------- 转回 BGR ----------
|
|
124
|
+
return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: myscent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 图像 -> 10种香料配比,CLIP零样本分类的轻量香调视觉识别库
|
|
5
|
+
Author-email: jeffchen1995 <10977437+jeffchen1995@user.noreply.gitee.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitee.com/jeffchen1995/myscent
|
|
8
|
+
Project-URL: Bug Reports, https://gitee.com/jeffchen1995/myscent/issues
|
|
9
|
+
Keywords: clip,scent,fragrance,vision,zero-shot,perfume
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: open_clip_torch>=2.24.0
|
|
21
|
+
Requires-Dist: torch>=2.0.0
|
|
22
|
+
Requires-Dist: torchvision>=0.15.0
|
|
23
|
+
Requires-Dist: Pillow>=9.0.0
|
|
24
|
+
Requires-Dist: opencv-python>=4.5.0
|
|
25
|
+
Requires-Dist: numpy<2,>=1.21.0
|
|
26
|
+
Requires-Dist: PyYAML>=6.0
|
|
27
|
+
Requires-Dist: huggingface_hub>=0.20.0
|
|
28
|
+
Requires-Dist: timm>=0.9.0
|
|
29
|
+
|
|
30
|
+
# Scent Vision Pipeline
|
|
31
|
+
图像输入 → CLIP 零样本分类 → 10种基础香料配比输出 的轻量算法。
|
|
32
|
+
|
|
33
|
+
## 核心特性
|
|
34
|
+
- 单模型 CLIP 一步输出 10 维香调配比,架构极简
|
|
35
|
+
- 零样本分类,无需训练,可快速扩展香调类别
|
|
36
|
+
- 可视化输出:主调标识 + 配比柱状图(支持中文)
|
|
37
|
+
- 支持国内 huggingface 镜像(hf-mirror.com),开箱即用
|
|
38
|
+
- 内置帧间平滑,支持视频流/实时摄像头输入
|
|
39
|
+
- 配置与代码分离,模块化设计,易于扩展
|
|
40
|
+
|
|
41
|
+
## 性能指标
|
|
42
|
+
| 设备 | 模型 | 速度 | 显存占用 |
|
|
43
|
+
|------|------|------|----------|
|
|
44
|
+
| GTX 1050 Ti (4GB) | ViT-B-32 | ~50 FPS | ~590 MB |
|
|
45
|
+
| CPU (i7) | ViT-B-32 | ~5 FPS | - |
|
|
46
|
+
|
|
47
|
+
## 环境要求
|
|
48
|
+
- Python: 3.10.x
|
|
49
|
+
- GPU: NVIDIA 显卡,显存 3GB+
|
|
50
|
+
- CUDA: 11.8
|
|
51
|
+
- 系统: Ubuntu 20.04+ / Windows 10+
|
|
52
|
+
|
|
53
|
+
## 快速开始
|
|
54
|
+
安装依赖:
|
|
55
|
+
pip install -r requirements.txt
|
|
56
|
+
|
|
57
|
+
运行测试:
|
|
58
|
+
python tests/test_scent.py --image data/input/test1.png --output data/output/result.jpg
|
|
59
|
+
|
|
60
|
+
自定义镜像(可选):
|
|
61
|
+
export HF_ENDPOINT=https://你的镜像地址
|
|
62
|
+
python tests/test_scent.py --image data/input/test1.png
|
|
63
|
+
|
|
64
|
+
## 10 种基础香料
|
|
65
|
+
柑橘调 / 木质调 / 草本调 / 花香调 / 海洋调 / 焦甜调 / 肉烤调 / 奶脂调 / 鲜腥调 / 酸酪调
|
|
66
|
+
注:鲜腥调、酸酪调默认有浓度上限(50%),可在 configs/base_scents.yaml 中调整。
|
|
67
|
+
|
|
68
|
+
## API 用法
|
|
69
|
+
from src.scent_pipeline import ScentPipeline
|
|
70
|
+
from src.visualizer import ResultVisualizer
|
|
71
|
+
import cv2
|
|
72
|
+
|
|
73
|
+
pipeline = ScentPipeline(config_dir="configs")
|
|
74
|
+
viz = ResultVisualizer()
|
|
75
|
+
|
|
76
|
+
img = cv2.imread("test.jpg")
|
|
77
|
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
|
78
|
+
|
|
79
|
+
ratio = pipeline.predict(img_rgb) # 10维数组,0~100
|
|
80
|
+
top_name, top_val = pipeline.get_top_scent(ratio)
|
|
81
|
+
|
|
82
|
+
# 可视化
|
|
83
|
+
result_img = viz.draw(img, f"主调: {top_name} ({top_val:.1f}%)", ratio, pipeline.scent_names)
|
|
84
|
+
cv2.imwrite("result.jpg", result_img)
|
|
85
|
+
|
|
86
|
+
## 目录结构
|
|
87
|
+
data/input/ 输入图片
|
|
88
|
+
data/output/ 可视化输出
|
|
89
|
+
src/ 源代码
|
|
90
|
+
models/ 模型缓存(自动下载)
|
|
91
|
+
configs/ 配置文件(香调定义、场景、气味源)
|
|
92
|
+
tests/ 测试脚本
|
|
93
|
+
logs/ 运行日志
|
|
94
|
+
|
|
95
|
+
## 配置说明
|
|
96
|
+
所有可配置项都在 configs/ 目录下:
|
|
97
|
+
- base_scents.yaml:10种基础香料定义(名称、浓度上限)
|
|
98
|
+
- odor_sources.yaml:常见气味源物体的香调特征向量
|
|
99
|
+
- scenes.yaml:不同场景的背景香调配比
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
myscent/__init__.py
|
|
4
|
+
myscent/logger.py
|
|
5
|
+
myscent/model_utils.py
|
|
6
|
+
myscent/scent_pipeline.py
|
|
7
|
+
myscent/visualizer.py
|
|
8
|
+
myscent.egg-info/PKG-INFO
|
|
9
|
+
myscent.egg-info/SOURCES.txt
|
|
10
|
+
myscent.egg-info/dependency_links.txt
|
|
11
|
+
myscent.egg-info/requires.txt
|
|
12
|
+
myscent.egg-info/top_level.txt
|
|
13
|
+
myscent/configs/base_scents.yaml
|
|
14
|
+
myscent/configs/odor_sources.yaml
|
|
15
|
+
myscent/configs/scenes.yaml
|
|
16
|
+
tests/test_scent.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
myscent
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "myscent"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "图像 -> 10种香料配比,CLIP零样本分类的轻量香调视觉识别库"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "jeffchen1995", email = "10977437+jeffchen1995@user.noreply.gitee.com"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["clip", "scent", "fragrance", "vision", "zero-shot", "perfume"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
"open_clip_torch>=2.24.0",
|
|
29
|
+
"torch>=2.0.0",
|
|
30
|
+
"torchvision>=0.15.0",
|
|
31
|
+
"Pillow>=9.0.0",
|
|
32
|
+
"opencv-python>=4.5.0",
|
|
33
|
+
"numpy>=1.21.0,<2",
|
|
34
|
+
"PyYAML>=6.0",
|
|
35
|
+
"huggingface_hub>=0.20.0",
|
|
36
|
+
"timm>=0.9.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
"Homepage" = "https://gitee.com/jeffchen1995/myscent"
|
|
41
|
+
"Bug Reports" = "https://gitee.com/jeffchen1995/myscent/issues"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["."]
|
|
45
|
+
include = ["myscent*"]
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.package-data]
|
|
48
|
+
myscent = ["configs/*.yaml"]
|
myscent-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
测试:图片 → 香调配比 + 可视化输出
|
|
3
|
+
"""
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
9
|
+
|
|
10
|
+
import cv2
|
|
11
|
+
from myscent import ScentPipeline
|
|
12
|
+
from myscent.visualizer import ResultVisualizer
|
|
13
|
+
from myscent.logger import setup_logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main():
|
|
17
|
+
parser = argparse.ArgumentParser(description="图像 → 香料配比")
|
|
18
|
+
parser.add_argument("--image", type=str, required=True, help="输入图片路径")
|
|
19
|
+
parser.add_argument("--output", type=str, default="data/output/result.jpg",
|
|
20
|
+
help="可视化输出路径")
|
|
21
|
+
args = parser.parse_args()
|
|
22
|
+
|
|
23
|
+
logger = setup_logger()
|
|
24
|
+
|
|
25
|
+
# 初始化
|
|
26
|
+
pipeline = ScentPipeline(config_dir="configs")
|
|
27
|
+
viz = ResultVisualizer()
|
|
28
|
+
|
|
29
|
+
# 预测
|
|
30
|
+
image_bgr = cv2.imread(args.image)
|
|
31
|
+
if image_bgr is None:
|
|
32
|
+
logger.error(f"无法读取图片: {args.image}")
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
|
|
36
|
+
ratio = pipeline.predict(image_rgb)
|
|
37
|
+
|
|
38
|
+
# 可视化
|
|
39
|
+
top_name, top_val = pipeline.get_top_scent(ratio)
|
|
40
|
+
title = f"主调: {top_name} ({top_val:.1f}%)"
|
|
41
|
+
viz_img = viz.draw(image_bgr, title, ratio, pipeline.scent_names)
|
|
42
|
+
|
|
43
|
+
# 保存
|
|
44
|
+
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
cv2.imwrite(args.output, viz_img)
|
|
46
|
+
logger.info(f"可视化结果已保存到: {args.output}")
|
|
47
|
+
|
|
48
|
+
# 打印
|
|
49
|
+
pipeline.print_result(ratio)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
main()
|