Skip to content

Commit a87f472

Browse files
author
Corentin
committed
upload and download ontologies format
1 parent 00e6f56 commit a87f472

File tree

11 files changed

+447
-23
lines changed

11 files changed

+447
-23
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,6 @@ IMPatienT
181181
.ruff_cache
182182
data/backup/*
183183
notebooks/*
184-
!notebooks/*.ipynb
184+
!notebooks/*.ipynb
185+
profile.json
186+
profile.html

app/dashapp/layout.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,19 @@ def get_external_stylesheets():
9292
dbc.Row(
9393
[
9494
dbc.Col(
95-
html.P(
96-
"This is the image annotation tool interface. "
97-
"Select the standard vocabulary term and draw on the image to annotate parts of the image. "
98-
"Then check the 'Compute Segmentation' tickbox to automatically expands your annotations to the whole image. "
99-
"You may add more marks to clarify parts of the image where the classifier was not successful",
100-
"and the classification will update. Once satisfied with the annotations area you can click the"
101-
"'Save Annotation To Database' to save your annotations.",
102-
),
95+
[
96+
html.B(
97+
"If your image is not displayed please hit F5 to refresh the page, it should solve most issues."
98+
),
99+
html.P(
100+
"This is the image annotation tool interface. "
101+
"Select the standard vocabulary term and draw on the image to annotate parts of the image. "
102+
"Then check the 'Compute Segmentation' tickbox to automatically expands your annotations to the whole image. "
103+
"You may add more marks to clarify parts of the image where the classifier was not successful",
104+
"and the classification will update. Once satisfied with the annotations area you can click the"
105+
"'Save Annotation To Database' to save your annotations.",
106+
),
107+
],
103108
md=True,
104109
),
105110
]

app/historeport/ocr.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ def __init__(self, file_obj, lang):
2828
self.ontology_path = os.path.join(
2929
current_app.config["ONTOLOGY_FOLDER"], "ontology.json"
3030
)
31-
self.image_stack = []
3231
self.raw_text = ""
3332
self.text_as_list = []
3433
self.sentence_as_list = []
@@ -43,6 +42,7 @@ def __init__(self, file_obj, lang):
4342
self.negex_sent = current_app.config["NEGEX_SENT_EN"]
4443
self.all_stopwords = self.nlp.Defaults.stop_words
4544
self.results_match_dict = {}
45+
self.image_stack = convert_from_bytes(self.file_obj.read())
4646

4747
def get_grayscale(self, image):
4848
"""Convert an image as numpy array to grayscale
@@ -73,7 +73,6 @@ def pdf_to_text(self):
7373
Returns:
7474
str: raw text as a string
7575
"""
76-
self.image_stack = convert_from_bytes(self.file_obj.read())
7776
page_list = []
7877
# Loop on each image (page) of the PDF file
7978
for image in self.image_stack:

app/historeport/onto_func.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,127 @@
11
import json
2+
import random
3+
from pronto import Ontology, Definition
4+
import io
5+
from flask_wtf.file import FileField
6+
7+
8+
class ImpatientVocab:
9+
def __init__(self) -> None:
10+
self.used_colors: list[str] = []
11+
self.impatient_json: list[dict] = []
12+
self.impatient_onto: Ontology = None
13+
self.list_of_terms: list[str] = []
14+
15+
def load_json(self, path: str) -> list[dict]:
16+
self.impatient_json = json.load(open(path, "r"))
17+
return self.impatient_json
18+
19+
def load_ontology(self, path: str) -> Ontology:
20+
self.impatient_onto = Ontology(path)
21+
return self.impatient_onto
22+
23+
def load_json_f(self, file: FileField) -> list[dict]:
24+
# Read the JSON data from the file object
25+
json_data = json.loads(file.read())
26+
self.impatient_json = json_data
27+
return json_data
28+
29+
def load_ontology_f(self, file: FileField) -> Ontology:
30+
# Read the ontology data from the file object
31+
ontology_data = io.BytesIO(file.read())
32+
ontology = Ontology(ontology_data)
33+
self.impatient_onto = ontology
34+
return ontology
35+
36+
def json_to_onto(self) -> Ontology:
37+
self.impatient_onto = Ontology()
38+
term_mapping = (
39+
{}
40+
) # A dictionary to store term IDs and their corresponding created terms
41+
42+
# First pass: Create terms without adding superclasses
43+
for term in self.impatient_json:
44+
term_id = term["id"].replace("_", ":")
45+
added_term = self.impatient_onto.create_term(term_id)
46+
added_term.name = term["text"]
47+
for syn in term["data"]["synonymes"].split(","):
48+
if syn.strip() != "":
49+
added_term.add_synonym(syn.strip(), scope="EXACT")
50+
if term["data"]["description"] != "":
51+
added_term.definition = Definition(term["data"]["description"])
52+
53+
term_mapping[term_id] = added_term # Store the term in the mapping
54+
55+
# Second pass: Add superclasses
56+
for term in self.impatient_json:
57+
term_id = term["id"].replace("_", ":")
58+
added_term = term_mapping[term_id]
59+
60+
if term["parent"] != "#":
61+
parent_id = term["parent"].replace("_", ":")
62+
parent_term = term_mapping.get(parent_id)
63+
if parent_term:
64+
added_term.superclasses().add(parent_term)
65+
66+
self.list_of_terms.append(added_term)
67+
68+
return self.impatient_onto
69+
70+
def onto_to_json(self) -> list[dict]:
71+
self.impatient_json = []
72+
index = 0
73+
for term in self.impatient_onto.terms():
74+
relationships = []
75+
for rel in term.superclasses():
76+
relationships.append(rel.id)
77+
relationships.pop(0)
78+
self.impatient_json.append(
79+
{
80+
"id": term.id.replace("_", ":"),
81+
"text": term.name if term.name is not None else "",
82+
"icon": True,
83+
"data": {
84+
"description": term.definition
85+
if term.definition is not None
86+
else "",
87+
"synonymes": ",".join(
88+
[syn.description for syn in term.synonyms]
89+
),
90+
"phenotype_datamined": "",
91+
"gene_datamined": "",
92+
"alternative_language": term.name
93+
if term.name is not None
94+
else "",
95+
"correlates_with": "",
96+
"image_annotation": True if index == 0 else False,
97+
"hex_color": self._generate_hex_color(),
98+
"hpo_datamined": "",
99+
},
100+
"parent": relationships[0].replace("_", ":")
101+
if relationships != []
102+
else "#",
103+
}
104+
)
105+
index += 1
106+
return self.impatient_json
107+
108+
def _generate_hex_color(self):
109+
while True:
110+
# Generate a random hex color
111+
color = "#{:06x}".format(random.randint(0, 0xFFFFFF))
112+
# Check if the color has already been used
113+
if color not in self.used_colors:
114+
# Add the color to the list of used colors and return it
115+
self.used_colors.append(color)
116+
return color
117+
118+
def dump_onto(self, path: str) -> None:
119+
with open(path, "wb") as f:
120+
self.impatient_onto.dump(f, format="obo")
121+
122+
def dump_json(self, path: str) -> None:
123+
with open(path, "w") as f:
124+
json.dump(self.impatient_json, f, indent=2)
2125

3126

4127
class StandardVocabulary:

app/ontocreate/forms.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,28 @@
11
from flask_wtf import FlaskForm
2+
from flask_wtf.file import FileAllowed, FileField, FileRequired
3+
from wtforms.validators import DataRequired
24
from wtforms import StringField, SubmitField, TextAreaField, BooleanField
35

46

7+
class OntoUpload(FlaskForm):
8+
"""Form for uploading new ontology.
9+
10+
Args:
11+
FlaskForm (FlaskForm Class): The FlaskForm Class
12+
"""
13+
14+
onto_file = FileField(
15+
validators=[
16+
FileAllowed(
17+
["json", "obo", "owl"],
18+
"This file is not a valid ontology file !",
19+
),
20+
],
21+
render_kw={"class": "form-control-file border"},
22+
)
23+
submit = SubmitField("Confirm Upload", render_kw={"class": "btn btn-warning"})
24+
25+
526
class OntologyDescript(FlaskForm):
627
"""Form used to show and save modification of nodes from the standard vocabulary
728
tree in the standard vocabulary creator module

app/ontocreate/routes.py

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import json
22
import os
3+
import jsonschema
4+
from jsonschema import validate
35

46
import bleach
5-
7+
from werkzeug.utils import secure_filename
68
from app import db
7-
from app.historeport.onto_func import StandardVocabulary
9+
from app.historeport.onto_func import StandardVocabulary, ImpatientVocab
810
from app.models import ReportHisto
911
from app.ontocreate import bp
10-
from app.ontocreate.forms import InvertLangButton, OntologyDescript
12+
from app.ontocreate.forms import InvertLangButton, OntologyDescript, OntoUpload
1113
from app.histostats.vizualisation import (
1214
db_to_df,
1315
table_to_df,
@@ -50,7 +52,50 @@ def ontocreate():
5052
"""
5153
form = OntologyDescript()
5254
form2 = InvertLangButton()
53-
return render_template("ontocreate.html", form=form, form2=form2)
55+
form_onto = OntoUpload()
56+
if form_onto.validate_on_submit() and form_onto.onto_file.data:
57+
# Get the uploaded file
58+
uploaded_file = form_onto.onto_file.data
59+
# Check if the file has an allowed extension
60+
if uploaded_file.filename[-4:] == "json":
61+
onto_data = ImpatientVocab()
62+
onto_data.load_json_f(uploaded_file)
63+
64+
else:
65+
onto_data = ImpatientVocab()
66+
onto_data.load_ontology_f(uploaded_file)
67+
onto_data.onto_to_json()
68+
69+
validate(
70+
instance=onto_data.impatient_json,
71+
schema=current_app.config["ONTO_SCHEMA"],
72+
)
73+
onto_data.impatient_json[0]["data"]["image_annotation"] = True
74+
file_path = os.path.join(current_app.config["ONTOLOGY_FOLDER"], "ontology.json")
75+
flag_valid = True
76+
77+
if flag_valid:
78+
onto_data.dump_json(file_path)
79+
for report in ReportHisto.query.all():
80+
report.ontology_tree = onto_data.impatient_json
81+
flag_modified(report, "ontology_tree")
82+
db.session.commit()
83+
# Update The DashApp Callback & layout
84+
# By Force reloading the layout code & callbacks
85+
dashapp = current_app.config["DASHAPP"]
86+
with current_app.app_context():
87+
import importlib
88+
import sys
89+
90+
importlib.reload(sys.modules["app.dashapp.callbacks"])
91+
import app.dashapp.layout
92+
93+
importlib.reload(app.dashapp.layout)
94+
dashapp.layout = app.dashapp.layout.layout
95+
return redirect(url_for("ontocreate.ontocreate"))
96+
return render_template(
97+
"ontocreate.html", form=form, form2=form2, form_onto=form_onto
98+
)
5499

55100

56101
@bp.route("/modify_onto", methods=["PATCH"])
@@ -88,8 +133,10 @@ def modify_onto():
88133
template_ontology = StandardVocabulary(clean_tree)
89134
for report in ReportHisto.query.all():
90135
current_report_ontology = StandardVocabulary(report.ontology_tree)
91-
updated_report_ontology = json.loads(bleach.clean(
92-
json.dumps(current_report_ontology.update_ontology(template_ontology)))
136+
updated_report_ontology = json.loads(
137+
bleach.clean(
138+
json.dumps(current_report_ontology.update_ontology(template_ontology))
139+
)
93140
)
94141
# Issue: SQLAlchemy not updating JSON https://stackoverflow.com/questions/42559434/updates-to-json-field-dont-persist-to-db
95142

@@ -125,6 +172,36 @@ def download_onto():
125172
)
126173

127174

175+
@bp.route("/upload_onto", methods=["POST"])
176+
@login_required
177+
def upload_onto():
178+
"""Route to upload an ontology in place as JSON, OWL or OBO file."""
179+
return send_from_directory(
180+
current_app.config["ONTOLOGY_FOLDER"], "ontology.json", as_attachment=True
181+
)
182+
183+
184+
@bp.route("/download_onto_as_obo", methods=["GET"])
185+
@login_required
186+
def download_onto_as_obo():
187+
"""Route to download the standard vocabulary JSON file.
188+
189+
Returns:
190+
File: returns the file
191+
"""
192+
my_onto = ImpatientVocab()
193+
my_onto.load_json(
194+
os.path.join(current_app.config["ONTOLOGY_FOLDER"], "ontology.json")
195+
)
196+
my_onto.json_to_onto()
197+
my_onto.dump_onto(
198+
os.path.join(current_app.config["ONTOLOGY_FOLDER"], "ontology.obo")
199+
)
200+
return send_from_directory(
201+
current_app.config["ONTOLOGY_FOLDER"], "ontology.obo", as_attachment=True
202+
)
203+
204+
128205
@bp.route("/invert_lang", methods=["POST"])
129206
@login_required
130207
def invert_lang():

app/ontocreate/templates/ontocreate.html

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,42 @@
2020
<h1>Standard Vocabulary Tree</h1>
2121
<a href="{{ url_for('ontocreate.download_onto') }}"><i class="fa-solid fa-download"></i> Download Vocabulary (.JSON)
2222
</a>
23+
<a href="{{ url_for('ontocreate.download_onto_as_obo') }}"><i class="fa-solid fa-download"></i> Download Vocabulary
24+
(.OBO)
25+
</a>
26+
27+
<!-- Button Show Popup -->
28+
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#RepredictPopup"><i
29+
class="fa-solid fa-upload"></i>
30+
Upload Vocab (Experimental)
31+
</button>
32+
33+
<!-- Reprediction Confirmation Popup -->
34+
<div class="modal fade" id="RepredictPopup" tabindex="-1" aria-labelledby="RepredictPopupLabel" aria-hidden="true">
35+
<div class="modal-dialog">
36+
<div class="modal-content">
37+
<div class="modal-header">
38+
<h5 class="modal-title" id="RepredictPopupLabel">Confirm Upload Vocabulary</h5>
39+
40+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
41+
</div>
42+
<div class="modal-footer">
43+
<p>This will replace the current vocabulary with your custom file. Please note that this will DELETE all
44+
terms annotation for each patient because you are totally repalce your previous vocabulary. You will have
45+
to re-annotate all patients from the database.This feature is experimental and might not work with all
46+
vocabularies in OWL and OBO formats. Espcially it has issue with big ones (>1000 classes). In case of
47+
error, the vocabulary will not be switched.</p>
48+
<form action="" method="post" enctype="multipart/form-data" style="display: inline">
49+
{{ form_onto.hidden_tag() }} {{ form_onto.onto_file }} {{ form_onto.submit }}
50+
</form>
51+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
52+
Close
53+
</button>
54+
</div>
55+
</div>
56+
</div>
57+
</div>
58+
2359
<input type="text" id="plugins4_q" value="" type="search" id="form1" class="form-control" placeholder="Search" />
2460
<div id="jstree" class="demo" style="overflow: scroll; max-height: 600px"></div>
2561

@@ -97,4 +133,4 @@ <h1>Vocabulary Properties</h1>
97133
<meta id="data-url" data-jstree="{{url_for('ontocreate.onto_json', filename='ontology.json')}}"
98134
data-savetree="{{url_for('ontocreate.modify_onto')}}" />
99135
<script src="{{ url_for('ontocreate.static', filename='ontocreate.js') }}"></script>
100-
{% endblock %}
136+
{% endblock %}

0 commit comments

Comments
 (0)