// Copyright (C) 2011 Davis E. King (davis@dlib.net) // License: Boost Software License See LICENSE.txt for the full license. #include "metadata_editor.h" #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace dlib; extern const char* VERSION; // ---------------------------------------------------------------------------------------- metadata_editor:: metadata_editor( const std::string& filename_ ) : mbar(*this), lb_images(*this), image_pos(0), display(*this), overlay_label_name(*this), overlay_label(*this), keyboard_jump_pos(0), last_keyboard_jump_pos_update(0) { file metadata_file(filename_); filename = metadata_file.full_name(); // Make our current directory be the one that contains the metadata file. We // do this because that file might contain relative paths to the image files // we are supposed to be loading. set_current_dir(get_parent_directory(metadata_file).full_name()); load_image_dataset_metadata(metadata, filename); dlib::array::expand_1a files; files.resize(metadata.images.size()); for (unsigned long i = 0; i < metadata.images.size(); ++i) { files[i] = metadata.images[i].filename; } lb_images.load(files); lb_images.enable_multiple_select(); lb_images.set_click_handler(*this, &metadata_editor::on_lb_images_clicked); overlay_label_name.set_text("Next Label: "); overlay_label.set_width(200); display.set_image_clicked_handler(*this, &metadata_editor::on_image_clicked); display.set_overlay_rects_changed_handler(*this, &metadata_editor::on_overlay_rects_changed); display.set_overlay_rect_selected_handler(*this, &metadata_editor::on_overlay_rect_selected); overlay_label.set_text_modified_handler(*this, &metadata_editor::on_overlay_label_changed); mbar.set_number_of_menus(2); mbar.set_menu_name(0,"File",'F'); mbar.set_menu_name(1,"Help",'H'); mbar.menu(0).add_menu_item(menu_item_text("Save",*this,&metadata_editor::file_save,'S')); mbar.menu(0).add_menu_item(menu_item_text("Save As",*this,&metadata_editor::file_save_as,'A')); mbar.menu(0).add_menu_item(menu_item_separator()); mbar.menu(0).add_menu_item(menu_item_text("Remove Selected Images",*this,&metadata_editor::remove_selected_images,'R')); mbar.menu(0).add_menu_item(menu_item_separator()); mbar.menu(0).add_menu_item(menu_item_text("Exit",static_cast(*this),&drawable_window::close_window,'x')); mbar.menu(1).add_menu_item(menu_item_text("About",*this,&metadata_editor::display_about,'A')); // set the size of this window. on_window_resized(); load_image_and_set_size(0); on_window_resized(); if (image_pos < lb_images.size() ) lb_images.select(image_pos); // make sure the window is centered on the screen. unsigned long width, height; get_size(width, height); unsigned long screen_width, screen_height; get_display_size(screen_width, screen_height); set_pos((screen_width-width)/2, (screen_height-height)/2); show(); } // ---------------------------------------------------------------------------------------- metadata_editor:: ~metadata_editor( ) { close_window(); } // ---------------------------------------------------------------------------------------- void metadata_editor:: add_labelable_part_name ( const std::string& name ) { display.add_labelable_part_name(name); } // ---------------------------------------------------------------------------------------- void metadata_editor:: file_save() { save_metadata_to_file(filename); } // ---------------------------------------------------------------------------------------- void metadata_editor:: save_metadata_to_file ( const std::string& file ) { try { save_image_dataset_metadata(metadata, file); } catch (dlib::error& e) { message_box("Error saving file", e.what()); } } // ---------------------------------------------------------------------------------------- void metadata_editor:: file_save_as() { save_file_box(*this, &metadata_editor::save_metadata_to_file); } // ---------------------------------------------------------------------------------------- void metadata_editor:: remove_selected_images() { dlib::queue::kernel_1a list; lb_images.get_selected(list); list.reset(); unsigned long min_idx = lb_images.size(); while (list.move_next()) { lb_images.unselect(list.element()); min_idx = std::min(min_idx, list.element()); } // remove all the selected items from metadata.images dlib::static_set::kernel_1a to_remove; to_remove.load(list); std::vector images; for (unsigned long i = 0; i < metadata.images.size(); ++i) { if (to_remove.is_member(i) == false) { images.push_back(metadata.images[i]); } } images.swap(metadata.images); // reload metadata into lb_images dlib::array::expand_1a files; files.resize(metadata.images.size()); for (unsigned long i = 0; i < metadata.images.size(); ++i) { files[i] = metadata.images[i].filename; } lb_images.load(files); if (min_idx != 0) min_idx--; select_image(min_idx); } // ---------------------------------------------------------------------------------------- void metadata_editor:: on_window_resized( ) { drawable_window::on_window_resized(); unsigned long width, height; get_size(width, height); lb_images.set_pos(0,mbar.bottom()+1); lb_images.set_size(180, height - mbar.height()); overlay_label_name.set_pos(lb_images.right()+10, mbar.bottom() + (overlay_label.height()-overlay_label_name.height())/2+1); overlay_label.set_pos(overlay_label_name.right(), mbar.bottom()+1); display.set_pos(lb_images.right(), overlay_label.bottom()+3); display.set_size(width - display.left(), height - display.top()); } // ---------------------------------------------------------------------------------------- void propagate_boxes( dlib::image_dataset_metadata::dataset& data, unsigned long prev, unsigned long next ) { if (prev == next || next >= data.images.size()) return; array2d img1, img2; dlib::load_image(img1, data.images[prev].filename); dlib::load_image(img2, data.images[next].filename); for (unsigned long i = 0; i < data.images[prev].boxes.size(); ++i) { correlation_tracker tracker; tracker.start_track(img1, data.images[prev].boxes[i].rect); tracker.update(img2); dlib::image_dataset_metadata::box box = data.images[prev].boxes[i]; box.rect = tracker.get_position(); data.images[next].boxes.push_back(box); } } // ---------------------------------------------------------------------------------------- void propagate_labels( const std::string& label, dlib::image_dataset_metadata::dataset& data, unsigned long prev, unsigned long next ) { if (prev == next || next >= data.images.size()) return; for (unsigned long i = 0; i < data.images[prev].boxes.size(); ++i) { if (data.images[prev].boxes[i].label != label) continue; // figure out which box in the next image matches the current one the best const rectangle cur = data.images[prev].boxes[i].rect; double best_overlap = 0; unsigned long best_idx = 0; for (unsigned long j = 0; j < data.images[next].boxes.size(); ++j) { const rectangle next_box = data.images[next].boxes[j].rect; const double overlap = cur.intersect(next_box).area()/(double)(cur+next_box).area(); if (overlap > best_overlap) { best_overlap = overlap; best_idx = j; } } // If we found a matching rectangle in the next image and the best match doesn't // already have a label. if (best_overlap > 0.5 && data.images[next].boxes[best_idx].label == "") { data.images[next].boxes[best_idx].label = label; } } } // ---------------------------------------------------------------------------------------- bool has_label_or_all_boxes_labeled ( const std::string& label, const dlib::image_dataset_metadata::image& img ) { if (label.size() == 0) return true; bool all_boxes_labeled = true; for (unsigned long i = 0; i < img.boxes.size(); ++i) { if (img.boxes[i].label == label) return true; if (img.boxes[i].label.size() == 0) all_boxes_labeled = false; } return all_boxes_labeled; } // ---------------------------------------------------------------------------------------- void metadata_editor:: on_keydown ( unsigned long key, bool is_printable, unsigned long state ) { drawable_window::on_keydown(key, is_printable, state); if (is_printable) { if (key == '\t') { overlay_label.give_input_focus(); overlay_label.select_all_text(); } // If the user types a number then jump to that image. if ('0' <= key && key <= '9' && metadata.images.size() != 0 && !overlay_label.has_input_focus()) { time_t curtime = time(0); // If it's been a while since the user typed numbers then forget the last jump // position and start accumulating numbers over again. if (curtime-last_keyboard_jump_pos_update >= 2) keyboard_jump_pos = 0; last_keyboard_jump_pos_update = curtime; keyboard_jump_pos *= 10; keyboard_jump_pos += key-'0'; if (keyboard_jump_pos >= metadata.images.size()) keyboard_jump_pos = metadata.images.size()-1; image_pos = keyboard_jump_pos; select_image(image_pos); } else { last_keyboard_jump_pos_update = 0; } if (key == 'd' && (state&base_window::KBD_MOD_ALT)) { remove_selected_images(); } if (key == 'e' && !overlay_label.has_input_focus()) { display_equialized_image = !display_equialized_image; select_image(image_pos); } // Make 'w' and 's' act like KEY_UP and KEY_DOWN if ((key == 'w' || key == 'W') && !overlay_label.has_input_focus()) { key = base_window::KEY_UP; } else if ((key == 's' || key == 'S') && !overlay_label.has_input_focus()) { key = base_window::KEY_DOWN; } else { return; } } if (key == base_window::KEY_UP) { if ((state&KBD_MOD_CONTROL) && (state&KBD_MOD_SHIFT)) { // Don't do anything if there are no boxes in the current image. if (metadata.images[image_pos].boxes.size() == 0) return; // Also don't do anything if there *are* boxes in the next image. if (image_pos > 1 && metadata.images[image_pos-1].boxes.size() != 0) return; propagate_boxes(metadata, image_pos, image_pos-1); } else if (state&base_window::KBD_MOD_CONTROL) { // If the label we are supposed to propagate doesn't exist in the current image // then don't advance. if (!has_label_or_all_boxes_labeled(display.get_default_overlay_rect_label(),metadata.images[image_pos])) return; // if the next image is going to be empty then fast forward to the next one while (image_pos > 1 && metadata.images[image_pos-1].boxes.size() == 0) --image_pos; propagate_labels(display.get_default_overlay_rect_label(), metadata, image_pos, image_pos-1); } select_image(image_pos-1); } else if (key == base_window::KEY_DOWN) { if ((state&KBD_MOD_CONTROL) && (state&KBD_MOD_SHIFT)) { // Don't do anything if there are no boxes in the current image. if (metadata.images[image_pos].boxes.size() == 0) return; // Also don't do anything if there *are* boxes in the next image. if (image_pos+1 < metadata.images.size() && metadata.images[image_pos+1].boxes.size() != 0) return; propagate_boxes(metadata, image_pos, image_pos+1); } else if (state&base_window::KBD_MOD_CONTROL) { // If the label we are supposed to propagate doesn't exist in the current image // then don't advance. if (!has_label_or_all_boxes_labeled(display.get_default_overlay_rect_label(),metadata.images[image_pos])) return; // if the next image is going to be empty then fast forward to the next one while (image_pos+1 < metadata.images.size() && metadata.images[image_pos+1].boxes.size() == 0) ++image_pos; propagate_labels(display.get_default_overlay_rect_label(), metadata, image_pos, image_pos+1); } select_image(image_pos+1); } } // ---------------------------------------------------------------------------------------- void metadata_editor:: select_image( unsigned long idx ) { if (idx < lb_images.size()) { // unselect all currently selected images dlib::queue::kernel_1a list; lb_images.get_selected(list); list.reset(); while (list.move_next()) { lb_images.unselect(list.element()); } lb_images.select(idx); load_image(idx); } else if (lb_images.size() == 0) { display.clear_overlay(); array2d empty_img; display.set_image(empty_img); } } // ---------------------------------------------------------------------------------------- void metadata_editor:: on_lb_images_clicked( unsigned long idx ) { load_image(idx); } // ---------------------------------------------------------------------------------------- std::vector get_overlays ( const dlib::image_dataset_metadata::image& data, color_mapper& string_to_color ) { std::vector temp(data.boxes.size()); for (unsigned long i = 0; i < temp.size(); ++i) { temp[i].rect = data.boxes[i].rect; temp[i].label = data.boxes[i].label; temp[i].parts = data.boxes[i].parts; temp[i].crossed_out = data.boxes[i].ignore; temp[i].color = string_to_color(data.boxes[i].label); } return temp; } // ---------------------------------------------------------------------------------------- void metadata_editor:: load_image( unsigned long idx ) { if (idx >= metadata.images.size()) return; image_pos = idx; array2d img; display.clear_overlay(); try { dlib::load_image(img, metadata.images[idx].filename); set_title(metadata.name + " #"+cast_to_string(idx)+": " +metadata.images[idx].filename); } catch (exception& e) { message_box("Error loading image", e.what()); } if (display_equialized_image) equalize_histogram(img); display.set_image(img); display.add_overlay(get_overlays(metadata.images[idx], string_to_color)); } // ---------------------------------------------------------------------------------------- void metadata_editor:: load_image_and_set_size( unsigned long idx ) { if (idx >= metadata.images.size()) return; image_pos = idx; array2d img; display.clear_overlay(); try { dlib::load_image(img, metadata.images[idx].filename); set_title(metadata.name + " #"+cast_to_string(idx)+": " +metadata.images[idx].filename); } catch (exception& e) { message_box("Error loading image", e.what()); } unsigned long screen_width, screen_height; get_display_size(screen_width, screen_height); unsigned long needed_width = display.left() + img.nc() + 4; unsigned long needed_height = display.top() + img.nr() + 4; if (needed_width < 300) needed_width = 300; if (needed_height < 300) needed_height = 300; if (needed_width > 100 + screen_width) needed_width = screen_width - 100; if (needed_height > 100 + screen_height) needed_height = screen_height - 100; set_size(needed_width, needed_height); if (display_equialized_image) equalize_histogram(img); display.set_image(img); display.add_overlay(get_overlays(metadata.images[idx], string_to_color)); } // ---------------------------------------------------------------------------------------- void metadata_editor:: on_overlay_rects_changed( ) { using namespace dlib::image_dataset_metadata; if (image_pos < metadata.images.size()) { const std::vector& rects = display.get_overlay_rects(); std::vector& boxes = metadata.images[image_pos].boxes; boxes.clear(); for (unsigned long i = 0; i < rects.size(); ++i) { box temp; temp.label = rects[i].label; temp.rect = rects[i].rect; temp.parts = rects[i].parts; temp.ignore = rects[i].crossed_out; boxes.push_back(temp); } } } // ---------------------------------------------------------------------------------------- void metadata_editor:: on_image_clicked( const point& /*p*/, bool /*is_double_click*/, unsigned long /*btn*/ ) { display.set_default_overlay_rect_color(string_to_color(trim(overlay_label.text()))); } // ---------------------------------------------------------------------------------------- void metadata_editor:: on_overlay_label_changed( ) { display.set_default_overlay_rect_label(trim(overlay_label.text())); } // ---------------------------------------------------------------------------------------- void metadata_editor:: on_overlay_rect_selected( const image_display::overlay_rect& orect ) { overlay_label.set_text(orect.label); display.set_default_overlay_rect_label(orect.label); display.set_default_overlay_rect_color(string_to_color(orect.label)); } // ---------------------------------------------------------------------------------------- void metadata_editor:: display_about( ) { std::ostringstream sout; sout << wrap_string("Image Labeler v" + string(VERSION) + "." ,0,0) << endl << endl; sout << wrap_string("This program is a tool for labeling images with rectangles. " ,0,0) << endl << endl; sout << wrap_string("You can add a new rectangle by holding the shift key, left clicking " "the mouse, and dragging it. New rectangles are given the label from the \"Next Label\" " "field at the top of the application. You can quickly edit the contents of the Next Label field " "by hitting the tab key. Double clicking " "a rectangle selects it and the delete key removes it. You can also mark " "a rectangle as ignored by hitting the i or END keys when it is selected. Ignored " "rectangles are visually displayed with an X through them. You can remove an image " "entirely by selecting it in the list on the left and pressing alt+d." ,0,0) << endl << endl; sout << wrap_string("It is also possible to label object parts by selecting a rectangle and " "then right clicking. A popup menu will appear and you can select a part label. " "Note that you must define the allowable part labels by giving --parts on the " "command line. An example would be '--parts \"leye reye nose mouth\"'." ,0,0) << endl << endl; sout << wrap_string("Press the down or s key to select the next image in the list and the up or w " "key to select the previous one.",0,0) << endl << endl; sout << wrap_string("Additionally, you can hold ctrl and then scroll the mouse wheel to zoom. A normal left click " "and drag allows you to navigate around the image. Holding ctrl and " "left clicking a rectangle will give it the label from the Next Label field. " "Holding shift + right click and then dragging allows you to move things around. " "Holding ctrl and pressing the up or down keyboard keys will propagate " "rectangle labels from one image to the next and also skip empty images. " "Similarly, holding ctrl+shift will propagate entire boxes via a visual tracking " "algorithm from one image to the next. " "Finally, typing a number on the keyboard will jump you to a specific image.",0,0) << endl << endl; sout << wrap_string("You can also toggle image histogram equalization by pressing the e key." ,0,0) << endl; message_box("About Image Labeler",sout.str()); } // ----------------------------------------------------------------------------------------