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>
167 <center><button class="btn btn-primary" id="image_analysis_save_results">Save Results</button></center>
174 <div class="well well-sm">
175 <div class="panel panel-default">
176 <div class="panel-body">
177 <div class="form-group">
178 <div class="col-sm-9">
179 <label class="col-sm-6 control-label">Image Analysis Usage </label>
181 <div class="col-sm-3" >
182 <label class="control-label">Date Range: </label>
183 <select class="form-control" id="usage_range_select" name="usage_range_select">
184 <option value="all">All Dates</option>
185 <option value="year">Latest Year</option>
186 <option value="month">Latest Month</option>
187 <option value="week">Latest Week</option>
192 <div class="form-group" id="image_analysis_usage">
198 <div class="modal fade" id="saveResultModal" tabindex="-1" role="dialog" aria-labelledby="saveResultModal" aria-hidden="true">
199 <div class="modal-dialog modal-dialog-centered" role="document">
200 <div class="modal-content">
201 <div class="modal-header">
202 <h5 class="modal-title" id="saveResultModalTitle">Image Analysis Save Status</h5>
203 <button type="button" class="close" data-dismiss="modal" aria-label="Close">
204 <span aria-hidden="true">×</span>
207 <div class="modal-body" id="saveResultModalBody">
209 <div class="modal-footer">
210 <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
219 var service_traits = {
221 "CBSDpct|CO_334:0002078" : "Cassava",
223 'largest_contour_percent': {},
224 'count_contours': {},
226 'whitefly_count': {},
229 jQuery(document).ready(function(){
231 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 });
233 _load_image_search_results();
235 var date_range = jQuery('#usage_range_select').val();
236 _load_analysis_activity_graph(date_range);
238 jQuery('#image_search_submit').click(function(){
239 if (jQuery('#html_image_analysis_trial_select').val() == '') {
240 alert("Please select a Field Trial first!");
243 _load_image_search_results();
246 jQuery("#image_submitter").autocomplete({
247 source: '/ajax/people/autocomplete'
250 jQuery('#image_search_form').keypress( function( e ) {
251 var code = e.keyCode || e.which;
253 jQuery('#image_search_submit').click();
257 jQuery('#image_analysis_image_search_results').on( 'draw.dt', function () {
258 jQuery('a.image_search_group').colorbox();
261 jQuery('#image_analysis_service_select').change(function() {
262 var service = jQuery('#image_analysis_service_select').val();
263 jQuery('#image_analysis_trait_select').html('<option value="">Select A Trait</option>');
265 jQuery('#working_modal').modal('show');
266 // add crop and service specific trait options
267 var all_traits = service_traits[service];
268 var trait_names = Object.keys(all_traits);
272 url: '/brapi/v2/commoncropnames/',
274 success: function(response) {
275 // console.log("Retrieved crop names and they are: "+response.result.data);
276 var supportedcrops = response.result.data;
277 supportedcrops.forEach(function(crop) {
278 trait_names.forEach(function(name) {
279 if (all_traits[name] == crop) {
280 // console.log("Id is "+id);
281 options.push('<option value="'+ name +'">'+ name +'</option>');
285 jQuery('#image_analysis_trait_select').append(options);
286 jQuery('#image_analysis_trait_group').show();
287 jQuery('#working_modal').modal('hide');
289 error: function(response) {
290 console.log("error retrieving crop names: "+response);
291 // just add all traits regardless of crop
292 trait_names.forEach(function(name) {
293 // console.log("Id is "+id);
294 options.push('<option value="'+ name +'">'+ name +'</option>');
297 jQuery('#image_analysis_trait_select').append(options);
298 jQuery('#image_analysis_trait_group').show();
299 jQuery('#working_modal').modal('hide');
305 jQuery('#image_analysis_trait_group').hide();
309 jQuery('#image_analysis_trait_select').change(function() {
310 if (jQuery('#image_analysis_trait_select').val()) {
311 jQuery('#image_analysis_submit').prop('disabled', false);
314 jQuery('#image_analysis_submit').prop('disabled', true);
318 jQuery('#image_analysis_submit').click(function(){
319 var selected_image_ids = [];
320 jQuery('input[name="image_analysis_select"]').each(function() {
322 selected_image_ids.push(this.value);
326 if (selected_image_ids.length < 1) {
327 alert('Please select at least one image first!');
332 var progress_modal = jQuery('#progress_modal');
333 var progress_bar = jQuery('#progress_bar');
334 var image_total = selected_image_ids.length;
335 var images_finished = 0;
336 var current_progress = 0;
337 progress_modal.modal('show');
338 jQuery('#progress_msg').text('Submitting images for analysis');
340 var deferred_calls = selected_image_ids.map(function(image_id, index) {
341 jQuery('#progress_msg').text('Submitting image '+index+' out of '+image_total+' images');
342 current_progress += (1 / image_total) * 10;
343 progress_bar.css("width", current_progress + "%")
344 .attr("aria-valuenow", current_progress)
345 .text(Math.round(current_progress) + "%");
347 var call = jQuery.ajax({
348 url: '/ajax/image_analysis/submit',
351 'selected_image_ids': image_id,
352 'service': jQuery('#image_analysis_service_select').val(),
353 'trait': jQuery('#image_analysis_trait_select').val(),
356 success: function(response) {
358 // console.log(response);
359 jQuery('#progress_msg').text('Responses received for '+images_finished+' out of '+image_total+' images');
360 current_progress += (1 / image_total) * 90;
361 progress_bar.css("width", current_progress + "%")
362 .attr("aria-valuenow", current_progress)
363 .text(Math.round(current_progress) + "%");
364 response.results.map(function(item) {
365 if (item.result.error) {
366 progress_modal.find('.modal-footer').append(
367 '<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>'
371 // results.push(item);
376 error: function(response) {
378 // console.log(response);
379 jQuery('#progress_msg').text('Finished analyzing '+images_finished+' out of '+image_total+' images');
380 current_progress += (1 / image_total) * 90
381 progress_modal.find('.modal-footer').append(
382 '<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>'
389 jQuery('#progress_msg').text('All '+image_total+' images submitted, waiting for responses.');
391 jQuery.when.apply(jQuery, deferred_calls).then(function() {
392 // console.log("Results are:");
393 // console.log(results);
394 if (results.length > 0) {
396 url: '/ajax/image_analysis/group',
398 data: { 'result': JSON.stringify(results) },
399 success: function(response) {
400 // console.log("Grouped Results are:");
401 // console.log(response);
402 current_progress = 100;
403 progress_bar.css("width", current_progress + "%")
404 .attr("aria-valuenow", current_progress)
405 .text(Math.round(current_progress) + "%");
406 jQuery('#progress_msg').text('Building results table.');
407 jQuery('#image_analysis_result').show();
409 var table = jQuery('#image_analysis_result_table').DataTable( {
411 "data": response.results,
414 'copy', 'excel', 'csv', 'pdf'
418 "className": 'details-control',
421 "defaultContent": '',
422 "render": function () {
423 return '<i class="fa fa-plus-square" aria-hidden="true"></i>';
427 { "data": "observationUnitName" },
428 { "data": "observationVariableName" },
429 { "data": "numberAnalyzed" },
432 "order": [[1, 'asc']]
435 // Add event listener for opening and closing details
436 jQuery('#image_analysis_result_table tbody').on('click', 'td.details-control', function () {
437 var tr = jQuery(this).closest('tr');
438 var tdi = tr.find("i.fa");
439 var row = table.row( tr );
441 if ( row.child.isShown() ) {
442 // This row is already open - close it
444 tr.removeClass('shown');
445 tdi.first().removeClass('fa-minus-square');
446 tdi.first().addClass('fa-plus-square');
450 row.child( format(row.data()) ).show();
451 tr.addClass('shown');
452 tdi.first().removeClass('fa-plus-square');
453 tdi.first().addClass('fa-minus-square');
457 setTimeout(function(){
458 progress_modal.find('.modal-footer').html('');
459 progress_modal.modal('hide');
463 error: function(response) {
464 // jQuery('#working_modal').modal('hide');
465 progress_modal.modal('hide');
466 console.log('Error: '+response);
467 // jQuery('#'+image_id).text('Error analyzing image number '+index+'. '+response);
468 alert("An error occurred while displaying image analysis results.");
473 progress_modal.find('.modal-footer').html('');
474 progress_modal.modal('hide');
475 alert("No usable results returned from the service, aborting analysis."); // alert("No usable results returned from the service, aborting analysis.");
480 jQuery('#image_analysis_save_results').click(function(){
482 var table_data = jQuery('#image_analysis_result_table').DataTable().rows().data().toArray();
483 // console.log(table_data);
485 url: '/brapi/v2/observations/',
487 headers: { "Authorization": "Bearer "+jQuery.cookie("sgn_session_id") },
488 data: JSON.stringify(table_data),
489 contentType: "application/json; charset=utf-8",
490 beforeSend: function() {
491 jQuery('#working_modal').modal('show');
493 success: function(response) {
494 // console.log(response);
495 jQuery('#working_modal').modal('hide');
496 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>');
497 jQuery('#saveResultModal').modal('show');
499 error: function(response) {
500 // console.log(response);
501 jQuery('#working_modal').modal('hide');
502 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>');
503 jQuery('#saveResultModal').modal('show');
512 function _load_image_search_results() {
513 images_table = jQuery('#image_analysis_image_search_results').DataTable({
520 'lengthMenu': [10,20,50,100,1000,5000],
521 'ajax': { 'url': '/ajax/search/images',
522 'data': function(d) {
523 d.html_select_box = "image_analysis_select";
524 d.image_description_filename_composite = jQuery('#image_description_filename_composite').val();
525 d.image_person = jQuery('#image_submitter').val();
526 d.image_tag = jQuery('#image_tag').val();
527 d.image_stock_uniquename = jQuery('#image_stock_uniquename').val();
528 d.image_project_name = jQuery('#html_image_analysis_trial_select').val();
534 function _load_analysis_activity_graph(date_range) {
536 d3.json('/ajax/image_analysis/activity', function(error, response) {
538 var activity = JSON.parse(response.activity);
539 // console.log("Activity array is: ");
540 // console.log(activity);
542 // Iterate through each data point and parse date strings into dates
543 var parseTime = d3.timeParse("%Y-%m-%d");
546 jQuery.each(activity, function (index, element) {
548 'date': parseTime(element.date)
552 // Set canvas margins
553 var margin = {top: 20, right: 50, bottom: 30, left: 50};
554 var width = 800 - margin.left - margin.right;
555 var height = 500 - margin.top - margin.bottom;
558 var svg = d3.select('#image_analysis_usage').append('svg')
559 .attr('width', width + margin.left + margin.right)
560 .attr('height', height + margin.top + margin.bottom)
562 .attr('transform', `translate(${margin.left}, ${margin.top})`);
564 // Set x (timeseries) and y (linear) scales
565 var xScale = d3.scaleTime().range([0, width]);
566 var yScale = d3.scaleLinear().range([height, 0]);
568 var dayExtent = d3.extent(data, function (d) { return d.date; });
570 console.log("day extent is "+dayExtent);
572 // Create one bin per day, use an offset to include the first and last days
573 var dayBins = d3.timeDays(d3.timeDay.offset(dayExtent[0],-1),
574 d3.timeDay.offset(dayExtent[1], 1));
576 var x = d3.scaleTime()
578 .rangeRound([0, width]);
580 // Scale the range of the data in the y domain
581 var y = d3.scaleLinear()
584 var xAxis = d3.axisBottom(x)
585 .tickArguments([d3.timeDay.every(1)])
586 .tickFormat(d3.timeFormat('%d-%b'))
589 // Set the parameters for the histogram
590 var histogram = d3.histogram()
591 .value(function(d) { return d.date; })
593 .thresholds(x.ticks(dayBins.length));
596 // Group the data for the bars
597 var bins = histogram(data);
599 y.domain([0, d3.max(bins, function(d) { return d.length; })]);
601 var hist = svg.selectAll("rect")
603 .enter().append("rect")
604 .attr("class", "bar")
606 .attr("transform", function(d) {
607 return "translate(" + x(d.x0) + "," + y(d.length) + ")";
609 .attr("width", function(d) {
610 return x(d.x1) - x(d.x0) -1 ;
612 .attr("height", function(d) {
613 return height - y(d.length);
618 .attr("transform", "translate(0," + height + ")")
623 // .call(d3.axisLeft(y).ticks(d3.max(bins, function(d) { return d.length; })))
624 .call(d3.axisLeft(y).ticks(15))
626 .attr("fill", "#000")
627 .attr("transform", "rotate(-90)")
628 .attr("y", 0 - margin.left)
629 .attr("x",0 - (height / 2))
631 .style("text-anchor", "middle")
632 .text("Number of Images Analyzed");
637 function format ( d ) {
638 var detail_rows = '';
639 d.details.forEach(function (image, index) {
640 var result = image.analyzed_link;
642 if (result.startsWith("Error: ")) {
645 text = '<img src="'+result+'">';
649 <td>`+image.image_name+`</td>
651 <td>`+image.value+`</td>
655 return `<table class="table">
658 <th scope="col">Image Name</th>
659 <th scope="col">Analyzed Image</th>
660 <th scope="col">Value</th>
663 <tbody>` + detail_rows + `</tbody>