Skip to content

AJP-3 introduce AI predictions #136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Generated by Django 3.2.7 on 2023-10-14 14:43

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('ajapaik', '0025_importblacklist'),
]

operations = [
migrations.CreateModel(
name='PhotoModelSuggestionResult',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, db_index=True)),
('viewpoint_elevation', models.PositiveSmallIntegerField(blank=True, choices=[(0, 'Ground'), (1, 'Raised'), (2, 'Aerial')], null=True, verbose_name='Viewpoint elevation')),
('scene', models.PositiveSmallIntegerField(blank=True, choices=[(0, 'Interior'), (1, 'Exterior')], null=True, verbose_name='Scene')),
('photo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ajapaik.photo')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='PhotoModelSuggestionAlternativeCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, db_index=True)),
('viewpoint_elevation_alternation', models.PositiveSmallIntegerField(blank=True, choices=[(0, 'Ground'), (1, 'Raised'), (2, 'Aerial')], null=True, verbose_name='Viewpoint elevation')),
('scene_alternation', models.PositiveSmallIntegerField(blank=True, choices=[(0, 'Interior'), (1, 'Exterior')], null=True, verbose_name='Scene')),
('photo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ajapaik.photo')),
('proposer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='photo_scene_suggestions_alternation', to='ajapaik.profile')),
],
options={
'db_table': 'ajapaik_photomodelsuggestionalternativecategory',
'unique_together': {('proposer', 'photo_id', 'scene_alternation'), ('proposer', 'photo_id', 'viewpoint_elevation_alternation')},
},
),
]
57 changes: 57 additions & 0 deletions ajapaik/ajapaik/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2074,6 +2074,63 @@ class PhotoViewpointElevationSuggestion(Suggestion):
proposer = ForeignKey('Profile', blank=True, null=True, related_name='photo_viewpoint_elevation_suggestions',
on_delete=CASCADE)

class PhotoModelSuggestionResult(Suggestion):
INTERIOR, EXTERIOR = range(2)
GROUND_LEVEL, RAISED, AERIAL = range(3)
SCENE_CHOICES = (
(INTERIOR, _('Interior')),
(EXTERIOR, _('Exterior'))
)
VIEWPOINT_ELEVATION_CHOICES = (
(GROUND_LEVEL, _('Ground')),
(RAISED, _('Raised')),
(AERIAL, _('Aerial'))
)
viewpoint_elevation = PositiveSmallIntegerField(_('Viewpoint elevation'), choices=VIEWPOINT_ELEVATION_CHOICES, blank=True, null=True)
scene = PositiveSmallIntegerField(_('Scene'), choices=SCENE_CHOICES, blank=True, null=True)


class PhotoModelSuggestionAlternativeCategory(Suggestion):
INTERIOR, EXTERIOR = range(2)
Copy link
Member

Choose a reason for hiding this comment

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

I think we should not duplicate

INTERIOR, EXTERIOR
....
....
VIEWPOINT_ELEVATION_CHOICES

Variables, as they are all the same. We can just define them outside of Class, on top of this file? In case we have any previous usages of the same variables, could remove those usages as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Addressed!

GROUND_LEVEL, RAISED, AERIAL = range(3)
SCENE_CHOICES = (
(INTERIOR, _('Interior')),
(EXTERIOR, _('Exterior'))
)
VIEWPOINT_ELEVATION_CHOICES = (
(GROUND_LEVEL, _('Ground')),
(RAISED, _('Raised')),
(AERIAL, _('Aerial'))
)
viewpoint_elevation_alternation = PositiveSmallIntegerField(_('Viewpoint elevation'),
choices=VIEWPOINT_ELEVATION_CHOICES, blank=True,
null=True)
scene_alternation = PositiveSmallIntegerField(_('Scene'), choices=SCENE_CHOICES, blank=True, null=True)
Copy link
Member

Choose a reason for hiding this comment

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

I feel like the _alternation suffix could be skipped here, as the class/model name already implies that this value is an alternate.

Same for viewpoint_elevation_alternation


proposer = ForeignKey('Profile', blank=True, null=True, related_name='photo_scene_suggestions_alternation',
on_delete=CASCADE)

def validate_unique(self, exclude=None):
# super().validate_unique(exclude)
Copy link
Member

Choose a reason for hiding this comment

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

I think that uniqueness in Django can be achieved by using

https://docs.djangoproject.com/en/3.2/ref/models/constraints/#django.db.models.UniqueConstraint

So that we don't need to write a custom validate_unique method.

Right now it's unique if:

scene_alternation is null/blank OR photo, proposer are unique together with scene_alternation?

It might not work here, but just thought to mention just in case :)

Copy link
Member

Choose a reason for hiding this comment

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

So, I'm just checking if uniqueness logic is this specific that we don't care if scene_alternation is null/blank?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, you are correct here. Indeed viewpoint elevation was missing. I addressed the comment accordingly.

queryset = self.__class__._default_manager.filter(
Q(scene_alternation=0) | Q(scene_alternation=1),
proposer=self.proposer,
photo_id=self.photo_id
).exclude(pk=self.pk)

if self.scene_alternation in ['0', '1'] and queryset.exists():
return False
return True

def save(self, *args, **kwargs):
if self.validate_unique():
super().save(*args, **kwargs)

class Meta:
db_table = 'ajapaik_photomodelsuggestionalternativecategory'
unique_together = (('proposer', 'photo_id', 'scene_alternation'),
('proposer', 'photo_id', 'viewpoint_elevation_alternation'))


class PhotoFlipSuggestion(Suggestion):
proposer = ForeignKey('Profile', blank=True, null=True, related_name='photo_flip_suggestions', on_delete=CASCADE)
Expand Down
98 changes: 98 additions & 0 deletions ajapaik/ajapaik/static/js/ajp-category-suggestion.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
function submitCategorySuggestion(photoIds, isMultiSelect) {
sendCategorySuggestionToAI(photoIds, scene, viewpointElevation)
$('#ajp-loading-overlay').show();
return fetch(photoSceneUrl, {
method: 'POST',
Expand Down Expand Up @@ -99,3 +100,100 @@ function clickSceneCategoryButton(buttonId) {
$('#' + buttonId).removeClass('btn-outline-dark');
$('#' + buttonId).removeClass('btn-light');
}

function displaySmallAIIcon(categoryMap) {
let scene = categoryMap["scene"]
let viewpointElevation = categoryMap["viewpoint_elevation"]

if (scene === "exterior") {
$("#exterior-ai").show();
}
if (scene === "interior") {
$("#interior-ai").show();
}
if (viewpointElevation === "ground") {
$("#ground-ai").show();
}
if (viewpointElevation === "aerial") {
$("#aerial-ai").show();
}
if (viewpointElevation === "raised") {
$("#raised-ai").show();
}
}

function determinePictureCategory(jsonData) {
let modelCategory = {};
if (jsonData && jsonData.length > 0) {
let fields = jsonData[0].fields;
if (fields && "scene" in fields) {
if (fields["scene"] === 0) {
modelCategory["scene"] = "interior";
} else {
modelCategory["scene"] = "exterior";
Copy link
Member

Choose a reason for hiding this comment

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

Usually we have used else if - is there a risk that scene can be null, but we handle it as exterior or do we need a fallback value (exterior) here?

}
}
if (fields && "viewpoint_elevation" in fields) {
if (fields["viewpoint_elevation"] === 0) {
modelCategory["viewpoint_elevation"] = "ground";
} else if (fields["viewpoint_elevation"] === 1) {
modelCategory["viewpoint_elevation"] = "raised";
} else if (fields["viewpoint_elevation"] === 2) {
modelCategory["viewpoint_elevation"] = "aerial";
}
}
}
return modelCategory;
}

function markButtonsWithCategories(scene, viewpointElevation) {
if (scene === "exterior") {
clickSceneCategoryButton('exterior-button');
}
if (scene === "interior") {
clickSceneCategoryButton('interior-button');
}
if (viewpointElevation === "ground") {
clickViewpointElevationCategoryButton('ground-button');
}
if (viewpointElevation === "aerial") {
clickViewpointElevationCategoryButton('aerial-button');
}
if (viewpointElevation === "raised") {
clickViewpointElevationCategoryButton('raised-button');
}
}

function sendCategorySuggestionToAI(photoIds, scene, viewpointElevation) {
let sceneVerdict = scene.toLowerCase();
let viewpointElevationVerdict = viewpointElevation.toLowerCase();

let payload = {
"photo_id": photoIds[0]
};

if (sceneVerdict === "interior") {
payload["scene_to_alternate"] = 0
}
if (sceneVerdict === "exterior") {
payload["scene_to_alternate"] = 1
}
if (viewpointElevationVerdict === "ground") {
payload["viewpoint_elevation_to_alternate"] = 0
}
if (viewpointElevationVerdict === "raised") {
payload["viewpoint_elevation_to_alternate"] = 1
}
if (viewpointElevationVerdict === "raised") {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (viewpointElevationVerdict === "raised") {
if (viewpointElevationVerdict === "aerial") {

payload["viewpoint_elevation_to_alternate"] = 2
Copy link
Member

Choose a reason for hiding this comment

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

Just to clarify, do we need to check for === "raised" twice? In any case, that would mean the value is always 2 - if it is raised.
I guess there is another viewpointElevationVerdict - "aerial" (or something of the sort)?

}

postRequest(
'/object-categorization/confirm-latest-category',
payload,
constants.translations.queries.POST_CATEGORY_CONFIRMATION_SUCCESS,
constants.translations.queries.POST_CATEGORY_CONFIRMATION_FAILED,
function () {
}
);
}
7 changes: 7 additions & 0 deletions ajapaik/ajapaik/static/js/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ var constants = {
},
},
queries: {
GET_CATEGORY_FAILED: gettext('Failed to load object category'),
POST_CATEGORY_CONFIRMATION_SUCCESS: gettext(
'Successfully posted object category confirmation'
),
POST_CATEGORY_CONFIRMATION_FAILED: gettext(
'Failed to post object category confirmation'
),
GET_ANNOTATION_CLASSES_FAILED: gettext(
'Failed to load object annotation classes'
),
Expand Down
Loading