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 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:不同场景的背景香调配比
@@ -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,9 @@
1
+ open_clip_torch>=2.24.0
2
+ torch>=2.0.0
3
+ torchvision>=0.15.0
4
+ Pillow>=9.0.0
5
+ opencv-python>=4.5.0
6
+ numpy<2,>=1.21.0
7
+ PyYAML>=6.0
8
+ huggingface_hub>=0.20.0
9
+ timm>=0.9.0
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()