or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

index.mdinference.mdlayer-management.mdmain-pipeline.mdnote-grouping.mdnotehead-extraction.mdstaffline-detection.md

note-grouping.mddocs/

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.