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