def render_masks_of_crop_objects_into_image(self, crop_objects: List[CropObject], destination_directory: str): for crop_object in tqdm( crop_objects, desc="Generating images from crop-object masks", smoothing=0.1): symbol_class = crop_object.clsname # Make a copy of the mask to not temper with the original data mask = crop_object.mask.copy() # We want to draw black symbols on white canvas. The mask encodes foreground pixels # that we are interested in with a 1 and background pixels with a 0 and stores those values in # an uint8 numpy array. To use Image.fromarray, we have to generate a greyscale mask, where # white pixels have the value 255 and black pixels have the value 0. To achieve this, we simply # subtract one from each uint, and by exploiting the underflow of the uint we get the following mapping: # 0 (background) => 255 (white) and 1 (foreground) => 0 (black) which is exactly what we wanted. mask -= 1 image = Image.fromarray(mask, mode="L") target_directory = os.path.join(destination_directory, symbol_class) os.makedirs(target_directory, exist_ok=True) export_path = ExportPath(destination_directory, symbol_class, crop_object.uid) image.save(export_path.get_full_path())
def test_draw_onto_canvas_random_position(self): # Arrange symbol = HomusSymbol("", [[Point2D(0, 0), Point2D(100, 100)]], "", Rectangle(Point2D(0, 0), 100, 100)) export_path = ExportPath("", "", "bitmap_random", "png", 2) bounding_boxes = dict() # Act symbol.draw_onto_canvas(export_path, stroke_thickness=2, margin=2, destination_width=1000, destination_height=1000, random_position_on_canvas=True, bounding_boxes=bounding_boxes) # Assert self.assertTrue(os.path.exists(export_path.get_full_path())) bounding_box = bounding_boxes["bitmap_random_2.png"] self.assertEqual(bounding_box.height, 100) self.assertEqual(bounding_box.width, 100) self.assertNotEqual(bounding_box.left, 450) self.assertNotEqual(bounding_box.top, 450) self.assertNotEqual(bounding_box.right, 550) self.assertNotEqual(bounding_box.bottom, 550) # Cleanup os.remove(export_path.get_full_path())
def test_draw_staff_lines(self): content = "Quarter-Note\n144,130;144,130;143,130;141,130;139,130;138,131;138,131;138,133;140,135;143,135;146," \ "135;150,132;154,128;155,126;153,124;150,124;146,124;142,125;140,127;140,128;141,129;144,130;149," \ "129;152,128;153,126;153,124;150,123;147,122;144,123;142,125;140,126;141,128;143,128;146,127;149," \ "126;151,124;151,122;148,121;145,122;141,125;138,129;137,131;138,132;141,132;144,131;147,128;148," \ "126;148,125;147,125;145,126;144,127;144,129;144,129;147,130;149,130;150,129;150,128;150,127;148," \ "127;147,128;147,129;147,130;148,131;151,131;153,130;153,129;154,127;152,126;150,126;148,126;147," \ "127;147,128;147,129;148,130;148,130;148,130;146,130;144,130;141,131;140,131;139,131;139,131;139," \ "131;139,131;139,131;140,131;140,133;141,137;141,143;141,150;141,158;139,172;139,180;139,188;140," \ "192;141,195;142,196;143,196;144,196;144,196;144,197;144,197;" export_path = ExportPath("", "", "test", "png", 3) symbol = HomusSymbol.initialize_from_string(content) # Act offsets = [18 + 7 * i for i in range(3)] # [18,25,32] bounding_boxes = dict() symbol.draw_onto_canvas(export_path, 3, 0, 128, 224, 14, offsets, bounding_boxes) # Assert bounding_box_in_image = bounding_boxes["test_3_offset_25.png"] self.assertEqual(bounding_box_in_image.origin, Point2D(109 / 2, 147 / 2)) self.assertEqual(bounding_box_in_image.width, 19) self.assertEqual(bounding_box_in_image.height, 77) # Cleanup for offset in offsets: os.remove(export_path.get_full_path(offset))
def test_get_class_name_and_file_path_with_offset(self): # Arrange export_path = ExportPath("data/images", "3-4-Time", "1-13", "png", 3) # Act full_path = export_path.get_class_name_and_file_path(33) # Assert full_path = full_path.replace('\\', '/') self.assertEqual("3-4-Time/1-13_3_offset_33.png", full_path)
def test_get_full_path_without_stroke_thickness(self): # Arrange export_path = ExportPath("data/images", "3-4-Time", "1-13", "png") # Act full_path = export_path.get_full_path() # Assert full_path = full_path.replace('\\', '/') self.assertEqual("data/images/3-4-Time/1-13.png", full_path)
def test_get_full_path(self): # Arrange export_path = ExportPath("data/images", "3-4-Time", "1-13", "png", 3) # Act full_path = export_path.get_full_path() # Assert full_path = full_path.replace('\\', '/') self.assertEqual("data/images/3-4-Time/1-13_3.png", full_path)
def test_draw_capitan_score_bitmap(self): # Arrange symbol = CapitanSymbol.initialize_from_string(self.real_content_sample) export_path = ExportPath("", "", "bitmap", "png", 2) # Act symbol.draw_capitan_score_bitmap(export_path) # Assert self.assertTrue(os.path.exists(export_path.get_full_path())) # Cleanup os.remove(export_path.get_full_path())
def test_draw_into_bitmap_without_larger_canvas(self): # Arrange symbol = HomusSymbol("", [[Point2D(0, 0), Point2D(100, 100)]], "", Rectangle(Point2D(0, 0), 100, 100)) export_path = ExportPath("", "", "bitmap", "png", 3) # Act symbol.draw_into_bitmap(export_path, stroke_thickness=3, margin=2) # Assert self.assertTrue(os.path.exists(export_path.get_full_path())) # Cleanup os.remove(export_path.get_full_path())
def test_draw_capitan_stroke_onto_canvas(self): # Arrange symbol = CapitanSymbol.initialize_from_string(self.real_content_sample) export_path = ExportPath("", "", "bitmap", "png", 2) # Act symbol.draw_capitan_stroke_onto_canvas(export_path, stroke_thickness=2, margin=2) # Assert self.assertTrue(os.path.exists(export_path.get_full_path())) # Cleanup os.remove(export_path.get_full_path())
def draw_capitan_stroke_onto_canvas(self, export_path: ExportPath, stroke_thickness: int, margin: int): """ Draws the symbol strokes onto a canvas :param export_path: The path, where the symbols should be created on disk :param stroke_thickness: :param margin: """ width = int(self.dimensions.width + 2 * margin) height = int(self.dimensions.height + 2 * margin) offset = Point2D(self.dimensions.origin.x - margin, self.dimensions.origin.y - margin) image = Image.new('RGB', (width, height), "white") # create a new white image draw = ImageDraw.Draw(image) black = (0, 0, 0) for i in range(0, len(self.stroke) - 1): start_point = self.__subtract_offset(self.stroke[i], offset) end_point = self.__subtract_offset(self.stroke[i + 1], offset) distance = self.__euclidean_distance(start_point, end_point) if distance > 1600: # User moved more than 40 pixels - probably we should not draw a line here continue draw.line((start_point.x, start_point.y, end_point.x, end_point.y), black, stroke_thickness) del draw image.save(export_path.get_full_path()) image.close()
def draw_capitan_score_bitmap(self, export_path: ExportPath) -> None: """ Draws the 30x30 symbol into the given file :param export_path: The path, where the symbols should be created on disk """ with Image.fromarray(self.image_data, mode='L') as image: image.save(export_path.get_full_path())
def test_draw_onto_canvas(self): # Arrange symbol = HomusSymbol("", [[Point2D(0, 0), Point2D(100, 100)]], "", Rectangle(Point2D(0, 0), 100, 100)) export_path = ExportPath("", "", "bitmap", "png", 2) # Act symbol.draw_onto_canvas(export_path, stroke_thickness=2, margin=2, destination_width=150, destination_height=150) # Assert self.assertTrue(os.path.exists(export_path.get_full_path())) # Cleanup os.remove(export_path.get_full_path())
def __extract_symbols(self, xml_file: str, image_file: str, destination_directory: str): # xml_file, image_file = 'data/audiveris_omr_raw\\IMSLP06053p1.xml', 'data/audiveris_omr_raw\\IMSLP06053p1.png' # xml_file, image_file = 'data/audiveris_omr_raw\\mops-1.xml', 'data/audiveris_omr_raw\\mops-1.png' # xml_file, image_file = 'data/audiveris_omr_raw\\mtest1-1.xml', 'data/audiveris_omr_raw\\mtest1-1.png' # xml_file, image_file = 'data/audiveris_omr_raw\\mtest2-1.xml', 'data/audiveris_omr_raw\\mtest2-1.png' image = Image.open(image_file) annotations = ElementTree.parse(xml_file).getroot() xml_symbols = annotations.findall("Symbol") file_name_without_extension = os.path.splitext( os.path.basename(xml_file))[0] symbols = [] for xml_symbol in xml_symbols: symbol_class = xml_symbol.get("shape") bounds = xml_symbol.find("Bounds") x, y, width, height = bounds.get("x"), bounds.get("y"), bounds.get( "w"), bounds.get("h") x, y, width, height = int(float(x)), int(float(y)), int( float(width)), int(float(height)) symbol = AudiverisOmrSymbol(symbol_class, x, y, width, height) symbols.append(symbol) symbol_number = 0 for symbol in symbols: symbol_class = symbol.symbol_class bounding_box_with_one_pixel_margin = symbol.as_bounding_box_with_margin( 1) symbol_image = image.crop(bounding_box_with_one_pixel_margin) target_directory = os.path.join(destination_directory, symbol_class) os.makedirs(target_directory, exist_ok=True) export_path = ExportPath( destination_directory, symbol_class, file_name_without_extension + str(symbol_number)) symbol_image.save(export_path.get_full_path()) symbol_number += 1
def draw_onto_canvas(self, export_path: ExportPath, stroke_thickness: int, margin: int, destination_width: int, destination_height: int, staff_line_spacing: int = 14, staff_line_vertical_offsets: List[int] = None, bounding_boxes: dict = None, random_position_on_canvas: bool = False) -> None: """ Draws the symbol onto a canvas with a fixed size :param bounding_boxes: The dictionary into which the bounding-boxes will be added of each generated image :param export_path: The path, where the symbols should be created on disk :param stroke_thickness: :param margin: :param destination_width: :param destination_height: :param staff_line_spacing: :param staff_line_vertical_offsets: Offsets used for drawing staff-lines. If None provided, no staff-lines will be drawn if multiple integers are provided, multiple images will be generated """ width = self.dimensions.width + 2 * margin height = self.dimensions.height + 2 * margin if random_position_on_canvas: # max is required for elements that are larger than the canvas, # where the possible range for the random value would be negative random_horizontal_offset = random.randint( 0, max(0, destination_width - width)) random_vertical_offset = random.randint( 0, max(0, destination_height - height)) offset = Point2D( self.dimensions.origin.x - margin - random_horizontal_offset, self.dimensions.origin.y - margin - random_vertical_offset) else: width_offset_for_centering = (destination_width - width) / 2 height_offset_for_centering = (destination_height - height) / 2 offset = Point2D( self.dimensions.origin.x - margin - width_offset_for_centering, self.dimensions.origin.y - margin - height_offset_for_centering) image_without_staff_lines = Image.new( 'RGB', (destination_width, destination_height), "white") # create a new white image draw = ImageDraw.Draw(image_without_staff_lines) black = (0, 0, 0) for stroke in self.strokes: for i in range(0, len(stroke) - 1): start_point = self.__subtract_offset(stroke[i], offset) end_point = self.__subtract_offset(stroke[i + 1], offset) draw.line( (start_point.x, start_point.y, end_point.x, end_point.y), black, stroke_thickness) location = self.__subtract_offset(self.dimensions.origin, offset) bounding_box_in_image = Rectangle(location, self.dimensions.width, self.dimensions.height) # self.draw_bounding_box(draw, location) del draw if staff_line_vertical_offsets is not None and staff_line_vertical_offsets: for staff_line_vertical_offset in staff_line_vertical_offsets: image_with_staff_lines = image_without_staff_lines.copy() self.__draw_staff_lines_into_image(image_with_staff_lines, stroke_thickness, staff_line_spacing, staff_line_vertical_offset) file_name_with_offset = export_path.get_full_path( staff_line_vertical_offset) image_with_staff_lines.save(file_name_with_offset) image_with_staff_lines.close() if bounding_boxes is not None: # Note that the ImageDatasetGenerator does not yield the full path, but only the class_name and # the file_name, e.g. '3-4-Time\\1-13_3_offset_74.png', so we store only that part in the dictionary class_and_file_name = export_path.get_class_name_and_file_path( staff_line_vertical_offset) bounding_boxes[class_and_file_name] = bounding_box_in_image else: image_without_staff_lines.save(export_path.get_full_path()) if bounding_boxes is not None: # Note that the ImageDatasetGenerator does not yield the full path, but only the class_name and # the file_name, e.g. '3-4-Time\\1-13_3_offset_74.png', so we store only that part in the dictionary class_and_file_name = export_path.get_class_name_and_file_path( ) bounding_boxes[class_and_file_name] = bounding_box_in_image image_without_staff_lines.close()
def create_images(raw_data_directory: str, destination_directory: str, stroke_thicknesses: List[int], canvas_width: int = None, canvas_height: int = None, staff_line_spacing: int = 14, staff_line_vertical_offsets: List[int] = None, random_position_on_canvas: bool = False) -> dict: """ Creates a visual representation of the Homus Dataset by parsing all text-files and the symbols as specified by the parameters by drawing lines that connect the points from each stroke of each symbol. Each symbol will be drawn in the center of a fixed canvas, specified by width and height. :param raw_data_directory: The directory, that contains the text-files that contain the textual representation of the music symbols :param destination_directory: The directory, in which the symbols should be generated into. One sub-folder per symbol category will be generated automatically :param stroke_thicknesses: The thickness of the pen, used for drawing the lines in pixels. If multiple are specified, multiple images will be generated that have a different suffix, e.g. 1-16-3.png for the 3-px version and 1-16-2.png for the 2-px version of the image 1-16 :param canvas_width: The width of the canvas, that each image will be drawn upon, regardless of the original size of the symbol. Larger symbols will be cropped. If the original size of the symbol should be used, provided None here. :param canvas_height: The height of the canvas, that each image will be drawn upon, regardless of the original size of the symbol. Larger symbols will be cropped. If the original size of the symbol should be used, provided None here :param staff_line_spacing: Number of pixels spacing between each of the five staff-lines :param staff_line_vertical_offsets: List of vertical offsets, where the staff-lines will be superimposed over the drawn images. If None is provided, no staff-lines will be superimposed. If multiple values are provided, multiple versions of each symbol will be generated with the appropriate staff-lines, e.g. 1-5_3_offset_70.png and 1-5_3_offset_77.png for two versions of the symbol 1-5 with stroke thickness 3 and staff-line offsets 70 and 77 pixels from the top. :param random_position_on_canvas: True, if the symbols should be randomly placed on the fixed canvas. False, if the symbols should be centered in the fixed canvas. Note that this flag only has an effect, if fixed canvas sizes are used. :return: A dictionary that contains the file-names of all generated symbols and the respective bounding-boxes of each symbol. """ all_symbol_files = [y for x in os.walk(raw_data_directory) for y in glob(os.path.join(x[0], '*.txt'))] staff_line_multiplier = 1 if staff_line_vertical_offsets is not None and staff_line_vertical_offsets: staff_line_multiplier = len(staff_line_vertical_offsets) total_number_of_symbols = len(all_symbol_files) * len(stroke_thicknesses) * staff_line_multiplier output = "Generating {0} images with {1} symbols in {2} different stroke thicknesses ({3})".format( total_number_of_symbols, len(all_symbol_files), len(stroke_thicknesses), stroke_thicknesses) if staff_line_vertical_offsets is not None: output += " and with staff-lines with {0} different offsets from the top ({1})".format( staff_line_multiplier, staff_line_vertical_offsets) if canvas_width is not None and canvas_height is not None: if random_position_on_canvas is False: output += "\nRandomly drawn on a fixed canvas of size {0}x{1} (Width x Height)".format(canvas_width, canvas_height) else: output += "\nCentrally drawn on a fixed canvas of size {0}x{1} (Width x Height)".format(canvas_width, canvas_height) print(output) print("In directory {0}".format(os.path.abspath(destination_directory)), flush=True) bounding_boxes = dict() progress_bar = tqdm(total=total_number_of_symbols, mininterval=0.25) for symbol_file in all_symbol_files: with open(symbol_file) as file: content = file.read() symbol = HomusSymbol.initialize_from_string(content) target_directory = os.path.join(destination_directory, symbol.symbol_class) os.makedirs(target_directory, exist_ok=True) raw_file_name_without_extension = os.path.splitext(os.path.basename(symbol_file))[0] for stroke_thickness in stroke_thicknesses: export_path = ExportPath(destination_directory, symbol.symbol_class, raw_file_name_without_extension, 'png', stroke_thickness) if canvas_width is None and canvas_height is None: symbol.draw_into_bitmap(export_path, stroke_thickness, margin=2) else: symbol.draw_onto_canvas(export_path, stroke_thickness, 0, canvas_width, canvas_height, staff_line_spacing, staff_line_vertical_offsets, bounding_boxes, random_position_on_canvas) progress_bar.update(1 * staff_line_multiplier) progress_bar.close() return bounding_boxes