Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import logging

import rodan
from rodan.jobs import module_loader

__version__ = "0.0.2"
logger = logging.getLogger("rodan")
module_loader("rodan.jobs.aquitanian_ref_line_finding.aquitanian_ref_line_finding")
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
from rodan.jobs.base import RodanTask
import cv2
import numpy as np
import math
from PIL import Image
import json
import json.encoder

#Helper function for distance formula
def dist(pt1, pt2):
return math.sqrt(((pt1[0] - pt2[0])**2) + ((pt1[1] - pt2[1])**2))

#Helper function to find the matching y value given an x value and 2 points of a line
def coords(x1, y1, x2, y2, new_x):
slope = (y2 - y1) / (x2 - x1)
b = (-1 * (slope * x1)) + y1
return (slope * new_x) + b

#Handles each line segment by adding extra space, drawing a bounding box, and drawing a line through the center
#Returns the 2 coords for the line
def process_section(sect):
#add extra space around the line segments
old_h, old_w, c = sect.shape
new_h = old_h + 100
new_w = old_w + 100
result = np.full((new_h, new_w, c), (255, 255, 255), dtype=np.uint8)
x_center = (new_w - old_w) // 2
y_center = (new_h - old_h) // 2
result[y_center:y_center+old_h, x_center:x_center+old_w] = sect

#preprocess image, draw bounding box around
gray = cv2.cvtColor(result, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (1, 1), 0)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
kernal = cv2.getStructuringElement(cv2.MORPH_RECT, (8, 1))
dilate = cv2.dilate(thresh, kernal, iterations=7)
conts = cv2.findContours(dilate, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

conts = conts[0] if len(conts) == 2 else conts[1]
conts = sorted(conts, key=lambda x: cv2.boundingRect(x)[0])

ret = []

#draw line through the bounding box
for c in conts:
rect = cv2.minAreaRect(c)
box = cv2.boxPoints(rect)
box = np.int0(box)
result = cv2.drawContours(result,[box],0,(0,255,0),2)
origin = box[0]
new_pts = sorted(box, key=lambda x: dist(x, origin))
pt0 = new_pts[0]
pt1 = new_pts[1]
pt2 = new_pts[2]
pt3 = new_pts[3]

mid1 = [int((pt0[0] + pt1[0]) / 2) - 50, int((pt0[1] + pt1[1]) / 2) - 50]
mid2 = [int((pt2[0] + pt3[0]) / 2) - 50, int((pt2[1] + pt3[1]) / 2) - 50]
ret = [mid1, mid2]

return ret

#Convert results into a JSOMR format
#Each reference line has 5 lines: 2 ledger lines below, the original line, and 2 ledger lines above
def to_json(img, data, neume_size):
h, w, _ = img.shape
staves = []
for i in range(0, len(data)):
cur = data[i]
box = cur[0]
x, y, width, height = box
lines = cur[1]
up_ledger_2 = [[x[0], x[1] - (2 * neume_size)] for x in lines]
up_ledger_1 = [[x[0], x[1] - neume_size] for x in lines]
down_ledger_1 = [[x[0], x[1] + neume_size] for x in lines]
down_ledger_2 = [[x[0], x[1] + (2 * neume_size)] for x in lines]
staves.append({
"staff_no": i+1,
"bounding_box":{
"ncols": width,
"nrows": height + (4 * neume_size),
"ulx": x,
"uly": y - (2 * neume_size)
},
"num_lines": 1,
"line_positions": [up_ledger_2, up_ledger_1, lines, down_ledger_1, down_ledger_2]
})
return {
"page":{
"resolution": 0.0,
"bounding_box":{
"ncols": w,
"nrows": h,
"ulx": 0,
"uly": 0
}
},
"staves": staves
}

class AquitanianReferenceLineFinding(RodanTask):
name = "Aquitanian Reference Line Finding"
author = "Deanna Chun"
description = "Trace single Aquitanian reference lines"
settings = {
'title': 'Settings',
'type': 'object',
'job_queue': 'Python3',
'required': ['Slices', 'Neume Height'],
'properties': {
'Slices': {
'type': 'integer',
'default': 8,
'minimum': 1,
'maximum': 24,
'description': 'Number of divisions per single reference line'
},
'Neume Height': {
'type': 'integer',
'default': 50,
'minimum': 1,
'maximum': 500,
'description': "Neume Height multiplied by 3 (for generating ledger lines)"
}
}
}

enabled = True
category = "Staff Detection"
interactive = False
input_port_types = [{
'name': 'Image containing staves (RGB, greyscale, or onebit)',
'resource_types': ['image/rgb+png', 'image/onebit+png', 'image/greyscale+png'],
'minimum': 1,
'maximum': 1,
'is_list': False
}]

output_port_types = [{
'name': 'JSOMR',
'resource_types': ['application/json'],
'minimum': 1,
'maximum': 1,
'is_list': False
},
{
'name': 'Overlayed Lines',
'resource_types': ['image/rgb+png'],
'minimum': 0,
'maximum': 1
}]

# Overall Workflow
# 1. Draw bounding boxes around each reference line
# 2. Split each bounding box into a number of sections given by slices
# 3. Add extra space around each line segment, then draws another bounding box (not necessarily rectangular)
# 4. Draw a line through the center of the line segment bounding box
# 5. Link each segment together
# 6. After all lines are found, convert into JSOMR format
def run_my_task(self, inputs, settings, outputs):
input_path = inputs["Image containing staves (RGB, greyscale, or onebit)"][0]["resource_path"]
overlay = "Overlayed Lines" in outputs
slices = settings['Slices']

img = cv2.imread(input_path)

#Image preprocessing to set up bounding boxes
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (1, 1), 0)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
kernal = cv2.getStructuringElement(cv2.MORPH_RECT, (8, 1))
dilate = cv2.dilate(thresh, kernal, iterations=7)
conts = cv2.findContours(dilate, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

conts = conts[0] if len(conts) == 2 else conts[1]
conts = sorted(conts, key=lambda x: cv2.boundingRect(x)[0])

ret = []

#Split each bounding box into sections, then connect the line segments
for c in conts:
x, y, w, h = cv2.boundingRect(c)
part = w // slices
last = []
lines = []
for i in range(0, slices):
img_sect = img[y:y+h, x+(part*i):x+(part*(i+1))]
line = process_section(img_sect)
if line != []:
line[0][0] += x+(part*i)
line[1][0] += x+(part*i)
line[0][1] += y
line[1][1] += y

#normalize lines to bounds
new_y1 = int(coords(line[0][0], line[0][1], line[1][0], line[1][1], x+(part*i)))
new_y2 = int(coords(line[0][0], line[0][1], line[1][0], line[1][1], x+(part*(i+1))))
line[0] = [x+(part*i), new_y1]
line[1] = [x+(part*(i+1)), new_y2]

#make sure line segments connect together
if last != []:
line[0] = last
last = line[1]
lines.append(line[0])
if i == (slices - 1):
lines.append(line[1])

#draw line
if overlay:
cv2.line(img, tuple(line[0]), tuple(line[1]), (255, 0, 0), 2)
#save bounding box and line points
ret.append(([x, y, w, h], lines))

#sort staff lines based on y height
ret = sorted(ret, key=lambda x: x[0][1])
neume_size = settings['Neume Height']

#convert data into jsomr format
jsomr = to_json(img, ret, neume_size)

outfile_path = outputs['JSOMR'][0]['resource_path']
with open(outfile_path, "w") as outfile:
outfile.write(json.dumps(jsomr))

if overlay:
outfile_path2 = outputs["Overlayed Lines"][0]["resource_path"]
overlay_save = Image.fromarray(img)
overlay_save.save(outfile_path2, 'PNG')

return True
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- mimetype: image/rgb+png
description: RGB PNG image
extension: png
- mimetype: image/onebit+png
description: One-bit (black and white) PNG image
extension: png
- mimetype: image/greyscale+png
description: Greyscale PNG image
extension: png
- mimetype: application/json
description: JSON
extension: json
3 changes: 3 additions & 0 deletions rodan-main/code/rodan/registerJobs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"rodan.jobs.labeler" : [ "Labeler" ] }

"RODAN_PYTHON3_JOBS": {
"rodan.jobs.aquitanian_ref_line_finding":{
"rodan.jobs.aquitanian_ref_line_finding.aquitanian_ref_line_finding": ["AquitanianReferenceLineFinding"]
},
"rodan.jobs.helloworld": {
"rodan.jobs.helloworld.helloworld" : ["HelloWorld"],
"rodan.jobs.helloworld.helloworld" : ["HelloWorldMultiPort"],
Expand Down