Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion app/db/db_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand All @@ -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({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

возможно, полезно будет сохранять ещё какую-то информацию об изображении (страница, где она расположена или другая информация, которая поможет найти это изображение в тексте?)

'check_id': check_id,
'image_data': image_data,
'caption': caption,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

откуда берется подпись / что она из себя представляет? есть ли у всех изображений - или это подпись "Рис. 2 - ...", которую кто-то может не указать?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Да, это подпись "Рис. 2 - ..."
Подпись извлекается в файле docx_uploader.py в строках 273-285

'image_size': image_size
})
images_collection.insert_one(image.pack())
print(str(check_id) + " " + str(caption))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Удалите отладочные комментари



# Returns user if user was created and None if already exists
def add_user(username, password_hash='', is_LTI=False):
Expand Down
17 changes: 17 additions & 0 deletions app/db/db_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions app/main/check_packs/pack_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
["max_abstract_size_check"],
["theme_in_report_check"],
["empty_task_page_check"],
['image_quality_check'],
]

DEFAULT_TYPE = 'pres'
Expand Down
1 change: 1 addition & 0 deletions app/main/checks/report_checks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions app/main/checks/report_checks/image_quality_check.py
Original file line number Diff line number Diff line change
@@ -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}' не может быть обработано.<br>")
continue

self.find_params(img_cv)

if self.laplacian_score is None or self.entropy_score is None:
deny_list.append(f"Изображение с подписью '{img.caption}' не может быть обработано.<br>")
continue

if self.laplacian_score < self.min_laplacian:
deny_list.append(f"Изображение с подписью '{img.caption}' имеет низкий показатель лапласиана: {self.laplacian_score} (минимум {self.min_laplacian}).<br>")

if self.entropy_score < self.min_entropy:
deny_list.append(f"Изображение с подписью '{img.caption}' имеет низкую энтропию: {self.entropy_score} (минимум {self.min_entropy}).<br>")
Comment on lines +35 to +39
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Рядовой студент точно не будет знать (или гуглить), кто такое лапласиан и энтропия - возможно, стоит сделать пояснение (= пользователь должен понять, что и как ему исправить)

else:
return answer(False, 'Изображения не найдены!')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Гипотетически работа может быть без рисунков (и вроде как это не будет нарушением)

if deny_list:
return answer(False, f'Изображения нечитаемы! <br>{"".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))
39 changes: 36 additions & 3 deletions app/main/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,39 @@
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')):
new_filepath = 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
Comment on lines +30 to +41
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не совсем понимаю происходящее, но проверки по имени файла (и затем поиск её по нему) - не подходит, поскольку студенты могут загружать файл с одним и тем же названием по несколько раз (при этом содержимое разное)



elif tmp_filepath.endswith(('.doc', '.odt', '.docx', )):
new_filepath = filepath
if tmp_filepath.endswith(('.doc', '.odt')):
Expand All @@ -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
Comment on lines +53 to 65
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

избавьтесь от дублирования кода (с блоком выше)


elif tmp_filepath.endswith('.md' ):
Expand All @@ -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
39 changes: 39 additions & 0 deletions app/main/presentations/pptx/presentation_pptx.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
1 change: 1 addition & 0 deletions app/main/reports/document_uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def __init__(self):
self.literature_page = 0
self.first_lines = []
self.page_count = 0
self.images = []

@abstractmethod
def upload(self):
Expand Down
65 changes: 65 additions & 0 deletions app/main/reports/docx_uploader/docx_uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,71 @@ def show_chapters(self, work_type):
chapters_str += "&nbsp;&nbsp;&nbsp;&nbsp;" + header["text"] + "<br>"
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'})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Правильно ли я понимаю, что 2006 тут и дальше - это год какого-то стандарта? не может ли в документе быть другого? (и вдруг мы что-то пропустим)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Да, 2006 в пространствах имен указывает на год, когда были определены стандарты Office Open XML.
Эти стандарты остаются актуальными и в более поздних версиях Office, включая Office 2016, Office 2019, Office 2021 и Microsoft 365, так что должны ничего не пропустить.

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
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading