diff --git a/Dockerfile b/Dockerfile index 5671d4d..d481a47 100644 --- a/Dockerfile +++ b/Dockerfile @@ -57,8 +57,9 @@ RUN mkdir -p /home/app && \ # add code while changing ownership WORKDIR $APP_HOME COPY --chown=app:app ./src ./src +COPY --chown=app:app ./core ./core # change to the app user USER app -ENTRYPOINT [ "gunicorn", "src.main:server", "-b", "0.0.0.0:8050" ] \ No newline at end of file +ENTRYPOINT [ "gunicorn", "src.main:server", "-b", "0.0.0.0:8050" ] diff --git a/core/database/__init__.py b/core/database/__init__.py new file mode 100644 index 0000000..ef9599c --- /dev/null +++ b/core/database/__init__.py @@ -0,0 +1,13 @@ +"""Database module content.""" +from __future__ import annotations + +from .classes import Dataset +from .classes import NoDocumentFoundException +from .classes import VisualCommunication +from .utils import connect +from .utils.count_documents import count_documents +from .utils.get_visual_communication import get_visual_communication +from .utils.list_names import list_names +from .utils.upsert_annotation import upsert_annotation +from .utils.upsert_prediction import upsert_prediction +from .utils.upsert_visual_communication import upsert_visual_communication diff --git a/core/database/classes/__init__.py b/core/database/classes/__init__.py new file mode 100755 index 0000000..9e260cd --- /dev/null +++ b/core/database/classes/__init__.py @@ -0,0 +1,6 @@ +"""Database classes module content.""" +from __future__ import annotations + +from .dataset import Dataset +from .exceptions import NoDocumentFoundException +from .visual_communication import VisualCommunication diff --git a/core/database/classes/dataset.py b/core/database/classes/dataset.py new file mode 100755 index 0000000..0c3fa1a --- /dev/null +++ b/core/database/classes/dataset.py @@ -0,0 +1,71 @@ +"""Definition of database Dataset class.""" +from __future__ import annotations + +import logging +import random +from datetime import datetime +from datetime import UTC + +from pydantic import BaseModel +from pydantic import Field +from pymongo.collection import Collection + + +class Dataset(BaseModel): + """Database Dataset model.""" + create_time: datetime = Field(default_factory=lambda: datetime.now(UTC)) + train_names: list[str] + test_names: list[str] + validation_names: list[str] + + @classmethod + def fraction_map(cls) -> dict[str, float]: + """Dict with train, test and validation fractions.""" + # define map + split_map = { + 'train': 0.7, + 'test': 0.2, + 'validation': 0.1, + } + # sanity check + assert sum(split_map.values()) == 1.0 + return split_map + + @classmethod + def new_from_name_list( + cls, + name_list: list[str], + ) -> Dataset: + """Generate new dataset from list of filenames.""" + # calculate split fractions + fraction_map = cls.fraction_map() + num_total = len(name_list) + num_validation = round(num_total * fraction_map['validation']) + num_test = round(num_total * fraction_map['test']) + # split data + validation_name_list = random.choices(name_list, k=num_validation) + name_list = [ + name for name in name_list if name not in validation_name_list + ] + test_name_list = random.choices(name_list, k=num_test) + train_name_list = [ + name for name in name_list if name not in test_name_list + ] + # instantiate object + dataset = Dataset( + train_names=train_name_list, + test_names=test_name_list, + validation_names=validation_name_list, + ) + logging.debug('finished') + return dataset + + def save( + self, + collection: Collection, + ) -> None: + """Save dataset to database.""" + res = collection.insert_one( + document=self.model_dump(), + ) + logging.debug('inserted document: %s', res) diff --git a/core/database/classes/exceptions.py b/core/database/classes/exceptions.py new file mode 100755 index 0000000..5372031 --- /dev/null +++ b/core/database/classes/exceptions.py @@ -0,0 +1,6 @@ +"""Definition of database exception.""" +from __future__ import annotations + + +class NoDocumentFoundException(Exception): + """Database exception for when no documents are found.""" diff --git a/core/database/classes/visual_communication.py b/core/database/classes/visual_communication.py new file mode 100755 index 0000000..78e9e61 --- /dev/null +++ b/core/database/classes/visual_communication.py @@ -0,0 +1,85 @@ +"""Definition of VisualCommunication model.""" +from __future__ import annotations + +import logging +from base64 import b64decode +from base64 import b64encode +from io import BytesIO +from pathlib import Path + +from PIL import Image +from pydantic import BaseModel +from pydantic import field_serializer +from pydantic import field_validator + +from core.dto import ModelData + + +class VisualCommunication(BaseModel): + """Visual communication model.""" + + name: str + image: Image.Image + annotation: ModelData | None = None + prediction: ModelData | None = None + + class Config: + """BaseModel configuration.""" + arbitrary_types_allowed = True + + @classmethod + def classname(cls) -> str: + """Return classname.""" + return cls.__name__ + + @classmethod + def from_file(cls, path: Path) -> VisualCommunication: + """Instantiate from file.""" + name = path.stem + image = Image.open(path) + image.load() + return VisualCommunication(name=name, image=image) + + @classmethod + def decode_image(cls, content: str) -> Image.Image: + """Extract image from webencoded content.""" + _, content_data = content.split(',') + return Image.open(BytesIO(b64decode(content_data))) + + @field_serializer('image') + @classmethod + def serialize_image(cls, image: Image.Image) -> bytes: # type: ignore + """Convert image to bytes for storage in database.""" + buffer = BytesIO() + image.save(buffer, format='JPEG') + return buffer.getvalue() + + @field_validator('image', mode='before') + @classmethod + def convert_to_image( + cls, + image: Image.Image | BytesIO | bytes, + ) -> Image.Image: + """Convert bytes input from database into image.""" + if isinstance(image, bytes): + image = BytesIO(image) + if isinstance(image, BytesIO): + image = Image.open(image) + return image + + def __repr__(self) -> str: + return f"{self.classname()}(name='{self.name}')" + + def webencoded_image(self) -> str: + """Convert image to be displayed on webpage.""" + # convert images to bytes string + buffer = BytesIO() + self.image.save(buffer, format='png') + img_enc = b64encode(buffer.getvalue()).decode('utf-8') + return f"data:image/png;base64, {img_enc}" + + def generate_random_prediction(self, force: bool = False) -> None: + """Generate random prediction values.""" + if not force and self.prediction is not None: + logging.warning('set force=True to overwrite existing values.') + self.prediction = ModelData.from_random() diff --git a/core/database/utils/__init__.py b/core/database/utils/__init__.py new file mode 100755 index 0000000..b75aff4 --- /dev/null +++ b/core/database/utils/__init__.py @@ -0,0 +1,4 @@ +"""Database utils module content.""" +from __future__ import annotations + +from .connect import connect diff --git a/core/database/utils/connect.py b/core/database/utils/connect.py new file mode 100644 index 0000000..98d4386 --- /dev/null +++ b/core/database/utils/connect.py @@ -0,0 +1,33 @@ +""" +Definition of function to connect to database +using environment variables. +""" +from __future__ import annotations + +import logging +import os + +from dotenv import load_dotenv +from pymongo import MongoClient + + +def connect(): + """Connect to MongoDB using env vars.""" + # load env vars + load_dotenv() + necessary_env_vars = [ + 'MONGO_HOST', + 'MONGO_DB', + 'MONGO_COLLECTION', + ] + for env_var in necessary_env_vars: + assert env_var in os.environ, f"{env_var} not found" + # connect to database + client = MongoClient(os.getenv('MONGO_HOST')) + db = client[os.getenv('MONGO_DB')] + # extract collection + collection = db[os.getenv('MONGO_COLLECTION')] + # set unique index on "name" + collection.create_index('name', unique=True) + logging.debug('finished') + return collection, db, client diff --git a/core/database/utils/count_documents.py b/core/database/utils/count_documents.py new file mode 100755 index 0000000..53a4a1c --- /dev/null +++ b/core/database/utils/count_documents.py @@ -0,0 +1,21 @@ +"""Definition of function to count documents in database.""" +from __future__ import annotations + +from pymongo.collection import Collection + + +def count_documents( + collection: Collection, + only_with_annotation: bool = False, +) -> int: + """ + Get the total number of documents + in database that matches the filters. + """ + assert isinstance(collection, Collection) + assert isinstance(only_with_annotation, bool) + # build query + query = {} + if only_with_annotation: + query['annotation'] = {'$ne': None} + return collection.count_documents(filter=query) diff --git a/core/database/utils/get_visual_communication.py b/core/database/utils/get_visual_communication.py new file mode 100755 index 0000000..9b0b043 --- /dev/null +++ b/core/database/utils/get_visual_communication.py @@ -0,0 +1,39 @@ +"""Definition of function to get visual communication from database.""" +from __future__ import annotations + +import logging + +from pymongo.collection import Collection + +from core.database import NoDocumentFoundException +from core.database import VisualCommunication + + +def get_visual_communication( + collection: Collection, + with_annotation: bool = False, +) -> VisualCommunication: + """Get a random visual communication from the database.""" + query = {} + if with_annotation: + query['annotation'] = {'$ne': None} + else: + query['annotation'] = {'$eq': None} + data = collection.aggregate( + pipeline=[ + { + '$match': query, # find using filters + }, + { + '$sample': { + 'size': 1, # get one random + }, + }, + ], + ) + data_list = list(data) # read data from cursor object + if len(data_list) == 0: + raise NoDocumentFoundException() + vis_com = VisualCommunication.model_validate(data_list[0]) + logging.debug('finished') + return vis_com diff --git a/core/database/utils/list_names.py b/core/database/utils/list_names.py new file mode 100755 index 0000000..6184166 --- /dev/null +++ b/core/database/utils/list_names.py @@ -0,0 +1,31 @@ +""" +Definition of function to list names +of all visual communication documents in database. +""" +from __future__ import annotations + +from pymongo.collection import Collection + + +def list_names( + collection: Collection, + only_with_annotation: bool = True, +) -> list[str]: + """List the names of entries that match the filters.""" + assert isinstance(collection, Collection) + assert isinstance(only_with_annotation, bool) + # build query + query = {} + if only_with_annotation: + query['annotation'] = {'$ne': None} + # execute query + res_list = collection.find( + filter=query, + projection={ + '_id': False, + 'name': True, + }, + ) + # extract information + name_list = [elem['name'] for elem in res_list] + return name_list diff --git a/core/database/utils/upsert_annotation.py b/core/database/utils/upsert_annotation.py new file mode 100755 index 0000000..2124d2f --- /dev/null +++ b/core/database/utils/upsert_annotation.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import logging + +from pymongo.collection import Collection + +from core.dto import ModelData + + +def upsert_annotation( + collection: Collection, + vis_com_name: str, + annotations: ModelData, +) -> None: + """Upserts annotation data in the database.""" + query = { + 'name': vis_com_name, + } + update = { + '$set': { + 'annotation': annotations.model_dump(), + }, + } + res = collection.update_one( + filter=query, + update=update, + upsert=True, + ) + logging.info('upserted document: %s', res) + logging.info('finished') diff --git a/core/database/utils/upsert_prediction.py b/core/database/utils/upsert_prediction.py new file mode 100755 index 0000000..f47d75b --- /dev/null +++ b/core/database/utils/upsert_prediction.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import logging + +from pymongo.collection import Collection + +from core.dto import ModelData + + +def upsert_prediction( + collection: Collection, + vis_com_name: str, + predictions: ModelData, +) -> None: + """Upsert prediction data in the database.""" + query = { + 'name': vis_com_name, + } + update = { + '$set': { + 'prediction': predictions.model_dump(), + }, + } + res = collection.update_one( + filter=query, + update=update, + upsert=True, + ) + logging.debug('upserted document: %s', res) + logging.info('finished') diff --git a/core/database/utils/upsert_visual_communication.py b/core/database/utils/upsert_visual_communication.py new file mode 100755 index 0000000..6744e01 --- /dev/null +++ b/core/database/utils/upsert_visual_communication.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from pymongo.collection import Collection + +from core.database import VisualCommunication + + +def upsert_visual_communication( + collection: Collection, + visual_communication_list: list[VisualCommunication], +) -> bool: + """ + Upsert VisualCommunication object in the database. + Returns bool stating success. + """ + response = collection.insert_many( + [ + vis_com.model_dump() + for vis_com + in visual_communication_list + ], + ) + return response.acknowledged diff --git a/core/dto/__init__.py b/core/dto/__init__.py new file mode 100644 index 0000000..be1cd38 --- /dev/null +++ b/core/dto/__init__.py @@ -0,0 +1,15 @@ +"""Data transfer objects module content.""" +from __future__ import annotations + +from .angle import AngleData +from .contact import ContactData +from .distance import DistanceData +from .framing import FramingData +from .information_value import InformationValueData +from .modality_color import ModalityColorData +from .modality_depth import ModalityDepthData +from .modality_lighting import ModalityLightingData +from .model_data import ModelData +from .point_of_view import PointOfViewData +from .salience import SalienceData +from .visual_syntax import VisualSyntaxData diff --git a/core/dto/angle.py b/core/dto/angle.py new file mode 100644 index 0000000..5ce2ea0 --- /dev/null +++ b/core/dto/angle.py @@ -0,0 +1,11 @@ +"""Definition of Angle data model.""" +from __future__ import annotations + +from .data_model import DataModel + + +class AngleData(DataModel): + """Angle data model.""" + high: float + eye_level: float + low: float diff --git a/core/dto/contact.py b/core/dto/contact.py new file mode 100644 index 0000000..9a5f1cd --- /dev/null +++ b/core/dto/contact.py @@ -0,0 +1,11 @@ +"""Definition of ContactData data model.""" +from __future__ import annotations + +from .data_model import DataModel + + +class ContactData(DataModel): + """ContactData data model.""" + + offer: float + demand: float diff --git a/core/dto/data_model.py b/core/dto/data_model.py new file mode 100644 index 0000000..70bb79f --- /dev/null +++ b/core/dto/data_model.py @@ -0,0 +1,67 @@ +"""Definition of DataModel base class.""" +from __future__ import annotations + +import random + +from pydantic import BaseModel +from pydantic import ValidationError + + +class DataModel(BaseModel): + """DataModel base class.""" + + @classmethod + def classname(cls) -> str: + """Return classname.""" + return cls.__name__ + + @classmethod + def list_fields(cls) -> list[str]: + """List options that are stored as attributes.""" + return list(cls.model_fields.keys()) + + @classmethod + def from_random(cls): + """Instantiate with random numbers.""" + kwargs = {field: random.random() for field in cls.list_fields()} + return cls(**kwargs) + + @classmethod + def from_choice(cls, option: str): + """Instantiate from choice.""" + if option is None: + raise ValidationError() + assert isinstance(option, str), 'option is not a string' + allowed_options_list = cls.list_fields() + assert option in allowed_options_list, \ + f"{option} is not among allowed fields {allowed_options_list}" + kwargs = {field: 0 for field in cls.list_fields()} + kwargs[option] = 1 + return cls(**kwargs) + + @classmethod + def from_list(cls, data_list: list[float]): + """Instantiate from list of values.""" + kwargs = {key: val for key, val in zip(cls.list_fields(), data_list)} + return cls(**kwargs) + + def __repr__(self) -> str: + model_dict = self.model_dump() + model_repr_str = f"{self.classname()}(" + model_repr_str += ', '.join([ + f"{field}={value:.3f}" + for field, value + in model_dict.items() + ]) + model_repr_str += ')' + return model_repr_str + + def highest_score_field(self) -> str: + """Return name of field with highest score.""" + model_dict = self.model_dump() + return max(model_dict, key=lambda k: model_dict[k]) + + def highest_score_value(self) -> float: + """Return value of field with highest score.""" + model_dict = self.model_dump() + return max(model_dict.values()) diff --git a/core/dto/distance.py b/core/dto/distance.py new file mode 100644 index 0000000..efd9461 --- /dev/null +++ b/core/dto/distance.py @@ -0,0 +1,12 @@ +"""Definition of DistanceData data model.""" +from __future__ import annotations + +from .data_model import DataModel + + +class DistanceData(DataModel): + """DistanceData data model.""" + + long: float + medium: float + close: float diff --git a/core/dto/framing.py b/core/dto/framing.py new file mode 100644 index 0000000..2381ebc --- /dev/null +++ b/core/dto/framing.py @@ -0,0 +1,13 @@ +"""Definition of FramingData data model.""" +from __future__ import annotations + +from .data_model import DataModel + + +class FramingData(DataModel): + """FramingData data model.""" + + frame_lines: float + empty_space: float + colour_contrast: float + form_contrast: float diff --git a/core/dto/information_value.py b/core/dto/information_value.py new file mode 100644 index 0000000..45d10f3 --- /dev/null +++ b/core/dto/information_value.py @@ -0,0 +1,12 @@ +"""Definition of InformationValueData data model.""" +from __future__ import annotations + +from .data_model import DataModel + + +class InformationValueData(DataModel): + """InformationValueData data model.""" + + given_new: float + ideal_real: float + central_marginal: float diff --git a/core/dto/modality_color.py b/core/dto/modality_color.py new file mode 100644 index 0000000..a34d6be --- /dev/null +++ b/core/dto/modality_color.py @@ -0,0 +1,12 @@ +"""Definition of ModalityColorData data model.""" +from __future__ import annotations + +from .data_model import DataModel + + +class ModalityColorData(DataModel): + """ModalityColorData data model.""" + + high: float + medium: float + low: float diff --git a/core/dto/modality_depth.py b/core/dto/modality_depth.py new file mode 100644 index 0000000..3f8fbe6 --- /dev/null +++ b/core/dto/modality_depth.py @@ -0,0 +1,12 @@ +"""Definition of ModalityDepthData data model.""" +from __future__ import annotations + +from .data_model import DataModel + + +class ModalityDepthData(DataModel): + """ModalityDepthData data model.""" + + high: float + medium: float + low: float diff --git a/core/dto/modality_lighting.py b/core/dto/modality_lighting.py new file mode 100644 index 0000000..fb04f4c --- /dev/null +++ b/core/dto/modality_lighting.py @@ -0,0 +1,12 @@ +"""Definition of ModalityLightingData data model.""" +from __future__ import annotations + +from .data_model import DataModel + + +class ModalityLightingData(DataModel): + """ModalityLightingData data model.""" + + high: float + medium: float + low: float diff --git a/core/dto/model_data.py b/core/dto/model_data.py new file mode 100644 index 0000000..157f788 --- /dev/null +++ b/core/dto/model_data.py @@ -0,0 +1,82 @@ +"""Definition of ModelData data model.""" +from __future__ import annotations + +from .angle import AngleData +from .contact import ContactData +from .data_model import DataModel +from .distance import DistanceData +from .framing import FramingData +from .information_value import InformationValueData +from .modality_color import ModalityColorData +from .modality_depth import ModalityDepthData +from .modality_lighting import ModalityLightingData +from .point_of_view import PointOfViewData +from .salience import SalienceData +from .visual_syntax import VisualSyntaxData + + +class ModelData(DataModel): + """ModelData model for data IO with combined ML model.""" + visual_syntax: VisualSyntaxData + contact: ContactData + angle: AngleData + point_of_view: PointOfViewData + distance: DistanceData + modality_lighting: ModalityLightingData + modality_color: ModalityColorData + modality_depth: ModalityDepthData + information_value: InformationValueData + framing: FramingData + salience: SalienceData + + @classmethod + def from_random(cls) -> ModelData: + """Instantiate with random numbers.""" + kwargs = { + field: field_info.annotation.from_random() # type: ignore + for field, field_info + in cls.model_fields.items() + } + return cls(**kwargs) + + @classmethod + def from_annotations( + cls, + visual_syntax: str, + contact: str, + angle: str, + point_of_view: str, + distance: str, + modality_lighting: str, + modality_color: str, + modality_depth: str, + information_value: str, + framing: str, + salience: str, + ) -> ModelData: + """Instantiate from annotation.""" + kwargs = { + 'visual_syntax': VisualSyntaxData + .from_choice(visual_syntax), + 'contact': ContactData + .from_choice(contact), + 'angle': AngleData + .from_choice(angle), + 'point_of_view': PointOfViewData + .from_choice(point_of_view), + 'distance': DistanceData + .from_choice(distance), + 'modality_lighting': ModalityLightingData + .from_choice(modality_lighting), + 'modality_color': ModalityColorData + .from_choice(modality_color), + 'modality_depth': ModalityDepthData + .from_choice(modality_depth), + 'information_value': InformationValueData + .from_choice(information_value), + 'framing': FramingData + .from_choice(framing), + 'salience': SalienceData + .from_choice(salience), + } + return cls(**kwargs) diff --git a/core/dto/point_of_view.py b/core/dto/point_of_view.py new file mode 100644 index 0000000..98d67c5 --- /dev/null +++ b/core/dto/point_of_view.py @@ -0,0 +1,11 @@ +"""Definition of PointOfViewData data model.""" +from __future__ import annotations + +from .data_model import DataModel + + +class PointOfViewData(DataModel): + """PointOfViewData data model.""" + + frontal: float + oblique: float diff --git a/core/dto/salience.py b/core/dto/salience.py new file mode 100644 index 0000000..580100d --- /dev/null +++ b/core/dto/salience.py @@ -0,0 +1,14 @@ +"""Definition of SalienceData data model.""" +from __future__ import annotations + +from .data_model import DataModel + + +class SalienceData(DataModel): + """SalienceData data model.""" + + size: float + colour: float + tone: float + form: float + positioning: float diff --git a/core/dto/visual_syntax.py b/core/dto/visual_syntax.py new file mode 100644 index 0000000..fd38639 --- /dev/null +++ b/core/dto/visual_syntax.py @@ -0,0 +1,27 @@ +"""Definition of VisualSyntaxData data model.""" +from __future__ import annotations + +from .data_model import DataModel + + +class VisualSyntaxData(DataModel): + """VisualSyntaxData data model.""" + + non_transactional_action: float + non_transactional_reaction: float + unidirectional_transactional_action: float + unidirectional_transactional_reaction: float + bidirectional_transactional_action: float + bidirectional_transactional_reaction: float + conversion: float + speech_process: float + classification_overt_taxonomy: float + analytical_exhaustive: float + analytical_disarranged: float + analytical_temporal: float + analytical_distributed: float + analytical_topological: float + analytical_exploded: float + analytical_inclusive: float + symbolic_suggestive: float + symbolic_attributive: float diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 5c237a1..011ee49 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -1,21 +1,21 @@ version: '3.7' services: - app: - image: visual_critical_discourse_analysis:dev - container_name: visual_critical_discourse_analysis - build: - context: . - dockerfile: Dockerfile - env_file: - - local.env - environment: - - ENV=DEV - ports: - - 8050:8050 - networks: - - backend - depends_on: - - mongo + # app: + # image: visual_critical_discourse_analysis:dev + # container_name: visual_critical_discourse_analysis + # build: + # context: . + # dockerfile: Dockerfile + # env_file: + # - local.env + # environment: + # - ENV=DEV + # ports: + # - 8050:8050 + # networks: + # - backend + # depends_on: + # - mongo mongo: image: mongo:latest container_name: mongo @@ -41,4 +41,4 @@ services: networks: backend: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/model/src/dataloader.py b/model/src/dataloader.py new file mode 100644 index 0000000..77c433e --- /dev/null +++ b/model/src/dataloader.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +import random +from io import BytesIO + +from torch import Tensor +from torch.utils.data import Dataset +from torchvision.io import read_image +from torchvision.transforms import ColorJitter +from torchvision.transforms import InterpolationMode +from torchvision.transforms import Normalize +from torchvision.transforms.functional import hflip +from torchvision.transforms.functional import pad +from torchvision.transforms.functional import resize +from torchvision.transforms.functional import rotate + +from core.database import connect + +# resnet18 original normalization values +RESNET_NORMALIZE_MEAN = [0.485, 0.456, 0.406] +RESNET_NORMALIZE_STD = [0.229, 0.224, 0.225] + + +class VCDADataset(Dataset): + def __init__( + self, + data_name_list: list[str], + do_augment: bool = False, + random_annotations: bool = False, + normalize_mean: list[float] = RESNET_NORMALIZE_MEAN, + normalize_std: list[float] = RESNET_NORMALIZE_STD, + ): + super().__init__() + self.data_name_list = data_name_list + self.do_augment = do_augment + self.random_annotations = random_annotations + self.normalize_mean = normalize_mean + self.normalize_std = normalize_std + # prepare augmentation functions + self.normalize = Normalize( + mean=normalize_mean, + std=normalize_std, + ) + self.color_jitter = ColorJitter( + brightness=1e-1, + contrast=8e-2, + saturation=8e-2, + ) + # connect to database + collection, _, _ = connect() + self.collection = collection + + def __len__(self): + return len(self.data_name_list) + + def __getitem__(self, idx): + # get image from database + name = self.data_name_list[idx] + query = { + 'name': name, + } + projection = { + '_id': False, + 'image': True, + } + img_bytes = self.collection.find_one( + filter=query, + projection=projection, + ) + img = self.load_image(img_bytes) + if self.do_augment: + img = self.augment(img) + return img + + def load_image( + self, + data: bytes, + ) -> Tensor: + """Load images tensor from bytes.""" + assert isinstance(data, bytes) + img = read_image(BytesIO(data)) + img /= 255 # normalize 8-bit image + img = self.square_pad(img) + img = resize( + img=img, + size=(512, 512), + interpolation=InterpolationMode.BICUBIC, + ) + return img + + @staticmethod + def square_pad( + img: Tensor, + ) -> Tensor: + """ + Pads image to a square with side length + equal to the largest side of the input image. + """ + assert isinstance(img, Tensor) + # B, nc, w, h = img.shape + h = img.shape[-2] + w = img.shape[-1] + if h == w: + return img + max_wh = max([h, w]) + hp = int((max_wh - w) / 2) + vp = int((max_wh - h) / 2) + padding = (hp, vp, hp, vp) + return pad(img, padding, 0, 'constant') + + def augment( + self, + img: Tensor, + ) -> Tensor: + """ + Augment image with random horizontal flips, + rotations and color jitter. + """ + assert isinstance(img, Tensor) + # left-right flip + if random.random() >= 0.5: + img = hflip(img) + # rotation + rnd = random.random() + if rnd < 0.25: + img = rotate(img, angle=90) + if rnd < 0.5: + img = rotate(img, angle=180) + if rnd < 0.75: + img = rotate(img, angle=270) + # color jitter + img = self.color_jitter(img) + return img + + def reverse_normalise( + self, + img: Tensor, + ) -> Tensor: + """ + Reverse normalization to get an image + that can be interpreted by humans. + """ + assert isinstance(img, Tensor) + img *= Tensor(self.normalize_std).reshape((3, 1, 1)) + img += Tensor(self.normalize_mean).reshape((3, 1, 1)) + return img diff --git a/model/src/models/__init__.py b/model/src/models/__init__.py new file mode 100644 index 0000000..1f69980 --- /dev/null +++ b/model/src/models/__init__.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +from .visual_communication import VisualCommunicationModel diff --git a/model/src/models/angle.py b/model/src/models/angle.py new file mode 100644 index 0000000..2fde0ad --- /dev/null +++ b/model/src/models/angle.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .fully_connected import FullyConnectedModel + + +class AngleTail(FullyConnectedModel): + def __init__(self): + super().__init__(num_out_features=3) diff --git a/model/src/models/contact.py b/model/src/models/contact.py new file mode 100644 index 0000000..be69b23 --- /dev/null +++ b/model/src/models/contact.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .fully_connected import FullyConnectedModel + + +class ContactTail(FullyConnectedModel): + def __init__(self): + super().__init__(num_out_features=2) diff --git a/model/src/models/distance.py b/model/src/models/distance.py new file mode 100644 index 0000000..d5a7d8c --- /dev/null +++ b/model/src/models/distance.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .fully_connected import FullyConnectedModel + + +class DistanceTail(FullyConnectedModel): + def __init__(self): + super().__init__(num_out_features=3) diff --git a/model/src/models/framing.py b/model/src/models/framing.py new file mode 100644 index 0000000..25a6c10 --- /dev/null +++ b/model/src/models/framing.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .fully_connected import FullyConnectedModel + + +class FramingTail(FullyConnectedModel): + def __init__(self): + super().__init__(num_out_features=4) diff --git a/model/src/models/fully_connected.py b/model/src/models/fully_connected.py new file mode 100644 index 0000000..a5bb915 --- /dev/null +++ b/model/src/models/fully_connected.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import torch.nn as nn + + +class FullyConnectedModel(nn.Module): + def __init__(self, num_out_features: int): + super().__init__() + # define layers + self.fc1 = nn.Linear(in_features=16*16*512, out_features=512) + self.af1 = nn.ReLU() + self.fc2 = nn.Linear(in_features=512, out_features=128) + self.af2 = nn.ReLU() + self.fc3 = nn.Linear(in_features=128, out_features=32) + self.af3 = nn.ReLU() + self.fc4 = nn.Linear(in_features=32, out_features=num_out_features) + + def forward(self, x): + x = self.fc1(x) + x = self.af1(x) + x = self.fc2(x) + x = self.af2(x) + x = self.fc3(x) + x = self.af3(x) + x = self.fc4(x) + return x diff --git a/model/src/models/information_value.py b/model/src/models/information_value.py new file mode 100644 index 0000000..31c2848 --- /dev/null +++ b/model/src/models/information_value.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .fully_connected import FullyConnectedModel + + +class InformationValueTail(FullyConnectedModel): + def __init__(self): + super().__init__(num_out_features=3) diff --git a/model/src/models/modality_color.py b/model/src/models/modality_color.py new file mode 100644 index 0000000..ae10bd8 --- /dev/null +++ b/model/src/models/modality_color.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .fully_connected import FullyConnectedModel + + +class ModalityColorTail(FullyConnectedModel): + def __init__(self): + super().__init__(num_out_features=3) diff --git a/model/src/models/modality_depth.py b/model/src/models/modality_depth.py new file mode 100644 index 0000000..3c0e5e8 --- /dev/null +++ b/model/src/models/modality_depth.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .fully_connected import FullyConnectedModel + + +class ModalityDepthTail(FullyConnectedModel): + def __init__(self): + super().__init__(num_out_features=3) diff --git a/model/src/models/modality_lighting.py b/model/src/models/modality_lighting.py new file mode 100644 index 0000000..8913281 --- /dev/null +++ b/model/src/models/modality_lighting.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .fully_connected import FullyConnectedModel + + +class ModalityLightingTail(FullyConnectedModel): + def __init__(self): + super().__init__(num_out_features=3) diff --git a/model/src/models/point_of_view.py b/model/src/models/point_of_view.py new file mode 100644 index 0000000..d0d8ae3 --- /dev/null +++ b/model/src/models/point_of_view.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .fully_connected import FullyConnectedModel + + +class PointOfViewTail(FullyConnectedModel): + def __init__(self): + super().__init__(num_out_features=2) diff --git a/model/src/models/resnet18_head.py b/model/src/models/resnet18_head.py new file mode 100644 index 0000000..e647fb5 --- /dev/null +++ b/model/src/models/resnet18_head.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import torch.nn as nn +import torchvision + + +class ResNet18Head(nn.Module): + def __init__(self): + super().__init__() + # copy out parts from ResNet18 with weights + resnet18 = torchvision.models.resnet18( + weights=torchvision.models.ResNet18_Weights.IMAGENET1K_V1, + ) + # save relevant layers + self.conv1 = resnet18.conv1 + self.bn1 = resnet18.bn1 + self.relu = resnet18.relu + self.maxpool = resnet18.maxpool + self.layer1 = resnet18.layer1 + self.layer2 = resnet18.layer2 + self.layer3 = resnet18.layer3 + self.layer4 = resnet18.layer4 + self.avgpool = resnet18.avgpool + self.flat = nn.Flatten() # size 512 + + def forward(self, x): + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + x = self.maxpool(x) + x = self.layer1(x) + x = self.layer2(x) + x = self.layer3(x) + x = self.layer4(x) + x = self.avgpool(x) + x = self.flat(x) + return x diff --git a/model/src/models/salience.py b/model/src/models/salience.py new file mode 100644 index 0000000..bc9f0af --- /dev/null +++ b/model/src/models/salience.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .fully_connected import FullyConnectedModel + + +class SalienceTail(FullyConnectedModel): + def __init__(self): + super().__init__(num_out_features=5) diff --git a/model/src/models/visual_communication.py b/model/src/models/visual_communication.py new file mode 100644 index 0000000..8d666f5 --- /dev/null +++ b/model/src/models/visual_communication.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +import torch.nn as nn + +from .angle import AngleTail +from .contact import ContactTail +from .distance import DistanceTail +from .framing import FramingTail +from .information_value import InformationValueTail +from .modality_color import ModalityColorTail +from .modality_depth import ModalityDepthTail +from .modality_lighting import ModalityLightingTail +from .point_of_view import PointOfViewTail +from .resnet18_head import ResNet18Head +from .salience import SalienceTail +from .visual_syntax import VisualSyntaxTail +from core.dto import AngleData +from core.dto import ContactData +from core.dto import DistanceData +from core.dto import FramingData +from core.dto import InformationValueData +from core.dto import ModalityColorData +from core.dto import ModalityDepthData +from core.dto import ModalityLightingData +from core.dto import PointOfViewData +from core.dto import SalienceData +from core.dto import VisualSyntaxData + + +class VisualCommunicationModel(nn.Module): + def __init__(self): + super().__init__() + # store other models + self.resnet_head = ResNet18Head() + self.visual_syntax_tail = VisualSyntaxTail() + self.contact_tail = ContactTail() + self.angle_tail = AngleTail() + self.point_of_view_tail = PointOfViewTail() + self.distance_tail = DistanceTail() + self.modality_lighting_tail = ModalityLightingTail() + self.modality_color_tail = ModalityColorTail() + self.modality_depth_tail = ModalityDepthTail() + self.information_value_tail = InformationValueTail() + self.framing_tail = FramingTail() + self.salience_tail = SalienceTail() + + def forward(self, x): + # generate visual representation + vis_rep = self.resnet_head(x) + # prepare result map + results = {} + # predict visual syntax + results['visual_syntax'] = VisualSyntaxData.from_list( + self.visual_syntax_tail( + vis_rep, + ).cpu(), + ) + # predict contact + results['contact'] = ContactData.from_list( + self.contact_tail( + vis_rep, + ).cpu(), + ) + # predict angle + results['angle'] = AngleData.from_list( + self.angle_tail( + vis_rep, + ).cpu(), + ) + # predict point of view + results['point_of_view'] = PointOfViewData.from_list( + self.point_of_view_tail( + vis_rep, + ).cpu(), + ) + # predict distance + results['distance'] = DistanceData.from_list( + self.distance_tail( + vis_rep, + ).cpu(), + ) + # predict modality lighting + results['modality_lighting'] = ModalityLightingData.from_list( + self.modality_lighting_tail( + vis_rep, + ).cpu(), + ) + # predict modality color + results['modality_color'] = ModalityColorData.from_list( + self.modality_color_tail( + vis_rep, + ).cpu(), + ) + # predict modality depth + results['modality_depth'] = ModalityDepthData.from_list( + self.modality_depth_tail( + vis_rep, + ).cpu(), + ) + # predict information value + results['information_value'] = InformationValueData.from_list( + self.information_value_tail( + vis_rep, + ).cpu(), + ) + # predict framing + results['framing'] = FramingData.from_list( + self.framing_tail( + vis_rep, + ).cpu(), + ) + # predict salience + results['salience'] = SalienceData.from_list( + self.salience_tail( + vis_rep, + ).cpu(), + ) + return results diff --git a/model/src/models/visual_syntax.py b/model/src/models/visual_syntax.py new file mode 100644 index 0000000..fe02a2b --- /dev/null +++ b/model/src/models/visual_syntax.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .fully_connected import FullyConnectedModel + + +class VisualSyntaxTail(FullyConnectedModel): + def __init__(self): + super().__init__(num_out_features=18) diff --git a/model/src/train.py b/model/src/train.py new file mode 100644 index 0000000..c95e37f --- /dev/null +++ b/model/src/train.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from dataloader import VCDADataset # noqa: F401 +from loss_fn import setup_criterion # noqa: F401 + +from model import ResNet18Head # noqa: F401 diff --git a/poetry.lock b/poetry.lock index 6dc5a65..230ad5c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -381,6 +381,22 @@ idna = ["idna (>=3.6)"] trio = ["trio (>=0.23)"] wmi = ["wmi (>=1.5.1)"] +[[package]] +name = "filelock" +version = "3.13.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + [[package]] name = "flake8" version = "7.0.0" @@ -419,6 +435,41 @@ Werkzeug = ">=3.0.0" async = ["asgiref (>=3.2)"] dotenv = ["python-dotenv"] +[[package]] +name = "fsspec" +version = "2024.2.0" +description = "File-system specification" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fsspec-2024.2.0-py3-none-any.whl", hash = "sha256:817f969556fa5916bc682e02ca2045f96ff7f586d45110fcb76022063ad2c7d8"}, + {file = "fsspec-2024.2.0.tar.gz", hash = "sha256:b6ad1a679f760dda52b1168c859d01b7b80648ea6f7f7c7f5a8a91dc3f3ecb84"}, +] + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +devel = ["pytest", "pytest-cov"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +tqdm = ["tqdm"] + [[package]] name = "gunicorn" version = "21.2.0" @@ -588,6 +639,23 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "mpmath" +version = "1.3.0" +description = "Python library for arbitrary-precision floating-point arithmetic" +optional = false +python-versions = "*" +files = [ + {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, + {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, +] + +[package.extras] +develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] +docs = ["sphinx"] +gmpy = ["gmpy2 (>=2.1.0a4)"] +tests = ["pytest (>=4.6)"] + [[package]] name = "mypy" version = "1.8.0" @@ -656,6 +724,24 @@ files = [ {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, ] +[[package]] +name = "networkx" +version = "3.2.1" +description = "Python package for creating and manipulating graphs and networks" +optional = false +python-versions = ">=3.9" +files = [ + {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, + {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, +] + +[package.extras] +default = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] +developer = ["changelist (==0.4)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] +doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] +extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"] +test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] + [[package]] name = "numpy" version = "1.26.4" @@ -701,6 +787,147 @@ files = [ {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] +[[package]] +name = "nvidia-cublas-cu12" +version = "12.1.3.1" +description = "CUBLAS native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728"}, + {file = "nvidia_cublas_cu12-12.1.3.1-py3-none-win_amd64.whl", hash = "sha256:2b964d60e8cf11b5e1073d179d85fa340c120e99b3067558f3cf98dd69d02906"}, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.1.105" +description = "CUDA profiling tools runtime libs." +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e"}, + {file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:bea8236d13a0ac7190bd2919c3e8e6ce1e402104276e6f9694479e48bb0eb2a4"}, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.1.105" +description = "NVRTC native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2"}, + {file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:0a98a522d9ff138b96c010a65e145dc1b4850e9ecb75a0172371793752fd46ed"}, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.1.105" +description = "CUDA Runtime native Libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40"}, + {file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:dfb46ef84d73fababab44cf03e3b83f80700d27ca300e537f85f636fac474344"}, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "8.9.2.26" +description = "cuDNN runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9"}, +] + +[package.dependencies] +nvidia-cublas-cu12 = "*" + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.0.2.54" +description = "CUFFT native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56"}, + {file = "nvidia_cufft_cu12-11.0.2.54-py3-none-win_amd64.whl", hash = "sha256:d9ac353f78ff89951da4af698f80870b1534ed69993f10a4cf1d96f21357e253"}, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.2.106" +description = "CURAND native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0"}, + {file = "nvidia_curand_cu12-10.3.2.106-py3-none-win_amd64.whl", hash = "sha256:75b6b0c574c0037839121317e17fd01f8a69fd2ef8e25853d826fec30bdba74a"}, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.4.5.107" +description = "CUDA solver native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd"}, + {file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-win_amd64.whl", hash = "sha256:74e0c3a24c78612192a74fcd90dd117f1cf21dea4822e66d89e8ea80e3cd2da5"}, +] + +[package.dependencies] +nvidia-cublas-cu12 = "*" +nvidia-cusparse-cu12 = "*" +nvidia-nvjitlink-cu12 = "*" + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.1.0.106" +description = "CUSPARSE native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c"}, + {file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-win_amd64.whl", hash = "sha256:b798237e81b9719373e8fae8d4f091b70a0cf09d9d85c95a557e11df2d8e9a5a"}, +] + +[package.dependencies] +nvidia-nvjitlink-cu12 = "*" + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.19.3" +description = "NVIDIA Collective Communication Library (NCCL) Runtime" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_nccl_cu12-2.19.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:a9734707a2c96443331c1e48c717024aa6678a0e2a4cb66b2c364d18cee6b48d"}, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.4.99" +description = "Nvidia JIT LTO Library" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_nvjitlink_cu12-12.4.99-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c6428836d20fe7e327191c175791d38570e10762edc588fb46749217cd444c74"}, + {file = "nvidia_nvjitlink_cu12-12.4.99-py3-none-win_amd64.whl", hash = "sha256:991905ffa2144cb603d8ca7962d75c35334ae82bf92820b6ba78157277da1ad2"}, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.1.105" +description = "NVIDIA Tools Extension" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5"}, + {file = "nvidia_nvtx_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:65f4d98982b31b60026e0e6de73fbdfc09d08a96f4656dd3665ca616a11e1e82"}, +] + [[package]] name = "outcome" version = "1.3.0.post0" @@ -1320,6 +1547,20 @@ files = [ {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] +[[package]] +name = "sympy" +version = "1.12" +description = "Computer algebra system (CAS) in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sympy-1.12-py3-none-any.whl", hash = "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5"}, + {file = "sympy-1.12.tar.gz", hash = "sha256:ebf595c8dac3e0fdc4152c51878b498396ec7f30e7a914d6071e674d49420fb8"}, +] + +[package.dependencies] +mpmath = ">=0.19" + [[package]] name = "tenacity" version = "8.2.3" @@ -1334,6 +1575,105 @@ files = [ [package.extras] doc = ["reno", "sphinx", "tornado (>=4.5)"] +[[package]] +name = "torch" +version = "2.2.1" +description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "torch-2.2.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:8d3bad336dd2c93c6bcb3268e8e9876185bda50ebde325ef211fb565c7d15273"}, + {file = "torch-2.2.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:5297f13370fdaca05959134b26a06a7f232ae254bf2e11a50eddec62525c9006"}, + {file = "torch-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:5f5dee8433798888ca1415055f5e3faf28a3bad660e4c29e1014acd3275ab11a"}, + {file = "torch-2.2.1-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:b6d78338acabf1fb2e88bf4559d837d30230cf9c3e4337261f4d83200df1fcbe"}, + {file = "torch-2.2.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:6ab3ea2e29d1aac962e905142bbe50943758f55292f1b4fdfb6f4792aae3323e"}, + {file = "torch-2.2.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:d86664ec85902967d902e78272e97d1aff1d331f7619d398d3ffab1c9b8e9157"}, + {file = "torch-2.2.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d6227060f268894f92c61af0a44c0d8212e19cb98d05c20141c73312d923bc0a"}, + {file = "torch-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:77e990af75fb1675490deb374d36e726f84732cd5677d16f19124934b2409ce9"}, + {file = "torch-2.2.1-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:46085e328d9b738c261f470231e987930f4cc9472d9ffb7087c7a1343826ac51"}, + {file = "torch-2.2.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:2d9e7e5ecbb002257cf98fae13003abbd620196c35f85c9e34c2adfb961321ec"}, + {file = "torch-2.2.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:ada53aebede1c89570e56861b08d12ba4518a1f8b82d467c32665ec4d1f4b3c8"}, + {file = "torch-2.2.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:be21d4c41ecebed9e99430dac87de1439a8c7882faf23bba7fea3fea7b906ac1"}, + {file = "torch-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:79848f46196750367dcdf1d2132b722180b9d889571e14d579ae82d2f50596c5"}, + {file = "torch-2.2.1-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:7ee804847be6be0032fbd2d1e6742fea2814c92bebccb177f0d3b8e92b2d2b18"}, + {file = "torch-2.2.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:84b2fb322ab091039fdfe74e17442ff046b258eb5e513a28093152c5b07325a7"}, + {file = "torch-2.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5c0c83aa7d94569997f1f474595e808072d80b04d34912ce6f1a0e1c24b0c12a"}, + {file = "torch-2.2.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:91a1b598055ba06b2c386415d2e7f6ac818545e94c5def597a74754940188513"}, + {file = "torch-2.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f93ddf3001ecec16568390b507652644a3a103baa72de3ad3b9c530e3277098"}, + {file = "torch-2.2.1-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:0e8bdd4c77ac2584f33ee14c6cd3b12767b4da508ec4eed109520be7212d1069"}, + {file = "torch-2.2.1-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:6a21bcd7076677c97ca7db7506d683e4e9db137e8420eb4a68fb67c3668232a7"}, + {file = "torch-2.2.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f1b90ac61f862634039265cd0f746cc9879feee03ff962c803486301b778714b"}, + {file = "torch-2.2.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:ed9e29eb94cd493b36bca9cb0b1fd7f06a0688215ad1e4b3ab4931726e0ec092"}, + {file = "torch-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:c47bc25744c743f3835831a20efdcfd60aeb7c3f9804a213f61e45803d16c2a5"}, + {file = "torch-2.2.1-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:0952549bcb43448c8d860d5e3e947dd18cbab491b14638e21750cb3090d5ad3e"}, + {file = "torch-2.2.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:26bd2272ec46fc62dcf7d24b2fb284d44fcb7be9d529ebf336b9860350d674ed"}, +] + +[package.dependencies] +filelock = "*" +fsspec = "*" +jinja2 = "*" +networkx = "*" +nvidia-cublas-cu12 = {version = "12.1.3.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-cupti-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-nvrtc-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-runtime-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cudnn-cu12 = {version = "8.9.2.26", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cufft-cu12 = {version = "11.0.2.54", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-curand-cu12 = {version = "10.3.2.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cusolver-cu12 = {version = "11.4.5.107", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cusparse-cu12 = {version = "12.1.0.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-nccl-cu12 = {version = "2.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-nvtx-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +sympy = "*" +typing-extensions = ">=4.8.0" + +[package.extras] +opt-einsum = ["opt-einsum (>=3.3)"] +optree = ["optree (>=0.9.1)"] + +[[package]] +name = "torchvision" +version = "0.17.1" +description = "image and video datasets and models for torch deep learning" +optional = false +python-versions = ">=3.8" +files = [ + {file = "torchvision-0.17.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:06418880212b66e45e855dd39f536e7fd48b4e6b034a11dd9fe9e2384afb51ec"}, + {file = "torchvision-0.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33d65d0c7fdcb3f7bc1dd8ed30ea3cd7e0587b4ad1b104b5677c8191a8bad9f1"}, + {file = "torchvision-0.17.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:aaefef2be6a02f206085ce4bb6c0078b03ebf48cb6ff82bd762ff6248475e08e"}, + {file = "torchvision-0.17.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:ebe5fdb466aff8a8e8e755de84a843418b6f8d500624752c05eaa638d7700f3d"}, + {file = "torchvision-0.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:9d4d45a996f4313e9c5db4da71d31508d44f7ccfbf29d3442bdcc2ad13e0b6f3"}, + {file = "torchvision-0.17.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:ea2ccdbf5974e0bf27fd6644a33b19cb0700297cf397bb0469e762c11c6c4105"}, + {file = "torchvision-0.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9106e32c9f1e70afa8172cf1b064cf9c2998d8dff0769ec69d537b20209ee43d"}, + {file = "torchvision-0.17.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:5966936c669a08870f6547cd0a90d08b157aeda03293f79e2adbb934687175ed"}, + {file = "torchvision-0.17.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:e74f5a26ef8190eab0c38b3f63914fea94e58e3b2f0e5466611c9f63bd91a80b"}, + {file = "torchvision-0.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:a2109c1a1dcf71e8940d43e91f78c4dd5bf0fcefb3a0a42244102752009f5862"}, + {file = "torchvision-0.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5d241d2a5fb4e608677fccf6f80b34a124446d324ee40c7814ce54bce888275b"}, + {file = "torchvision-0.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e0fe98d9d92c23d2262ff82f973242951b9357fb640f8888ac50848bd00f5b45"}, + {file = "torchvision-0.17.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:32dc5de86d2ade399e11087095674ca08a1649fb322cfe69336d28add467edcb"}, + {file = "torchvision-0.17.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:54902877410ffb5458ee52b6d0de4b25cf01496bee736d6825301a5f0398536e"}, + {file = "torchvision-0.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc22c1ed0f1aba3f98fd72b6f60021f57aec1d2f6af518522e8a0a83848de3a8"}, + {file = "torchvision-0.17.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:2621097065fa1c827885e2b52102e839a3541b933b7a90e0fa3c42c3de1bc3cf"}, + {file = "torchvision-0.17.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5ce76466af2b5a30573939cae1e6e62e29316ceb3ee748091002f312ab0912f6"}, + {file = "torchvision-0.17.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bd5dcd14a32945c72f5c19341add94aa7c23dd7bca2bafde44d0f3c4344d17ed"}, + {file = "torchvision-0.17.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:dca22795cc02ca0d5ddc08c1422ff620bc9899f63d15dc36f71ef37250e17b75"}, + {file = "torchvision-0.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:524405457dd97d9ab0e48df502f819d0f41a113ce8f00470bb9926d9d36efcf1"}, + {file = "torchvision-0.17.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:58299a724b37b893c7ce4d0b32ea1480c30e467cc114167964b45f6013f6c2d3"}, + {file = "torchvision-0.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8a1b17fb158b2b881f2c8796fe1839a624e49d5fd07aa61f6dae60ba4819421a"}, + {file = "torchvision-0.17.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:429d63eb7551aa4d8f6cdf08d109b5570c20cbcce36d9cb95b24556418e4dc82"}, + {file = "torchvision-0.17.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0ecc9a58171bd555aed583bf2f72e7fd6cc4f767c14f8b80b6a8725eacf4ceb1"}, + {file = "torchvision-0.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:5f427ebee15521edcd836bfe05e86feb5189b5c943b9e3999ed0e3f391fbaa1d"}, +] + +[package.dependencies] +numpy = "*" +pillow = ">=5.3.0,<8.3.dev0 || >=8.4.dev0" +torch = "2.2.1" + +[package.extras] +scipy = ["scipy"] + [[package]] name = "trio" version = "0.24.0" @@ -1486,4 +1826,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "849178e665e9a43935d7508528a82d8e0e173020b9ee4256e6a25d3e607f1bd8" +content-hash = "6c4bb66c774e93a08e4180f3148942edf91e2b5620ea1f6c3e4c30045bb643e5" diff --git a/pyproject.toml b/pyproject.toml index 4d330e0..95dfad4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,11 @@ selenium = "^4.18.1" webdriver-manager = "^4.0.1" retry = "^0.9.2" + +[tool.poetry.group.model.dependencies] +torch = "^2.2.1" +torchvision = "^0.17.1" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/src/database/__init__.py b/src/database/__init__.py deleted file mode 100644 index 0990ebc..0000000 --- a/src/database/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import annotations - -from .classes import ModelOutputs -from .classes import NoDocumentFoundException -from .classes import VisualCommunication -from .database import connect -from .utils import get_visual_communication -from .utils import total_annotated -from .utils import total_documents -from .utils import upsert_annotations -from .utils import upsert_predictions -from .utils import upsert_visual_communication diff --git a/src/database/classes.py b/src/database/classes.py deleted file mode 100644 index 165e949..0000000 --- a/src/database/classes.py +++ /dev/null @@ -1,165 +0,0 @@ -from __future__ import annotations - -import logging -from base64 import b64decode -from base64 import b64encode -from io import BytesIO -from pathlib import Path - -from PIL import Image -from pydantic import BaseModel -from pydantic import field_serializer -from pydantic import field_validator - -from src.model_experiential import ( - VisualSyntaxModelOutput, -) -from src.model_interpersonal import AngleModelOutput -from src.model_interpersonal import ContactModelOutput -from src.model_interpersonal import DistanceModelOutput -from src.model_interpersonal import ModalityColorModelOutput -from src.model_interpersonal import ModalityDepthModelOutput -from src.model_interpersonal import ModalityLightingModelOutput -from src.model_interpersonal import PointOfViewModelOutput -from src.model_textual import FramingModelOutput -from src.model_textual import InformationValueModelOutput -from src.model_textual import SalienceModelOutput - - -class NoDocumentFoundException(Exception): - pass - - -class ModelOutputs(BaseModel): - visual_syntax: VisualSyntaxModelOutput - contact: ContactModelOutput - angle: AngleModelOutput - point_of_view: PointOfViewModelOutput - distance: DistanceModelOutput - modality_lighting: ModalityLightingModelOutput - modality_color: ModalityColorModelOutput - modality_depth: ModalityDepthModelOutput - information_value: InformationValueModelOutput - framing: FramingModelOutput - salience: SalienceModelOutput - - @classmethod - def list_fields(cls) -> list[str]: - """List options that are stored as attributes.""" - return list(cls.model_fields.keys()) - - @classmethod - def from_random(cls) -> ModelOutputs: - """Instantiate with random numbers.""" - kwargs = { - field: field_info.annotation.from_random() # type: ignore - for field, field_info - in cls.model_fields.items() - } - return cls(**kwargs) - - @classmethod - def from_annotations( - cls, - visual_syntax: str, - contact: str, - angle: str, - point_of_view: str, - distance: str, - modality_lighting: str, - modality_color: str, - modality_depth: str, - information_value: str, - framing: str, - salience: str, - ) -> ModelOutputs: - """Instantiate from annotation.""" - kwargs = { - 'visual_syntax': VisualSyntaxModelOutput - .from_choice(visual_syntax), - 'contact': ContactModelOutput - .from_choice(contact), - 'angle': AngleModelOutput - .from_choice(angle), - 'point_of_view': PointOfViewModelOutput - .from_choice(point_of_view), - 'distance': DistanceModelOutput - .from_choice(distance), - 'modality_lighting': ModalityLightingModelOutput - .from_choice(modality_lighting), - 'modality_color': ModalityColorModelOutput - .from_choice(modality_color), - 'modality_depth': ModalityDepthModelOutput - .from_choice(modality_depth), - 'information_value': InformationValueModelOutput - .from_choice(information_value), - 'framing': FramingModelOutput - .from_choice(framing), - 'salience': SalienceModelOutput - .from_choice(salience), - } - return cls(**kwargs) - - -class VisualCommunication(BaseModel): - name: str - image: Image.Image - annotation: ModelOutputs | None = None - prediction: ModelOutputs | None = None - - class Config: - arbitrary_types_allowed = True - - @classmethod - def classname(cls) -> str: - """Return classname.""" - return cls.__name__ - - @classmethod - def from_file(cls, path: Path) -> VisualCommunication: - """Instantiate from file.""" - name = path.stem - image = Image.open(path) - image.load() - return VisualCommunication(name=name, image=image) - - @classmethod - def decode_image(cls, content: str) -> Image.Image: - """Decode image.""" - _, content_data = content.split(',') - return Image.open(BytesIO(b64decode(content_data))) - - @field_serializer('image') - def serialize_image(image: Image.Image) -> bytes: # type: ignore - buffer = BytesIO() - image.save(buffer, format='JPEG') - return buffer.getvalue() - - @field_validator('image', mode='before') - @classmethod - def convert_to_image( - cls, - image: Image.Image | BytesIO | bytes, - ) -> Image.Image: - if isinstance(image, bytes): - image = BytesIO(image) - if isinstance(image, BytesIO): - image = Image.open(image) - return image - - def __repr__(self) -> str: - return f"{self.classname()}(name='{self.name}')" - - def webencoded_image(self) -> str: - """Convert image to be displayed on webpage.""" - # convert images to bytes string - buffer = BytesIO() - self.image.save(buffer, format='png') - img_enc = b64encode(buffer.getvalue()).decode('utf-8') - return f"data:image/png;base64, {img_enc}" - - def generate_random_prediction(self, force: bool = False) -> None: - """Generate random prediction values.""" - if not force and self.prediction is not None: - logging.warning('set force=True to overwrite existing values.') - self.prediction = ModelOutputs.from_random() diff --git a/src/database/database.py b/src/database/database.py deleted file mode 100644 index 8f10ac8..0000000 --- a/src/database/database.py +++ /dev/null @@ -1,24 +0,0 @@ -from pymongo import MongoClient -from dotenv import load_dotenv -import logging -import os - - -def connect(): - """Connect to MongoDB.""" - # load env vars - load_dotenv() - necessary_env_vars = [ - "MONGO_HOST", - "MONGO_DB", - "MONGO_COLLECTION" - ] - for env_var in necessary_env_vars: - assert env_var in os.environ, f"{env_var} not found" - # connect to database - client = MongoClient(os.getenv("MONGO_HOST")) - db = client[os.getenv("MONGO_DB")] - collection = db[os.getenv("MONGO_COLLECTION")] - collection.create_index("name", unique=True) - logging.info("connected to database") - return collection, db, client diff --git a/src/database/utils.py b/src/database/utils.py deleted file mode 100644 index d43fa77..0000000 --- a/src/database/utils.py +++ /dev/null @@ -1,120 +0,0 @@ -from __future__ import annotations - -import logging - -from pymongo.collection import Collection - -from .classes import ModelOutputs -from .classes import NoDocumentFoundException -from .classes import VisualCommunication - - -def total_documents( - collection: Collection, -) -> int: - """Get total number of documents in database.""" - return collection.count_documents(filter={}) - - -def total_annotated( - collection: Collection, -) -> int: - """Get total number of annotated documents in database.""" - query = { - 'annotation': { - '$ne': None, - }, - } - return collection.count_documents(filter=query) - - -def get_visual_communication( - collection: Collection, - with_annotation: bool = False, -) -> VisualCommunication: - """Get a random visual communication from the database.""" - query = {} - if with_annotation: - query['annotation'] = {'$ne': None} - else: - query['annotation'] = {'$eq': None} - data = collection.aggregate([ - { - '$match': query, # find using filters - }, - { - '$sample': { - 'size': 1, # get one random - }, - }, - ]) - data_list = list(data) # read data from cursor object - if len(data_list) == 0: - logging.error('failed getting visual communication') - raise NoDocumentFoundException() - logging.info('finished') - return VisualCommunication.model_validate(data_list[0]) - - -def upsert_predictions( - collection: Collection, - vis_com_name: str, - predictions: ModelOutputs, -) -> None: - """Upsert prediction data in the database.""" - query = { - 'name': vis_com_name, - } - update = { - '$set': { - 'prediction': predictions.model_dump(), - }, - } - res = collection.update_one( - filter=query, - update=update, - upsert=True, - ) - logging.debug('upserted document: %s', res) - logging.info('finished') - - -def upsert_annotations( - collection: Collection, - vis_com_name: str, - annotations: ModelOutputs, -) -> None: - """Upserts annotation data in the database.""" - query = { - 'name': vis_com_name, - } - update = { - '$set': { - 'annotation': annotations.model_dump(), - }, - } - res = collection.update_one( - filter=query, - update=update, - upsert=True, - ) - logging.info('upserted document: %s', res) - logging.info('finished') - - -def upsert_visual_communication( - collection: Collection, - visual_communication_list: list[VisualCommunication], -) -> bool: - """ - Upsert VisualCommunication object in the database. - Returns bool stating success. - """ - response = collection.insert_many( - [ - vis_com.model_dump() - for vis_com - in visual_communication_list - ], - ) - return response.acknowledged diff --git a/src/web/app.py b/src/web/app.py index 4f2d4f9..151fcd9 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -13,15 +13,14 @@ from dash_auth import BasicAuth from pydantic import ValidationError from .layout import app_layout -from src.database import connect -from src.database import get_visual_communication -from src.database import ModelOutputs -from src.database import NoDocumentFoundException -from src.database import total_annotated -from src.database import total_documents -from src.database import upsert_annotations -from src.database import upsert_visual_communication -from src.database import VisualCommunication +from core.database import connect +from core.database import count_documents +from core.database import get_visual_communication +from core.database import NoDocumentFoundException +from core.database import upsert_annotation +from core.database import upsert_visual_communication +from core.database import VisualCommunication +from core.dto import ModelData # setup app app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) @@ -82,8 +81,14 @@ def update_progress_bar( logging.info('began updating progress bar') global collection # get values from database - num_total = total_documents(collection=collection) - num_handled = total_annotated(collection=collection) + num_total = count_documents( + collection=collection, + only_with_annotation=False, + ) + num_handled = count_documents( + collection=collection, + only_with_annotation=True, + ) limit = int(num_total/20) label_str = f"{num_handled}/{num_total}" if num_handled >= limit else '' return num_handled, num_total, label_str @@ -200,9 +205,9 @@ def cycle_visual_communication_data( in zip(annotation_keys, annotation_values) } # instantiate ModelOutputs object - annotations = ModelOutputs.from_annotations(**annotation_map) + annotations = ModelData.from_annotations(**annotation_map) # save data to database - upsert_annotations( + upsert_annotation( collection=collection, vis_com_name=vis_com_name, annotations=annotations, diff --git a/src/web/layout/labels.py b/src/web/layout/labels.py index b7081d7..510b70f 100644 --- a/src/web/layout/labels.py +++ b/src/web/layout/labels.py @@ -4,35 +4,24 @@ import dash_mantine_components as dmc from dash import dcc from dash import html -from src.model_experiential import VisualSyntaxModelOutput -from src.model_interpersonal import AngleModelOutput -from src.model_interpersonal import ContactModelOutput -from src.model_interpersonal import DistanceModelOutput -from src.model_interpersonal import ModalityColorModelOutput -from src.model_interpersonal import ModalityDepthModelOutput -from src.model_interpersonal import ModalityLightingModelOutput -from src.model_interpersonal import PointOfViewModelOutput -from src.model_textual import FramingModelOutput -from src.model_textual import InformationValueModelOutput -from src.model_textual import SalienceModelOutput - - -def generate_option_labels(model) -> list[str]: - """Generate presentable list of attributes from an OutputModel.""" - labels = [ - label.replace('_', ' ').title() - for label in model.list_fields() - ] - return labels +from core.dto import AngleData +from core.dto import ContactData +from core.dto import DistanceData +from core.dto import FramingData +from core.dto import InformationValueData +from core.dto import ModalityColorData +from core.dto import ModalityDepthData +from core.dto import ModalityLightingData +from core.dto import PointOfViewData +from core.dto import SalienceData +from core.dto import VisualSyntaxData def generate_visual_syntax_options_map(): """Generate map of titles and options for visual syntax labels.""" options_map = {} # add experiential labels - options_map['visual syntax'] = generate_option_labels( - VisualSyntaxModelOutput, - ) + options_map['visual syntax'] = VisualSyntaxData.list_fields() return options_map @@ -40,21 +29,13 @@ def generate_interpersonal_options_map(): """Generate map of titles and options for interpersonal labels.""" options_map = {} # add interpersonal labels - options_map['contact'] = generate_option_labels(ContactModelOutput) - options_map['angle'] = generate_option_labels(AngleModelOutput) - options_map['point of view'] = generate_option_labels( - PointOfViewModelOutput, - ) - options_map['distance'] = generate_option_labels(DistanceModelOutput) - options_map['modality lighting'] = generate_option_labels( - ModalityLightingModelOutput, - ) - options_map['modality color'] = generate_option_labels( - ModalityColorModelOutput, - ) - options_map['modality depth'] = generate_option_labels( - ModalityDepthModelOutput, - ) + options_map['contact'] = ContactData.list_fields() + options_map['angle'] = AngleData.list_fields() + options_map['point of view'] = PointOfViewData.list_fields() + options_map['distance'] = DistanceData.list_fields() + options_map['modality lighting'] = ModalityLightingData.list_fields() + options_map['modality color'] = ModalityColorData.list_fields() + options_map['modality depth'] = ModalityDepthData.list_fields() return options_map @@ -62,11 +43,9 @@ def generate_textual_options_map(): """Generate map of titles and options for textual labels.""" options_map = {} # add textual labels - options_map['information value'] = generate_option_labels( - InformationValueModelOutput, - ) - options_map['framing'] = generate_option_labels(FramingModelOutput) - options_map['salience'] = generate_option_labels(SalienceModelOutput) + options_map['information value'] = InformationValueData.list_fields() + options_map['framing'] = FramingData.list_fields() + options_map['salience'] = SalienceData.list_fields() return options_map @@ -83,7 +62,7 @@ for title, options in experiential_map.items(): dmc.Container([ html.B(title.title()), dcc.RadioItems( - options=options, + options=[text.replace('_', ' ') for text in options], id=id_dict, ), ]), @@ -101,7 +80,7 @@ for title, options in interpersonal_map.items(): dmc.Container([ html.B(title.title()), dcc.RadioItems( - options=options, + options=[text.replace('_', ' ') for text in options], id=id_dict, ), ]), @@ -117,9 +96,9 @@ for title, options in textual_map.items(): id_dict = {'type': 'annotation', 'index': title.replace('_', '-')} textual_container.children.append( dmc.Container([ - html.B(title), + html.B(title.title()), dcc.RadioItems( - options=options, + options=[text.replace('_', ' ') for text in options], id=id_dict, ), ]), diff --git a/tests/generate_random_prediction_test.py b/tests/generate_random_prediction_test.py index ead903a..c97e54c 100644 --- a/tests/generate_random_prediction_test.py +++ b/tests/generate_random_prediction_test.py @@ -2,7 +2,7 @@ from __future__ import annotations from pathlib import Path -from database import VisualCommunication +from core.database.classes import VisualCommunication if __name__ == '__main__': diff --git a/tests/get_visual_communication_test.py b/tests/get_visual_communication_test.py index f701f2a..6cd29cc 100644 --- a/tests/get_visual_communication_test.py +++ b/tests/get_visual_communication_test.py @@ -3,10 +3,11 @@ from __future__ import annotations import os from pathlib import Path -from database import connect -from database import get_visual_communication from dotenv import load_dotenv +from core.database import connect +from core.database import get_visual_communication + if __name__ == '__main__': # prepare env vars env_path = Path(__file__).parent.parent / 'local.env' diff --git a/tests/image_download_test.py b/tests/image_download_test.py index 76a166f..8f94244 100644 --- a/tests/image_download_test.py +++ b/tests/image_download_test.py @@ -3,10 +3,11 @@ from __future__ import annotations import os from pathlib import Path -from database import connect -from database import VisualCommunication from dotenv import load_dotenv +from core.database import connect +from core.database.classes import VisualCommunication + if __name__ == '__main__': # prepare env vars env_path = Path(__file__).parent.parent / 'local.env' diff --git a/tests/image_upload_test.py b/tests/image_upload_test.py index 30447e7..4a99c71 100644 --- a/tests/image_upload_test.py +++ b/tests/image_upload_test.py @@ -3,11 +3,12 @@ from __future__ import annotations import os from pathlib import Path -from database import connect -from database import VisualCommunication from dotenv import load_dotenv from pymongo.errors import DuplicateKeyError +from core.database import connect +from core.database.classes import VisualCommunication + if __name__ == '__main__': # get list of image paths test_dir = Path(__file__).parent diff --git a/tests/image_upload_to_server_test.py b/tests/image_upload_to_server_test.py index 3ce29b2..5db75e2 100644 --- a/tests/image_upload_to_server_test.py +++ b/tests/image_upload_to_server_test.py @@ -5,8 +5,8 @@ from pathlib import Path from dotenv import load_dotenv from pymongo.errors import DuplicateKeyError -from src.database import connect -from src.database import VisualCommunication +from core.database import connect +from core.database import VisualCommunication if __name__ == '__main__': # get list of image paths diff --git a/tests/model_outputs_from_annotation_test.py b/tests/model_outputs_from_annotation_test.py index e3dc04e..c4844fb 100644 --- a/tests/model_outputs_from_annotation_test.py +++ b/tests/model_outputs_from_annotation_test.py @@ -1,12 +1,12 @@ from __future__ import annotations -from database import ModelOutputs +from core.dto import ModelData if __name__ == '__main__': # instantiate data object vis_com_list = [ - ModelOutputs.from_random() + ModelData.from_random() for i in range(3) ] diff --git a/tests/prediction_upload_test.py b/tests/prediction_upload_test.py index e517b84..94246c0 100644 --- a/tests/prediction_upload_test.py +++ b/tests/prediction_upload_test.py @@ -4,11 +4,12 @@ import logging import os from pathlib import Path -from database import connect -from database import upsert_predictions -from database import VisualCommunication from dotenv import load_dotenv +from core.database import connect +from core.database import upsert_predictions +from core.database.classes import VisualCommunication + if __name__ == '__main__': # setup logging fmt = ( diff --git a/tests/total_annotated_test.py b/tests/total_annotated_test.py index 355a90d..0dafadb 100644 --- a/tests/total_annotated_test.py +++ b/tests/total_annotated_test.py @@ -3,10 +3,11 @@ from __future__ import annotations import os from pathlib import Path -from database import connect -from database import total_annotated from dotenv import load_dotenv +from core.database import connect +from core.database import total_documents + if __name__ == '__main__': # prepare env vars env_path = Path(__file__).parent.parent / 'local.env' @@ -16,5 +17,7 @@ if __name__ == '__main__': # connect to database collection, db, client = connect() # get visual communication - num_docs = total_annotated(collection) + num_docs = total_documents( + collection=collection, + ) print(f"number of annotated documents in database: {num_docs}") diff --git a/tests/total_documents_test.py b/tests/total_documents_test.py index 7a150f4..e6e04a7 100644 --- a/tests/total_documents_test.py +++ b/tests/total_documents_test.py @@ -3,10 +3,11 @@ from __future__ import annotations import os from pathlib import Path -from database import connect -from database import total_documents from dotenv import load_dotenv +from core.database import connect +from core.database import total_documents + if __name__ == '__main__': # prepare env vars env_path = Path(__file__).parent.parent / 'local.env' @@ -16,5 +17,7 @@ if __name__ == '__main__': # connect to database collection, db, client = connect() # get visual communication - num_docs = total_documents(collection) + num_docs = total_documents( + collection=collection, + ) print(f"total number of documents in database: {num_docs}")