deepdata: core product code (backend, frontend, runtime, deepflow module)
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from deepdata.common.utils.custom_importer import CustomImporter
|
||||
|
||||
|
||||
def _configure_importer():
|
||||
base_path = Path(__file__).resolve().parent
|
||||
sys.meta_path.append(CustomImporter(base_path))
|
||||
|
||||
|
||||
_configure_importer()
|
||||
|
||||
|
||||
def _import_operators():
|
||||
from . import file_with_high_repeat_phrase_rate_filter
|
||||
from . import file_with_high_repeat_word_rate_filter
|
||||
from . import file_with_high_special_char_rate_filter
|
||||
from . import remove_file_with_many_sensitive_words
|
||||
from . import remove_file_with_short_or_long_length
|
||||
from . import remove_duplicate_file
|
||||
from . import img_blurred_images_cleaner
|
||||
from . import img_duplicated_images_cleaner
|
||||
from . import img_similar_images_cleaner
|
||||
from . import img_advertisement_images_cleaner
|
||||
|
||||
|
||||
_import_operators()
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from deepdata.core.base_op import OPERATORS
|
||||
|
||||
OPERATORS.register_module(module_name='FileWithHighRepeatPhraseRateFilter',
|
||||
module_path="ops.filter.file_with_high_repeat_phrase_rate_filter.process")
|
||||
@@ -0,0 +1,31 @@
|
||||
name: '文档词重复率检查'
|
||||
description: '去除重复词过多的文档。'
|
||||
language: 'Python'
|
||||
vendor: 'Huawei'
|
||||
raw_id: 'FileWithHighRepeatPhraseRateFilter'
|
||||
version: '1.0.0'
|
||||
types:
|
||||
- 'cleanse'
|
||||
modal: 'text'
|
||||
effect:
|
||||
before: '机器机器机器机器机器机器机器机器机器机器学习学习学习学习学习'
|
||||
after: ''
|
||||
inputs: 'text'
|
||||
outputs: 'text'
|
||||
settings:
|
||||
repeatPhraseRatio:
|
||||
name: 文档词重复率
|
||||
description: 某个词的统计数/文档总词数 > 设定值,该文档被去除。
|
||||
type: slider
|
||||
defaultVal: 0.5
|
||||
min: 0
|
||||
max: 1
|
||||
step: 0.1
|
||||
hitStopwords:
|
||||
name: 去除停用词
|
||||
description: 统计重复词时,选择是否要去除停用词。
|
||||
type: switch
|
||||
defaultVal: false
|
||||
required: true
|
||||
checkedLabel: 去除
|
||||
unCheckedLabel: 不去除
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/user/bin/python
|
||||
# -- encoding: utf-8 --
|
||||
|
||||
"""
|
||||
Description: 词重复率过高文档过滤插件
|
||||
Create: 2023/11/7 9:26
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
from loguru import logger
|
||||
|
||||
import jieba
|
||||
from deepdata.core.base_op import Filter
|
||||
|
||||
|
||||
class FileWithHighRepeatPhraseRateFilter(Filter):
|
||||
"""词重复率过高文档过滤插件"""
|
||||
PUNCTUATION_PATTERN = re.compile(r'^[\u3000-\u303F\uff00-\uffef\s\W_]+$')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FileWithHighRepeatPhraseRateFilter, self).__init__(*args, **kwargs)
|
||||
self._min_threshold = kwargs.get("repeatPhraseRatio", 0.5) # 重复词符占全文的比例阈值,默认值为0.5
|
||||
self._hit_stopword_trigger = kwargs.get("hitStopwords", False) # 计算重复词率时是否去除停用词,默认为False不去除,True为去除
|
||||
self._file_path = Path(__file__).parent / 'resources' / 'hit_stopwords.txt'
|
||||
self._hit_stopwords = []
|
||||
if self._hit_stopword_trigger:
|
||||
with open(self._file_path, 'r', encoding='utf-8') as f:
|
||||
self._hit_stopwords = f.read().splitlines()
|
||||
|
||||
def execute(self, sample: Dict[str, Any]) -> Dict[str, Any]:
|
||||
start = time.time()
|
||||
self.read_file_first(sample)
|
||||
sample[self.text_key] = self._file_with_high_repeat_phrase_rate_filter(sample[self.text_key],
|
||||
sample[self.filename_key])
|
||||
logger.info(f"fileName: {sample[self.filename_key]}, "
|
||||
f"method: FileWithHighRepeatPhraseRateFilter costs {(time.time() - start):6f} s")
|
||||
return sample
|
||||
|
||||
def _tokenize_by_jieba(self, text: str):
|
||||
"""基于jieba对输入文本进行切分
|
||||
|
||||
Args:
|
||||
text: 输入文档内容
|
||||
Returns:
|
||||
words_list: 切割后的词列表
|
||||
"""
|
||||
|
||||
for word in jieba.lcut(text):
|
||||
if not self.PUNCTUATION_PATTERN.match(word) and word not in self._hit_stopwords:
|
||||
yield word
|
||||
|
||||
def _file_with_high_repeat_phrase_rate_filter(self, input_data: str, file_name):
|
||||
if len(input_data) < 2: # 词语长度至少2个字符
|
||||
return input_data
|
||||
words_list = self._tokenize_by_jieba(input_data)
|
||||
words_count = dict(Counter(words_list))
|
||||
words_count_max, words_total_count = 0, 0
|
||||
for words in words_count:
|
||||
# 只统计中文、字母,且长度大于1的词语
|
||||
if len(words) > 1 and words.isalpha():
|
||||
words_count_max = max(words_count_max, words_count.get(words))
|
||||
words_total_count += words_count.get(words)
|
||||
output_data = input_data
|
||||
repeat_phrase_rate = words_count_max / words_total_count if words_total_count > 0 else 0
|
||||
if repeat_phrase_rate >= self._min_threshold:
|
||||
# 只要有一个词重复率高于阈值,就会过滤文档
|
||||
output_data = ""
|
||||
logger.info(f"The repeat phrase rate of the input data is {repeat_phrase_rate}. "
|
||||
f"Threshold is {self._min_threshold}. The document {file_name} is filtered.")
|
||||
return output_data
|
||||
+1039
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from deepdata.core.base_op import OPERATORS
|
||||
|
||||
OPERATORS.register_module(module_name='FileWithHighRepeatWordRateFilter',
|
||||
module_path="ops.filter.file_with_high_repeat_word_rate_filter.process")
|
||||
@@ -0,0 +1,25 @@
|
||||
name: '文档字重复率检查'
|
||||
name_en: 'Word Repetition Rate Check'
|
||||
description: '去除重复字过多的文档。'
|
||||
description_en: 'Filters out files that contain excessive repeated words.'
|
||||
language: 'python'
|
||||
vendor: 'huawei'
|
||||
raw_id: 'FileWithHighRepeatWordRateFilter'
|
||||
version: '1.0.0'
|
||||
types:
|
||||
- 'cleanse'
|
||||
modal: 'text'
|
||||
effect:
|
||||
before: '机器学学学学学学学学学学学学学学学学学学学学学学学学学学学学学学习'
|
||||
after: ''
|
||||
inputs: 'text'
|
||||
outputs: 'text'
|
||||
settings:
|
||||
repeatWordRatio:
|
||||
name: 文档字重复率
|
||||
description: 某个字的统计数/文档总字数 > 设定值,该文档被去除。
|
||||
type: slider
|
||||
defaultVal: 0.5
|
||||
min: 0
|
||||
max: 1
|
||||
step: 0.1
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/user/bin/python
|
||||
# -- encoding: utf-8 --
|
||||
|
||||
"""
|
||||
Description: 检查文档字重复率插件
|
||||
Create: 2023/11/7 9:26
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
|
||||
from collections import Counter
|
||||
from typing import Dict, Any
|
||||
from loguru import logger
|
||||
|
||||
from deepdata.core.base_op import Filter
|
||||
|
||||
|
||||
class FileWithHighRepeatWordRateFilter(Filter):
|
||||
"""检查文档字重复率插件"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FileWithHighRepeatWordRateFilter, self).__init__(*args, **kwargs)
|
||||
self._min_threshold = kwargs.get("repeatWordRatio", 0.5) # 重复字符占整行的比例阈值,默认值为0.5
|
||||
|
||||
@staticmethod
|
||||
def _extract_word(input_data):
|
||||
# 只统计中文字的重复率
|
||||
extracted_word = re.sub(r'[^\u4e00-\u9fff]', '', input_data)
|
||||
return extracted_word
|
||||
|
||||
def execute(self, sample: Dict[str, Any]) -> Dict[str, Any]:
|
||||
start = time.time()
|
||||
self.read_file_first(sample)
|
||||
sample[self.text_key] = self._file_with_high_repeat_word_rate_filter(sample[self.text_key],
|
||||
sample[self.filename_key])
|
||||
logger.info(f"fileName: {sample[self.filename_key]}, "
|
||||
f"method: FileWithHighRepeatWordRateFilter costs {(time.time() - start):6f} s")
|
||||
return sample
|
||||
|
||||
def _file_with_high_repeat_word_rate_filter(self, input_data: str, file_name):
|
||||
tmp = self._extract_word(input_data)
|
||||
if not tmp:
|
||||
return input_data
|
||||
output_data = input_data
|
||||
words_count = Counter(tmp)
|
||||
max_value = max(words_count.values())
|
||||
repeat_word_rate = max_value / len(tmp)
|
||||
if repeat_word_rate >= self._min_threshold:
|
||||
output_data = ""
|
||||
logger.info(f"The repeat word rate of the input data is {repeat_word_rate}. "
|
||||
f"Threshold is {self._min_threshold}. The document %s is filtered.")
|
||||
return output_data
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from deepdata.core.base_op import OPERATORS
|
||||
|
||||
OPERATORS.register_module(module_name='FileWithHighSpecialCharRateFilter',
|
||||
module_path="ops.filter.file_with_high_special_char_rate_filter.process")
|
||||
@@ -0,0 +1,25 @@
|
||||
name: '文档特殊字符率检查'
|
||||
name_en: 'Special Character Rate Check'
|
||||
description: '去除特殊字符过多的文档。'
|
||||
description_en: 'Filters out files that contain excessive special characters.'
|
||||
language: 'python'
|
||||
vendor: 'huawei'
|
||||
raw_id: 'FileWithHighSpecialCharRateFilter'
|
||||
version: '1.0.0'
|
||||
types:
|
||||
- 'cleanse'
|
||||
modal: 'text'
|
||||
effect:
|
||||
before: '你好!@!@#!¥!@#'
|
||||
after: ''
|
||||
inputs: 'text'
|
||||
outputs: 'text'
|
||||
settings:
|
||||
specialCharRatio:
|
||||
name: 文档特殊字符率
|
||||
description: 特殊字符的统计数/文档总字数 > 设定值,该文档被去除。
|
||||
type: slider
|
||||
defaultVal: 0.3
|
||||
min: 0
|
||||
max: 1
|
||||
step: 0.1
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/user/bin/python
|
||||
# -- encoding: utf-8 --
|
||||
|
||||
"""
|
||||
Description: 文档特殊字符率检查
|
||||
Create: 2023/11/7 9:26
|
||||
"""
|
||||
import time
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
from loguru import logger
|
||||
|
||||
from deepdata.core.base_op import Filter
|
||||
|
||||
|
||||
class FileWithHighSpecialCharRateFilter(Filter):
|
||||
"""检查文档特殊字符率"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FileWithHighSpecialCharRateFilter, self).__init__(*args, **kwargs)
|
||||
self._min_threshold = kwargs.get("specialCharRatio", 0.3) # 特殊字符占全文比例阈值,默认值为0.3
|
||||
self._file_path = Path(__file__).parent / 'resources' / 'special_token.txt'
|
||||
with open(self._file_path, 'r', encoding='utf-8') as f:
|
||||
self._special_token = set(f.read().splitlines())
|
||||
|
||||
def execute(self, sample: Dict[str, Any]) -> Dict[str, Any]:
|
||||
start = time.time()
|
||||
self.read_file_first(sample)
|
||||
sample[self.text_key] = self._file_with_high_special_char_rate_filter(sample[self.text_key],
|
||||
sample[self.filename_key])
|
||||
logger.info(f"fileName: {sample[self.filename_key]}, "
|
||||
f"method: FileWithHighSpecialCharRateFilter costs {(time.time() - start):6f} s")
|
||||
return sample
|
||||
|
||||
def _file_with_high_special_char_rate_filter(self, input_data: str, file_name):
|
||||
if not input_data:
|
||||
return ""
|
||||
|
||||
output_data = input_data
|
||||
total = 0
|
||||
for token in self._special_token:
|
||||
total += input_data.count(token)
|
||||
|
||||
special_char_rate = total / len(input_data)
|
||||
if special_char_rate >= self._min_threshold:
|
||||
logger.info(f"The special char rate of the input data is {special_char_rate}. "
|
||||
f"Threshold is {self._min_threshold}. The document {file_name} is filtered.")
|
||||
output_data = ""
|
||||
return output_data
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
~
|
||||
·
|
||||
!
|
||||
@
|
||||
#
|
||||
¥
|
||||
%
|
||||
…
|
||||
&
|
||||
*
|
||||
(
|
||||
)
|
||||
—
|
||||
+
|
||||
-
|
||||
=
|
||||
{
|
||||
}
|
||||
|
|
||||
【
|
||||
】
|
||||
、
|
||||
:
|
||||
“
|
||||
;
|
||||
‘
|
||||
《
|
||||
》
|
||||
?
|
||||
,
|
||||
。
|
||||
`
|
||||
!
|
||||
$
|
||||
^
|
||||
(
|
||||
)
|
||||
_
|
||||
[
|
||||
]
|
||||
\
|
||||
:
|
||||
"
|
||||
;
|
||||
'
|
||||
<
|
||||
>
|
||||
?
|
||||
,
|
||||
/
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from deepdata.core.base_op import OPERATORS
|
||||
|
||||
OPERATORS.register_module(module_name='ImgAdvertisementImagesCleaner',
|
||||
module_path="ops.filter.img_advertisement_images_cleaner.process")
|
||||
@@ -0,0 +1,16 @@
|
||||
name: '广告图片过滤'
|
||||
name_en: 'Ad Image Filter'
|
||||
description: '去除包含二维码的图片。'
|
||||
description_en: 'Removes images containing QR codes.'
|
||||
language: 'python'
|
||||
vendor: 'huawei'
|
||||
raw_id: 'ImgAdvertisementImagesCleaner'
|
||||
version: '1.0.0'
|
||||
types:
|
||||
- 'cleanse'
|
||||
modal: 'image'
|
||||
effect:
|
||||
before: ''
|
||||
after: ''
|
||||
inputs: 'image'
|
||||
outputs: 'image'
|
||||
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
"""
|
||||
Description:
|
||||
Create: 2024/1/22 20:49
|
||||
"""
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from loguru import logger
|
||||
|
||||
from deepdata.common.utils import bytes_transform
|
||||
from deepdata.core.base_op import Filter
|
||||
|
||||
from .wechat_qrcode_model import WechatQRCodeModel
|
||||
|
||||
|
||||
class ImgAdvertisementImagesCleaner(Filter):
|
||||
"""去除广告图片的插件,当前仅支持去除二维码"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ImgAdvertisementImagesCleaner, self).__init__(*args, **kwargs)
|
||||
self.img_resize = 1000 # 大图片的最长边压缩为1000
|
||||
self.use_model = True
|
||||
self.model = self.get_model(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _detect_qr_code_using_anchor_point(img):
|
||||
# 有些二维码和边缘紧贴,无法识别出整个矩形,所以我们先对图片大小进行扩展
|
||||
expand_length = 10
|
||||
edge = expand_length // 2
|
||||
h, w = img.shape[:2]
|
||||
image_extend = np.zeros((img.shape[0] + expand_length, img.shape[1] + expand_length, 3), np.uint8)
|
||||
image_extend[:] = 255
|
||||
image_extend[edge:edge + h, edge:edge + w] = img
|
||||
|
||||
# 转灰度、二值化、找轮廓
|
||||
gray = cv2.cvtColor(image_extend, cv2.COLOR_BGR2GRAY)
|
||||
# 中值滤波
|
||||
blur_image = cv2.medianBlur(gray, 5)
|
||||
_, thresh = cv2.threshold(blur_image, 127, 255, cv2.THRESH_BINARY)
|
||||
contours, hir = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
# 三个“回”字特征轮廓存储
|
||||
parent_contours_list = []
|
||||
hir_list = hir[0]
|
||||
for i, item in enumerate(hir_list):
|
||||
# 判断A轮廓是否有B轮廓
|
||||
if item[2] == -1:
|
||||
continue
|
||||
else:
|
||||
hir_b_index = item[2]
|
||||
# 判断B轮廓是否有C轮廓
|
||||
if hir_list[hir_b_index][2] == -1:
|
||||
continue
|
||||
hir_c_index = hir_list[hir_b_index][2]
|
||||
# 计算A轮廓的周长和C轮廓周长的比值
|
||||
hir_c_arc_length = cv2.arcLength(contours[hir_c_index], True)
|
||||
if hir_c_arc_length:
|
||||
error = cv2.arcLength(contours[i], True) / hir_c_arc_length
|
||||
# 二维码每一个“回”的黑白框框的比例大概为1:1:3:1:1
|
||||
# 理论上,A轮廓周长为28,C轮廓周长为12,A/C = error = 2.3333
|
||||
if 1.5 <= error <= 3:
|
||||
parent_contours_list.append(contours[i])
|
||||
|
||||
# 若找到3个以上“回”字,该图片含有二维码
|
||||
return len(parent_contours_list) >= 3
|
||||
|
||||
@staticmethod
|
||||
def _detect_qr_code_using_wechat_model(img, file_name, model):
|
||||
res = ""
|
||||
try:
|
||||
res, points = model.detectAndDecode(img)
|
||||
except UnicodeDecodeError as ex:
|
||||
res = ex.object.decode('ISO-8859-1').split(" ")[0]
|
||||
except Exception as err:
|
||||
logger.exception(f"fileName: {file_name}, method: ImgAdvertisementImagesCleaner. "
|
||||
f"An error occurred when using the WeChat model to detect the QR code. "
|
||||
f"The error is: {err}")
|
||||
if res:
|
||||
return True
|
||||
return False
|
||||
|
||||
def init_model(self, *args, **kwargs):
|
||||
return WechatQRCodeModel(*args, **kwargs).wechat_qr_model
|
||||
|
||||
def resize_img(self, image):
|
||||
"""图片等比压缩"""
|
||||
height, width = image.shape[:2] # 获取原图像的水平方向尺寸和垂直方向尺寸。
|
||||
temp = max(height, width)
|
||||
# 若图片最长边大于限值,对图片进行压缩,否则返回原图
|
||||
if temp >= self.img_resize:
|
||||
mul_temp = temp / self.img_resize
|
||||
if height > width:
|
||||
res = cv2.resize(image, (int(width / mul_temp), self.img_resize), interpolation=cv2.INTER_AREA)
|
||||
elif height < width:
|
||||
res = cv2.resize(image, (self.img_resize, int(height / mul_temp)), interpolation=cv2.INTER_AREA)
|
||||
else:
|
||||
res = cv2.resize(image, (self.img_resize, self.img_resize), interpolation=cv2.INTER_AREA)
|
||||
return res
|
||||
return image
|
||||
|
||||
def execute(self, sample: Dict[str, Any]):
|
||||
start = time.time()
|
||||
self.read_file_first(sample)
|
||||
file_name = sample[self.filename_key]
|
||||
file_type = "." + sample[self.filetype_key]
|
||||
img_bytes = sample[self.data_key]
|
||||
if img_bytes:
|
||||
data = bytes_transform.bytes_to_numpy(img_bytes)
|
||||
image = self._detect_advertisement_img(data, file_name, self.model)
|
||||
sample[self.data_key] = bytes_transform.numpy_to_bytes(image, file_type)
|
||||
logger.info(f"fileName: {file_name}, "
|
||||
f"method: ImgAdvertisementImagesCleaner costs {(time.time() - start):6f} s")
|
||||
return sample
|
||||
|
||||
def _detect_advertisement_img(self, img, file_name, model):
|
||||
"""检测含有二维码的图片"""
|
||||
img_resize = self.resize_img(img)
|
||||
if self._detect_qr_code_using_wechat_model(img_resize, file_name, model) \
|
||||
or self._detect_qr_code_using_anchor_point(img_resize):
|
||||
logger.info(f"fileName: {file_name}, method: ImgAdvertisementImagesCleaner. "
|
||||
"The image contains advertisement. The image is filtered out.")
|
||||
return np.array([])
|
||||
return img
|
||||
@@ -0,0 +1,23 @@
|
||||
# -- encoding: utf-8 --
|
||||
|
||||
import gc
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
|
||||
|
||||
class WechatQRCodeModel:
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
models_path = os.getenv("MODELS_PATH", "/home/models")
|
||||
self.resources_path = str(Path(models_path, 'img_QRcode_detect', 'resources'))
|
||||
self.wechat_qr_model = cv2.wechat_qrcode_WeChatQRCode(
|
||||
str(Path(self.resources_path, 'detect.prototxt')),
|
||||
str(Path(self.resources_path, 'detect.caffemodel')),
|
||||
str(Path(self.resources_path, 'sr.prototxt')),
|
||||
str(Path(self.resources_path, 'sr.caffemodel')))
|
||||
|
||||
def __del__(self):
|
||||
del self.wechat_qr_model
|
||||
gc.collect()
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from deepdata.core.base_op import OPERATORS
|
||||
|
||||
OPERATORS.register_module(module_name='ImgBlurredImagesCleaner',
|
||||
module_path="ops.filter.img_blurred_images_cleaner.process")
|
||||
@@ -0,0 +1,25 @@
|
||||
name: '模糊图片过滤'
|
||||
name_en: 'Fuzzy Image Filter'
|
||||
description: '去除模糊的图片。'
|
||||
description_en: 'Filters out fuzzy images.'
|
||||
language: 'python'
|
||||
vendor: 'huawei'
|
||||
raw_id: 'ImgBlurredImagesCleaner'
|
||||
version: '1.0.0'
|
||||
types:
|
||||
- 'cleanse'
|
||||
modal: 'image'
|
||||
effect:
|
||||
before: ''
|
||||
after: ''
|
||||
inputs: 'image'
|
||||
outputs: 'image'
|
||||
settings:
|
||||
blurredThreshold:
|
||||
name: 梯度函数值
|
||||
description: 梯度函数值取值越小,图片模糊度越高。
|
||||
type: slider
|
||||
defaultVal: 1000
|
||||
min: 1
|
||||
max: 10000
|
||||
step: 1
|
||||
@@ -0,0 +1,51 @@
|
||||
# -- encoding: utf-8 --
|
||||
|
||||
"""
|
||||
Description:
|
||||
Create: 2025/01/17
|
||||
"""
|
||||
import time
|
||||
|
||||
from typing import Dict, Any
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from loguru import logger
|
||||
|
||||
|
||||
from deepdata.common.utils import bytes_transform
|
||||
from deepdata.core.base_op import Filter
|
||||
|
||||
|
||||
class ImgBlurredImagesCleaner(Filter):
|
||||
"""过滤模糊度低于阈值的图片插件"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ImgBlurredImagesCleaner, self).__init__(*args, **kwargs)
|
||||
# 设置模糊度阈值
|
||||
self._blurred_threshold = kwargs.get("blurredThreshold", 1000)
|
||||
|
||||
def execute(self, sample: Dict[str, Any]):
|
||||
start = time.time()
|
||||
self.read_file_first(sample)
|
||||
img_bytes = sample[self.data_key]
|
||||
file_name = sample[self.filename_key]
|
||||
file_type = "." + sample[self.filetype_key]
|
||||
if img_bytes:
|
||||
data = bytes_transform.bytes_to_numpy(img_bytes)
|
||||
blurred_images = self._blurred_images_filter(data, file_name)
|
||||
sample[self.data_key] = bytes_transform.numpy_to_bytes(blurred_images, file_type)
|
||||
logger.info(f"fileName: {file_name}, method: ImagesBlurredCleaner costs {(time.time() - start):6f} s")
|
||||
return sample
|
||||
|
||||
def _blurred_images_filter(self, image, file_name):
|
||||
# 为方便与其他图片比较可以将图片resize到同一个大小
|
||||
img_resize = cv2.resize(image, (112, 112))
|
||||
# 将图片压缩为单通道的灰度图
|
||||
gray = cv2.cvtColor(img_resize, cv2.COLOR_BGR2GRAY)
|
||||
score = cv2.Laplacian(gray, cv2.CV_64F).var()
|
||||
if score <= self._blurred_threshold:
|
||||
logger.info(f"The image blur is {self._blurred_threshold}, "
|
||||
f"which exceeds the threshold of {score}. {file_name} is filtered out.")
|
||||
return np.array([])
|
||||
return image
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from deepdata.core.base_op import OPERATORS
|
||||
|
||||
OPERATORS.register_module(module_name='ImgDuplicatedImagesCleaner',
|
||||
module_path="ops.filter.img_duplicated_images_cleaner.process")
|
||||
@@ -0,0 +1,16 @@
|
||||
name: '重复图片去除'
|
||||
name_en: 'Duplicate Image Removal'
|
||||
description: '去除重复的图片。'
|
||||
description_en: 'Removes duplicate images.'
|
||||
language: 'python'
|
||||
vendor: 'huawei'
|
||||
raw_id: 'ImgDuplicatedImagesCleaner'
|
||||
version: '1.0.0'
|
||||
types:
|
||||
- 'cleanse'
|
||||
modal: 'image'
|
||||
effect:
|
||||
before: ''
|
||||
after: ''
|
||||
inputs: 'image'
|
||||
outputs: 'image'
|
||||
@@ -0,0 +1,110 @@
|
||||
# -- encoding: utf-8 --
|
||||
|
||||
"""
|
||||
Description:
|
||||
于MD5值计算当前图片与数据集中其它图片是否相同。相同该图片过滤,保留原数据集图片。
|
||||
将文件特征数据即MD5值,存到数据库。根据任务uuid获取历史文件特征,遍历特征并进行去重比较
|
||||
Create: 2025/1/7
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
import cv2
|
||||
from Crypto.Hash import MD5
|
||||
from sqlalchemy import text
|
||||
from loguru import logger
|
||||
|
||||
from deepdata.sql_manager.sql_manager import SQLManager
|
||||
from deepdata.common.utils import get_now_time
|
||||
from deepdata.common.utils import bytes_to_numpy, numpy_to_bytes
|
||||
from deepdata.core.base_op import Filter
|
||||
|
||||
|
||||
class ImgDuplicatedImagesCleaner(Filter):
|
||||
"""去除重复图片插件
|
||||
基于MD5值计算当前图片与数据集中其它图片是否相同。相同该图片过滤,保留原数据集图片。
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# task_uuid为标识该数据集的唯一标志
|
||||
super().__init__(*args, **kwargs)
|
||||
self.task_uuid = kwargs.get("uuid", "")
|
||||
self.img_resize = 200 # 图片压缩尺寸
|
||||
# 获取数据库sql
|
||||
self.sql_dict = self.load_sql_dict()
|
||||
# 获取数据库连接池
|
||||
self.conn = None # 数据库连接
|
||||
self.trans = None # 数据库事务
|
||||
|
||||
@staticmethod
|
||||
def load_sql_dict():
|
||||
"""获取sql语句"""
|
||||
sql_config_path = str(Path(__file__).parent / 'sql' / 'sql_config.json')
|
||||
with open(sql_config_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def compute_md5(self, img_bytes: bytes) -> str:
|
||||
"""将图片统一转化为png无损格式,计算每张图像的md5值"""
|
||||
if not img_bytes:
|
||||
return ""
|
||||
img = bytes_to_numpy(img_bytes)
|
||||
height, width = img.shape[:2] # 获取原图像的水平方向尺寸和垂直方向尺寸。
|
||||
res = cv2.resize(img, (int(width / height * self.img_resize), self.img_resize), interpolation=cv2.INTER_AREA)
|
||||
img_bytes = numpy_to_bytes(res, ".png")
|
||||
hash_md5 = MD5.new()
|
||||
hash_md5.update(img_bytes)
|
||||
return hash_md5.hexdigest()
|
||||
|
||||
def execute(self, sample: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""重复图片去重算子执行入口"""
|
||||
start = time.time()
|
||||
self.read_file_first(sample)
|
||||
file_name = sample[self.filename_key]
|
||||
self.task_uuid = sample.get("instance_id") if not self.task_uuid else self.task_uuid
|
||||
img_data = self._duplicate_images_filter(file_name, sample[self.data_key])
|
||||
sample[self.data_key] = img_data
|
||||
logger.info(
|
||||
f"fileName: {file_name}, method: DuplicateImagesCleaner costs {(time.time() - start):6f} s")
|
||||
return sample
|
||||
|
||||
def execute_sql(self, md5: str, file_name: str,
|
||||
img_bytes: bytes) -> bytes:
|
||||
"""从数据库中获取文件特征、比较MD5,插入新的文件特征"""
|
||||
timestamp = get_now_time('Asia/Shanghai', '%Y-%m-%d %H:%M:%S', file_name,
|
||||
"DuplicateImagesCleaner")
|
||||
query_sql = str(self.sql_dict.get("query_sql"))
|
||||
insert_sql = str(self.sql_dict.get("insert_sql"))
|
||||
create_tables_sql = str(self.sql_dict.get("create_tables_sql"))
|
||||
query_sql_params = {"task_uuid": self.task_uuid, "file_feature": md5}
|
||||
insert_sql_params = {"task_uuid": self.task_uuid, "file_feature": md5, "file_name": file_name.encode("utf-8"),
|
||||
"timestamp": timestamp}
|
||||
|
||||
db_manager = SQLManager()
|
||||
try:
|
||||
self.conn = db_manager.create_connect()
|
||||
except Exception as e:
|
||||
logger.error(f"fileName: {file_name}, database connection failed: {str(e)}")
|
||||
raise RuntimeError(82000, str(e)) from None
|
||||
|
||||
with self.conn as connection:
|
||||
connection.execute(text(create_tables_sql))
|
||||
# 判断是否有重复文件
|
||||
result = connection.execute(text(query_sql), query_sql_params).fetchall()
|
||||
# 查询记录为空,无重复图片, 插入新文件特征
|
||||
if not result:
|
||||
connection.execute(text(insert_sql), insert_sql_params)
|
||||
return img_bytes
|
||||
logger.info(f"taskId: {self.task_uuid} fileName: {file_name}, method: Duplicate ImagesCleaner. "
|
||||
f"The image is duplicated and filtered ")
|
||||
return b""
|
||||
|
||||
def _duplicate_images_filter(self, file_name: str, img_bytes: bytes) -> bytes:
|
||||
"""重复图片去重算子执行逻辑"""
|
||||
# 如果文件为空,则无需去重,返回原图
|
||||
if not img_bytes:
|
||||
return img_bytes
|
||||
md5 = self.compute_md5(img_bytes)
|
||||
return self.execute_sql(md5, file_name, img_bytes)
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"query_sql": "SELECT * FROM operator_duplicate_img_features WHERE task_uuid = :task_uuid AND file_feature = :file_feature",
|
||||
"insert_sql": "INSERT INTO operator_duplicate_img_features (task_uuid, file_feature, file_name, timestamp) VALUES (:task_uuid, :file_feature, :file_name, :timestamp)",
|
||||
"create_tables_sql": "CREATE TABLE IF NOT EXISTS operator_duplicate_img_features (id SERIAL PRIMARY KEY, task_uuid VARCHAR(255), file_feature TEXT, file_name TEXT, timestamp TIMESTAMP);"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from deepdata.core.base_op import OPERATORS
|
||||
|
||||
OPERATORS.register_module(module_name='ImgSimilarImagesCleaner',
|
||||
module_path="ops.filter.img_similar_images_cleaner.process")
|
||||
@@ -0,0 +1,25 @@
|
||||
name: '相似图片去除'
|
||||
name_en: 'Similar Image Removal'
|
||||
description: '去除相似的图片。'
|
||||
description_en: 'Removes similar images.'
|
||||
language: 'python'
|
||||
vendor: 'huawei'
|
||||
raw_id: 'ImgSimilarImagesCleaner'
|
||||
version: '1.0.0'
|
||||
types:
|
||||
- 'cleanse'
|
||||
modal: 'image'
|
||||
effect:
|
||||
before: ''
|
||||
after: ''
|
||||
inputs: 'image'
|
||||
outputs: 'image'
|
||||
settings:
|
||||
similarThreshold:
|
||||
name: 相似度
|
||||
description: 相似度取值越大,图片相似度越高。
|
||||
type: slider
|
||||
defaultVal: 0.8
|
||||
min: 0
|
||||
max: 1
|
||||
step: 0.01
|
||||
@@ -0,0 +1,240 @@
|
||||
# -- encoding: utf-8 --
|
||||
|
||||
"""
|
||||
Description:
|
||||
1.本算子结合感知哈希算法和ORB两个算法判断图片的相似性
|
||||
2.感知哈希算法则是从图像的整体结构和特征维度来计算图片的相似度。
|
||||
3.ORB算法可以用来对图像中的关键点快速创建特征向量,这些特征向量可以用来识别图像中的对象。通过比较两张图片的特征向量计算相似度。
|
||||
4.感知哈希算法和ORB算法计算相似度高于0.75,则选择二者较大值;若低于0.75,则选择二者最小值作为相似度
|
||||
5.将文件特征数据存到数据库。根据任务uuid获取历史文件特征,遍历特征并进行去重比较
|
||||
Create: 2025/1/7
|
||||
"""
|
||||
import json
|
||||
import time
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from sqlalchemy import text
|
||||
from loguru import logger
|
||||
|
||||
from deepdata.sql_manager.sql_manager import SQLManager
|
||||
from deepdata.common.utils import get_now_time
|
||||
from deepdata.common.utils import bytes_to_numpy
|
||||
from deepdata.core.base_op import Filter
|
||||
|
||||
MAX_RETRIES = 5
|
||||
BASE_DELAY = 1
|
||||
MAX_DELAY = 30 # 最大延时设置为30秒
|
||||
JITTER_FACTOR = 0.25 # 抖动因子为等待时间的25%
|
||||
MAX_FEATURES_NUM = 200
|
||||
|
||||
|
||||
def get_orb_des(image: np.ndarray) -> np.ndarray:
|
||||
"""检测图像中的特征点kp和计算这些特征点的描述符矩阵des_matrix"""
|
||||
if not image.size:
|
||||
return np.array([])
|
||||
orb = cv2.ORB_create() # 初始化ORB检测器
|
||||
orb.setMaxFeatures(MAX_FEATURES_NUM) # 设置最大特征点数量为200
|
||||
kp, des_matrix = orb.detectAndCompute(image, None)
|
||||
if des_matrix is None:
|
||||
# 若没有提取出图像特征,描述符矩阵置为空
|
||||
des_matrix = np.array([])
|
||||
return des_matrix
|
||||
|
||||
|
||||
class ImgSimilarImagesCleaner(Filter):
|
||||
"""去除相似图片的插件"""
|
||||
|
||||
DEFAULT_SIMILAR_THRESHOLD = 0.8 # 默认相似度阈值
|
||||
DEFAULT_ORB_RATIO = 0.8 # 默认特征点距离比率
|
||||
DEFAULT_MIX_SIMILARITY = 0.75 # 默认相似度算法阈值
|
||||
DEFAULT_IMG_RESIZE = 200 # 默认图片压缩尺寸
|
||||
DEFAULT_PAGE_SIZE = 500 # 默认每页数据量
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.similar_threshold = kwargs.get("similarThreshold", self.DEFAULT_SIMILAR_THRESHOLD) # 默认相似度阈值为0.8
|
||||
# task_uuid为标识该数据集的唯一标志
|
||||
self.task_uuid = kwargs.get("uuid", "")
|
||||
self.orb_ratio = self.DEFAULT_ORB_RATIO # 特征点距离的比率,该数值为经验值
|
||||
self.mix_similarity = self.DEFAULT_MIX_SIMILARITY # 选择相似度算法的阈值,该数值为经验值
|
||||
self.img_resize = self.DEFAULT_IMG_RESIZE # 图片压缩尺寸
|
||||
self.conn = None # 数据库连接
|
||||
self.trans = None # 数据库事务
|
||||
self.page_size = self.DEFAULT_PAGE_SIZE # 每页数据量
|
||||
# 获取数据库sql
|
||||
self.sql_dict = self.load_sql_dict()
|
||||
|
||||
@staticmethod
|
||||
def load_sql_dict():
|
||||
"""获取sql语句"""
|
||||
sql_config_path = str(Path(__file__).parent / 'sql' / 'sql_config.json')
|
||||
with open(sql_config_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
@staticmethod
|
||||
def get_p_hash(image: np.ndarray) -> str:
|
||||
"""计算pHash值"""
|
||||
hashed_value = ""
|
||||
if not image.size:
|
||||
return hashed_value
|
||||
gray_image = cv2.cvtColor(cv2.resize(image, (8, 8), interpolation=cv2.INTER_AREA), cv2.COLOR_BGR2GRAY)
|
||||
dct_image = cv2.dct(np.float32(gray_image))
|
||||
hashed_value = ''.join(['1' if x >= 0 else '0' for x in dct_image[:8, :8].flatten()])
|
||||
return hashed_value
|
||||
|
||||
@staticmethod
|
||||
def get_phash_similarity(hash_comparison: str, hash_compared: str) -> float:
|
||||
"""通过计算汉明距离,获取图片相似度"""
|
||||
# 若哈希值为空,则相似度为0
|
||||
if not hash_comparison or not hash_compared:
|
||||
return 0.0
|
||||
# 计算汉明距离
|
||||
distance = sum(
|
||||
bit_comparison != bit_compared for bit_comparison, bit_compared in zip(hash_comparison, hash_compared))
|
||||
similarity = 1 - distance / len(hash_comparison)
|
||||
return similarity
|
||||
|
||||
def filter_similar_images(self, img: np.ndarray, file_name: str) -> np.ndarray:
|
||||
"""判断数据集中是否存在相似图片"""
|
||||
# 如果文件为空,则无需去重,返回原图
|
||||
if not img.size:
|
||||
return img
|
||||
p_hash = self.get_p_hash(img)
|
||||
height, width = img.shape[:2] # 获取原图像的水平方向尺寸和垂直方向尺寸。
|
||||
img_resize = cv2.resize(img, (int(width / height * self.img_resize), self.img_resize),
|
||||
interpolation=cv2.INTER_AREA)
|
||||
des_matrix = get_orb_des(img_resize)
|
||||
return self.execute_sql(p_hash, des_matrix, file_name, img)
|
||||
|
||||
def get_orb_similarity(self, des_matrix: np.ndarray, des_matrix_history: np.ndarray, file_name: str,
|
||||
file_name_history: str) -> float:
|
||||
"""获取图片orb相似度"""
|
||||
# 若描述符矩阵为空,则相似度为0
|
||||
if not des_matrix.size or not des_matrix_history.size:
|
||||
return 0.0
|
||||
# 根据矩阵对角线上元素和的大小,选择描述符矩阵作为训练或查询矩阵
|
||||
train_matrix, query_matrix = des_matrix, des_matrix_history
|
||||
if train_matrix.shape[0] > des_matrix_history.shape[0]:
|
||||
train_matrix, query_matrix = des_matrix_history, des_matrix
|
||||
elif des_matrix.shape[0] == des_matrix_history.shape[0]:
|
||||
if np.trace(des_matrix) > np.trace(des_matrix_history):
|
||||
train_matrix, query_matrix = des_matrix_history, des_matrix
|
||||
|
||||
try:
|
||||
# knn筛选结果
|
||||
matches = (cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False).
|
||||
knnMatch(query_matrix, trainDescriptors=train_matrix, k=2))
|
||||
if not matches:
|
||||
return 0.0
|
||||
# 遍历每一对特征点,筛选距离更近的特征点
|
||||
count = 0
|
||||
for (m, n) in matches:
|
||||
if m.distance < self.orb_ratio * n.distance:
|
||||
count += 1
|
||||
orb_similarity = count / len(matches)
|
||||
return orb_similarity
|
||||
except Exception as e:
|
||||
logger.exception(f"taskId: {self.task_uuid}, failed to compare the similarity between "
|
||||
f"{file_name} and {file_name_history}: {e}")
|
||||
return 0.0
|
||||
|
||||
def execute_sql(self, p_hash: str, des_matrix: np.ndarray, file_name: str,
|
||||
img: np.ndarray) -> np.ndarray:
|
||||
des_matrix_binary = zlib.compress(des_matrix.tobytes()) # 使用 zlib 进行压缩数组
|
||||
timestamp = get_now_time('Asia/Shanghai', '%Y-%m-%d %H:%M:%S', file_name,
|
||||
"ImgSimilarCleaner")
|
||||
query_task_uuid_sql = str(self.sql_dict.get("query_task_uuid_sql"))
|
||||
insert_sql = str(self.sql_dict.get("insert_sql"))
|
||||
create_tables_sql = str(self.sql_dict.get("create_tables_sql"))
|
||||
|
||||
db_manager = SQLManager()
|
||||
try:
|
||||
self.conn = db_manager.create_connect()
|
||||
except Exception as e:
|
||||
logger.error(f"fileName: {file_name}, database connection failed: {str(e)}")
|
||||
raise RuntimeError(82000, str(e)) from None
|
||||
|
||||
with self.conn as connection:
|
||||
"""从数据库中获取文件特征、比较相似度,插入新的文件特征"""
|
||||
connection.execute(text(create_tables_sql))
|
||||
result = connection.execute(text(query_task_uuid_sql), {"task_uuid": self.task_uuid}).fetchall()
|
||||
total_count = len(result)
|
||||
if self.has_similar_images(connection, des_matrix, file_name, p_hash, total_count):
|
||||
return np.array([])
|
||||
|
||||
insert_data = {
|
||||
"task_uuid": self.task_uuid,
|
||||
"p_hash": p_hash,
|
||||
"des_matrix": des_matrix_binary,
|
||||
"matrix_shape": str(des_matrix.shape),
|
||||
"file_name": file_name.encode("utf-8").hex(),
|
||||
"timestamp": timestamp
|
||||
}
|
||||
connection.execute(text(insert_sql),insert_data)
|
||||
return img
|
||||
|
||||
def has_similar_images(self, connection, des_matrix, file_name, p_hash, total_count):
|
||||
for i in range(0, total_count, self.page_size):
|
||||
query_sql = self.sql_dict.get("query_sql")
|
||||
rows = connection.execute(text(query_sql), {"task_uuid": self.task_uuid, "ge": self.page_size, "le": i}).fetchall()
|
||||
# 对应任务uuid,最后一页没有数据,跳出循环
|
||||
if not rows:
|
||||
break # 对两张图片进行相似度比较
|
||||
if self.determine_similar_images(rows, p_hash, des_matrix, file_name):
|
||||
return True
|
||||
return False
|
||||
|
||||
def determine_similar_images(self, file_features: List, p_hash: str, des_matrix: np.ndarray,
|
||||
file_name: str) -> bool:
|
||||
"""根据文件特征,判断两张图片相似度是否超过指定阈值"""
|
||||
for signature in file_features:
|
||||
pash_feature, orb_feature, matrix_shape, file_name_history = signature[2], signature[3], signature[4], \
|
||||
signature[5]
|
||||
if not pash_feature:
|
||||
# 若图片为空,p_hash、des_matrix为空,跳过比对
|
||||
continue
|
||||
# 解压缩数据
|
||||
decompressed_data = zlib.decompress(orb_feature)
|
||||
# 将字节流转换回矩阵
|
||||
des_matrix_history = np.frombuffer(decompressed_data, dtype=np.uint8).reshape(eval(matrix_shape))
|
||||
# 移除转义字符 '\' 并将十六进制字符串转换为字节序列
|
||||
bytes_data = bytes.fromhex(file_name_history)
|
||||
# 解码字节序列为 UTF-8 编码的字符串
|
||||
file_name_decoded = bytes_data.decode('utf-8')
|
||||
|
||||
phash_similarity = self.get_phash_similarity(p_hash, pash_feature)
|
||||
orb_similarity = self.get_orb_similarity(des_matrix, des_matrix_history, file_name, file_name_decoded)
|
||||
max_similarity = max(phash_similarity, orb_similarity)
|
||||
min_similarity = min(phash_similarity, orb_similarity)
|
||||
if max_similarity >= self.mix_similarity:
|
||||
result = max_similarity
|
||||
else:
|
||||
result = min_similarity
|
||||
similarity = round(result, 2)
|
||||
if similarity >= self.similar_threshold:
|
||||
logger.info(
|
||||
f"fileName: {file_name}, method: ImgSimilarCleaner, dataset: {self.task_uuid}. "
|
||||
f"This picture is similar to {file_name_decoded}, "
|
||||
f"and the similarity is {similarity:.4f}. The picture is filtered."
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
def execute(self, sample: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""去除相似图片算子执行入口"""
|
||||
start = time.time()
|
||||
self.read_file_first(sample)
|
||||
file_name = sample[self.filename_key]
|
||||
img_bytes = sample[self.data_key]
|
||||
self.task_uuid = sample.get("instance_id") if not self.task_uuid else self.task_uuid
|
||||
data = bytes_to_numpy(img_bytes) if img_bytes else np.array([])
|
||||
similar_images = self.filter_similar_images(data, file_name)
|
||||
# 若相似图片,sample[self.data_key]设为空
|
||||
if not similar_images.size:
|
||||
sample[self.data_key] = b""
|
||||
logger.info(f"fileName: {file_name}, method: ImgSimilarCleaner costs {(time.time() - start):6f} s")
|
||||
return sample
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"query_sql": "SELECT * FROM operator_similar_img_features WHERE task_uuid = :task_uuid ORDER BY timestamp LIMIT :ge OFFSET :le",
|
||||
"insert_sql": "INSERT INTO operator_similar_img_features (task_uuid,p_hash,des_matrix,matrix_shape,file_name,timestamp) VALUES (:task_uuid,:p_hash,:des_matrix,:matrix_shape,:file_name,:timestamp)",
|
||||
"query_task_uuid_sql": "SELECT * FROM operator_similar_img_features WHERE task_uuid = :task_uuid",
|
||||
"create_tables_sql": "CREATE TABLE IF NOT EXISTS operator_similar_img_features (id SERIAL PRIMARY KEY,task_uuid VARCHAR(255),p_hash TEXT,des_matrix BYTEA,matrix_shape TEXT,file_name TEXT,timestamp TIMESTAMP);"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from deepdata.core.base_op import OPERATORS
|
||||
|
||||
OPERATORS.register_module(module_name='DuplicateFilesFilter',
|
||||
module_path="ops.filter.remove_duplicate_file.process")
|
||||
@@ -0,0 +1,25 @@
|
||||
name: '相似文档去除'
|
||||
name_en: 'Similar Document Removal'
|
||||
description: '相似文档去除。'
|
||||
description_en: 'Removes similar documents.'
|
||||
language: 'python'
|
||||
vendor: 'huawei'
|
||||
raw_id: 'DuplicateFilesFilter'
|
||||
version: '1.0.0'
|
||||
types:
|
||||
- 'cleanse'
|
||||
modal: 'text'
|
||||
effect:
|
||||
before: '这篇文档跟数据集中的另一篇文档内容几乎一样,执行该算子后,这篇文档会被去除。'
|
||||
after: ''
|
||||
inputs: 'text'
|
||||
outputs: 'text'
|
||||
settings:
|
||||
fileDuplicateThreshold:
|
||||
name: 文档相似度
|
||||
description: 基于MinHash算法和Jaccard相似度,计算当前文档与数据集中其它文档相似性,超过设定值,该文档被去除。
|
||||
type: slider
|
||||
defaultVal: 0.5
|
||||
min: 0
|
||||
max: 1
|
||||
step: 0.1
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/user/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Description: 文档局部内容去重
|
||||
Create: 2025/01/07
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
|
||||
import numpy as np
|
||||
from datasketch import MinHash
|
||||
from sqlalchemy import text
|
||||
from loguru import logger
|
||||
|
||||
from deepdata.sql_manager.sql_manager import SQLManager
|
||||
from deepdata.common.utils import get_now_time
|
||||
from deepdata.core.base_op import Filter
|
||||
|
||||
|
||||
class DuplicateFilesFilter(Filter):
|
||||
"""相似文档去除插件
|
||||
|
||||
基于MinHash计算当前文档与数据集中其它文档相似性,相似性高于设定阈值则返回空。
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# 标点符号
|
||||
super().__init__(*args, **kwargs)
|
||||
self.punctuation_pattern = "。.??!!,,;;::()()【】{}[]“”""‘’''/\n"
|
||||
# 默认相似度阈值为0.5
|
||||
self.duplicate_th = kwargs.get("fileDuplicateThreshold", 0.5)
|
||||
# task_uuid为标识该数据集的唯一标志
|
||||
self.task_uuid = kwargs.get("uuid", "")
|
||||
# 数据库连接
|
||||
self.conn = None
|
||||
# 数据库事务
|
||||
self.trans = None
|
||||
# 每页数据量
|
||||
self.page_size = 500
|
||||
# 获取数据库sql
|
||||
self.sql_dict = self.load_sql_dict()
|
||||
|
||||
@staticmethod
|
||||
def load_sql_dict():
|
||||
"""获取sql语句"""
|
||||
sql_config_path = str(Path(__file__).parent / 'sql' / 'sql_config.json')
|
||||
with open(sql_config_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def get_minhash(self, input_text: str) -> MinHash:
|
||||
"""获取输入文档的minhash
|
||||
|
||||
Args:
|
||||
input_text: 输入文档内容
|
||||
|
||||
Returns:
|
||||
text_minhash: 输入文档对应的minhash值
|
||||
"""
|
||||
text_minhash = MinHash()
|
||||
for word in re.split(f"[{re.escape(self.punctuation_pattern)}]", input_text.strip()):
|
||||
text_minhash.update(word.strip().encode('utf8'))
|
||||
return text_minhash
|
||||
|
||||
def deduplicate_files(self, sample: Dict[str, Any], file_name: str) -> str:
|
||||
"""去除相似文件
|
||||
|
||||
Args:
|
||||
content: 待处理的Content对象
|
||||
file_name: 文件名称
|
||||
|
||||
Returns:
|
||||
input_text: 去重后的文件内容,大于相似度值返回空,否则返回原始文本内容。
|
||||
"""
|
||||
input_text = sample[self.text_key]
|
||||
if not input_text:
|
||||
return input_text
|
||||
text_minhash = self.get_minhash(input_text)
|
||||
return self.execute_sql(text_minhash, file_name, input_text)
|
||||
|
||||
def execute_sql(self, text_minhash: MinHash, file_name: str,
|
||||
input_text: str) -> str:
|
||||
"""从数据库中获取文件特征、比较相似度,插入新的文件特征"""
|
||||
timestamp = get_now_time('Asia/Shanghai', '%Y-%m-%d %H:%M:%S', file_name,
|
||||
"DuplicateFilesFilter")
|
||||
minhash_values = text_minhash.hashvalues
|
||||
# 将 NumPy 数组转换为字符串
|
||||
minhash_values_string = np.array2string(minhash_values)
|
||||
query_task_uuid_sql = self.sql_dict.get("query_task_uuid_sql")
|
||||
insert_sql = self.sql_dict.get("insert_sql")
|
||||
create_tables_sql = self.sql_dict.get("create_tables_sql")
|
||||
db_manager = SQLManager()
|
||||
try:
|
||||
self.conn = db_manager.create_connect()
|
||||
except Exception as e:
|
||||
logger.error(f"fileName: {file_name}, database connection failed: {str(e)}")
|
||||
raise RuntimeError(82000, str(e)) from None
|
||||
with self.conn as connection:
|
||||
connection.execute(text(create_tables_sql))
|
||||
result = connection.execute(text(query_task_uuid_sql), {"task_uuid": self.task_uuid}).fetchall()
|
||||
total_count = len(result)
|
||||
if self.has_similar_text(connection, file_name, text_minhash, total_count):
|
||||
return ""
|
||||
insert_data = {
|
||||
"task_uuid": self.task_uuid,
|
||||
"file_feature": minhash_values_string,
|
||||
"file_name": file_name.encode("utf-8").hex(),
|
||||
"timestamp": timestamp
|
||||
}
|
||||
connection.execute(text(insert_sql), insert_data)
|
||||
return input_text
|
||||
|
||||
def has_similar_text(self, connection, file_name, text_minhash, total_count) -> bool:
|
||||
query_sql = self.sql_dict.get("query_sql")
|
||||
for i in range(0, total_count, self.page_size):
|
||||
rows = connection.execute(
|
||||
text(query_sql), {"task_uuid": self.task_uuid, "file_name": file_name, "ge": self.page_size, "le": i}).fetchall()
|
||||
# 对应任务uuid,最后一页没有数据,跳出循环
|
||||
if not rows:
|
||||
break
|
||||
# 对两个文本进行相似度比较
|
||||
if self.determine_similar_text(rows, text_minhash, file_name):
|
||||
return True
|
||||
return False
|
||||
|
||||
def determine_similar_text(self, file_features: List, text_minhash: MinHash, file_name: str) -> bool:
|
||||
for signature in file_features:
|
||||
# 历史文件特征和历史文件名称
|
||||
file_feature, file_name_history = signature[2], signature[3]
|
||||
if not file_feature:
|
||||
continue
|
||||
minhash_obj = MinHash(num_perm=128)
|
||||
minhash_obj.hashvalues = np.fromstring(file_feature.strip('[]'), dtype=np.uint64, sep=' ')
|
||||
similarity = text_minhash.jaccard(minhash_obj)
|
||||
|
||||
# 移除转义字符 '\' 并将十六进制字符串转换为字节序列
|
||||
bytes_data = bytes.fromhex(file_name_history)
|
||||
# 解码字节序列为 UTF-8 编码的字符串
|
||||
file_name_decoded = bytes_data.decode('utf-8')
|
||||
|
||||
if similarity >= self.duplicate_th:
|
||||
logger.info(f"taskId: {self.task_uuid}, fileName: {file_name} is similar to {file_name_decoded}, "
|
||||
f"and the similarity is {similarity:4f}")
|
||||
return True
|
||||
return False
|
||||
|
||||
def execute(self, sample: Dict[str, Any]) -> Dict[str, Any]:
|
||||
start = time.time()
|
||||
self.read_file_first(sample)
|
||||
file_name = sample[self.filename_key]
|
||||
self.task_uuid = sample.get("instance_id") if not self.task_uuid else self.task_uuid
|
||||
sample[self.text_key] = self.deduplicate_files(sample, file_name)
|
||||
logger.info(f"taskId: {self.task_uuid} fileName: {file_name}, "
|
||||
f"method: DuplicateFilesFilter costs {(time.time() - start):6f} s")
|
||||
return sample
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"query_sql": "SELECT * FROM operators_similar_text_features WHERE task_uuid = :task_uuid AND file_name <> :file_name ORDER BY timestamp LIMIT :ge OFFSET :le",
|
||||
"create_tables_sql": "CREATE TABLE IF NOT EXISTS operators_similar_text_features (id SERIAL PRIMARY KEY, task_uuid VARCHAR(255), file_feature TEXT, file_name TEXT, timestamp TIMESTAMP);",
|
||||
"insert_sql": "INSERT INTO operators_similar_text_features (task_uuid, file_feature, file_name, timestamp) VALUES (:task_uuid, :file_feature, :file_name, :timestamp)",
|
||||
"query_task_uuid_sql": "SELECT * FROM operators_similar_text_features WHERE task_uuid = :task_uuid"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from deepdata.core.base_op import OPERATORS
|
||||
|
||||
OPERATORS.register_module(module_name='FileWithManySensitiveWordsFilter',
|
||||
module_path="ops.filter.remove_file_with_many_sensitive_words.process")
|
||||
@@ -0,0 +1,25 @@
|
||||
name: '文档敏感词率检查'
|
||||
name_en: 'Sensitive Word Rate Check'
|
||||
description: '去除敏感词过多的文档。'
|
||||
description_en: 'Filters out files that contain excessive sensitive phrases.'
|
||||
language: 'python'
|
||||
vendor: 'huawei'
|
||||
raw_id: 'FileWithManySensitiveWordsFilter'
|
||||
version: '1.0.0'
|
||||
types:
|
||||
- 'cleanse'
|
||||
modal: 'text'
|
||||
effect:
|
||||
before: '出售硝酸甘油出售硝酸甘油出售硝酸甘油出售硝酸甘油'
|
||||
after: ''
|
||||
inputs: 'text'
|
||||
outputs: 'text'
|
||||
settings:
|
||||
sensitiveWordsRate:
|
||||
name: 文档敏感词率
|
||||
description: 敏感词的字数/文档总字数 > 设定值,该文档被去除。
|
||||
type: slider
|
||||
defaultVal: 0.01
|
||||
min: 0
|
||||
max: 1
|
||||
step: 0.01
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/user/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Description: 过滤语言概率太低的文档(支持自定义阈值)
|
||||
Create: 2023/12/7 15:43
|
||||
"""
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from deepdata.core.base_op import Filter
|
||||
from deepdata.common.utils.aho_corasick import build_trie, add_fail_pointer
|
||||
|
||||
sys.setrecursionlimit(5000)
|
||||
|
||||
|
||||
class AhoCorasic:
|
||||
"""AC自动机算法进行目标字符串搜索"""
|
||||
|
||||
def __init__(self, words):
|
||||
self._root = add_fail_pointer(build_trie(words))
|
||||
|
||||
def search_and_count(self, text: str, special_symbols: set):
|
||||
"""
|
||||
匹配敏感词,统计敏感词字数。
|
||||
|
||||
Args:
|
||||
text: 文本
|
||||
special_symbols: 特殊字符(需跳过)
|
||||
Returns:
|
||||
统计敏感词字数
|
||||
"""
|
||||
target_count = 0
|
||||
node = self._root
|
||||
|
||||
valid_len = 0 # 当前遍历的有效长度
|
||||
for _, s in enumerate(text):
|
||||
if s in special_symbols: # 跳过特殊字符
|
||||
continue
|
||||
|
||||
matched = True
|
||||
while s not in node.child: # 当node.child没有字符s
|
||||
if node == self._root: # 当node为root(无node.fail),有效长度归0且跳出
|
||||
valid_len = 0
|
||||
matched = False
|
||||
break
|
||||
elif node.fail == self._root: # node.fail为root场景,有效长度归0,但可继续
|
||||
valid_len = 0
|
||||
node = node.fail # 移动到失败指针节点
|
||||
if not matched:
|
||||
continue
|
||||
|
||||
node = node.child.get(s)
|
||||
valid_len += 1
|
||||
if node.word: # node是单词尾字母
|
||||
target_count += valid_len
|
||||
valid_len = 0
|
||||
return target_count
|
||||
|
||||
|
||||
class FileWithManySensitiveWordsFilter(Filter):
|
||||
"""外部输入的暴力、色情文本过滤插件"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FileWithManySensitiveWordsFilter, self).__init__(*args, **kwargs)
|
||||
root_path = Path(__file__).parent / 'resources'
|
||||
violent_file_path = str(root_path / 'violent.txt')
|
||||
sexual_file_path = str(root_path / 'sexual.txt')
|
||||
political_file_path = str(root_path / 'political.txt')
|
||||
special_symbols_path = str(root_path / 'special_symbols.txt')
|
||||
self._file_sensitive_words_rate = kwargs.get("sensitiveWordsRate", 0.01) # 参数默认值为0.01
|
||||
self.violent_words = self.load_words_list(violent_file_path)
|
||||
self.sexual_words = self.load_words_list(sexual_file_path)
|
||||
self.political_words = self.load_words_list(political_file_path)
|
||||
self.special_symbols = self.load_words_list(special_symbols_path)
|
||||
self.symbols = self.special_symbols | {"\n", "\t", "\r"} # 符号,不纳入文本字数统计
|
||||
self.words = self.violent_words | self.sexual_words | self.political_words
|
||||
self.ac_automaton = AhoCorasic(self.words)
|
||||
|
||||
@staticmethod
|
||||
def load_words_list(path):
|
||||
"""词表加载"""
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
words = set(f.read().splitlines())
|
||||
return words
|
||||
|
||||
def execute(self, sample: Dict[str, Any]) -> Dict[str, Any]:
|
||||
start = time.time()
|
||||
self.read_file_first(sample)
|
||||
sample[self.text_key] = self._file_with_many_sensitive_words_filter(sample[self.text_key],
|
||||
sample[self.filename_key])
|
||||
logger.info(f"fileName: {sample[self.filename_key]}, "
|
||||
f"method: FileWithManySensitiveWordsFilter costs {(time.time() - start):6f} s")
|
||||
return sample
|
||||
|
||||
def _file_with_many_sensitive_words_filter(self, input_data: str, file_name):
|
||||
"""过滤敏感词过多的文档"""
|
||||
total_count = 0
|
||||
for s in input_data:
|
||||
if s not in self.symbols:
|
||||
total_count += 1
|
||||
if total_count == 0:
|
||||
return input_data
|
||||
|
||||
# 敏感词率 = 敏感词字数 / 总字数,符号不纳入统计
|
||||
sensitive_rate = self.ac_automaton.search_and_count(input_data, self.special_symbols) / total_count
|
||||
if sensitive_rate >= self._file_sensitive_words_rate:
|
||||
logger.info(f"This document contains too many sensitive words. "
|
||||
f"The proportion of sensitive words is {sensitive_rate}. "
|
||||
f"Threshold is {self._file_sensitive_words_rate}. The document {file_name} is filtered.")
|
||||
return ""
|
||||
else:
|
||||
return input_data
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
!
|
||||
|
||||
.
|
||||
,
|
||||
#
|
||||
$
|
||||
%
|
||||
&
|
||||
*
|
||||
(
|
||||
)
|
||||
|
|
||||
?
|
||||
/
|
||||
@
|
||||
"
|
||||
'
|
||||
;
|
||||
[
|
||||
]
|
||||
{
|
||||
}
|
||||
+
|
||||
~
|
||||
-
|
||||
_
|
||||
=
|
||||
^
|
||||
<
|
||||
>
|
||||
!
|
||||
。
|
||||
,
|
||||
¥
|
||||
(
|
||||
)
|
||||
?
|
||||
、
|
||||
“
|
||||
‘
|
||||
;
|
||||
【
|
||||
】
|
||||
——
|
||||
…
|
||||
……
|
||||
《
|
||||
》
|
||||
:
|
||||
:
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from deepdata.core.base_op import OPERATORS
|
||||
|
||||
OPERATORS.register_module(module_name='FileWithShortOrLongLengthFilter',
|
||||
module_path="ops.filter.remove_file_with_short_or_long_length.process")
|
||||
@@ -0,0 +1,34 @@
|
||||
name: '文档字数检查'
|
||||
name_en: 'Word Count Check'
|
||||
description: '字数不在指定范围会被过滤掉。'
|
||||
description_en: 'Filters out documents whose word count is not in the specified range.'
|
||||
language: 'python'
|
||||
vendor: 'huawei'
|
||||
raw_id: 'FileWithShortOrLongLengthFilter'
|
||||
version: '1.0.0'
|
||||
types:
|
||||
- 'cleanse'
|
||||
modal: 'text'
|
||||
effect:
|
||||
before: '过短文本'
|
||||
after: ''
|
||||
inputs: 'text'
|
||||
outputs: 'text'
|
||||
settings:
|
||||
fileLength:
|
||||
name: 文档字数
|
||||
description: '过滤字数不在指定范围内的文档,如[10,10000000]。若输入为空,则不对字数上/下限做限制。'
|
||||
type: range
|
||||
properties:
|
||||
- name: fileMinimumLength
|
||||
type: inputNumber
|
||||
defaultVal: 10
|
||||
min: 0
|
||||
max: 10000000000000000
|
||||
step: 1
|
||||
- name: fileMaximumLength
|
||||
type: inputNumber
|
||||
defaultVal: 10000000
|
||||
min: 0
|
||||
max: 10000000000000000
|
||||
step: 1
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/user/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Description: 词数目不在指定范围会被过滤掉(支持自定义阈值)
|
||||
Create: 2025/01/16
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from deepdata.core.base_op import Filter
|
||||
|
||||
|
||||
class FileWithShortOrLongLengthFilter(Filter):
|
||||
"""检查文档字数目,词数目不在指定范围会被过滤掉(支持自定义阈值)"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
file_length_list = kwargs.get("fileLength", [10, 10000000]) # [下限,上限],默认字数下限为10, 默认字数上限为10000000
|
||||
if len(file_length_list) != 2: # 要求传入字数目上限和字数目下限
|
||||
logger.error(f"method: FileWithShortOrLongLengthFilter expected 2 arguments, got {len(file_length_list)}")
|
||||
raise RuntimeError(82001, "method: FileWithShortOrLongLengthFilter expected 2 arguments") from None
|
||||
# 用户不输入下限参数时前端传入'',则不对字数目下限控制
|
||||
self._file_minimum_length = 0 if not file_length_list[0] else file_length_list[0]
|
||||
# 用户不输入上限参数时前端传入'',则不对字数目上限控制
|
||||
self._file_maximum_length = float("inf") if not file_length_list[1] else file_length_list[1]
|
||||
|
||||
def execute(self, sample: Dict[str, Any]) -> Dict[str, Any]:
|
||||
start = time.time()
|
||||
self.read_file_first(sample)
|
||||
sample[self.text_key] = self._file_with_short_or_long_length_filter(sample[self.text_key],
|
||||
sample[self.filename_key])
|
||||
logger.info(f"fileName: {sample[self.filename_key]}, "
|
||||
f"method: FileWithShortOrLongLengthFilter costs {(time.time() - start):6f} s")
|
||||
return sample
|
||||
|
||||
def _strip_unicode_whitespace(self, text: str):
|
||||
# 常见 Unicode 空格符(涵盖普通空格、全角空格、零宽空格等)
|
||||
pattern = r'[\u0020\u00A0\u1680\u2000-\u200F\u202F\u205F\u3000]+'
|
||||
# 匹配首尾的空格符
|
||||
pattern = fr'^{pattern}|{pattern}$'
|
||||
return re.sub(pattern, '', text)
|
||||
|
||||
def _file_with_short_or_long_length_filter(self, input_data: str, file_name):
|
||||
input_data_tmp = self._strip_unicode_whitespace(input_data)
|
||||
if len(input_data_tmp) < self._file_minimum_length or len(input_data_tmp) > self._file_maximum_length:
|
||||
logger.info(f"The length of input_data is: {len(input_data_tmp)}, "
|
||||
f"which is not within the threshold range of {self._file_minimum_length} "
|
||||
f"and {self._file_maximum_length}. {file_name} is filtered.")
|
||||
return ""
|
||||
return input_data
|
||||
Reference in New Issue
Block a user