0
# Note Grouping and Rhythm Analysis
1
2
Advanced grouping of individual noteheads into musical chords and rhythm pattern recognition. This module combines related noteheads based on stems, beams, and spatial proximity to create meaningful musical structures.
3
4
## Capabilities
5
6
### Note Grouping
7
8
Group individual noteheads into chord structures based on stems and beams.
9
10
```python { .api }
11
def extract() -> Tuple[List[NoteGroup], ndarray]:
12
"""
13
Group noteheads by stems and beams into chord groups.
14
15
Analyzes the spatial relationships between noteheads and identifies
16
groups that should be played simultaneously (chords) or in sequence
17
(beamed note groups).
18
19
Returns:
20
Tuple containing:
21
- List[NoteGroup]: List of detected note groups
22
- ndarray: Group mapping array showing which group each pixel belongs to
23
24
Raises:
25
KeyError: If required layers (notes, stems_rests_pred) are not available
26
"""
27
28
def group_noteheads() -> Tuple[Dict[int, List[int]], ndarray]:
29
"""
30
Create initial groupings of noteheads based on spatial proximity.
31
32
Returns:
33
Tuple containing:
34
- Dict[int, List[int]]: Mapping of group IDs to note IDs
35
- ndarray: Group mapping visualization
36
"""
37
38
def get_possible_nearby_gid(cur_note: NoteHead, group_map: ndarray, scan_range_ratio: float = 5) -> List[int]:
39
"""
40
Find group IDs near a given notehead.
41
42
Parameters:
43
- cur_note (NoteHead): The notehead to search around
44
- group_map (ndarray): Current group mapping array
45
- scan_range_ratio (float): Search radius as multiple of unit size
46
47
Returns:
48
List[int]: List of nearby group IDs
49
"""
50
51
def check_valid_new_group(ori_grp: List[int], tar_grp: List[int], group_map: ndarray, max_x_diff_ratio: float = 0.5) -> bool:
52
"""
53
Check if two groups can be validly merged.
54
55
Parameters:
56
- ori_grp (List[int]): Original group note IDs
57
- tar_grp (List[int]): Target group note IDs
58
- group_map (ndarray): Group mapping array
59
- max_x_diff_ratio (float): Maximum horizontal separation ratio
60
61
Returns:
62
bool: True if groups can be merged, False otherwise
63
"""
64
```
65
66
### Rhythm Analysis
67
68
Extract rhythm information from beams, flags, and augmentation dots.
69
70
```python { .api }
71
def extract(min_area_ratio: float = 0.08, max_area_ratio: float = 0.2, beam_th: float = 0.5) -> None:
72
"""
73
Extract rhythm information from beams, flags, and dots.
74
75
Analyzes beam structures, note flags, and augmentation dots to
76
determine the final rhythm values for each note and note group.
77
78
Parameters:
79
- min_area_ratio (float): Minimum area ratio for valid rhythm elements
80
- max_area_ratio (float): Maximum area ratio for valid rhythm elements
81
- beam_th (float): Threshold for beam detection
82
83
Raises:
84
KeyError: If required layers are not available
85
"""
86
87
def get_rhythm_class(region: ndarray, model_name: str = "rhythm") -> str:
88
"""
89
Classify rhythm type from image region using trained models.
90
91
Parameters:
92
- region (ndarray): Image region containing rhythm elements
93
- model_name (str): Name of sklearn model for rhythm classification
94
95
Returns:
96
str: Predicted rhythm class (beam, flag, etc.)
97
"""
98
99
def check_beam_connection(note1: NoteHead, note2: NoteHead, beam_predictions: ndarray) -> bool:
100
"""
101
Check if two noteheads are connected by a beam.
102
103
Parameters:
104
- note1 (NoteHead): First notehead
105
- note2 (NoteHead): Second notehead
106
- beam_predictions (ndarray): Beam detection predictions
107
108
Returns:
109
bool: True if noteheads are beam-connected
110
"""
111
```
112
113
## NoteGroup Class
114
115
Represents a group of noteheads that form a musical chord or beamed group.
116
117
```python { .api }
118
class NoteGroup:
119
"""
120
Represents a group of notes connected by stems, beams, or forming chords.
121
122
Attributes:
123
- id (Optional[int]): Unique identifier for this group
124
- bbox (BBox): Bounding box encompassing all notes in the group
125
- note_ids (List[int]): IDs of noteheads belonging to this group
126
- top_note_ids (List[int]): IDs of the highest notes (for multi-voice)
127
- bottom_note_ids (List[int]): IDs of the lowest notes (for multi-voice)
128
- stem_up (Optional[bool]): Direction of stem (True=up, False=down)
129
- has_stem (Optional[bool]): Whether this group has a visible stem
130
- all_same_type (Optional[bool]): Whether all notes have same rhythm type
131
- group (Optional[int]): Staff group number
132
- track (Optional[int]): Track number for multi-staff systems
133
"""
134
135
@property
136
def x_center(self) -> float:
137
"""
138
Get the horizontal center of this note group.
139
140
Returns:
141
float: X-coordinate of the group center
142
"""
143
144
def __len__(self) -> int:
145
"""Get the number of notes in this group."""
146
147
def __repr__(self) -> str:
148
"""String representation of the note group."""
149
```
150
151
## Processing Algorithms
152
153
### Stem-Based Grouping
154
155
The grouping algorithm identifies noteheads that share common stems:
156
157
1. **Stem Detection**: Analyze stem predictions to find vertical lines
158
2. **Stem-Note Association**: Connect noteheads to nearby stems
159
3. **Group Formation**: Group all noteheads sharing the same stem
160
4. **Direction Analysis**: Determine stem direction (up/down)
161
162
### Beam Analysis
163
164
For beamed note groups (eighth notes and shorter):
165
166
1. **Beam Detection**: Identify horizontal beam structures
167
2. **Beam-Note Connection**: Associate beams with connected noteheads
168
3. **Rhythm Determination**: Count beam levels to determine note values
169
4. **Group Refinement**: Merge beam-connected groups
170
171
### Chord Detection
172
173
For simultaneous notes (chords):
174
175
1. **Vertical Alignment**: Find noteheads aligned vertically
176
2. **Stem Sharing**: Group notes sharing a common stem
177
3. **Timing Alignment**: Ensure notes have same horizontal position
178
4. **Voice Separation**: Separate multiple voices when present
179
180
## Usage Examples
181
182
### Basic Note Grouping
183
184
```python
185
from oemer.note_group_extraction import extract
186
from oemer.layers import get_layer
187
import numpy as np
188
189
# Ensure required layers are available
190
try:
191
notes = get_layer('notes')
192
stems_rests = get_layer('stems_rests_pred')
193
194
# Extract note groups
195
note_groups, group_map = extract()
196
197
print(f"Found {len(note_groups)} note groups")
198
199
# Analyze each group
200
for i, group in enumerate(note_groups):
201
print(f"\nGroup {i}:")
202
print(f" Notes: {len(group)} noteheads")
203
print(f" Stem up: {group.stem_up}")
204
print(f" Has stem: {group.has_stem}")
205
print(f" Track: {group.track}")
206
print(f" Center: ({group.x_center:.1f})")
207
print(f" Same type: {group.all_same_type}")
208
209
# List individual notes in group
210
notes_layer = get_layer('notes')
211
for note_id in group.note_ids:
212
note = notes_layer[note_id]
213
print(f" Note {note_id}: {note.label}, pitch={note.pitch}")
214
215
except KeyError as e:
216
print(f"Required layer missing: {e}")
217
```
218
219
### Chord Analysis
220
221
```python
222
from oemer.note_group_extraction import extract
223
from oemer.layers import get_layer
224
225
# Extract groups and analyze chords
226
note_groups, group_map = extract()
227
notes = get_layer('notes')
228
229
# Find chord groups (multiple notes at same time position)
230
chords = []
231
single_notes = []
232
233
for group in note_groups:
234
if len(group) > 1:
235
# This is potentially a chord
236
chord_notes = [notes[nid] for nid in group.note_ids]
237
238
# Check if notes are vertically aligned (true chord)
239
x_positions = [note.bbox[0] for note in chord_notes]
240
x_variance = np.var(x_positions)
241
242
if x_variance < 100: # Low variance indicates vertical alignment
243
chords.append(group)
244
print(f"Chord found: {len(chord_notes)} notes")
245
246
# Show chord notes from bottom to top
247
chord_notes.sort(key=lambda n: n.staff_line_pos)
248
for note in chord_notes:
249
print(f" {note.label.name} at position {note.staff_line_pos}")
250
else:
251
single_notes.append(group)
252
else:
253
single_notes.append(group)
254
255
print(f"\nFound {len(chords)} chords and {len(single_notes)} single note groups")
256
```
257
258
### Beam Analysis
259
260
```python
261
from oemer.note_group_extraction import extract, check_beam_connection
262
from oemer.rhythm_extraction import extract as rhythm_extract
263
from oemer.layers import get_layer
264
265
# Extract groups and analyze beamed notes
266
note_groups, group_map = extract()
267
notes = get_layer('notes')
268
269
# Run rhythm analysis to detect beams
270
rhythm_extract()
271
272
# Find beamed groups
273
beamed_groups = []
274
for group in note_groups:
275
if len(group) > 1 and group.has_stem:
276
# Check if this is a beamed group
277
group_notes = [notes[nid] for nid in group.note_ids]
278
279
# Sort by horizontal position
280
group_notes.sort(key=lambda n: n.bbox[0])
281
282
# Check for beam connections between consecutive notes
283
is_beamed = False
284
beam_pred = get_layer('stems_rests_pred') # Contains beam info
285
286
for i in range(len(group_notes) - 1):
287
if check_beam_connection(group_notes[i], group_notes[i+1], beam_pred):
288
is_beamed = True
289
break
290
291
if is_beamed:
292
beamed_groups.append(group)
293
print(f"Beamed group: {len(group_notes)} notes")
294
295
# Show note sequence
296
for note in group_notes:
297
print(f" {note.label.name} at x={note.bbox[0]}")
298
299
print(f"\nFound {len(beamed_groups)} beamed note groups")
300
```
301
302
### Multi-Voice Separation
303
304
```python
305
from oemer.note_group_extraction import extract
306
from oemer.layers import get_layer
307
from collections import defaultdict
308
309
# Extract groups and separate voices
310
note_groups, group_map = extract()
311
notes = get_layer('notes')
312
313
# Group by track and analyze stem directions
314
by_track = defaultdict(list)
315
for group in note_groups:
316
by_track[group.track].append(group)
317
318
for track, groups in by_track.items():
319
print(f"\nTrack {track}:")
320
321
# Separate by stem direction (voices)
322
stem_up_groups = [g for g in groups if g.stem_up == True]
323
stem_down_groups = [g for g in groups if g.stem_up == False]
324
no_stem_groups = [g for g in groups if g.stem_up is None]
325
326
print(f" Stem up (voice 1): {len(stem_up_groups)} groups")
327
print(f" Stem down (voice 2): {len(stem_down_groups)} groups")
328
print(f" No stem: {len(no_stem_groups)} groups")
329
330
# Analyze voice crossing
331
if stem_up_groups and stem_down_groups:
332
up_positions = []
333
down_positions = []
334
335
for group in stem_up_groups:
336
for note_id in group.note_ids:
337
up_positions.append(notes[note_id].staff_line_pos)
338
339
for group in stem_down_groups:
340
for note_id in group.note_ids:
341
down_positions.append(notes[note_id].staff_line_pos)
342
343
if up_positions and down_positions:
344
avg_up = np.mean(up_positions)
345
avg_down = np.mean(down_positions)
346
347
if avg_up < avg_down: # Voice crossing detected
348
print(f" Voice crossing detected: up voice avg={avg_up:.1f}, down voice avg={avg_down:.1f}")
349
```
350
351
## Integration with Pipeline
352
353
The note grouping module integrates with other pipeline components:
354
355
**Input Dependencies:**
356
- `notes` layer: Individual noteheads from notehead extraction
357
- `stems_rests_pred` layer: Stem and rest predictions from neural network
358
- `staffs` layer: Staff line information for context
359
360
**Output Products:**
361
- `note_groups` layer: Array of NoteGroup instances
362
- `group_map` layer: Pixel-level mapping of group assignments
363
364
**Downstream Usage:**
365
- Rhythm extraction uses groups to determine final timing
366
- MusicXML builder uses groups to create proper voice structures
367
- Beam analysis depends on group spatial relationships
368
369
This modular approach allows the grouping algorithm to be refined independently while maintaining integration with the broader OMR pipeline.