maque 0.2.1__py3-none-any.whl
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.
- maque/__init__.py +30 -0
- maque/__main__.py +926 -0
- maque/ai_platform/__init__.py +0 -0
- maque/ai_platform/crawl.py +45 -0
- maque/ai_platform/metrics.py +258 -0
- maque/ai_platform/nlp_preprocess.py +67 -0
- maque/ai_platform/webpage_screen_shot.py +195 -0
- maque/algorithms/__init__.py +78 -0
- maque/algorithms/bezier.py +15 -0
- maque/algorithms/bktree.py +117 -0
- maque/algorithms/core.py +104 -0
- maque/algorithms/hilbert.py +16 -0
- maque/algorithms/rate_function.py +92 -0
- maque/algorithms/transform.py +27 -0
- maque/algorithms/trie.py +272 -0
- maque/algorithms/utils.py +63 -0
- maque/algorithms/video.py +587 -0
- maque/api/__init__.py +1 -0
- maque/api/common.py +110 -0
- maque/api/fetch.py +26 -0
- maque/api/static/icon.png +0 -0
- maque/api/static/redoc.standalone.js +1782 -0
- maque/api/static/swagger-ui-bundle.js +3 -0
- maque/api/static/swagger-ui.css +3 -0
- maque/cli/__init__.py +1 -0
- maque/cli/clean_invisible_chars.py +324 -0
- maque/cli/core.py +34 -0
- maque/cli/groups/__init__.py +26 -0
- maque/cli/groups/config.py +205 -0
- maque/cli/groups/data.py +615 -0
- maque/cli/groups/doctor.py +259 -0
- maque/cli/groups/embedding.py +222 -0
- maque/cli/groups/git.py +29 -0
- maque/cli/groups/help.py +410 -0
- maque/cli/groups/llm.py +223 -0
- maque/cli/groups/mcp.py +241 -0
- maque/cli/groups/mllm.py +1795 -0
- maque/cli/groups/mllm_simple.py +60 -0
- maque/cli/groups/quant.py +210 -0
- maque/cli/groups/service.py +490 -0
- maque/cli/groups/system.py +570 -0
- maque/cli/mllm_run.py +1451 -0
- maque/cli/script.py +52 -0
- maque/cli/tree.py +49 -0
- maque/clustering/__init__.py +52 -0
- maque/clustering/analyzer.py +347 -0
- maque/clustering/clusterers.py +464 -0
- maque/clustering/sampler.py +134 -0
- maque/clustering/visualizer.py +205 -0
- maque/constant.py +13 -0
- maque/core.py +133 -0
- maque/cv/__init__.py +1 -0
- maque/cv/image.py +219 -0
- maque/cv/utils.py +68 -0
- maque/cv/video/__init__.py +3 -0
- maque/cv/video/keyframe_extractor.py +368 -0
- maque/embedding/__init__.py +43 -0
- maque/embedding/base.py +56 -0
- maque/embedding/multimodal.py +308 -0
- maque/embedding/server.py +523 -0
- maque/embedding/text.py +311 -0
- maque/git/__init__.py +24 -0
- maque/git/pure_git.py +912 -0
- maque/io/__init__.py +29 -0
- maque/io/core.py +38 -0
- maque/io/ops.py +194 -0
- maque/llm/__init__.py +111 -0
- maque/llm/backend.py +416 -0
- maque/llm/base.py +411 -0
- maque/llm/server.py +366 -0
- maque/mcp_server.py +1096 -0
- maque/mllm_data_processor_pipeline/__init__.py +17 -0
- maque/mllm_data_processor_pipeline/core.py +341 -0
- maque/mllm_data_processor_pipeline/example.py +291 -0
- maque/mllm_data_processor_pipeline/steps/__init__.py +56 -0
- maque/mllm_data_processor_pipeline/steps/data_alignment.py +267 -0
- maque/mllm_data_processor_pipeline/steps/data_loader.py +172 -0
- maque/mllm_data_processor_pipeline/steps/data_validation.py +304 -0
- maque/mllm_data_processor_pipeline/steps/format_conversion.py +411 -0
- maque/mllm_data_processor_pipeline/steps/mllm_annotation.py +331 -0
- maque/mllm_data_processor_pipeline/steps/mllm_refinement.py +446 -0
- maque/mllm_data_processor_pipeline/steps/result_validation.py +501 -0
- maque/mllm_data_processor_pipeline/web_app.py +317 -0
- maque/nlp/__init__.py +14 -0
- maque/nlp/ngram.py +9 -0
- maque/nlp/parser.py +63 -0
- maque/nlp/risk_matcher.py +543 -0
- maque/nlp/sentence_splitter.py +202 -0
- maque/nlp/simple_tradition_cvt.py +31 -0
- maque/performance/__init__.py +21 -0
- maque/performance/_measure_time.py +70 -0
- maque/performance/_profiler.py +367 -0
- maque/performance/_stat_memory.py +51 -0
- maque/pipelines/__init__.py +15 -0
- maque/pipelines/clustering.py +252 -0
- maque/quantization/__init__.py +42 -0
- maque/quantization/auto_round.py +120 -0
- maque/quantization/base.py +145 -0
- maque/quantization/bitsandbytes.py +127 -0
- maque/quantization/llm_compressor.py +102 -0
- maque/retriever/__init__.py +35 -0
- maque/retriever/chroma.py +654 -0
- maque/retriever/document.py +140 -0
- maque/retriever/milvus.py +1140 -0
- maque/table_ops/__init__.py +1 -0
- maque/table_ops/core.py +133 -0
- maque/table_viewer/__init__.py +4 -0
- maque/table_viewer/download_assets.py +57 -0
- maque/table_viewer/server.py +698 -0
- maque/table_viewer/static/element-plus-icons.js +5791 -0
- maque/table_viewer/static/element-plus.css +1 -0
- maque/table_viewer/static/element-plus.js +65236 -0
- maque/table_viewer/static/main.css +268 -0
- maque/table_viewer/static/main.js +669 -0
- maque/table_viewer/static/vue.global.js +18227 -0
- maque/table_viewer/templates/index.html +401 -0
- maque/utils/__init__.py +56 -0
- maque/utils/color.py +68 -0
- maque/utils/color_string.py +45 -0
- maque/utils/compress.py +66 -0
- maque/utils/constant.py +183 -0
- maque/utils/core.py +261 -0
- maque/utils/cursor.py +143 -0
- maque/utils/distance.py +58 -0
- maque/utils/docker.py +96 -0
- maque/utils/downloads.py +51 -0
- maque/utils/excel_helper.py +542 -0
- maque/utils/helper_metrics.py +121 -0
- maque/utils/helper_parser.py +168 -0
- maque/utils/net.py +64 -0
- maque/utils/nvidia_stat.py +140 -0
- maque/utils/ops.py +53 -0
- maque/utils/packages.py +31 -0
- maque/utils/path.py +57 -0
- maque/utils/tar.py +260 -0
- maque/utils/untar.py +129 -0
- maque/web/__init__.py +0 -0
- maque/web/image_downloader.py +1410 -0
- maque-0.2.1.dist-info/METADATA +450 -0
- maque-0.2.1.dist-info/RECORD +143 -0
- maque-0.2.1.dist-info/WHEEL +4 -0
- maque-0.2.1.dist-info/entry_points.txt +3 -0
- maque-0.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
聚类可视化工具
|
|
4
|
+
|
|
5
|
+
支持多种降维算法:UMAP、t-SNE、PCA
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Literal, Optional, Tuple, Union
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
# 设置 matplotlib 后端(服务器环境兼容)
|
|
15
|
+
import matplotlib
|
|
16
|
+
matplotlib.use('Agg')
|
|
17
|
+
import matplotlib.pyplot as plt
|
|
18
|
+
|
|
19
|
+
from sklearn.manifold import TSNE
|
|
20
|
+
from sklearn.decomposition import PCA
|
|
21
|
+
|
|
22
|
+
DimReductionMethod = Literal["umap", "tsne", "pca"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ClusterVisualizer:
|
|
26
|
+
"""
|
|
27
|
+
聚类结果可视化器
|
|
28
|
+
|
|
29
|
+
支持多种降维算法:
|
|
30
|
+
- UMAP: 速度快,保持全局结构(推荐)
|
|
31
|
+
- t-SNE: 保持局部结构,适合小数据集
|
|
32
|
+
- PCA: 最快,线性降维
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
>>> visualizer = ClusterVisualizer(method="umap")
|
|
36
|
+
>>> visualizer.plot(embeddings, labels, "clusters.png")
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
method: DimReductionMethod = "umap",
|
|
42
|
+
figsize: Tuple[int, int] = (12, 8),
|
|
43
|
+
dpi: int = 150,
|
|
44
|
+
random_state: int = 42,
|
|
45
|
+
# UMAP 参数
|
|
46
|
+
n_neighbors: int = 15,
|
|
47
|
+
min_dist: float = 0.1,
|
|
48
|
+
# t-SNE 参数
|
|
49
|
+
perplexity: int = 30,
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Args:
|
|
53
|
+
method: 降维方法 ("umap", "tsne", "pca")
|
|
54
|
+
figsize: 图片大小
|
|
55
|
+
dpi: 图片分辨率
|
|
56
|
+
random_state: 随机种子
|
|
57
|
+
n_neighbors: UMAP 邻居数
|
|
58
|
+
min_dist: UMAP 最小距离
|
|
59
|
+
perplexity: t-SNE perplexity 参数
|
|
60
|
+
"""
|
|
61
|
+
self.method = method
|
|
62
|
+
self.figsize = figsize
|
|
63
|
+
self.dpi = dpi
|
|
64
|
+
self.random_state = random_state
|
|
65
|
+
self.n_neighbors = n_neighbors
|
|
66
|
+
self.min_dist = min_dist
|
|
67
|
+
self.perplexity = perplexity
|
|
68
|
+
|
|
69
|
+
def reduce_dimensions(self, embeddings: np.ndarray) -> np.ndarray:
|
|
70
|
+
"""
|
|
71
|
+
将高维向量降维到 2D
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
embeddings: 向量矩阵 (n_samples, n_features)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
2D 坐标矩阵 (n_samples, 2)
|
|
78
|
+
"""
|
|
79
|
+
if self.method == "umap":
|
|
80
|
+
return self._reduce_umap(embeddings)
|
|
81
|
+
|
|
82
|
+
elif self.method == "tsne":
|
|
83
|
+
return self._reduce_tsne(embeddings)
|
|
84
|
+
|
|
85
|
+
elif self.method == "pca":
|
|
86
|
+
return self._reduce_pca(embeddings)
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
raise ValueError(f"未知降维方法: {self.method}")
|
|
90
|
+
|
|
91
|
+
def _reduce_umap(self, embeddings: np.ndarray) -> np.ndarray:
|
|
92
|
+
"""UMAP 降维"""
|
|
93
|
+
try:
|
|
94
|
+
import umap
|
|
95
|
+
except ImportError:
|
|
96
|
+
raise ImportError("UMAP 需要: pip install umap-learn")
|
|
97
|
+
|
|
98
|
+
logger.info(f"使用 UMAP 降维 (n_neighbors={self.n_neighbors}, min_dist={self.min_dist})...")
|
|
99
|
+
reducer = umap.UMAP(
|
|
100
|
+
n_components=2,
|
|
101
|
+
n_neighbors=min(self.n_neighbors, len(embeddings) - 1),
|
|
102
|
+
min_dist=self.min_dist,
|
|
103
|
+
random_state=self.random_state,
|
|
104
|
+
metric='cosine',
|
|
105
|
+
)
|
|
106
|
+
return reducer.fit_transform(embeddings)
|
|
107
|
+
|
|
108
|
+
def _reduce_tsne(self, embeddings: np.ndarray) -> np.ndarray:
|
|
109
|
+
"""t-SNE 降维"""
|
|
110
|
+
logger.info(f"使用 t-SNE 降维 (perplexity={self.perplexity})...")
|
|
111
|
+
tsne = TSNE(
|
|
112
|
+
n_components=2,
|
|
113
|
+
random_state=self.random_state,
|
|
114
|
+
perplexity=min(self.perplexity, len(embeddings) - 1),
|
|
115
|
+
)
|
|
116
|
+
return tsne.fit_transform(embeddings)
|
|
117
|
+
|
|
118
|
+
def _reduce_pca(self, embeddings: np.ndarray) -> np.ndarray:
|
|
119
|
+
"""PCA 降维"""
|
|
120
|
+
logger.info("使用 PCA 降维...")
|
|
121
|
+
pca = PCA(n_components=2, random_state=self.random_state)
|
|
122
|
+
return pca.fit_transform(embeddings)
|
|
123
|
+
|
|
124
|
+
def plot(
|
|
125
|
+
self,
|
|
126
|
+
embeddings: np.ndarray,
|
|
127
|
+
labels: np.ndarray,
|
|
128
|
+
output_path: Union[str, Path],
|
|
129
|
+
title: Optional[str] = None,
|
|
130
|
+
embeddings_2d: Optional[np.ndarray] = None,
|
|
131
|
+
) -> bool:
|
|
132
|
+
"""
|
|
133
|
+
绘制聚类可视化图
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
embeddings: 向量矩阵(如果提供 embeddings_2d 则忽略)
|
|
137
|
+
labels: 聚类标签
|
|
138
|
+
output_path: 输出文件路径
|
|
139
|
+
title: 图片标题
|
|
140
|
+
embeddings_2d: 预计算的 2D 坐标(可选)
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
bool: 是否成功生成图片
|
|
144
|
+
"""
|
|
145
|
+
output_path = Path(output_path)
|
|
146
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
|
|
148
|
+
# 降维
|
|
149
|
+
if embeddings_2d is None:
|
|
150
|
+
embeddings_2d = self.reduce_dimensions(embeddings)
|
|
151
|
+
|
|
152
|
+
# 绘图
|
|
153
|
+
logger.info("生成可视化图...")
|
|
154
|
+
plt.figure(figsize=self.figsize)
|
|
155
|
+
|
|
156
|
+
unique_labels = sorted(set(labels))
|
|
157
|
+
n_clusters = len(unique_labels) - (1 if -1 in unique_labels else 0)
|
|
158
|
+
|
|
159
|
+
# 根据簇数量选择颜色方案
|
|
160
|
+
if n_clusters <= 20:
|
|
161
|
+
colors = plt.cm.tab20(np.linspace(0, 1, 20))
|
|
162
|
+
else:
|
|
163
|
+
# 簇数 > 20 时使用连续色图
|
|
164
|
+
colors = plt.cm.gist_ncar(np.linspace(0.05, 0.95, n_clusters))
|
|
165
|
+
|
|
166
|
+
color_idx = 0
|
|
167
|
+
for label in unique_labels:
|
|
168
|
+
mask = labels == label
|
|
169
|
+
if label == -1:
|
|
170
|
+
plt.scatter(
|
|
171
|
+
embeddings_2d[mask, 0],
|
|
172
|
+
embeddings_2d[mask, 1],
|
|
173
|
+
c='gray',
|
|
174
|
+
alpha=0.3,
|
|
175
|
+
s=10,
|
|
176
|
+
label='Noise'
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
if n_clusters <= 20:
|
|
180
|
+
color = colors[label % 20]
|
|
181
|
+
else:
|
|
182
|
+
color = colors[color_idx]
|
|
183
|
+
color_idx += 1
|
|
184
|
+
plt.scatter(
|
|
185
|
+
embeddings_2d[mask, 0],
|
|
186
|
+
embeddings_2d[mask, 1],
|
|
187
|
+
c=[color],
|
|
188
|
+
alpha=0.6,
|
|
189
|
+
s=15,
|
|
190
|
+
label=f'Cluster {label}'
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if n_clusters <= 10:
|
|
194
|
+
plt.legend(loc='best', fontsize=8)
|
|
195
|
+
|
|
196
|
+
method_name = self.method.upper()
|
|
197
|
+
plt.title(title or f'Cluster Visualization ({method_name}, n={n_clusters})')
|
|
198
|
+
plt.xlabel(f'{method_name} dim 1')
|
|
199
|
+
plt.ylabel(f'{method_name} dim 2')
|
|
200
|
+
plt.tight_layout()
|
|
201
|
+
plt.savefig(output_path, dpi=self.dpi)
|
|
202
|
+
plt.close()
|
|
203
|
+
|
|
204
|
+
logger.info(f"可视化图片保存到: {output_path}")
|
|
205
|
+
return True
|
maque/constant.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""常用数学和物理常量的定义模块。"""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
# Math
|
|
6
|
+
PI = math.pi
|
|
7
|
+
TAU = 2 * PI
|
|
8
|
+
|
|
9
|
+
# Physics
|
|
10
|
+
G = 9.8
|
|
11
|
+
|
|
12
|
+
e = "2.71828182845904523536028747135266249775724709369995957496696762772407663035354759457138217852516642742746639193200305992181741359662904357290033429526059563073813232862794349076323382988075319525101901157383418793070215408914993488416750924476146066808226480016847741185374234544243710753907774499206955170276183860626133138458300075204493382656029760673711320070932870912744374704723069697720931014169283681902551510865746377211125238978442505695369677078544996996794686445490598793163688923009879312773617821542499922957635148220826989519366803318252886939849646510582093923982948879332036250944311730123819706841614039701983767932068328237646480429531180232878250981945581530175671736133206981125099618188159304169035159888851934580727386673858942287922849989208680582574927961048419844436346324496848756023362482704197862320900216099023530436994184914631409343173814364054625315209618369088870701676839642437814059271456354906130310720851038375051011574770417189861068739696552126715468895703503"
|
|
13
|
+
pi = "3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989"
|
maque/core.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
from functools import wraps
|
|
3
|
+
import pickle
|
|
4
|
+
import numpy as np
|
|
5
|
+
from deprecated import deprecated
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def clamp(x, x_min, x_max):
|
|
9
|
+
"""Clamp a number to same range.
|
|
10
|
+
It's equivalent to np.clip()
|
|
11
|
+
Examples:
|
|
12
|
+
>>> clamp(-1, 0, 1)
|
|
13
|
+
>>> 0
|
|
14
|
+
>>> clamp([-1, 2, 3], [0, 0, 0], [1, 1, 1])
|
|
15
|
+
>>> [0, 1, 1]
|
|
16
|
+
"""
|
|
17
|
+
return np.maximum(x_min, np.minimum(x_max, x))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def broadcast(func): # It can be replaced by `np.vectorize`
|
|
21
|
+
"""Only for a functions with a single argument
|
|
22
|
+
example:
|
|
23
|
+
@broadcast
|
|
24
|
+
def f(x):
|
|
25
|
+
# A function that can map only a single element
|
|
26
|
+
if x==1 or x==0:
|
|
27
|
+
return x
|
|
28
|
+
else:
|
|
29
|
+
return f(x-1)+f(x-2)
|
|
30
|
+
|
|
31
|
+
>> f([2,4,10])
|
|
32
|
+
>> (1, 3, 832040)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@wraps(func)
|
|
36
|
+
def wrapper(*args, **kwargs):
|
|
37
|
+
value_list = []
|
|
38
|
+
for arg in args:
|
|
39
|
+
value_list.append(func(arg, **kwargs))
|
|
40
|
+
return tuple(value_list)
|
|
41
|
+
|
|
42
|
+
return wrapper
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _Dict_enhance(dict):
|
|
46
|
+
"""Enables the dictionary to be dot operated"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, *args, **kwargs):
|
|
49
|
+
dict.__init__(self, *args, **kwargs)
|
|
50
|
+
self.__dict__ = self
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def dict_dotable(dic):
|
|
54
|
+
"""
|
|
55
|
+
: input: a dictionary
|
|
56
|
+
: output: an enhanced dictionary
|
|
57
|
+
Example:
|
|
58
|
+
enhance_dic = dict_dotable(dic)
|
|
59
|
+
then, you can operate an enhanced dictionary like this:
|
|
60
|
+
enhance_dic.key1.key2. ...
|
|
61
|
+
"""
|
|
62
|
+
dic = _Dict_enhance(dic)
|
|
63
|
+
for i in dic:
|
|
64
|
+
if type(dic[i]) == dict:
|
|
65
|
+
dic[i] = dict_dotable(dic[i])
|
|
66
|
+
return dic
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Constant:
|
|
70
|
+
"""
|
|
71
|
+
define a constant like C language.
|
|
72
|
+
`object.__setattr__(self, name, value)`
|
|
73
|
+
this built-in function will called when assigning values to properties of the class
|
|
74
|
+
|
|
75
|
+
`object.__dict__` holds all writable attributes in object,
|
|
76
|
+
key as variable name and value as variable value.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __setattr__(self, name, value):
|
|
80
|
+
if hasattr(self, name):
|
|
81
|
+
raise ValueError("Constant value can't be changed")
|
|
82
|
+
else:
|
|
83
|
+
self.__dict__[name] = value
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def number_digits(number):
|
|
87
|
+
res = number
|
|
88
|
+
digit = 1
|
|
89
|
+
if res >= 1:
|
|
90
|
+
while res > 10:
|
|
91
|
+
digit += 1
|
|
92
|
+
# res, mod = np.divmod(res, 10)
|
|
93
|
+
res //= 10
|
|
94
|
+
else:
|
|
95
|
+
while res < 1:
|
|
96
|
+
digit -= 1
|
|
97
|
+
res *= 10
|
|
98
|
+
return digit
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def num_digits(number_like):
|
|
102
|
+
number_str = str(int(str(number_like)))
|
|
103
|
+
return len(number_str)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def sort_count(_list: list):
|
|
107
|
+
"""
|
|
108
|
+
返回lis的由大到小排序好的元素列表
|
|
109
|
+
Example:
|
|
110
|
+
l = np.array([2,2,2,2,5,5,3,9,9])
|
|
111
|
+
sort_count(l) : [(2, 4), (5, 2), (9, 2), (3, 1)]
|
|
112
|
+
# return [2, 5, 9,3], [4, 2, 2, 1]
|
|
113
|
+
"""
|
|
114
|
+
a = Counter(_list)
|
|
115
|
+
b = sorted(a.items(), key=lambda item: item[1], reverse=True)
|
|
116
|
+
# idx, counts = [b[i][0] for i in range(len(b))], [b[i][1] for i in range(len(b))]
|
|
117
|
+
return b
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def reduce_list_element(array, *elems):
|
|
121
|
+
"""
|
|
122
|
+
example:
|
|
123
|
+
a = [ 5, 6, 6, 7, 8, 9, 9]
|
|
124
|
+
reduce_list_element(a, 6, 9)
|
|
125
|
+
print(a)
|
|
126
|
+
>> [ 5, 7, 8]
|
|
127
|
+
"""
|
|
128
|
+
length = len(array)
|
|
129
|
+
for idx in range(length):
|
|
130
|
+
index = length - idx - 1
|
|
131
|
+
for elem in elems:
|
|
132
|
+
if array[index] == elem:
|
|
133
|
+
array.pop(index)
|
maque/cv/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .utils import *
|
maque/cv/image.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
from PIL import Image
|
|
4
|
+
import base64
|
|
5
|
+
import io
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import List, Union, Optional, Literal
|
|
8
|
+
|
|
9
|
+
ReturnFormat = Literal['pil', 'numpy', 'base64']
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class FrameExtractionResult:
|
|
13
|
+
width: int
|
|
14
|
+
height: int
|
|
15
|
+
total_frames: int
|
|
16
|
+
original_fps: float
|
|
17
|
+
extracted_frames: List[Union[Image.Image, np.ndarray, str]] # str for base64
|
|
18
|
+
start_time: float
|
|
19
|
+
end_time: float
|
|
20
|
+
extraction_fps: Optional[float] = None
|
|
21
|
+
format: ReturnFormat = 'pil'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class VideoFrameExtractor:
|
|
25
|
+
def __init__(self, video_path: str):
|
|
26
|
+
"""初始化视频帧提取器
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
video_path (str): 视频文件路径
|
|
30
|
+
"""
|
|
31
|
+
self.video_path = video_path
|
|
32
|
+
self._video = None
|
|
33
|
+
self._init_video_capture()
|
|
34
|
+
|
|
35
|
+
def _init_video_capture(self):
|
|
36
|
+
"""初始化视频捕获对象"""
|
|
37
|
+
self._video = cv2.VideoCapture(self.video_path)
|
|
38
|
+
if not self._video.isOpened():
|
|
39
|
+
raise ValueError(f"无法打开视频文件: {self.video_path}")
|
|
40
|
+
|
|
41
|
+
self.video_fps = self._video.get(cv2.CAP_PROP_FPS)
|
|
42
|
+
self.total_frames = int(self._video.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
43
|
+
self.width = int(self._video.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
44
|
+
self.height = int(self._video.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
45
|
+
|
|
46
|
+
def extract_frames(
|
|
47
|
+
self,
|
|
48
|
+
start_time: float,
|
|
49
|
+
end_time: float = -1,
|
|
50
|
+
fps: Optional[float] = None,
|
|
51
|
+
n_frames: Optional[int] = None,
|
|
52
|
+
return_format: ReturnFormat = 'pil',
|
|
53
|
+
max_width: Optional[int] = None,
|
|
54
|
+
max_height: Optional[int] = None
|
|
55
|
+
) -> FrameExtractionResult:
|
|
56
|
+
"""提取视频帧
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
start_time (float): 开始时间(秒)
|
|
60
|
+
end_time (float): 结束时间(秒),-1表示到视频结束
|
|
61
|
+
fps (float, optional): 目标帧率
|
|
62
|
+
n_frames (int, optional): 需要提取的帧数
|
|
63
|
+
return_format (str): 返回格式,支持 'pil'、'numpy' 或 'base64'
|
|
64
|
+
max_width (int, optional): 最大宽度,保持宽高比缩放
|
|
65
|
+
max_height (int, optional): 最大高度,保持宽高比缩放
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
FrameExtractionResult: 包含提取结果的数据类
|
|
69
|
+
"""
|
|
70
|
+
# 计算开始和结束帧
|
|
71
|
+
start_frame = int(start_time * self.video_fps)
|
|
72
|
+
if end_time == -1:
|
|
73
|
+
end_frame = self.total_frames
|
|
74
|
+
end_time = self.total_frames / self.video_fps
|
|
75
|
+
else:
|
|
76
|
+
end_frame = int(end_time * self.video_fps)
|
|
77
|
+
|
|
78
|
+
# 计算需要提取的帧数
|
|
79
|
+
if fps:
|
|
80
|
+
n_frames = int((end_time - start_time) * fps)
|
|
81
|
+
else:
|
|
82
|
+
fps = self.video_fps
|
|
83
|
+
if n_frames is None:
|
|
84
|
+
raise ValueError("fps和n_frames不能同时为None")
|
|
85
|
+
|
|
86
|
+
if n_frames <= 0:
|
|
87
|
+
raise ValueError("n_frames必须大于0")
|
|
88
|
+
|
|
89
|
+
# 计算采样步长
|
|
90
|
+
step = (end_frame - start_frame) / n_frames
|
|
91
|
+
|
|
92
|
+
# 计算目标尺寸
|
|
93
|
+
target_width = self.width
|
|
94
|
+
target_height = self.height
|
|
95
|
+
if max_width or max_height:
|
|
96
|
+
scale_w = max_width / self.width if max_width else float('inf')
|
|
97
|
+
scale_h = max_height / self.height if max_height else float('inf')
|
|
98
|
+
scale = min(scale_w, scale_h, 1.0) # 确保不会放大
|
|
99
|
+
target_width = int(self.width * scale)
|
|
100
|
+
target_height = int(self.height * scale)
|
|
101
|
+
|
|
102
|
+
# 提取帧
|
|
103
|
+
extracted_frames = []
|
|
104
|
+
for i in np.arange(start_frame, end_frame, step):
|
|
105
|
+
self._video.set(cv2.CAP_PROP_POS_FRAMES, int(i))
|
|
106
|
+
ret, frame = self._video.read()
|
|
107
|
+
if ret:
|
|
108
|
+
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
109
|
+
|
|
110
|
+
# 如果需要调整大小
|
|
111
|
+
if target_width != self.width or target_height != self.height:
|
|
112
|
+
rgb_frame = cv2.resize(rgb_frame, (target_width, target_height),
|
|
113
|
+
interpolation=cv2.INTER_AREA)
|
|
114
|
+
|
|
115
|
+
if return_format == 'pil':
|
|
116
|
+
frame_data = Image.fromarray(rgb_frame)
|
|
117
|
+
elif return_format == 'numpy':
|
|
118
|
+
frame_data = rgb_frame
|
|
119
|
+
elif return_format == 'base64': # base64
|
|
120
|
+
pil_image = Image.fromarray(rgb_frame)
|
|
121
|
+
buffer = io.BytesIO()
|
|
122
|
+
pil_image.save(buffer, format='PNG')
|
|
123
|
+
frame_data = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
|
124
|
+
else:
|
|
125
|
+
raise TypeError(f"不支持的返回格式: {return_format}")
|
|
126
|
+
extracted_frames.append(frame_data)
|
|
127
|
+
else:
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
return FrameExtractionResult(
|
|
131
|
+
width=target_width,
|
|
132
|
+
height=target_height,
|
|
133
|
+
total_frames=self.total_frames,
|
|
134
|
+
original_fps=self.video_fps,
|
|
135
|
+
extracted_frames=extracted_frames,
|
|
136
|
+
start_time=start_time,
|
|
137
|
+
end_time=end_time,
|
|
138
|
+
extraction_fps=fps,
|
|
139
|
+
format=return_format
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def frames_to_video(
|
|
143
|
+
self,
|
|
144
|
+
frames_result: FrameExtractionResult,
|
|
145
|
+
output_path: str,
|
|
146
|
+
fps: Optional[float] = None,
|
|
147
|
+
codec: str = 'mp4v'
|
|
148
|
+
) -> None:
|
|
149
|
+
"""将提取的帧重新组装为视频
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
frames_result (FrameExtractionResult): 帧提取结果
|
|
153
|
+
output_path (str): 输出视频的路径
|
|
154
|
+
fps (float, optional): 输出视频的帧率,默认使用提取时的帧率
|
|
155
|
+
codec (str, optional): 视频编码器,默认为'mp4v'
|
|
156
|
+
"""
|
|
157
|
+
if not frames_result.extracted_frames:
|
|
158
|
+
raise ValueError("没有可用的帧进行视频合成")
|
|
159
|
+
|
|
160
|
+
# 使用提取时的帧率或指定的帧率
|
|
161
|
+
output_fps = fps if fps is not None else (
|
|
162
|
+
frames_result.extraction_fps if frames_result.extraction_fps
|
|
163
|
+
else frames_result.original_fps
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# 创建VideoWriter对象
|
|
167
|
+
fourcc = cv2.VideoWriter_fourcc(*codec)
|
|
168
|
+
out = cv2.VideoWriter(
|
|
169
|
+
output_path,
|
|
170
|
+
fourcc,
|
|
171
|
+
output_fps,
|
|
172
|
+
(frames_result.width, frames_result.height)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
for frame in frames_result.extracted_frames:
|
|
177
|
+
# 根据不同的格式转换为OpenCV可用的格式
|
|
178
|
+
if frames_result.format == 'pil':
|
|
179
|
+
cv_frame = cv2.cvtColor(np.array(frame), cv2.COLOR_RGB2BGR)
|
|
180
|
+
elif frames_result.format == 'numpy':
|
|
181
|
+
cv_frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
|
|
182
|
+
elif frames_result.format == 'base64':
|
|
183
|
+
# 解码base64字符串
|
|
184
|
+
img_data = base64.b64decode(frame)
|
|
185
|
+
nparr = np.frombuffer(img_data, np.uint8)
|
|
186
|
+
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
|
187
|
+
cv_frame = img
|
|
188
|
+
else:
|
|
189
|
+
raise ValueError(f"不支持的帧格式: {frames_result.format}")
|
|
190
|
+
|
|
191
|
+
out.write(cv_frame)
|
|
192
|
+
finally:
|
|
193
|
+
out.release()
|
|
194
|
+
|
|
195
|
+
def __del__(self):
|
|
196
|
+
"""析构函数,确保视频资源被释放"""
|
|
197
|
+
if self._video is not None:
|
|
198
|
+
self._video.release()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
extractor = VideoFrameExtractor('input.mp4')
|
|
203
|
+
|
|
204
|
+
# 限制最大宽度为 1280,高度会按比例缩放
|
|
205
|
+
frames = extractor.extract_frames(
|
|
206
|
+
start_time=0,
|
|
207
|
+
end_time=10,
|
|
208
|
+
fps=30,
|
|
209
|
+
max_width=1280
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# 同时限制最大宽度和高度,会按照最小缩放比例进行缩放
|
|
213
|
+
frames = extractor.extract_frames(
|
|
214
|
+
start_time=0,
|
|
215
|
+
end_time=10,
|
|
216
|
+
fps=30,
|
|
217
|
+
max_width=1280,
|
|
218
|
+
max_height=720
|
|
219
|
+
)
|
maque/cv/utils.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
import numpy as np
|
|
5
|
+
import cv2
|
|
6
|
+
except ImportError:
|
|
7
|
+
# print("cv2 is not installed, please install it using 'pip install opencv-python'")
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def put_chinese_text(img, text, pos, font_size, color, font_path=None):
|
|
12
|
+
"""插入中文文本"""
|
|
13
|
+
import freetype
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
if font_path is None:
|
|
17
|
+
font_path = "/System/Library/Fonts/PingFang.ttc"
|
|
18
|
+
face = freetype.Face(font_path)
|
|
19
|
+
face.set_char_size(font_size * 64)
|
|
20
|
+
|
|
21
|
+
for i, char in enumerate(text):
|
|
22
|
+
face.load_char(char)
|
|
23
|
+
bitmap = face.glyph.bitmap
|
|
24
|
+
h, w = bitmap.rows, bitmap.width
|
|
25
|
+
x, y = pos[0] + i * w, pos[1]
|
|
26
|
+
|
|
27
|
+
img_char = np.array(bitmap.buffer, dtype=np.uint8).reshape(h, w)
|
|
28
|
+
for c in range(3):
|
|
29
|
+
img[y : y + h, x : x + w, c] = color[c] * (img_char / 255.0) + img[y : y + h, x : x + w, c] * (
|
|
30
|
+
1 - img_char / 255.0
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def base64_to_numpy(image_base64):
|
|
34
|
+
image_bytes = base64.b64decode(image_base64)
|
|
35
|
+
image_np = np.frombuffer(image_bytes, dtype=np.uint8)
|
|
36
|
+
image_np2 = cv2.imdecode(image_np, cv2.IMREAD_COLOR)
|
|
37
|
+
# image_np2 = cv2.cvtColor(image_np2, cv2.COLOR_BGR2RGB)
|
|
38
|
+
image_np2 = image_np2 / 255
|
|
39
|
+
return image_np2.astype(np.float32)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def numpy_to_base64(image, unnormalize: bool):
|
|
43
|
+
if unnormalize:
|
|
44
|
+
image = image / 2 + 0.5 # unnormalize
|
|
45
|
+
if image.dtype in (np.float32, np.double) and image.max() <= 1:
|
|
46
|
+
image = (image * 255).astype('uint8')
|
|
47
|
+
retval, buffer = cv2.imencode('.jpg', image)
|
|
48
|
+
pic_str = base64.b64encode(buffer)
|
|
49
|
+
return pic_str.decode()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def numpy_to_tensor(image_np, normalize=True):
|
|
53
|
+
if normalize:
|
|
54
|
+
image_np = (image_np - 0.5) * 2
|
|
55
|
+
return np.transpose(image_np, (2, 0, 1))[None, ...]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def tensor_to_base64(image_tensor, unnormalize=True):
|
|
59
|
+
import torch
|
|
60
|
+
if image_tensor.dtype == torch.float:
|
|
61
|
+
image_tensor = np.array(image_tensor.cpu())
|
|
62
|
+
b64_list = [numpy_to_base64(np.transpose(image, (1, 2, 0)), unnormalize) for image in image_tensor]
|
|
63
|
+
return b64_list
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def base64_list_to_tensor(b64_list, normalize=True):
|
|
67
|
+
tensor_list = [numpy_to_tensor(base64_to_numpy(b64), normalize) for b64 in b64_list]
|
|
68
|
+
return np.concatenate(tensor_list, 0)
|