| | |
| | |
| |
|
| | #include "metadata_editor.h" |
| | #include <dlib/array.h> |
| | #include <dlib/queue.h> |
| | #include <dlib/static_set.h> |
| | #include <dlib/misc_api.h> |
| | #include <dlib/image_io.h> |
| | #include <dlib/array2d.h> |
| | #include <dlib/pixel.h> |
| | #include <dlib/image_transforms.h> |
| | #include <dlib/image_processing.h> |
| | #include <sstream> |
| | #include <ctime> |
| |
|
| | using namespace std; |
| | using namespace dlib; |
| |
|
| | extern const char* VERSION; |
| |
|
| | |
| |
|
| | metadata_editor:: |
| | metadata_editor( |
| | const std::string& filename_, |
| | const std::string& font_path |
| | ) : |
| | 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(); |
| |
|
| | |
| | if (!font_path.empty()) |
| | { |
| | const auto custom_font = std::make_shared<dlib::bdf_font>(); |
| | std::ifstream fin(font_path); |
| | if (fin.good()) |
| | { |
| | custom_font->read_bdf_file(fin, 0xFFFF); |
| | custom_font->adjust_metrics(); |
| | font = std::move(custom_font); |
| | } |
| | else |
| | { |
| | std::cerr << "WARNING: could not open file '" + font_path + "', using default font\n."; |
| | } |
| | } |
| | display.set_main_font(font); |
| | overlay_label.set_main_font(font); |
| |
|
| | |
| | |
| | |
| | set_current_dir(get_parent_directory(metadata_file).full_name()); |
| |
|
| | load_image_dataset_metadata(metadata, filename); |
| |
|
| | dlib::array<std::string>::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<base_window&>(*this),&drawable_window::close_window,'x')); |
| |
|
| | mbar.menu(1).add_menu_item(menu_item_text("About",*this,&metadata_editor::display_about,'A')); |
| |
|
| | |
| | on_window_resized(); |
| | load_image_and_set_size(0); |
| | on_window_resized(); |
| | if (image_pos < lb_images.size() ) |
| | lb_images.select(image_pos); |
| |
|
| | |
| | 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<unsigned long>::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()); |
| | } |
| |
|
| |
|
| | |
| | dlib::static_set<unsigned long>::kernel_1a to_remove; |
| | to_remove.load(list); |
| | std::vector<dlib::image_dataset_metadata::image> 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); |
| |
|
| |
|
| | |
| | dlib::array<std::string>::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<rgb_pixel> img1, img2; |
| | dlib::load_image(img1, data.images[prev].filename); |
| | data.images[prev].width = img1.nc(); |
| | data.images[prev].height = img1.nr(); |
| |
|
| | dlib::load_image(img2, data.images[next].filename); |
| | data.images[next].width = img2.nc(); |
| | data.images[next].height = img2.nr(); |
| | 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; |
| |
|
| | |
| | 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 (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 ('0' <= key && key <= '9' && metadata.images.size() != 0 && !overlay_label.has_input_focus()) |
| | { |
| | time_t curtime = time(0); |
| | |
| | |
| | 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 == '=') |
| | { |
| | display.zoom_in(); |
| | } |
| |
|
| | if (key == '-') |
| | { |
| | display.zoom_out(); |
| | } |
| |
|
| | 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); |
| | } |
| |
|
| | |
| | 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)) |
| | { |
| | |
| | if (metadata.images[image_pos].boxes.size() == 0) |
| | return; |
| | |
| | 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 (!has_label_or_all_boxes_labeled(display.get_default_overlay_rect_label(),metadata.images[image_pos])) |
| | return; |
| |
|
| | |
| | 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)) |
| | { |
| | |
| | if (metadata.images[image_pos].boxes.size() == 0) |
| | return; |
| | |
| | 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 (!has_label_or_all_boxes_labeled(display.get_default_overlay_rect_label(),metadata.images[image_pos])) |
| | return; |
| |
|
| | |
| | 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()) |
| | { |
| | |
| | dlib::queue<unsigned long>::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<unsigned char> empty_img; |
| | display.set_image(empty_img); |
| | } |
| | } |
| |
|
| | |
| |
|
| | void metadata_editor:: |
| | on_lb_images_clicked( |
| | unsigned long idx |
| | ) |
| | { |
| | load_image(idx); |
| | } |
| |
|
| | |
| |
|
| | std::vector<dlib::image_display::overlay_rect> get_overlays ( |
| | const dlib::image_dataset_metadata::image& data, |
| | color_mapper& string_to_color |
| | ) |
| | { |
| | std::vector<dlib::image_display::overlay_rect> 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<rgb_pixel> img; |
| | display.clear_overlay(); |
| | try |
| | { |
| | dlib::load_image(img, metadata.images[idx].filename); |
| | metadata.images[idx].width = img.nc(); |
| | metadata.images[idx].height = img.nr(); |
| | 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<rgb_pixel> img; |
| | display.clear_overlay(); |
| | try |
| | { |
| | dlib::load_image(img, metadata.images[idx].filename); |
| | metadata.images[idx].width = img.nc(); |
| | metadata.images[idx].height = img.nr(); |
| | 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<image_display::overlay_rect>& rects = display.get_overlay_rects(); |
| |
|
| | std::vector<box>& 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& , bool , unsigned long |
| | ) |
| | { |
| | 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\"'. " |
| | "Alternatively, if you don't give --parts you can simply select a rectangle and shift+left " |
| | "click to add parts. Parts added this way will be labeled with integer labels starting from 0. " |
| | "You can only use this simpler part adding mode if all the parts in a rectangle are already " |
| | "labeled with integer labels or the rectangle has no parts at all." |
| | ,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()); |
| | } |
| |
|
| | |
| |
|
| |
|