diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..b6d488b --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 160, + "tabWidth": 2, + "endOfLine": "auto" +} diff --git a/.run/main (check).run.xml b/.run/main (check).run.xml index 05df7e2..2e3cbb2 100644 --- a/.run/main (check).run.xml +++ b/.run/main (check).run.xml @@ -11,7 +11,7 @@ - + diff --git a/.run/main (empty).run.xml b/.run/main (empty).run.xml index 823eb43..7c1930e 100644 --- a/.run/main (empty).run.xml +++ b/.run/main (empty).run.xml @@ -11,7 +11,7 @@ - + diff --git a/.run/main (not empty).run.xml b/.run/main (not empty).run.xml index 19f240f..3222eb1 100644 --- a/.run/main (not empty).run.xml +++ b/.run/main (not empty).run.xml @@ -11,7 +11,7 @@ - + diff --git a/.run/webApp.run.xml b/.run/webApp.run.xml new file mode 100644 index 0000000..90b9467 --- /dev/null +++ b/.run/webApp.run.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..efd465f --- /dev/null +++ b/.vscode/launch.json @@ -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"] + } + ] +} diff --git a/README.md b/README.md index 1730407..b32a89f 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,34 @@ -Окружение: Python 3.10 +# Сервис анализа фотографий при помощи базы знаний -Сервис: http://kb.athene.tech/swagger-ui/index.html +## Настройка и запуск -Ручная установка зависимостей: +Требуемое окружение: Python 3.10+. + +Настройка виртуальной среды и установка зависимостей: ```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 -pip install -r requirements-mac.txt +python -m pip install -r requirements.txt ``` Запуск: ```commandline -main.py -main.py 5cc5570b-6ed9-3b33-9db4-bdb8ecb9f890 "test-data/lectionAudi/2021-03-12 13-48-31.JPG" +python -m flask --app src/webApp.py run ``` + +После этого в браузере необходимо перейти по ссылке . + +## Полезные ссылки + +* REST-сервис работы с онтологиями: . diff --git a/ontologyWorking.py b/ontologyWorking.py deleted file mode 100644 index 11e7f94..0000000 --- a/ontologyWorking.py +++ /dev/null @@ -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 diff --git a/requirements-mac.txt b/requirements-mac.txt deleted file mode 100644 index e8f8297..0000000 --- a/requirements-mac.txt +++ /dev/null @@ -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 diff --git a/requirements.txt b/requirements.txt index 545ff0b..008fb9b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,52 +1,41 @@ -absl-py==1.4.0 -astunparse==1.6.3 -cachetools==5.3.0 +blinker==1.6.2 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 +click==8.1.3 +colorama==0.4.6 +contourpy==1.1.0 +cycler==0.11.0 +filelock==3.12.2 +Flask==2.3.2 +fonttools==4.40.0 idna==3.4 -jax==0.4.10 -keras==2.12.0 -libclang==16.0.0 -Markdown==3.4.3 -MarkupSafe==2.1.2 +itsdangerous==2.1.2 +Jinja2==3.1.2 +kiwisolver==1.4.4 +MarkupSafe==2.1.3 matplotlib==3.7.1 -ml-dtypes==0.1.0 -numpy==1.23.5 -oauthlib==3.2.2 +mpmath==1.3.0 +networkx==3.1 +numpy==1.25.0 opencv-python==4.7.0.72 -opt-einsum==3.3.0 packaging==23.1 -pandas==2.0.1 +pandas==2.0.2 Pillow==9.5.0 -protobuf==4.23.1 psutil==5.9.5 +pyparsing==3.1.0 +python-dateutil==2.8.2 +pytz==2023.3 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 +requests==2.31.0 +scipy==1.11.0 +seaborn==0.12.2 six==1.16.0 -tensorboard==2.12.3 -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 +sympy==1.12 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 +typing_extensions==4.6.3 +tzdata==2023.3 +ultralytics==8.0.123 +urllib3==2.0.3 +Werkzeug==2.3.6 diff --git a/imageWorking.py b/src/imageWorking.py similarity index 52% rename from imageWorking.py rename to src/imageWorking.py index eb63ade..f5e30c9 100644 --- a/imageWorking.py +++ b/src/imageWorking.py @@ -1,18 +1,18 @@ import cv2 as cv import numpy as np -from main import img_size as size +img_size = (1280, 720) # Размер изображения для нормализации. def image_transform(image: np.ndarray) -> np.ndarray: ''' Трансформирует изображение нужным образом. @param image: Исходная матрица с представлением изображения. ''' - image = cv.resize(image, (size[0], size[1])) + image = cv.resize(image, (img_size[0], img_size[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: Путь до изображения. @@ -21,3 +21,13 @@ def get_image_as_array(image_name: str) -> np.ndarray: image: np.ndarray # приведение типов image = image_transform(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 diff --git a/main.py b/src/main.py similarity index 64% rename from main.py rename to src/main.py index 643f279..a2f7b01 100644 --- a/main.py +++ b/src/main.py @@ -2,31 +2,25 @@ import os import sys import cv2 as cv -import requests +import numpy as np import imageWorking import neuralNetwork import ontologyWorking -url = 'http://kb.athene.tech/api/1.0/ontology/' -img_path = 'data' -img_size = (1280, 720) # Размер изображения для нормализации. - -def analyze_file(uid: str, image_path: str) -> None: +def analyze_base(ontology_uid: str, image: np.ndarray, queries: list[str]) -> tuple[2]: ''' - Анализирует файл и выводит результат в консоль. - @param uid: УИД онтологии. - @param url: Базовый URL сервиса. + Базовая функция анализа файла и вывода результатов обработки. + @param ontology_uid: УИД онтологии. + @param image: Изображение. ''' - if not ontologyWorking.is_ontology_exists(uid, url): - raise Exception(f'Онтология с uid {uid} не существует') - if not os.path.isfile(image_path): - raise Exception(f'Изображение {image_path} не существует') + if image is None: + raise Exception(f'Изображение не указано') model = neuralNetwork.load_model() # Распознавание изображения. - results = model.predict(source=imageWorking.get_image_as_array(image_path)) + results = model.predict(source=image) # Создание аксиом онтологии на основе результатов распознавания. object_properties = list() @@ -39,23 +33,32 @@ def analyze_file(uid: str, image_path: str) -> None: object_properties += request[0] data_properties += request[1] - # Формирование данных для запроса к сервису работы с онтологиями. - data = { - 'data': - { - 'objectPropertyAssertions': object_properties, - 'dataPropertyAssertions': data_properties - } - } + + # Выполнение запроса к сервису работы с онтологиями + response = ontologyWorking.analyze(ontology_uid, object_properties, data_properties, queries) + + return results, response + + +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 = { 'QueryGetNotEmpty': '', 'QueryGetCheck': '', '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']: raise Exception(response['error']) for query in response['response']: @@ -79,6 +82,7 @@ def analyze_file(uid: str, image_path: str) -> None: cv.waitKey(0) cv.destroyAllWindows() + # Точка входа в приложение. if __name__ == '__main__': if len(sys.argv) != 3: diff --git a/neuralNetwork.py b/src/neuralNetwork.py similarity index 100% rename from neuralNetwork.py rename to src/neuralNetwork.py diff --git a/src/ontologyWorking.py b/src/ontologyWorking.py new file mode 100644 index 0000000..e369600 --- /dev/null +++ b/src/ontologyWorking.py @@ -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 diff --git a/src/webApp.py b/src/webApp.py new file mode 100644 index 0000000..ff31882 --- /dev/null +++ b/src/webApp.py @@ -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, + } diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..048c9a3 --- /dev/null +++ b/static/index.html @@ -0,0 +1,73 @@ + + + + + + + Анализ аудиторий + + + + + + + + + + + + + + Загрузите изображение и, если требуется, собственную онтологию для анализа. + + + Изображение для анализа + + + + Онтология предметной области + + Для анализа заполненности аудиторий оставьте это поле пустым. + + + Набор запросов для запуска + + Для анализа заполненности аудиторий не изменяйте это поле. + + + Отправить + + + + + + + Загрузка... + + + + + + + + + + + + diff --git a/static/none.png b/static/none.png new file mode 100644 index 0000000..087d75a Binary files /dev/null and b/static/none.png differ diff --git a/static/site.js b/static/site.js new file mode 100644 index 0000000..40001a3 --- /dev/null +++ b/static/site.js @@ -0,0 +1,53 @@ +const renderResultMarkup = (query, result) => + ` +${query} + + + ${result.columns.map((column) => `${column}`).join('')} + +${result.rows.map( + (row) => + `${Object.entries(row) + .map(([key, value]) => `${value.value}`) + .join('')}`, +)} +`; + +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 = `${JSON.stringify(data.data.error)}`; + } +}; + +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); +}); diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..7482e44 --- /dev/null +++ b/static/style.css @@ -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; +}
Загрузите изображение и, если требуется, собственную онтологию для анализа.