-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnosebook_visual_smell_references.py
643 lines (538 loc) · 32.7 KB
/
nosebook_visual_smell_references.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
# -*- coding: utf-8 -*-
"""Nosebook: Visual Smell References
Automatically generated by Colaboratory.
Original file is located at
https://colab.research.google.com/drive/19Pr1ehm1TftXNXKJ0WGGh2jzaZ-ixhJ7
# Image Nosebook: Tracing Visual Smell Trends
![Odereuropa Image Data Teaser](https://lh3.googleusercontent.com/drive-viewer/AK7aPaACCJOuTPv6r9fjnWuRoD21WiSitS3aEbV2pLtXzVlUeOiFRgYXy7kPg8kv3yGWPOdg8QVHoMCzH8NqhFN3ZJJerjtB=s1600)
Welcome to this Jupyter Notebook designed for the exploration of the Odeuropa Project Image Data. It consists of three main parts:
1. Code and data preparation. This part can mostly be ignored. Just run it once by clicking on "run cell" button (right-facing triangle button to the left of the cells).
2. Query Image Data. This section enables you to explore our image data by querying for smell-related objects annotated in the images, or by the image meta data.
3. Quantitative Analysis. In this section, you will see an examplarily walkthrough of how the annotations and meta data can be used to gain statistical insights.
Each code cell is collapsed by default. Feel free to expand the cells, explore and play around with the code. Don't be afraid to break things, you are working on a local copy of the notebook and the original version can always be restored.
# Code and Data Preparation
Run the following cells to prepare the code and data for exploration. As long as the section is collapsed, you don't have to do anything but click on the triangle within the circle and wait. Note that this will take 15-20 minutes as the images have to loaded into the colab instance. In the meantime, we suggest you grab a cup of tea or coffee and enjoy its beautiful smell or coffee or explore the textual reference [nosebook](https://bit.ly/odeuropa-text).
## Code Preparation
### import libraries
"""
import pycocotools.coco as coco
import os
import matplotlib.pyplot as plt
import json
import cv2
import pandas as pd
import ipywidgets as widgets
from ipywidgets.widgets import interact, interact_manual
import time
import io
from IPython.display import display, clear_output, Image, HTML
widgets.__version__
"""### Define helper functions"""
from google.colab import drive
def mount_drive():
drive.mount('/content/drive')
def filter_cats_conf(coco_json, cat_ids, min_confidence):
anns = [ann for ann in coco_json['annotations'] if ann['category_id'] in cat_ids and ann['score'] >= min_confidence]
return anns
def get_anns_for_conf(confidence):
return [ann for ann in annotations_json['annotations'] if ann['score'] >= confidence]
def filter_years(anns, year_min, year_max):
return [ann for ann in anns if ann['year'] is not None if ann['year'] >= year_min if ann['year'] <= year_max]
def get_subcats(supercat):
return [cat['name'] for cat in annotations_json['categories'] if cat['supercategory'] == supercat]
def get_supercat(subcat):
subcat_id = cat_name_to_id[subcat]
return [cat['supercategory'] for cat in annotations_json['categories'] if cat['id'] == subcat_id][0]
def get_cat_family(supercat):
return get_subcats(supercat)+ [supercat]
def filter_category_intersection(coco_json, cat_ids, min_confidence):
cat_imids = []
candidate_anns = filter_cats_conf(coco_json, cat_ids, min_confidence)
for cat_id in cat_ids:
imids = set([ann['image_id'] for ann in candidate_anns if ann['category_id'] == cat_id and ann['score'] >= min_confidence])
cat_imids.append(imids)
matching_imids = set.intersection(*cat_imids)
imgs = [img for img in coco_json['images'] if img['id'] in matching_imids]
anns = [ann for ann in candidate_anns if ann['image_id'] in matching_imids]
return imgs, anns
def get_meta(df, img):
fn = os.path.basename(img['file_name'])
meta = df[df['File Name'] == fn]
return meta
def draw_boxes(coco_img, anns):
cv2img = cv2.imread(f'{IMG_DIR}/{coco_img["file_name"]}')
if cv2img is None:
return np.zeros((256,256))
cv2img = cv2.cvtColor(cv2img,cv2.COLOR_BGR2RGB)
cur_anns = [ann for ann in anns if ann['image_id'] == coco_img['id']]
box_color = (255,255,0)
for ann in cur_anns:
score_str = f'{ann["score"]:.2f}'
[x,y,w,h] = ann['bbox']
cv2img = cv2.rectangle(cv2img, (int(x), int(y)), (int(x+w), int(y+h)), box_color, 2)
cv2img = cv2.putText(cv2img, score_str, (int(x) + 5, int(y) + 25), cv2.FONT_HERSHEY_SIMPLEX, 0.8, box_color, 1)
return cv2img
def display_hack():
buf = io.BytesIO()
plt.savefig(buf, format='png', bbox_inches='tight',pad_inches=0)
buf.seek(0)
plt.close()
clear_output(wait=True)
display(Image(data=buf.read(), format='png'))
def browse_images(anns, imgs, extra_meta=[]):
n = len(imgs)
def view_image(i):
coco_img = imgs[i]
meta = get_meta(df, coco_img)
img = draw_boxes(coco_img, anns)
h,w,_ = img.shape
figsize = [min((w/100) + 10, 20), min((h/100),8)]
fig, ax_image = plt.subplots(1,1,figsize=figsize)
ax_image.axis('off')
ax_image.imshow(img)
display_hack()
print(f'Image Source: {meta["Details URL"].values[0]}')
for meta_key in extra_meta:
meta_val = meta[meta_key].values[0]
print(f'{meta_key}: {meta_val}')
interact(view_image, i=(0,n-1), continuous_update=False)
def extract_nested_vals(df, key):
vals = set()
for val_str in df[df[key].notnull()][key].values:
vals.update([val.strip() for val in val_str.split(',')])
return list(vals)
def generate_nested_val_map(df, col_name):
key_to_fns = {}
for _,row in df[df[col_name].notnull()].iterrows():
for val in row[col_name].split(','):
val = val.strip().lower()
if val in key_to_fns.keys():
key_to_fns[val].append(row['File Name'])
else:
key_to_fns[val] = [row['File Name']]
return key_to_fns
def generate_val_map(df, col_name):
col_name_to_fns = {}
for _,row in df[df[col_name].notnull()].iterrows():
val = row[col_name].strip().lower()
if val in col_name_to_fns.keys():
col_name_to_fns[val].append(row['File Name'])
else:
col_name_to_fns[val] = [row['File Name']]
return col_name_to_fns
def distinct_vals(df, key):
matches = df[df[key].notnull()]
return list(matches[key].unique())
"""### Define Widgets for Object-Based Search"""
def create_obj_query_ui():
def search_by_obj(b):
def is_selection_search():
return tab.selected_index == 1
with out:
clear_output(wait=True)
if is_selection_search():
cats = cat_selector.value
else: # text search
if cat_picker.value not in catnames:
print(f'{cat_picker.value} not in searchable categories. Please try something else.')
return
cats = [cat_picker.value]
if preds_picker.value is False:
conf = 1.0
else:
conf = conf_picker.value
print(f'Searching for {cats} with minimum confidence {conf}')
cat_ids = [cat_name_to_id[cat] for cat in cats]
matching_imgs, matching_annotations = filter_category_intersection(annotations_json, cat_ids, conf)
if len(matching_imgs) ==0:
print(f'No images found where {cats} are all present. Please try a different combination.')
return
print(f'{len(matching_annotations)} instances on {len(matching_imgs)} images found.')
browse_images(matching_annotations, matching_imgs)
out = widgets.Output()
cat_picker = widgets.Combobox(
options=catnames,
description='Type Name'
)
cat_selector = widgets.SelectMultiple(
options=catnames,
description='Select Object'
)
conf_picker = widgets.FloatSlider(
value=0.5,
min=0.0,
max=1.0,
step=0.05,
description='Confidence'
)
preds_picker = widgets.Checkbox(
value=True,
description='Include Predictions'
)
button = widgets.Button(
description = 'Search',
icon='search',
align='center'
)
inner_box = widgets.VBox([conf_picker, preds_picker, button],align='center')
pickerbox = widgets.Box([cat_picker, inner_box])
selectorbox = widgets.Box([cat_selector, inner_box])
tab = widgets.Tab([pickerbox,selectorbox])
tab.set_title(0,'Type Name')
tab.set_title(1,'Pick Category')
inoutbox = widgets.VBox([tab, out])
button.on_click(search_by_obj)
return inoutbox
"""### Define Widgets for Metadata Search"""
def generate_metadata_ui():
kw_select = widgets.Combobox(
placeholder='Type Keyword',
options=keywords,
description='Keyword'
)
artist_select = widgets.Combobox(
placeholder='Type Artist',
options=artists,
description='Keyword'
)
title_select = widgets.Text(
placeholder='Type Title Query',
description='Title'
)
desc_select = widgets.Text(
placeholder='Type Description Query',
description='Description'
)
ic_select = widgets.Combobox(
placeholder='Type Iconclass Code',
options=iconclass_codes,
description='Iconclass Code'
)
search_button = widgets.Button(
description = 'Search',
icon='search',
align='center'
)
kw_search_box = widgets.HBox([kw_select, search_button])
desc_search_box = widgets.HBox([desc_select, search_button])
artist_search_box = widgets.HBox([artist_select, search_button])
title_search_box = widgets.HBox([title_select, search_button])
iconclass_search_box = widgets.HBox([ic_select, search_button])
out = widgets.Output()
tab = widgets.Tab([kw_search_box, desc_search_box, artist_search_box, title_search_box, iconclass_search_box])
tab.set_title(0,'By Keyword')
tab.set_title(1,'By Description')
tab.set_title(2, 'By Artist')
tab.set_title(3, 'By Title')
tab.set_title(4, 'By Iconclass Code')
inoutbox = widgets.VBox([tab, out])
def coco_from_fns(fns):
return [img for img in annotations_json['images'] if img['file_name'] in fns]
def filter_by_kw(kw):
kw = kw.lower()
matching_fns = keyword_to_filenames[kw]
return coco_from_fns(matching_fns)
def filter_by_substring(key, substring):
candidates = df[df[key].notnull()]
matching_fns = []
for _, row in candidates.iterrows():
if substring in row[key]:
matching_fns.append(row['File Name'])
return coco_from_fns(matching_fns)
def filter_by_desc(desc_str):
desc_df = df[df['Description'].notnull()]
#print(f'Querying {len(desc_df)} image descriptions..')
matching_fns = []
for _,row in desc_df.iterrows():
if desc_str in row['Description']:
matching_fns.append(row['File Name'])
return coco_from_fns(matching_fns)
def filter_by_artist(art_str):
artist = art_str.strip().lower()
matching_fns = artist_to_filenames[artist]
return coco_from_fns(matching_fns)
def filter_by_iconclass(ic_str):
ic = ic_str.strip().lower()
matching_fns = iconclass_to_fns[ic]
return coco_from_fns(matching_fns)
def search_by_meta(b):
try:
if tab.selected_index == 0:
matching_imgs = filter_by_kw(kw_select.value)
extra_meta = ['Keywords']
if tab.selected_index == 1:
matching_imgs = filter_by_desc(desc_select.value)
extra_meta = ['Description']
if tab.selected_index == 2:
matching_imgs = filter_by_artist(artist_select.value)
extra_meta = ['Artist']
if tab.selected_index == 3:
matching_imgs = filter_by_substring('Title', title_select.value)
extra_meta = ['Title']
if tab.selected_index == 4:
matching_imgs = filter_by_iconclass(ic_select.value)
extra_meta = ['Iconclass code']
except KeyError:
print('Metadata not found. Try again.')
return
if len(matching_imgs) == 0:
print('No results for query.')
return
with out:
clear_output(wait=True)
print(f'Loading {len(matching_imgs)} results..')
browse_images([], matching_imgs, extra_meta=extra_meta)
search_button.on_click(search_by_meta)
return inoutbox
"""## Prepare the data
#### Collect Images, Annotations, and Metadata
"""
print('Collecting data from zenodo...')
!wget 'https://zenodo.org/records/10209441/files/demonstrator_images.zip?download=1' -O data.zip
# !wget 'https://zenodo.org/records/10124448/files/demonstrator_images.zip?download=1' -O data.zip
print('Unzipping data...')
!unzip -o data.zip | awk 'BEGIN {ORS=" "} {if(NR%10==0)print "."}'
IMG_DIR = 'compressed'
preds_pth = 'preds_with_years.json'
meta_pth = 'meta.csv'
"""#### Prepare Data for Processing"""
with open(preds_pth) as f:
annotations_json = json.load(f)
# annotations_df = pd.DataFrame(annotations_json['annotations'])
fn_to_imgid = {os.path.basename(img['file_name']):img['id'] for img in annotations_json['images']}
imgid_to_fn = {v:k for k,v in fn_to_imgid.items()}
cat_id_to_name = {cat['id']: cat['name'] for cat in annotations_json['categories']}
cat_name_to_id = {v:k for k,v in cat_id_to_name.items()}
catnames = list(cat_name_to_id.keys())
supercats = list(set([cat['supercategory'] for cat in annotations_json['categories']]))
df = pd.read_csv(meta_pth)
keywords = extract_nested_vals(df, 'Keywords')
keyword_to_filenames = generate_nested_val_map(df, 'Keywords')
artist_to_filenames = generate_val_map(df, 'Artist')
artists = distinct_vals(df, 'Artist')
iconclass_to_fns = generate_nested_val_map(df, 'Iconclass code')
iconclass_codes = extract_nested_vals(df, 'Iconclass code')
"""# Query the data
The data can be explored according to smell-related objects, we have found in the artworks, or by the image meta-data provided by the source collections.
## Query by Object
Use this widget to query the Odeuropa image data by smell-related object.
### Query modes
In the `Type Name` tab, you can type names of objects, that you are interested in. The autocompletion gives you hints about which objects are available in our database.
For a list of available object types, please refer to the `Pick Category` tab. Here, you can select one or multiple objects from the full list of smell-related objects we processed. To query for co-occurring objects, select multiple objects from the list by holding the `ctrl`, `shift`, or `command` key while clicking.
### Query options
You can define a minimum confidence using the `Confidence` slider. The confidence score represents the models certainty of having found the query object. Note that these scores are not consistent across categories, i.e. a high score for an object that was rare in the training set might still not guarantee reliable predictions. If you do not want to rely on objects found by the computer at all, you can restrict the query to manual annotations by unticking the `Include Predictions` checkbox.
"""
display(HTML('''<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> '''))
create_obj_query_ui()
"""## Query by Metadata
Use this widget to query the Odeuropa image data based on image-level metadata provided by the source collections. For these queries, no image processing results are utilised.
Search **by description** and **by title** is based on substring matching, i.e. for an image to be returned, your search string must be a substring of the respective metadata field.
Search **by keyword**, **by artist**, and **by iconclass code**, on the other hand, requires exact matches of the query string and the metadata field. But you will get suggestions as you start typing.
"""
display(HTML('''<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> '''))
generate_metadata_ui()
"""# Statistical Analysis
In addition to using the data for querying and conducting manual analysis on the query results, the extracted smell references can serve as a foundation for exploring quantitative approaches and research questions.
Below, we will provide examples, illustratiing how the data can be analyzed quantitatively. Note that there are countless more possibilities.
We try to make the code as accessible as possible so the statistics can be generated without technical background. However, to go beyond the pre-defined examples and derive different statistics, a certain level of coding remains inevitable.
If you have ideas that you would like to try out but don't know how to implement, feel free to reach out any time.
#### Define helper functions and datastructures for statistical analysis
"""
from matplotlib_venn import venn2, venn3
import math
import numpy as np
cat_id_to_name = {cat['id']: cat['name'] for cat in annotations_json['categories']}
cat_id_to_supercat = {cat['id']: cat['supercategory'] for cat in annotations_json['categories']}
def add_to_dictlist(dct, key, val):
if key in dct.keys():
dct[key].append(val)
else:
dct[key] = [val]
def generate_cat_to_image_map(confidence):
cat_to_imgs = {}
filtered_anns = get_anns_for_conf(confidence)
for ann in filtered_anns:
catid = ann['category_id']
imid = ann['image_id']
catname = cat_id_to_name[catid]
supercat = cat_id_to_supercat[catid]
add_to_dictlist(cat_to_imgs,catname,imid)
add_to_dictlist(cat_to_imgs,supercat,imid)
for cat, imgs in cat_to_imgs.items():
cat_to_imgs[cat] = set(imgs) # make sure every image only appears once
return cat_to_imgs
def visualise_cooccurences(obj1, obj2='', obj3='', min_confidence=1.0):
cat_to_imgs = generate_cat_to_image_map(min_confidence)
imgs1 = cat_to_imgs[obj1]
if obj2 == '':
return venn2(subsets=[imgs1,imgs1], set_labels = (obj1,''))
imgs2 = cat_to_imgs[obj2]
if obj3 == '':
return venn2(subsets=[imgs1,imgs2], set_labels = (obj1, obj2))
imgs3 = cat_to_imgs[obj3]
return venn3(
subsets = [imgs1,imgs2,imgs3],
set_labels= (obj1, obj2, obj3))
def generate_co_occurrence_ui():
out = widgets.Output()
cat1_picker = widgets.Combobox(
placeholder = 'First Object',
options=catnames,
# description='First'
)
cat2_picker = widgets.Combobox(
placeholder = 'Second Object',
options=catnames,
# description='Second'
)
cat3_picker = widgets.Combobox(
placeholder = 'Third Object',
options=catnames,
# description='Third'
)
conf_slider = widgets.FloatSlider(
min = 0.0,
max = 1.0,
value = 0.5,
step = 0.05
)
button = widgets.Button(
description='Compute',
icon='play',
align='center'
)
pickerbox = widgets.HBox([cat1_picker, cat2_picker, cat3_picker])
buttonbox = widgets.HBox([conf_slider, button])
inoutbox = widgets.VBox([pickerbox, buttonbox, out])
def trigger_fig(b):
# clear_output(wait=True)
with out:
clear_output(wait=True)
try:
conf = conf_slider.value
fig = visualise_cooccurences(cat1_picker.value, cat2_picker.value, cat3_picker.value, min_confidence=conf)
except KeyError as e :
print('This did not work. Please enter existing categories from left to right into the text boxes.')
# raise e
return
display_hack()
button.on_click(trigger_fig)
return inoutbox
def get_annotation_years(categories, confidence, year_min=1600, year_max=1920):
if categories is None:
anns_filtered = [ann for ann in annotations_json if ann['confidence'] >= confidence]
else:
cat_ids = [cat_name_to_id[cat] for cat in categories]
anns_filtered = filter_cats_conf(annotations_json, cat_ids, confidence)
if year_min is not None and year_max is not None:
anns_filtered = filter_years(anns_filtered, year_min, year_max)
return np.array([ann['year'] for ann in anns_filtered if ann['year'] is not None])
def generate_timeplot_ui():
out = widgets.Output()
button = widgets.Button(
description='Plot graph',
icon='hourglass',
)
conf_slider = widgets.FloatSlider(
min = 0.0,
max = 1.0,
value = 0.5,
step = 0.05
)
obj_picker = widgets.Combobox(
options=catnames,
description='Object',
placeholder='Type Object',
required=True
)
ref_obj_picker = widgets.Combobox(
options=catnames,
description='Reference',
placeholder='Type Object'
)
obj_box = widgets.VBox([obj_picker, ref_obj_picker])
norm_selector = widgets.RadioButtons(
options=['None', 'All Instances', 'Superclass'],
description='Normalization'
)
inp_box = widgets.HBox([obj_box, norm_selector])
trigger_box = widgets.HBox([conf_slider, button])
inoutbox = widgets.VBox([inp_box, trigger_box, out])
def trigger_timeplot(b):
with out:
clear_output(wait=True)
try:
create_timeplot(obj_picker.value, ref_obj_picker.value,
min_confidence=conf_slider.value,
normalization=norm_selector.value)
except KeyError:
print("Category does not exist, try something else.")
return
display_hack()
button.on_click(trigger_timeplot)
return inoutbox
def annotation_share(counts, min_conf, n_bins):
anns_years = get_annotation_years(catnames, min_conf)
ref_counts, _ = np.histogram(anns_years, bins=n_bins)
return np.divide(counts, ref_counts)
def supercat_share(obj, counts, min_conf, n_bins):
supercat = get_supercat(obj)
supercat_family = get_cat_family(supercat)
sc_years = get_annotation_years(supercat_family, min_conf)
sc_counts, _ = np.histogram(sc_years, bins=n_bins)
return np.divide(counts, sc_counts)
def create_timeplot(obj, ref_obj, min_confidence, normalization, n_bins=50):
if obj in supercats:
objs = get_subcats(obj)
else:
objs = [obj]
obj_years = get_annotation_years(objs, min_confidence)
fig, axs = plt.subplots(1,1)
axs.set_title('Distribution of annotations over time')
obj_counts, bins = np.histogram(obj_years,bins=n_bins)
if normalization == 'All Instances':
ylabel = f'{obj} instances / all instances'
obj_counts = annotation_share(obj_counts, min_confidence, n_bins)
elif normalization == 'Superclass':
ylabel = f'{obj} instances / {get_supercat(obj)} instances'
obj_counts = supercat_share(obj, obj_counts, min_confidence, n_bins)
else:
ylabel = f'# {obj} instances'
if ref_obj != '':
if ref_obj in supercats:
ref_objs = get_subcats(ref_obj)
else:
ref_objs = [ref_obj]
ref_years = get_annotation_years(ref_objs, min_confidence)
ref_counts, _ = np.histogram(ref_years, bins=n_bins)
if normalization == 'All Instances':
ylabel = ylabel + f"\n {ref_obj} instances / all instances"
ref_counts = annotation_share(ref_counts, min_confidence, n_bins)
elif normalization == 'Superclass':
ref_counts = supercat_share(ref_obj, ref_counts, min_confidence, n_bins)
ylabel = ylabel + f"\n {ref_obj} instances / {get_supercat(ref_obj)} instances"
else:
ylabel = ylabel + f"\n # {ref_obj} instances"
axs.plot(bins[1:], ref_counts, label=ref_obj)
axs.plot(bins[1:], obj_counts, label=obj)
axs.set_xlabel('Year')
axs.set_ylabel(ylabel)
axs.legend()
"""#### Object co-occurrences
To gain a deeper understanding of the extracted smell references, one effective approach involves analyzing the co-occurrences of objects. To generate a visual representation of these co-occurring objects, please input up to three object categories in the designated fields below. The ensuing Venn diagram will depict the frequency with which these objects appear together in images as opposed to their individual occurrences, offering a visual guide to their interconnectedness.
"""
display(HTML('''<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> '''))
generate_co_occurrence_ui()
"""#### Development over time
Another possibility of exploring visual smell references quantitatively is by visualising their appearances over time.
The below widget displays object occurrences throughout the timeframe of the Odeuropa project image analysis (1600-1920). Type the object of interest into the `Object` text input. Additionally, you can visualise the temporal distribution of one comparison object by specifying it in the `Reference` field.
As the analyzed images are not equally distributed over time, the visualisation will likely be biased towards time periods for which we have more images in our dataset. To mitigate this biases, you can select normalisation by `All Instances` or by `Supercategory` which will display the percentage of annotations compared to all annotations, or annotations of a similar type, respectively.
"""
#### Appearances over time
display(HTML('''<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> '''))
generate_timeplot_ui()
"""# Funding
---
![Screenshot 2023-11-24 at 15.11.52.png](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAAyCAYAAAAUYybjAAAK3WlDQ1BJQ0MgUHJvZmlsZQAASImVlwdUU9kWhs+96Y2WEAEpoXekE0BKCC0UQTqISkgCCSWEhKBiQ2VwBMeCigioAzooouDoUGQsiAXboNiwD8ggoIyDBRsqc4FHmJm33nvr7ayd861999lnn7vOyfoDACWEK5FkwCoAZIpzpBEBPoy4+AQGbgCQgAYgAgi4c3kyCSs8PAQgNj3+3d7dRfIQu2U9Uevfn/9XU+MLZDwAoESEk/kyXibCbYiP8CTSHABQR5G44ZIcyQTfRpgmRRpEeHCCU6f48wQnTzJaZTInKoKNsBEAeDKXK00FgGyLxBm5vFSkDjkcYVsxXyRGOB9hT56Qy0cYWRdYZWZmTfAwwmZIvgQACg1hZvJfaqb+rX6yoj6Xm6rgqX1NGt5XJJNkcJf9n6/mf1tmhnx6DRPEyUJpYAQyaiDv7156VrCCxcnzwqZZxJ/Mn2ShPDB6mnkydsI087m+wYq5GfNCpjlF5M9R1MnhRE2zQOYXOc3SrAjFWilSNmuaudKZdeXp0Yq4UMBR1M8TRsVOc64oZt40y9Ijg2dy2Iq4VB6h6F8gDvCZWddfsfdM2V/2K+Io5uYIowIVe+fO9C8Qs2ZqyuIUvfEFvn4zOdGKfEmOj2ItSUa4Il+QEaCIy3IjFXNzkMM5Mzdc8Q7TuEHh0wx8gR8IQT4MEAnsgQvi9kiMnSNYmjOxGXaWZJlUlCrMYbCQGydgcMQ8GyuGva29AwAT93fqSLy5N3kvITp+JpbrDIDbxF2rn4ktQMYWTwCoejMx43zk3KcAcCaJJ5fmTsXQE18Y5FdBGdCAJtAFhsAMWCOdOQN34I10HATCQBSIB4sADwhBJpCCJWAFWAMKQTHYAnaAcrAX7AMHwRFwDDSDk+AsuAiughvgDngIekA/eAFGwDswBkEQDqJAVEgT0oOMIUvIHmJCnpAfFAJFQPFQEpQKiSE5tAJaBxVDJVA5VAXVQj9CJ6Cz0GWoC7oP9UJD0GvoE4yCyTAN1oFN4DkwE2bBwXAUvBBOhbPhPLgA3gSXwdXwYbgJPgtfhe/APfALeBQFUCQUHaWPskYxUWxUGCoBlYKSolahilClqGpUPaoV1YG6hepBDaM+orFoKpqBtka7owPR0WgeOhu9Cr0RXY4+iG5Cn0ffQveiR9BfMRSMNsYS44bhYOIwqZglmEJMKaYG04i5gLmD6ce8w2KxdKwp1gUbiI3HpmGXYzdid2MbsG3YLmwfdhSHw2niLHEeuDAcF5eDK8Ttwh3GncHdxPXjPuBJeD28Pd4fn4AX49fiS/GH8KfxN/ED+DGCCsGY4EYII/AJywibCfsJrYTrhH7CGFGVaEr0IEYR04hriGXEeuIF4iPiGxKJZEByJc0niUj5pDLSUdIlUi/pI1mNbEFmkxPJcvIm8gFyG/k++Q2FQjGheFMSKDmUTZRayjnKE8oHJaqSjRJHia+0WqlCqUnpptJLZYKysTJLeZFynnKp8nHl68rDKgQVExW2CldllUqFygmVbpVRVaqqnWqYaqbqRtVDqpdVB9VwaiZqfmp8tQK1fWrn1PqoKKohlU3lUddR91MvUPtpWJopjUNLoxXTjtA6aSPqauqO6jHqS9Ur1E+p99BRdBM6h55B30w/Rr9L/zRLZxZrlmDWhln1s27Oeq8xW8NbQ6BRpNGgcUfjkyZD008zXXOrZrPmYy20loXWfK0lWnu0LmgNz6bNdp/Nm100+9jsB9qwtoV2hPZy7X3a17RHdXR1AnQkOrt0zukM69J1vXXTdLfrntYd0qPqeeqJ9LbrndF7zlBnsBgZjDLGecaIvrZ+oL5cv0q/U3/MwNQg2mCtQYPBY0OiIdMwxXC7YbvhiJGeUajRCqM6owfGBGOmsdB4p3GH8XsTU5NYk/UmzSaDphqmHNM80zrTR2YUMy+zbLNqs9vmWHOmebr5bvMbFrCFk4XQosLiuiVs6Wwpstxt2WWFsXK1EltVW3Vbk61Z1rnWdda9NnSbEJu1Ns02L+cYzUmYs3VOx5yvtk62Gbb7bR/aqdkF2a21a7V7bW9hz7OvsL/tQHHwd1jt0OLwytHSUeC4x/GeE9Up1Gm9U7vTF2cXZ6lzvfOQi5FLkkulSzeTxgxnbmRecsW4+riudj3p+tHN2S3H7ZjbH+7W7unuh9wH55rOFczdP7fPw8CD61Hl0ePJ8Ezy/N6zx0vfi+tV7fXU29Cb713jPcAyZ6WxDrNe+tj6SH0afd6z3dgr2W2+KN8A3yLfTj81v2i/cr8n/gb+qf51/iMBTgHLA9oCMYHBgVsDuzk6HB6nljMS5BK0Muh8MDk4Mrg8+GmIRYg0pDUUDg0K3Rb6aJ7xPPG85jAQxgnbFvY43DQ8O/zn+dj54fMr5j+LsItYEdERSY1cHHko8l2UT9TmqIfRZtHy6PYY5ZjEmNqY97G+sSWxPXFz4lbGXY3XihfFtyTgEmISahJGF/gt2LGgP9EpsTDx7kLThUsXXl6ktShj0anFyou5i48nYZJikw4lfeaGcau5o8mc5MrkER6bt5P3gu/N384fEngISgQDKR4pJSmDqR6p21KHhF7CUuGwiC0qF71KC0zbm/Y+PSz9QPp4RmxGQyY+MynzhFhNnC4+n6WbtTSrS2IpKZT0ZLtl78gekQZLa2SQbKGsJYeGCKVrcjP5N/LeXM/citwPS2KWHF+qulS89Noyi2Ublg3k+ef9sBy9nLe8fYX+ijUreleyVlatglYlr2pfbbi6YHV/fkD+wTXENelrfllru7Zk7dt1setaC3QK8gv6vgn4pq5QqVBa2L3eff3eb9Hfir7t3OCwYdeGr0X8oivFtsWlxZ838jZe+c7uu7LvxjelbOrc7Lx5zxbsFvGWu1u9th4sUS3JK+nbFrqtaTtje9H2tzsW77hc6li6dydxp3xnT1lIWcsuo11bdn0uF5bfqfCpaKjUrtxQ+X43f/fNPd576vfq7C3e++l70ff3qgKqmqpNqkv3Yffl7nu2P2Z/xw/MH2prtGqKa74cEB/oORhx8HytS23tIe1Dm+vgOnnd0OHEwzeO+B5pqbeur2qgNxQfBUflR5//mPTj3WPBx9qPM4/X/2T8U2UjtbGoCWpa1jTSLGzuaYlv6ToRdKK91b218Webnw+c1D9ZcUr91ObTxNMFp8fP5J0ZbZO0DZ9NPdvXvrj94bm4c7fPzz/feSH4wqWL/hfPdbA6zlzyuHTystvlE1eYV5qvOl9tuuZ0rfEXp18aO507m667XG+54XqjtWtu1+mbXjfP3vK9dfE25/bVO/PudN2NvnuvO7G75x7/3uD9jPuvHuQ+GHuY/wjzqOixyuPSJ9pPqn81/7Whx7nnVK9v77WnkU8f9vH6Xvwm++1zf8EzyrPSAb2B2kH7wZND/kM3ni943v9C8mJsuPB31d8rX5q9/OkP7z+ujcSN9L+Svhp/vfGN5psDbx3fto+Gjz55l/lu7H3RB80PBz8yP3Z8iv00MLbkM+5z2RfzL61fg78+Gs8cH5dwpdxJKYBCHE5B9MHrA4hOiEe0ww0AiAum9PWkQVP/CSYJ/Cee0uCThuiS2jYAohDdEeQNQBXCJshIQTwc8ShvADs4KPxfJktxsJ+qRWpGpEnp+PgbRD/izAH40j0+PtY8Pv6lBmn2AQBt76Z0/YSpHEbq33IIcvV50kzPB/+wKc3/lz3+cwQTHTiCf45/AjB0GocHCRQiAAAAVmVYSWZNTQAqAAAACAABh2kABAAAAAEAAAAaAAAAAAADkoYABwAAABIAAABEoAIABAAAAAEAAABLoAMABAAAAAEAAAAyAAAAAEFTQ0lJAAAAU2NyZWVuc2hvdDKOQ6oAAAHUaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjUwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjc1PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6VXNlckNvbW1lbnQ+U2NyZWVuc2hvdDwvZXhpZjpVc2VyQ29tbWVudD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CuH96Q8AAAuOSURBVGgF7VtbbBzVGf5mZu/2ru117PUlxo6dcGuIKBQSSOktFLUSEBRxUSrERaVCVSVo1cdKbYWQykMpEpWoWrW0vNCqrVCQCi23IgEKIS2FhpDEgdhObMeOb+vd9d53Zvr9Z7wyJjI1+OzGDznxzM71nP//zn8/E6Nj68/cTN4GYHA73z4JAZ9pmLAMl1CdB+uTgJJ7JiBAnW+rQYBgrZ/mkhRHyHF5JCfrrK0rsAQhBxYcirps662tK7BcESejgvaWHDriC0rA1hNgdQVLqRmdiVhJOfY22YsYufD5HIQDFezeNYJvfnUIoUAZfks89cebvFN/TfV9nIxanQtEjmHANF20RrOYywTh2N5cET8CZePKbbP42o4Z3Hj9OEqOi66EgdfebMOBQ+2wq8/yuhWw0dZSwsRMkIhZtSL5rH7rKFlExHUQC5ew96YhdLRk4KqQhdcJou1YGBmNIBjMYGCTjS19DkwjjxOnG1BxPQPm0vA7ho3WeA633XgC8cY8+/Sk7CzOanChbpLV2lTEzitHsXnTHG67Oa1s0uBwA/65vxenp2NKJSemwpieDeP4UFqp5PRsCKfPyD2Tiupg88Ykdlw1ir6NedzyjTQaQlkcOtKK/e92I5mK1ACe5V3WDaxszkQjpequ21Po6XKwsXMOv3vawTMvkEklOAYagjYmzsRx870X01aVsOOKM1TbNKWuUanwxEwDtgzk8O075hAJA3tuyuDYh21IZ6Oi5IuWcDmDOs+saOL6n5YrIsq6fXVVPbx+y1SzqbkwejqyZNTGyXHgBw9dg2wuKFqoWsm2cPTDKIrFALIFC8eGW6hmIaqrUOeiaPtx6L0W7LruNBZyPrx+oBlPP9uHDI/lvtdN7SxLTSRLAksPfk+BBAlJpxoI0sF3Y/jL8534wrYsEk0LyC0QDAWV94wg5yjGRfU89mVvuASBNq8jXsBzL3fi34casDFRRiTiwEl6YHqALb611Oli72v/Mboue8TN5oU9j7C1dik0KqDIdDScpQRQX9i3XAvSi/mMMrIlHyIBekafi4WsgLI6aRCP2hApwykD+YqFSMiG4/iQK8qYBsNZAxuaZpFMx+GIOCpC1srR0vuro3Lp+RWPPJC4lwNiH23M4e5bjyPAOEm8mNzIlwxkij7FWKZoIEX1UTq2Yq/Lb8h0LmT9WKj4CAaQzfsJlDcREn4E/Dncf+8xNDWlFBluVb+Xd/OZz7SpoZhXiaP6Ogu45ooJ9HbN4c5bZ2GZxzA1HcErr3djKs24iM8JJyIFyu0LAqtuVDE+bwn28j432fd0V7DzinHGZVl8a/cCTOc4jn6QwP632yEeVlfTJlmMNZUy5bIuLkgk8Z07p9DbU8H37z+Fvt40JcBPmkVZqv88Vj12V89OFSLVC1VNzpNzZXR3zuLB+06is93B3j1z6GjjmDm5q69p84YGg0WxPVmCMjYewKWbU/AHXJw8VcaDD12LfIFCrIJQjQyorlyUKiYOH9mAL20/hVLJj8NHG/HLpy6k942oqdEFlzY1FLmquu8QJf+V1+N48s9h7No5z/ipgDxtlTYxXsY9pcv1IRwq4F/vdeDvr2zARQMpGv/Ksqd0nNTEG4YZfIptyectxGNl5EpijAMKzk+rdqtikibAH6wwCS8hxVAkHKK7pFXMMVaTKdTV9EkWYyDxPgbtiHgpFUi6NmbnJahklUriJH10L+ef/RYpuYWyF5xmCwEOJXKsPMHyZ9dwpg0s1gJIBssptEsW6/q2oCUGXdmpGgK1yLzpDadciEiVxQmS8V1Fx+JDa/zRZkZcs0KcTPT1pbD9slEELaqCBD81E6eVOHepjgu4lkl7fy8LiCs99hmua5OsMHvq6sjgy9tnsO3iKdhUyZHxOGZnI3WpoggoIk2JtixBSmHvLSM4+A7rYMx7J2dC9MbMQdcInbbQobmphHtvex97d4/j8m0FJsxJzLA4N3SqSasqrCQQEhQHWEDctX2UlY0T+MrOHLb0zzO9KmJwqAnpnH/Nxl6bGs6lAjjwbgLRmMscEBgdi+Ltw23M3VZiT991yRlBqSqwmrr/vwlMzUQQIA3RBuDge52YZLXDpLNZa9OmhjbnbT4ZxN/+0cLyS6NKeCviAetksySDkGazzDzB9OrhR2PYRJuVzVgsVjC50uCJ9cVZpo2opH6mw4QZaG3KY5bVywrdeT2b31ehdJeRXAiikYm1XQkhwyqH+Jq1qpE+Tli4S7LUY7p+ZUgnWdWUuGutBH5aoEusSEzPeWylS0wlBCSqqCT5Kvj7tB1+5HltYIndMBhfCXWOChl89E5MOdRqqRguxmEq9vnI6BoPJUUH81O11sPYSs7FIxuysWZkOaRtjaqobeKFKCmdKHpIbNBXxtaBWbX0pQyGd0MjPEtdibmSzSINV10+xrpWxTsnHRY3U5bL1giUjKYNLCFWWoALpfFYnisxKXz3niG1MBGP5uEzJV+rTQv4bbTEchjoTuKB+4ZxUe8s4tGSokXniNrUUBRNJq+ztYi7bh3EjqvT2Hop462uQ3jnSAse+9UWGl0pmehvTVy0feC+QVy5NceAOI+WHx/FGwfb8NRf+zA+yfhB06jaJEvMlBjRU1zr2/dyBwz68kYuJjRy0fT3fxrAvKziKOXQCRYH5d/sfAR/+GM/Qv48ghzTgB/PvZjA6amQNqCEam2SpYw3PaEriwkFPyYm/XjhVRd93SVMTodVYr20XqMJMLV0T9fiBDHNGM/0+/D8iz6U6ZlTDBdsmzU0jaKsDSwFhJRpZC6tAB779SU4PhzF168bUgsJC4VmzjiRpMHln5IIWb736k2r40g8rnhakWKTUuyCyTt7kAXWWBR4+BcX49WDcfR3Z9i/KM3iQJrmRl9QukiQYkiI5KxLJUIRzZBBKhIGmfPRMW3qyWDkdAQ2F1OFeQ+w1XAksDisgpZx4QULeP9EC6WH4/BVsZkmV5EkXFAzwSDC5FVtdkb656a5LRJLgCS2kTNJRSTaMS0DCX57dc/tH6C/K0UPKR9/CKvVjUcieZRQWeoS4OVf9UhqYwL25p4U7r5jiN9xZbl6xImQQUTSFPAeQCJ3nmTxR1PTpoZVehQ4CiIJUmXhU9g10LEhw6rECXxxexoXDmSx4/M5jE2E8Mjj/Vymb+ezAhDDR05fa7iIimkilQ1TIikzClATF/WewQ+/N4yBjQVc0FNkVeEQ3jjQgief2YRp5oMyIQKQB5Ps9bYaSJY3n0K2JzHe+XQyjGdfSDDZdtEcc7gQmsXjT/Tjw7EmsicLsVQa9ZKLPTcM4oadw3y/QsnjPbnP46HRFvzmtx1o5/cS8ZiNNE3TvpfaMJX0ltm8kZbG92hQhGjZaZeslaiSj9GSmRAZYynnP37E+c2H5HCVCnNJcuUzi7ikd4rBbBp775hkMm6jVLBZD2vH4Kk4ykQy73CZbaaRnxcFMTbG/ljfX2BIgrJUFXRDczYndQNLhjYsC0/v68HgSDOu3noGzQxgT/KjDkmTbAIBM4I9u0/i8s9RlqiSUzfN4+dPdKgP3cR4y9bcbOFHDw/g8AcbsJVrk4bppxmXZTYx8bVtdQTLxfiZMManGpRteunNXoJDBsUmeekvjp5oZuTdhO5EUdmfN99qwpERqqkAJfaIf8eGY3h/qFmh8to7jM4lDmE/fERu17TVDSxlfCX2UYx7ptLlFzCeOfZ4DFgVRt0x/OTRVmXTI5ESpU6SYnneg8Jd9LDqPfZFjHhHoJSj2ra6gSVseN5Kfj0P6VU3l+Shwo/VXnurQ2UA8lSQi6YSp1Xfkz4ENg8Wea96p/ZAydh1BUsGFAa9/RJI6gJ3EmLkxGBLHYxL8gUulipoPma8z36z2kNtf88BWJ/AkDI8Ur6j/KjYqmq0zxU8y2ldX2ApCfrof+fzbNtyks/dmc6k/NxxUaeRfRvi/OqkIDWg8+3/IfA/GWlHBtR9Ro8AAAAASUVORK5CYII=) This work has been realised in the context of Odeuropa, a research project that has received funding from the European Union's Horizon 2020 research and innovation programme under grant agreement No. 101004469.
---
"""