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.
Files changed (143) hide show
  1. maque/__init__.py +30 -0
  2. maque/__main__.py +926 -0
  3. maque/ai_platform/__init__.py +0 -0
  4. maque/ai_platform/crawl.py +45 -0
  5. maque/ai_platform/metrics.py +258 -0
  6. maque/ai_platform/nlp_preprocess.py +67 -0
  7. maque/ai_platform/webpage_screen_shot.py +195 -0
  8. maque/algorithms/__init__.py +78 -0
  9. maque/algorithms/bezier.py +15 -0
  10. maque/algorithms/bktree.py +117 -0
  11. maque/algorithms/core.py +104 -0
  12. maque/algorithms/hilbert.py +16 -0
  13. maque/algorithms/rate_function.py +92 -0
  14. maque/algorithms/transform.py +27 -0
  15. maque/algorithms/trie.py +272 -0
  16. maque/algorithms/utils.py +63 -0
  17. maque/algorithms/video.py +587 -0
  18. maque/api/__init__.py +1 -0
  19. maque/api/common.py +110 -0
  20. maque/api/fetch.py +26 -0
  21. maque/api/static/icon.png +0 -0
  22. maque/api/static/redoc.standalone.js +1782 -0
  23. maque/api/static/swagger-ui-bundle.js +3 -0
  24. maque/api/static/swagger-ui.css +3 -0
  25. maque/cli/__init__.py +1 -0
  26. maque/cli/clean_invisible_chars.py +324 -0
  27. maque/cli/core.py +34 -0
  28. maque/cli/groups/__init__.py +26 -0
  29. maque/cli/groups/config.py +205 -0
  30. maque/cli/groups/data.py +615 -0
  31. maque/cli/groups/doctor.py +259 -0
  32. maque/cli/groups/embedding.py +222 -0
  33. maque/cli/groups/git.py +29 -0
  34. maque/cli/groups/help.py +410 -0
  35. maque/cli/groups/llm.py +223 -0
  36. maque/cli/groups/mcp.py +241 -0
  37. maque/cli/groups/mllm.py +1795 -0
  38. maque/cli/groups/mllm_simple.py +60 -0
  39. maque/cli/groups/quant.py +210 -0
  40. maque/cli/groups/service.py +490 -0
  41. maque/cli/groups/system.py +570 -0
  42. maque/cli/mllm_run.py +1451 -0
  43. maque/cli/script.py +52 -0
  44. maque/cli/tree.py +49 -0
  45. maque/clustering/__init__.py +52 -0
  46. maque/clustering/analyzer.py +347 -0
  47. maque/clustering/clusterers.py +464 -0
  48. maque/clustering/sampler.py +134 -0
  49. maque/clustering/visualizer.py +205 -0
  50. maque/constant.py +13 -0
  51. maque/core.py +133 -0
  52. maque/cv/__init__.py +1 -0
  53. maque/cv/image.py +219 -0
  54. maque/cv/utils.py +68 -0
  55. maque/cv/video/__init__.py +3 -0
  56. maque/cv/video/keyframe_extractor.py +368 -0
  57. maque/embedding/__init__.py +43 -0
  58. maque/embedding/base.py +56 -0
  59. maque/embedding/multimodal.py +308 -0
  60. maque/embedding/server.py +523 -0
  61. maque/embedding/text.py +311 -0
  62. maque/git/__init__.py +24 -0
  63. maque/git/pure_git.py +912 -0
  64. maque/io/__init__.py +29 -0
  65. maque/io/core.py +38 -0
  66. maque/io/ops.py +194 -0
  67. maque/llm/__init__.py +111 -0
  68. maque/llm/backend.py +416 -0
  69. maque/llm/base.py +411 -0
  70. maque/llm/server.py +366 -0
  71. maque/mcp_server.py +1096 -0
  72. maque/mllm_data_processor_pipeline/__init__.py +17 -0
  73. maque/mllm_data_processor_pipeline/core.py +341 -0
  74. maque/mllm_data_processor_pipeline/example.py +291 -0
  75. maque/mllm_data_processor_pipeline/steps/__init__.py +56 -0
  76. maque/mllm_data_processor_pipeline/steps/data_alignment.py +267 -0
  77. maque/mllm_data_processor_pipeline/steps/data_loader.py +172 -0
  78. maque/mllm_data_processor_pipeline/steps/data_validation.py +304 -0
  79. maque/mllm_data_processor_pipeline/steps/format_conversion.py +411 -0
  80. maque/mllm_data_processor_pipeline/steps/mllm_annotation.py +331 -0
  81. maque/mllm_data_processor_pipeline/steps/mllm_refinement.py +446 -0
  82. maque/mllm_data_processor_pipeline/steps/result_validation.py +501 -0
  83. maque/mllm_data_processor_pipeline/web_app.py +317 -0
  84. maque/nlp/__init__.py +14 -0
  85. maque/nlp/ngram.py +9 -0
  86. maque/nlp/parser.py +63 -0
  87. maque/nlp/risk_matcher.py +543 -0
  88. maque/nlp/sentence_splitter.py +202 -0
  89. maque/nlp/simple_tradition_cvt.py +31 -0
  90. maque/performance/__init__.py +21 -0
  91. maque/performance/_measure_time.py +70 -0
  92. maque/performance/_profiler.py +367 -0
  93. maque/performance/_stat_memory.py +51 -0
  94. maque/pipelines/__init__.py +15 -0
  95. maque/pipelines/clustering.py +252 -0
  96. maque/quantization/__init__.py +42 -0
  97. maque/quantization/auto_round.py +120 -0
  98. maque/quantization/base.py +145 -0
  99. maque/quantization/bitsandbytes.py +127 -0
  100. maque/quantization/llm_compressor.py +102 -0
  101. maque/retriever/__init__.py +35 -0
  102. maque/retriever/chroma.py +654 -0
  103. maque/retriever/document.py +140 -0
  104. maque/retriever/milvus.py +1140 -0
  105. maque/table_ops/__init__.py +1 -0
  106. maque/table_ops/core.py +133 -0
  107. maque/table_viewer/__init__.py +4 -0
  108. maque/table_viewer/download_assets.py +57 -0
  109. maque/table_viewer/server.py +698 -0
  110. maque/table_viewer/static/element-plus-icons.js +5791 -0
  111. maque/table_viewer/static/element-plus.css +1 -0
  112. maque/table_viewer/static/element-plus.js +65236 -0
  113. maque/table_viewer/static/main.css +268 -0
  114. maque/table_viewer/static/main.js +669 -0
  115. maque/table_viewer/static/vue.global.js +18227 -0
  116. maque/table_viewer/templates/index.html +401 -0
  117. maque/utils/__init__.py +56 -0
  118. maque/utils/color.py +68 -0
  119. maque/utils/color_string.py +45 -0
  120. maque/utils/compress.py +66 -0
  121. maque/utils/constant.py +183 -0
  122. maque/utils/core.py +261 -0
  123. maque/utils/cursor.py +143 -0
  124. maque/utils/distance.py +58 -0
  125. maque/utils/docker.py +96 -0
  126. maque/utils/downloads.py +51 -0
  127. maque/utils/excel_helper.py +542 -0
  128. maque/utils/helper_metrics.py +121 -0
  129. maque/utils/helper_parser.py +168 -0
  130. maque/utils/net.py +64 -0
  131. maque/utils/nvidia_stat.py +140 -0
  132. maque/utils/ops.py +53 -0
  133. maque/utils/packages.py +31 -0
  134. maque/utils/path.py +57 -0
  135. maque/utils/tar.py +260 -0
  136. maque/utils/untar.py +129 -0
  137. maque/web/__init__.py +0 -0
  138. maque/web/image_downloader.py +1410 -0
  139. maque-0.2.1.dist-info/METADATA +450 -0
  140. maque-0.2.1.dist-info/RECORD +143 -0
  141. maque-0.2.1.dist-info/WHEEL +4 -0
  142. maque-0.2.1.dist-info/entry_points.txt +3 -0
  143. 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)
@@ -0,0 +1,3 @@
1
+ from .keyframe_extractor import AdvancedKeyframeExtractor, Method
2
+
3
+ __all__ = ["AdvancedKeyframeExtractor", "Method"]