diff --git a/app/db/db_methods.py b/app/db/db_methods.py
index 215c6e4c..c2436d95 100644
--- a/app/db/db_methods.py
+++ b/app/db/db_methods.py
@@ -7,7 +7,7 @@
from pymongo import MongoClient
from utils import convert_to
-from .db_types import User, Presentation, Check, Consumers, Logs
+from .db_types import User, Presentation, Check, Consumers, Logs, Image
client = MongoClient("mongodb://mongodb:27017")
db = client['pres-parser-db']
@@ -21,11 +21,32 @@
logs_collection = db.create_collection(
'logs', capped=True, size=5242880) if not db['logs'] else db['logs']
celery_check_collection = db['celery_check'] # collection for mapping celery_task to check
+images_collection = db['images'] # коллекция для хранения изображений
def get_client():
return client
+def get_images(check_id):
+ images = images_collection.find({'check_id': str(check_id)})
+ if images is not None:
+ image_list = []
+ for img in images:
+ image_list.append(Image(img))
+ return image_list
+ else:
+ return None
+
+def save_image_to_db(check_id, image_data, caption, image_size):
+ image = Image({
+ 'check_id': check_id,
+ 'image_data': image_data,
+ 'caption': caption,
+ 'image_size': image_size
+ })
+ images_collection.insert_one(image.pack())
+ print(str(check_id) + " " + str(caption))
+
# Returns user if user was created and None if already exists
def add_user(username, password_hash='', is_LTI=False):
diff --git a/app/db/db_types.py b/app/db/db_types.py
index 049e5bdc..029be0f6 100644
--- a/app/db/db_types.py
+++ b/app/db/db_types.py
@@ -150,3 +150,20 @@ def none_to_false(x):
is_ended = none_to_true(self.is_ended) # None for old checks => True, True->True, False->False
is_failed = none_to_false(self.is_failed) # None for old checks => False, True->True, False->False
return {'is_ended': is_ended, 'is_failed': is_failed}
+
+class Image(PackableWithId):
+ def __init__(self, dictionary=None):
+ super().__init__(dictionary)
+ dictionary = dictionary or {}
+ self.check_id = dictionary.get('check_id') # Привязка к check_id
+ self.caption = dictionary.get('caption', '') # Подпись к изображению
+ self.image_data = dictionary.get('image_data') # Файл изображения в формате bindata
+ self.image_size = dictionary.get('image_size') # Размер изображения в сантимерах
+
+ def pack(self):
+ package = super().pack()
+ package['check_id'] = str(self.check_id)
+ package['caption'] = self.caption
+ package['image_data'] = self.image_data
+ package['image_size'] = self.image_size
+ return package
diff --git a/app/main/check_packs/pack_config.py b/app/main/check_packs/pack_config.py
index 407d212d..9174674a 100644
--- a/app/main/check_packs/pack_config.py
+++ b/app/main/check_packs/pack_config.py
@@ -45,6 +45,7 @@
["max_abstract_size_check"],
["theme_in_report_check"],
["empty_task_page_check"],
+ ['image_quality_check'],
]
DEFAULT_TYPE = 'pres'
diff --git a/app/main/checks/report_checks/__init__.py b/app/main/checks/report_checks/__init__.py
index e8e58aad..3da2b1af 100644
--- a/app/main/checks/report_checks/__init__.py
+++ b/app/main/checks/report_checks/__init__.py
@@ -25,6 +25,7 @@
from .max_abstract_size_check import ReportMaxSizeOfAbstractCheck
from .template_name import ReportTemplateNameCheck
from .empty_task_page_check import EmptyTaskPageCheck
+from .image_quality_check import ImageQualityCheck
from .sw_section_banned_words import SWSectionBannedWordsCheck
from .sw_section_lit_reference import SWSectionLiteratureReferenceCheck
from .sw_tasks import SWTasksCheck
diff --git a/app/main/checks/report_checks/image_quality_check.py b/app/main/checks/report_checks/image_quality_check.py
new file mode 100644
index 00000000..68eca342
--- /dev/null
+++ b/app/main/checks/report_checks/image_quality_check.py
@@ -0,0 +1,54 @@
+from ..base_check import BaseReportCriterion, answer
+import cv2
+import numpy as np
+
+class ImageQualityCheck(BaseReportCriterion):
+ label = "Проверка качества изображений"
+ description = ''
+ id = 'image_quality_check'
+ # необходимо подобрать min_laplacian и min_entropy
+ def __init__(self, file_info, min_laplacian=10, min_entropy=1):
+ super().__init__(file_info)
+ self.images = self.file.images
+ self.min_laplacian = min_laplacian
+ self.min_entropy = min_entropy
+ self.laplacian_score = None
+ self.entropy_score = None
+
+ def check(self):
+ deny_list = []
+ if self.images:
+ for img in self.images:
+ image_array = np.frombuffer(img.image_data, dtype=np.uint8)
+ img_cv = cv2.imdecode(image_array, cv2.IMREAD_COLOR)
+
+ if img_cv is None:
+ deny_list.append(f"Изображение с подписью '{img.caption}' не может быть обработано.
")
+ continue
+
+ self.find_params(img_cv)
+
+ if self.laplacian_score is None or self.entropy_score is None:
+ deny_list.append(f"Изображение с подписью '{img.caption}' не может быть обработано.
")
+ continue
+
+ if self.laplacian_score < self.min_laplacian:
+ deny_list.append(f"Изображение с подписью '{img.caption}' имеет низкий показатель лапласиана: {self.laplacian_score} (минимум {self.min_laplacian}).
")
+
+ if self.entropy_score < self.min_entropy:
+ deny_list.append(f"Изображение с подписью '{img.caption}' имеет низкую энтропию: {self.entropy_score} (минимум {self.min_entropy}).
")
+ else:
+ return answer(False, 'Изображения не найдены!')
+ if deny_list:
+ return answer(False, f'Изображения нечитаемы!
{"".join(deny_list)}')
+ else:
+ return answer(True, 'Изображения корректны!')
+
+ def find_params(self, image):
+ if image is None or image.size == 0:
+ return None, None
+ gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
+ self.laplacian_score = cv2.Laplacian(gray_image, cv2.CV_64F).var()
+ hist, _ = np.histogram(gray_image.flatten(), bins=256, range=[0, 256])
+ hist = hist / hist.sum()
+ self.entropy_score = -np.sum(hist * np.log2(hist + 1e-10))
\ No newline at end of file
diff --git a/app/main/parser.py b/app/main/parser.py
index 593b8cfd..fb60a19d 100644
--- a/app/main/parser.py
+++ b/app/main/parser.py
@@ -8,10 +8,15 @@
from main.reports.md_uploader import MdUploader
from utils import convert_to
-logger = logging.getLogger('root_logger')
+from os.path import basename
+from app.db.db_methods import add_check
+from app.db.db_types import Check
+logger = logging.getLogger('root_logger')
def parse(filepath, pdf_filepath):
+ from app.db.db_methods import files_info_collection
+
tmp_filepath = filepath.lower()
try:
if tmp_filepath.endswith(('.odp', '.ppt', '.pptx')):
@@ -19,7 +24,23 @@ def parse(filepath, pdf_filepath):
if tmp_filepath.endswith(('.odp', '.ppt')):
logger.info(f"Презентация {filepath} старого формата. Временно преобразована в pptx для обработки.")
new_filepath = convert_to(filepath, target_format='pptx')
- file_object = PresentationPPTX(new_filepath)
+
+ presentation = PresentationPPTX(new_filepath)
+
+ check = Check({
+ 'filename': basename(new_filepath),
+ })
+
+ file_id = 0
+ file = files_info_collection.find_one({'name': basename(new_filepath)})
+ if file:
+ file_id = file['_id']
+
+ check_id = add_check(file_id, check)
+ presentation.extract_images_with_captions(check_id)
+ file_object = presentation
+
+
elif tmp_filepath.endswith(('.doc', '.odt', '.docx', )):
new_filepath = filepath
if tmp_filepath.endswith(('.doc', '.odt')):
@@ -28,7 +49,19 @@ def parse(filepath, pdf_filepath):
docx = DocxUploader()
docx.upload(new_filepath, pdf_filepath)
+
+ check = Check({
+ 'filename': basename(new_filepath),
+ })
+
+ file_id = 0
+ file = files_info_collection.find_one({'name': basename(new_filepath)})
+ if file:
+ file_id = file['_id']
+
+ check_id = add_check(file_id, check)
docx.parse()
+ docx.extract_images_with_captions(check_id)
file_object = docx
elif tmp_filepath.endswith('.md' ):
@@ -54,4 +87,4 @@ def save_to_temp_file(file):
temp_file.write(file.read())
temp_file.close()
file.seek(0)
- return temp_file.name
+ return temp_file.name
\ No newline at end of file
diff --git a/app/main/presentations/pptx/presentation_pptx.py b/app/main/presentations/pptx/presentation_pptx.py
index dd909f8c..a8b8581f 100644
--- a/app/main/presentations/pptx/presentation_pptx.py
+++ b/app/main/presentations/pptx/presentation_pptx.py
@@ -1,4 +1,7 @@
+from io import BytesIO
+
from pptx import Presentation
+from pptx.enum.shapes import MSO_SHAPE_TYPE
from .slide_pptx import SlidePPTX
from ..presentation_basic import PresentationBasic
@@ -17,3 +20,39 @@ def add_slides(self):
def __str__(self):
return super().__str__()
+
+ def extract_images_with_captions(self, check_id):
+ from app.db.db_methods import save_image_to_db
+
+ # Проход по каждому слайду в презентации
+ for slide in self.slides:
+ image_found = False
+ image_data = None
+ caption_text = None
+
+ # Проход по всем фигурам на слайде
+ for shape in slide.slide.shapes: # Используем slide.slide для доступа к текущему слайду
+ if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
+ image_found = True
+ image_part = shape.image # Получаем объект изображения
+
+ # Извлекаем бинарные данные изображения
+ image_stream = image_part.blob
+ image_data = BytesIO(image_stream)
+
+ # Если мы нашли изображение, ищем следующий непустой текст как подпись
+ if image_found:
+ for shape in slide.slide.shapes:
+ if not shape.has_text_frame:
+ continue
+ text = shape.text.strip()
+ if text: # Находим непустое текстовое поле (предположительно, это подпись)
+ caption_text = text
+ # Сохраняем изображение и его подпись
+ save_image_to_db(check_id, image_data.getvalue(), caption_text)
+ break # Предполагаем, что это подпись к текущему изображению
+
+ # Сброс флага и данных изображения для следующего цикла
+ image_found = False
+ image_data = None
+ caption_text = None
diff --git a/app/main/reports/document_uploader.py b/app/main/reports/document_uploader.py
index d0653fae..8a6a7303 100644
--- a/app/main/reports/document_uploader.py
+++ b/app/main/reports/document_uploader.py
@@ -12,6 +12,7 @@ def __init__(self):
self.literature_page = 0
self.first_lines = []
self.page_count = 0
+ self.images = []
@abstractmethod
def upload(self):
diff --git a/app/main/reports/docx_uploader/docx_uploader.py b/app/main/reports/docx_uploader/docx_uploader.py
index ac30dee4..421dfbe2 100644
--- a/app/main/reports/docx_uploader/docx_uploader.py
+++ b/app/main/reports/docx_uploader/docx_uploader.py
@@ -242,6 +242,71 @@ def show_chapters(self, work_type):
chapters_str += " " + header["text"] + "
"
return chapters_str
+ def extract_images_with_captions(self, check_id):
+ from app.db.db_methods import save_image_to_db, get_images
+
+ emu_to_cm = 360000
+ image_found = False
+ image_data = None
+ if not self.images:
+ # Проход по всем параграфам документа
+ for i, paragraph in enumerate(self.file.paragraphs):
+ width_emu = None
+ height_emu = None
+ # Проверяем, есть ли в параграфе встроенные объекты
+ for run in paragraph.runs:
+ if "graphic" in run._element.xml: # может быть изображение
+
+ # Извлечение бинарных данных изображения
+ image_streams = run._element.findall('.//a:blip', namespaces={
+ 'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'})
+ for image_stream in image_streams:
+ embed_id = image_stream.get(
+ '{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed')
+ if embed_id:
+ image_found = True
+ image_part = self.file.part.related_parts[embed_id]
+ image_data = image_part.blob
+ extent = run._element.find('.//wp:extent', namespaces={
+ 'wp': 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing'})
+ if extent is not None:
+ width_emu = int(extent.get('cx'))
+ height_emu = int(extent.get('cy'))
+ width_cm = width_emu / emu_to_cm
+ height_cm = height_emu / emu_to_cm
+ # Если мы уже нашли изображение, ищем следующий непустой параграф для подписи
+ if image_found:
+ # Переход к следующему параграфу
+ next_paragraph_index = i + 1
+
+ # Проверяем, есть ли следующий параграф
+ if next_paragraph_index < len(self.file.paragraphs):
+ while next_paragraph_index < len(self.file.paragraphs):
+ next_paragraph = self.file.paragraphs[next_paragraph_index]
+ next_paragraph_text = next_paragraph.text.strip()
+
+ # Проверка, не содержит ли следующий параграф также изображение
+ contains_image = any(
+ "graphic" in run._element.xml for run in next_paragraph.runs
+ )
+
+ # Если параграф не содержит изображения и текст не пуст, то это подпись
+ if not contains_image and next_paragraph_text:
+ # Сохраняем изображение и его подпись
+ save_image_to_db(check_id, image_data, next_paragraph_text, (width_cm, height_cm))
+ break
+ else:
+ save_image_to_db(check_id, image_data, "picture without caption", (width_cm, height_cm))
+ break
+ else:
+ save_image_to_db(check_id, image_data, "picture without caption", (width_cm, height_cm))
+
+ image_found = False # Сброс флага, чтобы искать следующее изображение
+ image_data = None # Очистка данных изображения
+ self.images = get_images(check_id)
+
+
+
def main(args):
file = args.file
diff --git a/requirements.txt b/requirements.txt
index 8710f80b..23e7a0c1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -35,3 +35,4 @@ filetype==1.2.0
language-tool-python==2.8.1
markdown==3.4.4
md2pdf==1.0.1
+opencv-python==4.5.5.64
\ No newline at end of file