Compare commits

...

13 Commits

Author SHA1 Message Date
9b946a9c5a Merge pull request 'Добавление веб-версии сервиса' (#5) from webapp into master
Reviewed-on: #5
2023-07-11 13:25:17 +04:00
Vladislav Moiseev
3a365101bf Исправлено отображение на мобильных устройствах 2023-07-09 10:10:08 +04:00
Vladislav Moiseev
5bcd006624 Убраны требования отдельно под mac 2023-07-09 00:18:59 +04:00
Vladislav Moiseev
8bba4e72d6 Добавлена загрузка своих онтологий, немного красоты в webapp 2023-07-09 00:02:26 +04:00
Vladislav Moiseev
898a94a541 Исправление пути до файлов в конфигурации PyCharm 2023-07-08 21:18:37 +04:00
Vladislav Moiseev
9a874a3b8d Обновлена инструкция по запуску через CLI 2023-07-08 11:00:28 +04:00
Vladislav Moiseev
8e92237fec Добавлен показатель загрузки 2023-07-08 10:18:50 +04:00
Vladislav Moiseev
e1c1ca56ec Скрипты и стили вынесены в отдельные файлы 2023-07-08 09:44:50 +04:00
Vladislav Moiseev
354d5fdbd5 Немного красоты 2023-07-07 23:34:24 +04:00
Vladislav Moiseev
d70592fc64 Немного унифицирована работа с запросами к KB 2023-07-07 23:21:12 +04:00
Vladislav Moiseev
52335870fd Перемещение исходников в директорию src 2023-07-07 22:14:30 +04:00
Vladislav Moiseev
b7c7f8e066 Небольшое приведение кода и README в порядок 2023-07-07 22:10:13 +04:00
Vladislav Moiseev
5da1ecb2e2 Первая версия webapp 2023-07-06 07:32:59 +04:00
19 changed files with 476 additions and 194 deletions

8
.prettierrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 160,
"tabWidth": 2,
"endOfLine": "auto"
}

View File

@ -11,7 +11,7 @@
<option name="IS_MODULE_SDK" value="true" /> <option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/main.py" /> <option name="SCRIPT_NAME" value="$PROJECT_DIR$/src/main.py" />
<option name="PARAMETERS" value="5cc5570b-6ed9-3b33-9db4-bdb8ecb9f890 &quot;test-data/lectionAudi/2021-03-12 13-51-07.JPG&quot;" /> <option name="PARAMETERS" value="5cc5570b-6ed9-3b33-9db4-bdb8ecb9f890 &quot;test-data/lectionAudi/2021-03-12 13-51-07.JPG&quot;" />
<option name="SHOW_COMMAND_LINE" value="false" /> <option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" /> <option name="EMULATE_TERMINAL" value="false" />

View File

@ -11,7 +11,7 @@
<option name="IS_MODULE_SDK" value="true" /> <option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/main.py" /> <option name="SCRIPT_NAME" value="$PROJECT_DIR$/src/main.py" />
<option name="PARAMETERS" value="5cc5570b-6ed9-3b33-9db4-bdb8ecb9f890 &quot;test-data/lectionAudi/2021-03-12 13-49-32.JPG&quot;" /> <option name="PARAMETERS" value="5cc5570b-6ed9-3b33-9db4-bdb8ecb9f890 &quot;test-data/lectionAudi/2021-03-12 13-49-32.JPG&quot;" />
<option name="SHOW_COMMAND_LINE" value="false" /> <option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" /> <option name="EMULATE_TERMINAL" value="false" />

View File

@ -11,7 +11,7 @@
<option name="IS_MODULE_SDK" value="true" /> <option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/main.py" /> <option name="SCRIPT_NAME" value="$PROJECT_DIR$/src/main.py" />
<option name="PARAMETERS" value="5cc5570b-6ed9-3b33-9db4-bdb8ecb9f890 &quot;test-data/lectionAudi/2021-03-12 13-48-31.JPG&quot;" /> <option name="PARAMETERS" value="5cc5570b-6ed9-3b33-9db4-bdb8ecb9f890 &quot;test-data/lectionAudi/2021-03-12 13-48-31.JPG&quot;" />
<option name="SHOW_COMMAND_LINE" value="false" /> <option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" /> <option name="EMULATE_TERMINAL" value="false" />

25
.run/webApp.run.xml Normal file
View File

@ -0,0 +1,25 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="webApp" type="PythonConfigurationType" factoryName="Python">
<module name="VideoAnalysis" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="FLASK_APP" value="src/webApp.py" />
<env name="FLASK_DEBUG" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="flask" />
<option name="PARAMETERS" value="run" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="true" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>

20
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "src/webApp.py",
"FLASK_DEBUG": "1"
},
"args": ["run", "--no-debugger", "--no-reload"]
}
]
}

View File

@ -1,22 +1,34 @@
Окружение: Python 3.10 # Сервис анализа фотографий при помощи базы знаний
Сервис: http://kb.athene.tech/swagger-ui/index.html ## Настройка и запуск
Ручная установка зависимостей: Требуемое окружение: Python 3.10+.
Настройка виртуальной среды и установка зависимостей:
```commandline ```commandline
pip install -r requirements.txt python3 -m venv --clear .venv
``` # или
python -m venv --clear .venv
Ручная установка зависимостей для mac с Apple Silicon: # для batch
.venv\Scripts\activate.bat
# для powershell
.\.venv\Scripts\Activate.ps1
# для bash (в т.ч. mac os x)
source .venv/bin/activate
```commandline python -m pip install -r requirements.txt
pip install -r requirements-mac.txt
``` ```
Запуск: Запуск:
```commandline ```commandline
main.py <Ontology UID> <Image Path> python -m flask --app src/webApp.py run
main.py 5cc5570b-6ed9-3b33-9db4-bdb8ecb9f890 "test-data/lectionAudi/2021-03-12 13-48-31.JPG"
``` ```
После этого в браузере необходимо перейти по ссылке <http://127.0.0.1:5000/>.
## Полезные ссылки
* REST-сервис работы с онтологиями: <http://kb.athene.tech/swagger-ui/index.html>.

View File

@ -1,63 +0,0 @@
import numpy as np
import requests
def is_ontology_exists(uid: str, url: str) -> bool:
'''
Проверяет, существует ли онтология в сервисе.
@param uid: УИД онтологии.
@param url: Базовый URL сервиса.
'''
list_ontologies = requests.get(url).json()['response']['items']
for onto in list_ontologies:
if onto['uid'] == uid:
return True
return False
def rename_entity(list_names: dict) -> dict:
'''
Нормализация названий объектов.
@param list_names: Список названий объектов.
'''
temp_list = list()
for entity in list_names.values():
entity: str
temp_list.append(entity.title().replace(' ', ''))
return temp_list
def get_entity_square(width: float, height: float) -> float:
'''
Получение площади занимаемой области.
@param width: Ширина области в px.
@param height: Высота области в px.
'''
return abs(width * height)
def get_request_data(entities: dict, objects: np.ndarray, confs: np.ndarray, boxes: np.ndarray) -> tuple[list, list]:
'''
Формирование данных для сервиса онтологий.
@param entities: Список имён объектов.
@param results_ndarray: Результат распознавания объектов.
'''
classroom = 'classroom'
entities = rename_entity(entities)
object_properties = list()
data_properties = list()
for entity_idx, entity in enumerate(entities):
if (entity_idx in objects):
object_properties.append({'domain': entity, 'property': 'locatedIn', 'range': classroom})
else:
object_properties.append({'domain': entity, 'property': 'notLocatedIn', 'range': classroom})
for object_idx, object in enumerate(objects):
conf = confs[object_idx]
box = boxes[object_idx]
entity = entities[object.item()]
data_properties.append({'domain': entity, 'property': 'hasArea', 'value': get_entity_square(float(box[2]), float(box[3]))})
data_properties.append({'domain': entity, 'property': 'hasConfidence', 'value': float(conf)})
return object_properties, data_properties

View File

@ -1,50 +0,0 @@
absl-py==1.4.0
astunparse==1.6.3
cachetools==5.3.0
certifi==2023.5.7
charset-normalizer==3.1.0
flatbuffers==23.5.9
gast==0.4.0
google-auth==2.18.1
google-auth-oauthlib==1.0.0
google-pasta==0.2.0
grpcio==1.54.2
h5py==3.8.0
idna==3.4
jax==0.4.10
keras==2.12.0
libclang==16.0.0
Markdown==3.4.3
MarkupSafe==2.1.2
matplotlib==3.7.1
ml-dtypes==0.1.0
numpy==1.23.5
oauthlib==3.2.2
opencv-python==4.7.0.72
opt-einsum==3.3.0
packaging==23.1
pandas==2.0.1
Pillow==9.5.0
protobuf==4.23.1
psutil==5.9.5
PyYAML==6.0
pyasn1==0.5.0
pyasn1-modules==0.3.0
requests==2.30.0
requests-oauthlib==1.3.1
rsa==4.9
scipy==1.10.1
six==1.16.0
tensorboard==2.12.3
tensorboard-data-server==0.7.0
tensorflow_macos==2.12.0
tensorflow-estimator==2.12.0
termcolor==2.3.0
torch==2.0.1
torchvision==0.15.2
tqdm==4.65.0
typing_extensions==4.5.0
ultralytics==8.0.105
urllib3==1.26.15
Werkzeug==2.3.4
wrapt==1.14.1

View File

@ -1,52 +1,41 @@
absl-py==1.4.0 blinker==1.6.2
astunparse==1.6.3
cachetools==5.3.0
certifi==2023.5.7 certifi==2023.5.7
charset-normalizer==3.1.0 charset-normalizer==3.1.0
flatbuffers==23.5.9 click==8.1.3
gast==0.4.0 colorama==0.4.6
google-auth==2.18.1 contourpy==1.1.0
google-auth-oauthlib==1.0.0 cycler==0.11.0
google-pasta==0.2.0 filelock==3.12.2
grpcio==1.54.2 Flask==2.3.2
h5py==3.8.0 fonttools==4.40.0
idna==3.4 idna==3.4
jax==0.4.10 itsdangerous==2.1.2
keras==2.12.0 Jinja2==3.1.2
libclang==16.0.0 kiwisolver==1.4.4
Markdown==3.4.3 MarkupSafe==2.1.3
MarkupSafe==2.1.2
matplotlib==3.7.1 matplotlib==3.7.1
ml-dtypes==0.1.0 mpmath==1.3.0
numpy==1.23.5 networkx==3.1
oauthlib==3.2.2 numpy==1.25.0
opencv-python==4.7.0.72 opencv-python==4.7.0.72
opt-einsum==3.3.0
packaging==23.1 packaging==23.1
pandas==2.0.1 pandas==2.0.2
Pillow==9.5.0 Pillow==9.5.0
protobuf==4.23.1
psutil==5.9.5 psutil==5.9.5
pyparsing==3.1.0
python-dateutil==2.8.2
pytz==2023.3
PyYAML==6.0 PyYAML==6.0
pyasn1==0.5.0 requests==2.31.0
pyasn1-modules==0.3.0 scipy==1.11.0
requests==2.30.0 seaborn==0.12.2
requests-oauthlib==1.3.1
rsa==4.9
scipy==1.10.1
six==1.16.0 six==1.16.0
tensorboard==2.12.3 sympy==1.12
tensorboard-data-server==0.7.0
tensorflow==2.12.0
tensorflow-estimator==2.12.0
tensorflow-intel==2.12.0
tensorflow-io-gcs-filesystem==0.31.0
termcolor==2.3.0
torch==2.0.1 torch==2.0.1
torchvision==0.15.2 torchvision==0.15.2
tqdm==4.65.0 tqdm==4.65.0
typing_extensions==4.5.0 typing_extensions==4.6.3
ultralytics==8.0.105 tzdata==2023.3
urllib3==1.26.15 ultralytics==8.0.123
Werkzeug==2.3.4 urllib3==2.0.3
wrapt==1.14.1 Werkzeug==2.3.6

View File

@ -1,18 +1,18 @@
import cv2 as cv import cv2 as cv
import numpy as np import numpy as np
from main import img_size as size img_size = (1280, 720) # Размер изображения для нормализации.
def image_transform(image: np.ndarray) -> np.ndarray: def image_transform(image: np.ndarray) -> np.ndarray:
''' '''
Трансформирует изображение нужным образом. Трансформирует изображение нужным образом.
@param image: Исходная матрица с представлением изображения. @param image: Исходная матрица с представлением изображения.
''' '''
image = cv.resize(image, (size[0], size[1])) image = cv.resize(image, (img_size[0], img_size[1]))
return image[:, :, ::-1] return image[:, :, ::-1]
def get_image_as_array(image_name: str) -> np.ndarray: def get_image_file_as_array(image_name: str) -> np.ndarray:
''' '''
Получает изображение из файла и нормализует его. Получает изображение из файла и нормализует его.
@param image_name: Путь до изображения. @param image_name: Путь до изображения.
@ -21,3 +21,13 @@ def get_image_as_array(image_name: str) -> np.ndarray:
image: np.ndarray # приведение типов image: np.ndarray # приведение типов
image = image_transform(image) image = image_transform(image)
return image return image
def get_image_buf_as_array(buf) -> np.ndarray:
'''
Получает изображение из буфера и нормализует его.
@param image_name: Путь до изображения.
'''
image = cv.imdecode(buf, cv.IMREAD_COLOR)
image: np.ndarray # приведение типов
image = image_transform(image)
return image

View File

@ -2,31 +2,25 @@ import os
import sys import sys
import cv2 as cv import cv2 as cv
import requests import numpy as np
import imageWorking import imageWorking
import neuralNetwork import neuralNetwork
import ontologyWorking import ontologyWorking
url = 'http://kb.athene.tech/api/1.0/ontology/'
img_path = 'data'
img_size = (1280, 720) # Размер изображения для нормализации.
def analyze_base(ontology_uid: str, image: np.ndarray, queries: list[str]) -> tuple[2]:
def analyze_file(uid: str, image_path: str) -> None:
''' '''
Анализирует файл и выводит результат в консоль. Базовая функция анализа файла и вывода результатов обработки.
@param uid: УИД онтологии. @param ontology_uid: УИД онтологии.
@param url: Базовый URL сервиса. @param image: Изображение.
''' '''
if not ontologyWorking.is_ontology_exists(uid, url): if image is None:
raise Exception(f'Онтология с uid {uid} не существует') raise Exception(f'Изображение не указано')
if not os.path.isfile(image_path):
raise Exception(f'Изображение {image_path} не существует')
model = neuralNetwork.load_model() model = neuralNetwork.load_model()
# Распознавание изображения. # Распознавание изображения.
results = model.predict(source=imageWorking.get_image_as_array(image_path)) results = model.predict(source=image)
# Создание аксиом онтологии на основе результатов распознавания. # Создание аксиом онтологии на основе результатов распознавания.
object_properties = list() object_properties = list()
@ -39,23 +33,32 @@ def analyze_file(uid: str, image_path: str) -> None:
object_properties += request[0] object_properties += request[0]
data_properties += request[1] data_properties += request[1]
# Формирование данных для запроса к сервису работы с онтологиями.
data = { # Выполнение запроса к сервису работы с онтологиями
'data': response = ontologyWorking.analyze(ontology_uid, object_properties, data_properties, queries)
{
'objectPropertyAssertions': object_properties, return results, response
'dataPropertyAssertions': data_properties
}
} def analyze_file(ontology_uid: str, image_path: str) -> None:
'''
Анализирует файл и выводит результат в консоль.
@param ontology_uid: УИД онтологии.
@param image_path: Путь до изображения.
'''
if not os.path.isfile(image_path):
raise Exception(f'Изображение {image_path} не существует')
image = imageWorking.get_image_file_as_array(image_path)
queries = [ 'QueryGetNotEmpty', 'QueryGetCheck', 'QueryGetEmpty' ]
# Распознавание изображения.
results, response = analyze_base(ontology_uid, image, queries)
result = { result = {
'QueryGetNotEmpty': '', 'QueryGetNotEmpty': '',
'QueryGetCheck': '', 'QueryGetCheck': '',
'QueryGetEmpty': '' 'QueryGetEmpty': ''
} }
params = '&'.join([f'names={query}' for query in result.keys()])
# Выполнение запроса.
response = requests.post(url + f'{uid}/query/multi?{params}', json=data).json()
if response['error']: if response['error']:
raise Exception(response['error']) raise Exception(response['error'])
for query in response['response']: for query in response['response']:
@ -79,6 +82,7 @@ def analyze_file(uid: str, image_path: str) -> None:
cv.waitKey(0) cv.waitKey(0)
cv.destroyAllWindows() cv.destroyAllWindows()
# Точка входа в приложение. # Точка входа в приложение.
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) != 3: if len(sys.argv) != 3:

128
src/ontologyWorking.py Normal file
View File

@ -0,0 +1,128 @@
import numpy as np
import requests
url = 'http://kb.athene.tech/api/1.0/ontology/'
def is_ontology_exists(ontology_uid: str, url: str) -> bool:
'''
Проверяет, существует ли онтология в сервисе.
@param ontology_uid: УИД онтологии.
@param url: Базовый URL сервиса.
'''
list_ontologies = requests.get(url).json()
list_ontologies = list_ontologies['response']['items']
for onto in list_ontologies:
if onto['uid'] == ontology_uid:
return True
return False
def upload_ontology(name: str, file_buf) -> str:
"""
Загружает файл.
:param name: Имя файла.
:param file_buf: Содержимое файла.
:return: УИД загруженной онтологии.
"""
files = {'file': file_buf}
response = requests.post(f'{url}?name={name}', files=files)
response = response.json()
return response['response']['uid']
def delete_ontology(ontology_uid: str) -> bool:
"""
Загружает файл.
:param name: Имя файла.
:param file_buf: Содержимое файла.
:return: УИД загруженной онтологии.
"""
response = requests.delete(url + ontology_uid)
response = response.json()
return False if response['error'] else True
def rename_entity(list_names: dict) -> dict:
'''
Нормализация названий объектов.
@param list_names: Список названий объектов.
'''
temp_list = list()
for entity in list_names.values():
entity: str
temp_list.append(entity.title().replace(' ', ''))
return temp_list
def get_entity_square(width: float, height: float) -> float:
'''
Получение площади занимаемой области.
@param width: Ширина области в px.
@param height: Высота области в px.
'''
return abs(width * height)
def get_request_data(entities: dict, objects: np.ndarray, confs: np.ndarray, boxes: np.ndarray) -> tuple[list, list]:
'''
Формирование данных для сервиса онтологий.
@param entities: Список имён объектов.
@param results_ndarray: Результат распознавания объектов.
'''
classroom = 'classroom'
entities = rename_entity(entities)
object_properties = list()
data_properties = list()
for entity_idx, entity in enumerate(entities):
if (entity_idx in objects):
object_properties.append(
{'domain': entity, 'property': 'locatedIn', 'range': classroom})
else:
object_properties.append(
{'domain': entity, 'property': 'notLocatedIn', 'range': classroom})
for object_idx, object in enumerate(objects):
conf = confs[object_idx]
box = boxes[object_idx]
entity = entities[object.item()]
data_properties.append(
{'domain': entity,
'property': 'hasArea',
'value': get_entity_square(float(box[2]), float(box[3]))})
data_properties.append(
{'domain': entity,
'property': 'hasConfidence',
'value': float(conf)})
return object_properties, data_properties
def analyze(ontology_uid: str, object_properties: list, data_properties: list, queries: list[str]) -> tuple[2]:
'''
Базовая функция анализа.
@param ontology_uid: УИД онтологии.
@param object_properties: Объектные свойства.
@param data_properties: Свойства данных.
@param queries: Список запросов для запуска.
'''
if not is_ontology_exists(ontology_uid, url):
raise Exception(f'Онтология с uid {ontology_uid} не существует')
# Формирование данных для запроса к сервису работы с онтологиями.
data = {
'data':
{
'objectPropertyAssertions': object_properties,
'dataPropertyAssertions': data_properties
}
}
params = '&'.join([f'names={query}' for query in queries])
# Выполнение запроса.
response = requests.post(
url + f'{ontology_uid}/query/multi?{params}', json=data).json()
return response

58
src/webApp.py Normal file
View File

@ -0,0 +1,58 @@
import base64
import cv2 as cv
from flask import Flask, redirect, request
import numpy
from imageWorking import get_image_buf_as_array
from main import analyze_base
from ontologyWorking import delete_ontology, upload_ontology
app = Flask(__name__, static_folder="../static", static_url_path="/")
@app.route("/")
def main():
return redirect('index.html')
@app.route("/analyze", methods=["POST"])
def analyze():
# Первоначальные проверки.
if 'image' not in request.files or request.files['image'].filename == '':
return {
'success': False,
'error': 'Укажите изображение',
}
ontology_uid = '5cc5570b-6ed9-3b33-9db4-bdb8ecb9f890'
remove_ontology = False
if 'ontology' in request.files and request.files['ontology'].filename != '':
ontology_uid = upload_ontology(
request.files['ontology'].filename, request.files['ontology'])
remove_ontology = True
# Подготовка исходного изображения.
image_source = request.files['image'].read()
image_source = numpy.fromstring(image_source, numpy.uint8)
image_source = get_image_buf_as_array(image_source)
# Подготовка прочих данных и выполнение запроса.
queries = request.form['queries'].split(
',') if request.form['queries'] is not None else []
results, response = analyze_base(ontology_uid, image_source, queries)
# Если требуется, чистим за собой.
if remove_ontology:
delete_ontology(ontology_uid)
# Подготовка изображения с ответом.
image_result = results[0].plot()
image_result = cv.cvtColor(image_result, cv.COLOR_BGR2RGB)
image_result = cv.imencode(".jpg", image_result)[1]
image_result = base64.b64encode(image_result).decode("utf-8")
# Вывод ответа.
return {
'success': True,
'data': response,
'image': image_result,
}

73
static/index.html Normal file
View File

@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="en" class="h-100">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Анализ аудиторий</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossorigin="anonymous"
/>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
crossorigin="anonymous"
></script>
<link rel="stylesheet" href="style.css" />
<script src="site.js"></script>
</head>
<body class="bg-body-secondary h-100">
<div id="wrapper" class="d-flex align-items-center">
<div class="container py-3">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-lg">
<p class="alert alert-info">Загрузите изображение и, если требуется, собственную онтологию для анализа.</p>
<form id="uploadForm" action="analyze">
<div class="my-3">
<label for="image" class="form-label">Изображение для анализа</label>
<input type="file" class="form-control" name="image" id="image" />
</div>
<div class="my-3">
<label for="ontology" class="form-label">Онтология предметной области</label>
<input type="file" class="form-control" name="ontology" id="ontology" aria-describedby="ontologyHelp" />
<div id="ontologyHelp" class="form-text">Для анализа заполненности аудиторий оставьте это поле пустым.</div>
</div>
<div class="my-3">
<label for="queries" class="form-label">Набор запросов для запуска</label>
<input
type="text"
class="form-control"
name="queries"
id="queries"
aria-describedby="queriesHelp"
value="QueryGetNotEmpty,QueryGetCheck,QueryGetEmpty"
/>
<div id="queriesHelp" class="form-text">Для анализа заполненности аудиторий не изменяйте это поле.</div>
</div>
<div class="my-3">
<button type="submit" class="btn btn-primary">Отправить</button>
</div>
</form>
</div>
<div class="col-lg">
<div id="loaderWrapper" class="d-none">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
<img src="none.png" alt="Результат" id="imgslot" class="w-100" />
<div id="queriesResult"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

BIN
static/none.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

53
static/site.js Normal file
View File

@ -0,0 +1,53 @@
const renderResultMarkup = (query, result) =>
`
<h1 class="display-6">${query}</h1>
<table class="table table-bordered table-striped">
<tr>
${result.columns.map((column) => `<th>${column}</th>`).join('')}
</tr>
${result.rows.map(
(row) =>
`<tr>${Object.entries(row)
.map(([key, value]) => `<td>${value.value}</td>`)
.join('')}</tr>`,
)}
</table>`;
const processAnalyzeResult = (data) => {
const img = document.getElementById('imgslot');
const queriesResult = document.getElementById('queriesResult');
img.src = 'none.png';
if (data.image) {
img.src = 'data:image/jpg;base64,' + data.image;
}
queriesResult.innerHTML = '';
if (data.data && data.data.response) {
for (const [query, result] of Object.entries(data.data.response)) {
queriesResult.innerHTML += renderResultMarkup(query, result);
}
} else if (data.data && data.data.error) {
queriesResult.innerHTML = `<div class="alert alert-danger">${JSON.stringify(data.data.error)}</div>`;
}
};
const handleFormSubmit = (event) => {
event.preventDefault();
const data = new FormData(event.target);
const loaderWrapper = document.getElementById('loaderWrapper');
loaderWrapper.classList.remove('d-none');
fetch('/analyze', { method: 'POST', body: data })
.then((res) => res.json())
.then(processAnalyzeResult)
.catch(() => {
alert('Произошла внутренняя ошибка.');
})
.finally(() => {
loaderWrapper.classList.add('d-none');
});
};
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('uploadForm').addEventListener('submit', handleFormSubmit);
});

15
static/style.css Normal file
View File

@ -0,0 +1,15 @@
#wrapper {
min-height: 100%;
}
#loaderWrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: #33333333;
display: flex;
justify-content: center;
align-items: center;
}