fix run_model call so that it works with non-default options as well.
[sgn.git] / mason / tools / image_analysis.mas
blobf4bba7dcf9078ff84ef7c76fed89105e16680edc
2 <%args>
4 </%args>
6 <& /util/import_javascript.mas,
7   classes => ['jquery.dataTables-buttons-min',
8       'jquery.iframe-post-form',
9       'jszip-min', 'pdfmake.pdfmake-min',
10       'pdfmake.vfs_fonts',
11       'buttons.bootstrap-min',
12       'buttons.html5-min',
13       'jquery',
14       'jquery.cookie',
15       'thickbox',
16       'CXGN.BreedersToolbox.HTMLSelect',
17       'd3.d3v4Min'
18   ]
21 <style>
22 td.details-control {
23     text-align:center;
24     color:forestgreen;
25     cursor: pointer;
27 tr.shown td.details-control {
28     text-align:center;
29     color:red;
32 .bar {
33   fill: steelblue;
34   shape-rendering: crispEdges;
36 </style>
38 <link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" />
40 <& /page/page_title.mas, title=>"Image Analysis" &>
41 <hr>
43 <div class="well well-sm">
44     <center>
45     <h4>The Necrosis Image Analysis is explained in the paper here: <a href="https://csce.ucmss.com/cr/books/2018/LFS/CSREA2018/IPC3638.pdf">Necrosis Image Analysis</a>.</h4>
46     </center>
47 </div>
49 <div class="well well-sm">
50     <center>
51     <button class="btn btn-primary" id="upload_images_link">Upload New Images</button>
52     </center>
53 </div>
54 <& /breeders_toolbox/upload_images.mas &>
55 <& /breeders_toolbox/trial/create_spreadsheet_dialog.mas &>
57 <&| /page/info_section.mas, title=>"Image Search Criteria",  collapsible => 1, collapsed=>0, subtitle => "All images may not have names, descriptions, or tags associated with them."&>
59     <div id="image_search_form" class="well well-sm">
60         <div class="form-horizontal" >
61             <div class="form-group">
62                 <label class="col-sm-3 control-label">Select a Field Trial: </label>
63                 <div class="col-sm-9" >
64                     <div id ="image_analysis_trial_select">
65                     </div>
66                 </div>
67             </div>
68             <div class="form-group">
69                 <label class="col-sm-6 control-label">Image descriptors (name, description, or filename): </label>
70                 <div class="col-sm-6" >
71                     <input class="form-control" type="text" id="image_description_filename_composite" name="image_description_filename_composite" placeholder="e.g. MyImageName" />
72                 </div>
73             </div>
74             <div class="form-group">
75                 <label class="col-sm-6 control-label">Submitter: </label>
76                 <div class="col-sm-6" >
77                     <input class="form-control" type="text" id="image_submitter" name="image_submitter" placeholder="e.g. JaneDoe" />
78                 </div>
79             </div>
80             <div class="form-group">
81                 <label class="col-sm-6 control-label">Image tag: </label>
82                 <div class="col-sm-6" >
83                     <input class="form-control" type="text" id="image_tag" name="image_tag" placeholder="e.g. ImageTagName" />
84                 </div>
85             </div>
86             <div class="form-group">
87                 <label class="col-sm-6 control-label">Associated stock: </label>
88                 <div class="col-sm-6" >
89                     <input class="form-control" type="text" id="image_stock_uniquename" name="image_stock_uniquename" placeholder="e.g. FieldPlot100" />
90                 </div>
91             </div>
92         </div>
94         <center>
95         <button class="btn btn-primary" id="image_search_submit" >Search</button>
96         </center>
97     </div>
98 </&>
100 <&| /page/info_section.mas, title=>"Image Search Results",  collapsible => 1, collapsed=>0 &>
101     <div class="well well-sm">
102         <div class="panel panel-default">
103             <div class="panel-body">
104                 <table id="image_analysis_image_search_results" class="table table-hover table-striped">
105                 <thead>
106                   <tr>
107                     <th>Select</th>
108                     <th>Image Thumbnail</th>
109                     <th>Filename</th>
110                     <th>Description</th>
111                     <th>Submitter</th>
112                     <th>Associations</th>
113                     <th>Observations</th>
114                     <th>Tags</th>
115                 </tr>
116                 </thead>
117                 </table>
118             </div>
119         </div>
120     </div>
121 </&>
123 <&| /page/info_section.mas, title=>"Image Analysis", collapsible => 1, collapsed=>0 &>
124     <div class="well well-sm">
125         <div class="panel panel-default">
126             <div class="panel-body">
127                 <div class="form-group">
128                     <label class="col-sm-6 control-label">Image Analysis Service: </label>
129                     <div class="col-sm-6" >
130                         <select class="form-control" id="image_analysis_service_select" name="image_analysis_service_select">
131                             <option value="">Select An Analysis Service</option>
132                             <option value="necrosis">Necrosis(Makerere AIR Lab)</option>
133                             <option value="largest_contour_percent">Necrosis Largest Contour Mask Percent</option>
134                             <option value="count_contours">Count Contours</option>
135                             <option value="count_sift">SIFT Feature Count</option>
136                             <option value="whitefly_count">Whitefly Count (Makerere AIR Lab)</option>
137                         </select>
138                     </div>
139                 </div>
140                 <br/>
141                 <div class="form-group" id="image_analysis_trait_group" style="display: none;">
142                     <label class="col-sm-6 control-label">Trait to be Analyzed: </label>
143                     <div class="col-sm-6" >
144                         <select class="form-control" id="image_analysis_trait_select" name="image_analysis_service_select"></select>
145                     </div>
146                 </div>
147                 <br/><br/><br/>
149                 <hr>
150                 <center>
151                 <button class="btn btn-primary" id="image_analysis_submit" disabled>Submit for Analysis</button>
152                 </center>
153                 <hr>
154                 <div id="image_analysis_result" style="display: none;">
155                     <table class="display" style="width:100%" id="image_analysis_result_table">
156                         <thead>
157                            <tr>
158                                <th></th>
159                                <th>Stock</th>
160                                <th>Trait</th>
161                                <th># Analyzed Images</th>
162                                <th>Mean Value</th>
163                            </tr>
164                        </thead>
165                        <caption class="well well-sm" style="caption-side: bottom;margin-top: 10px;">
166                         <center> Analysis Service Details: <a id="model_metrics_link"  style="cursor: pointer;">Model Metrics</a> </center>
167                        </caption>
168                     </table>
169                     <hr>
170                     <center><button class="btn btn-primary" id="image_analysis_save_results">Save Results</button></center>
171                 </div>
172             </div>
173         </div>
174     </div>
176     <br/><br/>
177     <div class="well well-sm">
178         <div class="panel panel-default">
179             <div class="panel-body">
180                 <div class="form-group">
181                     <div class="col-sm-9">
182                     <label class="col-sm-6 control-label">Image Analysis Usage </label>
183                     </div>
184                     <div class="col-sm-3" >
185                         <label class="control-label">Date Range: </label>
186                         <select class="form-control" id="usage_range_select" name="usage_range_select">
187                             <option value="all">All Dates</option>
188                             <option value="year">Latest Year</option>
189                             <option value="month">Latest Month</option>
190                             <option value="week">Latest Week</option>
191                         </select>
192                     </div>
193                 </div>
194                 <br/>
195                 <div class="form-group" id="image_analysis_usage">
196                 </div>
197             </div>
198         </div>
199     </div>
201     <div class="modal fade" id="modelMetricsDialog" tabindex="-1" role="dialog" aria-labelledby="modelMetricsDialog" aria-hidden="true">
202       <div class="modal-dialog modal-dialog-centered modal-lg" role="document">
203         <div class="modal-content">
204           <div class="modal-header">
205             <h5 class="modal-title" id="modelMetricsDialogTitle">Model Metrics</h5>
206             <button type="button" class="close" data-dismiss="modal" aria-label="Close">
207               <span aria-hidden="true">&times;</span>
208             </button>
209           </div>
210           <div class="modal-body" id="modelMetricsDialogBody">
211               <table class="display" style="width:100%" id="metrics_table">
212               </table>
213           </div>
214           <div class="modal-footer">
215             <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
216           </div>
217         </div>
218       </div>
219     </div>
221     <div class="modal fade" id="saveResultModal" tabindex="-1" role="dialog" aria-labelledby="saveResultModal" aria-hidden="true">
222       <div class="modal-dialog modal-dialog-centered" role="document">
223         <div class="modal-content">
224           <div class="modal-header">
225             <h5 class="modal-title" id="saveResultModalTitle">Image Analysis Save Status</h5>
226             <button type="button" class="close" data-dismiss="modal" aria-label="Close">
227               <span aria-hidden="true">&times;</span>
228             </button>
229           </div>
230           <div class="modal-body" id="saveResultModalBody">
231           </div>
232           <div class="modal-footer">
233             <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
234           </div>
235         </div>
236       </div>
237     </div>
238 </&>
240 <script>
242 var service_traits = {
243     'necrosis': {
244         "CBSDpct|CO_334:0002078" : "Cassava",
245     },
246     'largest_contour_percent': {},
247     'count_contours': {},
248     'count_sift': {},
249     'whitefly_count': {},
252 jQuery(document).ready(function(){
254     jQuery('#model_metrics_link').click( function() {
255         jQuery('#modelMetricsDialog').modal("show");
256     });
258     get_select_box('trials', 'image_analysis_trial_select', { 'name' : 'html_image_analysis_trial_select', 'id' : 'html_image_analysis_trial_select', 'multiple':0, 'size':10, 'trial_name_values':1 });
260     _load_image_search_results();
262     var date_range = jQuery('#usage_range_select').val();
263     _load_analysis_activity_graph(date_range);
265     jQuery('#image_search_submit').click(function(){
266         if (jQuery('#html_image_analysis_trial_select').val() == '') {
267             alert("Please select a Field Trial first!");
268             return false;
269         }
270         _load_image_search_results();
271     });
273     jQuery("#image_submitter").autocomplete({
274         source: '/ajax/people/autocomplete'
275     });
277     jQuery('#image_search_form').keypress( function( e ) {
278         var code = e.keyCode || e.which;
279         if( code == 13 ) {
280             jQuery('#image_search_submit').click();
281         }
282     });
284     jQuery('#image_analysis_image_search_results').on( 'draw.dt', function () {
285         jQuery('a.image_search_group').colorbox();
286     });$
288     jQuery('#image_analysis_service_select').change(function() {
289         var service = jQuery('#image_analysis_service_select').val();
290         jQuery('#image_analysis_trait_select').html('<option value="">Select A Trait</option>');
291         if (service) {
292             jQuery('#working_modal').modal('show');
293             // add crop and service specific trait options
294             var all_traits = service_traits[service];
295             var trait_names = Object.keys(all_traits);
296             var options =  [];
298             jQuery.ajax({
299                 url: '/brapi/v2/commoncropnames/',
300                 method: 'GET',
301                 success: function(response) {
302                     // console.log("Retrieved crop names and they are: "+response.result.data);
303                     var supportedcrops = response.result.data;
304                     supportedcrops.forEach(function(crop) {
305                         trait_names.forEach(function(name) {
306                             if (all_traits[name] == crop) {
307                                 // console.log("Id is "+id);
308                                 options.push('<option value="'+ name +'">'+ name +'</option>');
309                             }
310                         });
311                     });
312                     jQuery('#image_analysis_trait_select').append(options);
313                     jQuery('#image_analysis_trait_group').show();
314                     jQuery('#working_modal').modal('hide');
315                 },
316                 error: function(response) {
317                     console.log("error retrieving crop names: "+response);
318                     // just add all traits regardless of crop
319                     trait_names.forEach(function(name) {
320                         // console.log("Id is "+id);
321                         options.push('<option value="'+ name +'">'+ name +'</option>');
323                     });
324                     jQuery('#image_analysis_trait_select').append(options);
325                     jQuery('#image_analysis_trait_group').show();
326                     jQuery('#working_modal').modal('hide');
327                 }
328             });
329       }
330       else {
331           // hide trait select
332           jQuery('#image_analysis_trait_group').hide();
333       }
334     });
336     jQuery('#image_analysis_trait_select').change(function() {
337         if (jQuery('#image_analysis_trait_select').val()) {
338             jQuery('#image_analysis_submit').prop('disabled', false);
339         }
340         else {
341             jQuery('#image_analysis_submit').prop('disabled', true);
342         }
343     });
345     jQuery('#image_analysis_submit').click(function(){
346         var selected_image_ids = [];
347         jQuery('input[name="image_analysis_select"]').each(function() {
348             if (this.checked){
349                 selected_image_ids.push(this.value);
350             }
351         });
353         if (selected_image_ids.length < 1) {
354             alert('Please select at least one image first!');
355             return false;
356         }
358         var results = [];
359         var progress_modal = jQuery('#progress_modal');
360         var progress_bar = jQuery('#progress_bar');
361         var image_total = selected_image_ids.length;
362         var images_finished = 0;
363         var current_progress = 0;
364         progress_modal.modal('show');
365         jQuery('#progress_msg').text('Submitting images for analysis');
367         var deferred_calls = selected_image_ids.map(function(image_id, index) {
368             jQuery('#progress_msg').text('Submitting image '+index+' out of '+image_total+' images');
369             current_progress += (1 / image_total) * 10;
370             progress_bar.css("width", current_progress + "%")
371             .attr("aria-valuenow", current_progress)
372             .text(Math.round(current_progress) + "%");
374             var call = jQuery.ajax({
375                 url: '/ajax/image_analysis/submit',
376                 method: 'POST',
377                 data: {
378                     'selected_image_ids': image_id,
379                     'service': jQuery('#image_analysis_service_select').val(),
380                     'trait': jQuery('#image_analysis_trait_select').val(),
381                 },
382                 dataType:'json',
383                 success: function(response) {
384                     images_finished++;
385                     // console.log(response);
386                     jQuery('#progress_msg').text('Responses received for '+images_finished+' out of '+image_total+' images');
387                     current_progress += (1 / image_total) * 90;
388                     progress_bar.css("width", current_progress + "%")
389                     .attr("aria-valuenow", current_progress)
390                     .text(Math.round(current_progress) + "%");
391                     response.results.map(function(item) {
392                         if (item.result.error) {
393                             progress_modal.find('.modal-footer').append(
394                                 '<ul class="list-group"><li class="list-group-item list-group-item-danger"><span class="badge"><span class="glyphicon glyphicon-remove"></span></span>Error analyzing image number '+index+': '+item.result.error+'</li></ul>'
395                             );
396                         }
397                         else {
398                         }
399                         results.push(item);
400                     });
401                 },
402                 error: function(response) {
403                     images_finished++;
404                     // console.log(response);
405                     jQuery('#progress_msg').text('Finished analyzing '+images_finished+' out of '+image_total+' images');
406                     current_progress += (1 / image_total) * 90
407                     progress_modal.find('.modal-footer').append(
408                         '<ul class="list-group"><li class="list-group-item list-group-item-danger"><span class="badge"><span class="glyphicon glyphicon-remove"></span></span>Error analyzing image number '+index+': '+response+'</li></ul>'
409                     );
410                 }
411             });
412             return call;
413         });
415         jQuery('#progress_msg').text('All '+image_total+' images submitted, waiting for responses.');
417         jQuery.when.apply(jQuery, deferred_calls).then(function() {
418             // console.log("Results are:");
419             // console.log(results);
420             var first = results[0];
421             let metricsData = Object.entries(first.result.analysis_info).map(( [key, value] ) => ({ key : value }));
423             jQuery('#metrics_table').DataTable({
424                 "data": metricsData,
425                 "columns": [
426                     { "data": "metric" },
427                     { "data": "value" },
428                 ]
429             });
431             if (results.length > 0) {
432                 jQuery.ajax({
433                     url: '/ajax/image_analysis/group',
434                     method: 'POST',
435                     data: { 'result': JSON.stringify(results) },
436                     success: function(response) {
437                         // console.log("Grouped Results are:");
438                         // console.log(response);
439                         current_progress = 100;
440                         progress_bar.css("width", current_progress + "%")
441                         .attr("aria-valuenow", current_progress)
442                         .text(Math.round(current_progress) + "%");
443                         jQuery('#progress_msg').text('Building results table.');
444                         jQuery('#image_analysis_result').show();
446                         var table = jQuery('#image_analysis_result_table').DataTable( {
447                             "destroy" : true,
448                             "data": response.results,
449                             "dom": 'Bfrtip',
450                             "buttons":  [
451                                 'copy', 'excel', 'csv', 'pdf'
452                             ],
453                             "columns": [
454                                 {
455                                     "className":      'details-control',
456                                     "orderable":      false,
457                                     "data":           null,
458                                     "defaultContent": '',
459                                     "render": function () {
460                                          return '<i class="fa fa-plus-square" aria-hidden="true"></i>';
461                                      },
462                                      width:"15px"
463                                 },
464                                 { "data": "observationUnitName" },
465                                 { "data": "observationVariableName" },
466                                 { "data": "numberAnalyzed" },
467                                 { "data": "value" }
468                             ],
469                             "order": [[1, 'asc']]
470                         } );
472                         // Add event listener for opening and closing details
473                         jQuery('#image_analysis_result_table tbody').on('click', 'td.details-control', function () {
474                             var tr = jQuery(this).closest('tr');
475                             var tdi = tr.find("i.fa");
476                             var row = table.row( tr );
478                             if ( row.child.isShown() ) {
479                                 // This row is already open - close it
480                                 row.child.hide();
481                                 tr.removeClass('shown');
482                                 tdi.first().removeClass('fa-minus-square');
483                                 tdi.first().addClass('fa-plus-square');
484                             }
485                             else {
486                                 // Open this row
487                                 row.child( format(row.data()) ).show();
488                                 tr.addClass('shown');
489                                 tdi.first().removeClass('fa-plus-square');
490                                 tdi.first().addClass('fa-minus-square');
491                             }
492                         } );
494                         setTimeout(function(){
495                             progress_modal.find('.modal-footer').html('');
496                             progress_modal.modal('hide');
497                         }, 1000);
499                     },
500                     error: function(response) {
501                         // jQuery('#working_modal').modal('hide');
502                         progress_modal.modal('hide');
503                         console.log('Error: '+response);
504                         // jQuery('#'+image_id).text('Error analyzing image number '+index+'. '+response);
505                         alert("An error occurred while displaying image analysis results.");
506                     }
507                 });
508             }
509             else {
510                 progress_modal.find('.modal-footer').html('');
511                 progress_modal.modal('hide');
512                 alert("No usable results returned from the service, aborting analysis.");                 // alert("No usable results returned from the service, aborting analysis.");
513             }
514         });
515     });
517     jQuery('#image_analysis_save_results').click(function(){
519         var table_data = jQuery('#image_analysis_result_table').DataTable().rows().data().toArray();
520         // console.log(table_data);
521         jQuery.ajax({
522             url: '/brapi/v2/observations/',
523             method: 'POST',
524             headers: { "Authorization": "Bearer "+jQuery.cookie("sgn_session_id") },
525             data: JSON.stringify(table_data),
526             contentType: "application/json; charset=utf-8",
527             beforeSend: function() {
528                 jQuery('#working_modal').modal('show');
529             },
530             success: function(response) {
531                 // console.log(response);
532                 jQuery('#working_modal').modal('hide');
533                 jQuery('#saveResultModalBody').html('<ul class="list-group"><li class="list-group-item list-group-item-success"><span class="badge"><span class="glyphicon glyphicon-ok"></span></span>Analysis results saved successfully in the database.</li></ul>');
534                 jQuery('#saveResultModal').modal('show');
535             },
536             error: function(response) {
537                 // console.log(response);
538                 jQuery('#working_modal').modal('hide');
539                 jQuery('#saveResultModalBody').html('<ul class="list-group"><li class="list-group-item list-group-item-danger"><span class="badge"><span class="glyphicon glyphicon-remove"></span></span>Error while trying to save the analysis results.</li></ul>');
540                 jQuery('#saveResultModal').modal('show');
541             }
542         });
545     });
549 function _load_image_search_results() {
550     images_table = jQuery('#image_analysis_image_search_results').DataTable({
551         'destroy' : true,
552         'searching' : false,
553         'ordering'  : false,
554         'processing': true,
555         'serverSide': true,
556         'scrollX': true,
557         'lengthMenu': [10,20,50,100,1000,5000],
558         'ajax': { 'url':  '/ajax/search/images',
559             'data': function(d) {
560               d.html_select_box = "image_analysis_select";
561               d.image_description_filename_composite = jQuery('#image_description_filename_composite').val();
562               d.image_person = jQuery('#image_submitter').val();
563               d.image_tag = jQuery('#image_tag').val();
564               d.image_stock_uniquename = jQuery('#image_stock_uniquename').val();
565               d.image_project_name = jQuery('#html_image_analysis_trial_select').val();
566             }
567         }
568     });
571 function _load_analysis_activity_graph(date_range) {
573     d3.json('/ajax/image_analysis/activity', function(error, response) {
575         if (response.activity) {
576             var activity = JSON.parse(response.activity);
577             // console.log("Activity array is: ");
578             // console.log(activity);
580             // Iterate through each data point and parse date strings into dates
581             var parseTime = d3.timeParse("%Y-%m-%d");
582             var data = [];
584             jQuery.each(activity, function (index, element) {
585                 data.push({
586                     'date': parseTime(element.date)
587                 })
588             });
590             // Set canvas margins
591             var margin = {top: 20, right: 50, bottom: 30, left: 50};
592             var width = 800 - margin.left - margin.right;
593             var height = 500 - margin.top - margin.bottom;
595             // Create svg object
596             var svg = d3.select('#image_analysis_usage').append('svg')
597                 .attr('width', width + margin.left + margin.right)
598                 .attr('height', height + margin.top + margin.bottom)
599                 .append('g')
600                 .attr('transform', `translate(${margin.left}, ${margin.top})`);
602             // Set x (timeseries) and y (linear) scales
603             var xScale = d3.scaleTime().range([0, width]);
604             var yScale = d3.scaleLinear().range([height, 0]);
606            var dayExtent = d3.extent(data, function (d) { return d.date; });
608            console.log("day extent is "+dayExtent);
610            // Create one bin per day, use an offset to include the first and last days
611            var dayBins = d3.timeDays(d3.timeDay.offset(dayExtent[0],-1),
612                                      d3.timeDay.offset(dayExtent[1], 1));
614            var x = d3.scaleTime()
615                .domain(dayExtent)
616                .rangeRound([0, width]);
618            // Scale the range of the data in the y domain
619            var y = d3.scaleLinear()
620                       .range([height, 0]);
622            var xAxis = d3.axisBottom(x)
623                           .tickArguments([d3.timeDay.every(1)])
624                           .tickFormat(d3.timeFormat('%d-%b'))
625                           .ticks(10);
627            // Set the parameters for the histogram
628            var histogram = d3.histogram()
629                               .value(function(d) { return d.date; })
630                               .domain(x.domain())
631                               .thresholds(x.ticks(dayBins.length));
634            // Group the data for the bars
635            var bins = histogram(data);
637            y.domain([0, d3.max(bins, function(d) { return d.length; })]);
639            var hist = svg.selectAll("rect")
640                 .data(bins)
641                 .enter().append("rect")
642                 .attr("class", "bar")
643                 .attr("x", 1)
644                 .attr("transform", function(d) {
645                    return "translate(" + x(d.x0) + "," + y(d.length) + ")";
646                 })
647                 .attr("width", function(d) {
648                    return x(d.x1) - x(d.x0) -1 ;
649                 })
650                 .attr("height", function(d) {
651                    return height - y(d.length);
652                 });
654            // Add the x axis
655            svg.append("g")
656                 .attr("transform", "translate(0," + height + ")")
657                 .call(xAxis)
659            // Add the y axis
660            svg.append("g")
661                 // .call(d3.axisLeft(y).ticks(d3.max(bins, function(d) { return d.length; })))
662                 .call(d3.axisLeft(y).ticks(15))
663                 .append("text")
664                 .attr("fill", "#000")
665                 .attr("transform", "rotate(-90)")
666                 .attr("y", 0 - margin.left)
667                 .attr("x",0 - (height / 2))
668                 .attr("dy", "1em")
669                 .style("text-anchor", "middle")
670                 .text("Number of Images Analyzed");
671         } else {
672             document.getElementById('image_analysis_usage').innerHTML += '<center>No image analysis usage data found.</center>';
673         }
675     });
678 function format ( d ) {
679     var detail_rows = '';
680     d.details.forEach(function (image, index) {
681         var result = image.analyzed_link;
682         var text;
683         if (result.startsWith("Error: ")) {
684             text = result;
685         } else {
686             text = '<img src="'+result+'">';
687         }
688         detail_rows +=
689         `<tr>
690           <td>`+image.image_name+`</td>
691           <td>`+text+`</td>
692           <td>`+image.value+`</td>
693         </tr>`
694     });
696 return `<table class="table">
697             <thead>
698                 <tr>
699                   <th scope="col">Image Name</th>
700                   <th scope="col">Analyzed Image</th>
701                   <th scope="col">Value</th>
702                 </tr>
703              </thead>
704              <tbody>` + detail_rows + `</tbody>
705         </table>`;
709 </script>