6 <& /util/import_javascript.mas,
7 classes => ['jquery.dataTables-buttons-min',
8 'jquery.iframe-post-form',
9 'jszip-min', 'pdfmake.pdfmake-min',
11 'buttons.bootstrap-min',
16 'CXGN.BreedersToolbox.HTMLSelect',
27 tr.shown td.details-control {
34 shape-rendering: crispEdges;
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" &>
43 <div class="well well-sm">
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>
49 <div class="well well-sm">
51 <button class="btn btn-primary" id="upload_images_link">Upload New Images</button>
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">
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" />
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" />
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" />
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" />
95 <button class="btn btn-primary" id="image_search_submit" >Search</button>
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">
108 <th>Image Thumbnail</th>
112 <th>Associations</th>
113 <th>Observations</th>
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>
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>
151 <button class="btn btn-primary" id="image_analysis_submit" disabled>Submit for Analysis</button>
154 <div id="image_analysis_result" style="display: none;">
155 <table class="display" style="width:100%" id="image_analysis_result_table">
161 <th># Analyzed Images</th>
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>
170 <center><button class="btn btn-primary" id="image_analysis_save_results">Save Results</button></center>
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>
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>
195 <div class="form-group" id="image_analysis_usage">
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">×</span>
210 <div class="modal-body" id="modelMetricsDialogBody">
211 <table class="display" style="width:100%" id="metrics_table">
214 <div class="modal-footer">
215 <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
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">×</span>
230 <div class="modal-body" id="saveResultModalBody">
232 <div class="modal-footer">
233 <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
242 var service_traits = {
244 "CBSDpct|CO_334:0002078" : "Cassava",
246 'largest_contour_percent': {},
247 'count_contours': {},
249 'whitefly_count': {},
252 jQuery(document).ready(function(){
254 jQuery('#model_metrics_link').click( function() {
255 jQuery('#modelMetricsDialog').modal("show");
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!");
270 _load_image_search_results();
273 jQuery("#image_submitter").autocomplete({
274 source: '/ajax/people/autocomplete'
277 jQuery('#image_search_form').keypress( function( e ) {
278 var code = e.keyCode || e.which;
280 jQuery('#image_search_submit').click();
284 jQuery('#image_analysis_image_search_results').on( 'draw.dt', function () {
285 jQuery('a.image_search_group').colorbox();
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>');
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);
299 url: '/brapi/v2/commoncropnames/',
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>');
312 jQuery('#image_analysis_trait_select').append(options);
313 jQuery('#image_analysis_trait_group').show();
314 jQuery('#working_modal').modal('hide');
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>');
324 jQuery('#image_analysis_trait_select').append(options);
325 jQuery('#image_analysis_trait_group').show();
326 jQuery('#working_modal').modal('hide');
332 jQuery('#image_analysis_trait_group').hide();
336 jQuery('#image_analysis_trait_select').change(function() {
337 if (jQuery('#image_analysis_trait_select').val()) {
338 jQuery('#image_analysis_submit').prop('disabled', false);
341 jQuery('#image_analysis_submit').prop('disabled', true);
345 jQuery('#image_analysis_submit').click(function(){
346 var selected_image_ids = [];
347 jQuery('input[name="image_analysis_select"]').each(function() {
349 selected_image_ids.push(this.value);
353 if (selected_image_ids.length < 1) {
354 alert('Please select at least one image first!');
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',
378 'selected_image_ids': image_id,
379 'service': jQuery('#image_analysis_service_select').val(),
380 'trait': jQuery('#image_analysis_trait_select').val(),
383 success: function(response) {
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>'
402 error: function(response) {
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>'
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({
426 { "data": "metric" },
431 if (results.length > 0) {
433 url: '/ajax/image_analysis/group',
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( {
448 "data": response.results,
451 'copy', 'excel', 'csv', 'pdf'
455 "className": 'details-control',
458 "defaultContent": '',
459 "render": function () {
460 return '<i class="fa fa-plus-square" aria-hidden="true"></i>';
464 { "data": "observationUnitName" },
465 { "data": "observationVariableName" },
466 { "data": "numberAnalyzed" },
469 "order": [[1, 'asc']]
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
481 tr.removeClass('shown');
482 tdi.first().removeClass('fa-minus-square');
483 tdi.first().addClass('fa-plus-square');
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');
494 setTimeout(function(){
495 progress_modal.find('.modal-footer').html('');
496 progress_modal.modal('hide');
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.");
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.");
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);
522 url: '/brapi/v2/observations/',
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');
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');
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');
549 function _load_image_search_results() {
550 images_table = jQuery('#image_analysis_image_search_results').DataTable({
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();
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");
584 jQuery.each(activity, function (index, element) {
586 'date': parseTime(element.date)
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;
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)
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()
616 .rangeRound([0, width]);
618 // Scale the range of the data in the y domain
619 var y = d3.scaleLinear()
622 var xAxis = d3.axisBottom(x)
623 .tickArguments([d3.timeDay.every(1)])
624 .tickFormat(d3.timeFormat('%d-%b'))
627 // Set the parameters for the histogram
628 var histogram = d3.histogram()
629 .value(function(d) { return d.date; })
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")
641 .enter().append("rect")
642 .attr("class", "bar")
644 .attr("transform", function(d) {
645 return "translate(" + x(d.x0) + "," + y(d.length) + ")";
647 .attr("width", function(d) {
648 return x(d.x1) - x(d.x0) -1 ;
650 .attr("height", function(d) {
651 return height - y(d.length);
656 .attr("transform", "translate(0," + height + ")")
661 // .call(d3.axisLeft(y).ticks(d3.max(bins, function(d) { return d.length; })))
662 .call(d3.axisLeft(y).ticks(15))
664 .attr("fill", "#000")
665 .attr("transform", "rotate(-90)")
666 .attr("y", 0 - margin.left)
667 .attr("x",0 - (height / 2))
669 .style("text-anchor", "middle")
670 .text("Number of Images Analyzed");
672 document.getElementById('image_analysis_usage').innerHTML += '<center>No image analysis usage data found.</center>';
678 function format ( d ) {
679 var detail_rows = '';
680 d.details.forEach(function (image, index) {
681 var result = image.analyzed_link;
683 if (result.startsWith("Error: ")) {
686 text = '<img src="'+result+'">';
690 <td>`+image.image_name+`</td>
692 <td>`+image.value+`</td>
696 return `<table class="table">
699 <th scope="col">Image Name</th>
700 <th scope="col">Analyzed Image</th>
701 <th scope="col">Value</th>
704 <tbody>` + detail_rows + `</tbody>