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

staffline-detection.mddocs/

0

# Staffline Detection and Analysis

1

2

Detection and analysis of musical staff lines, which form the foundation for all subsequent processing steps. Staff lines provide the coordinate system for pitch interpretation and serve as reference points for symbol positioning.

3

4

## Capabilities

5

6

### Primary Staff Extraction

7

8

Extract staff lines and organize them into complete staff structures.

9

10

```python { .api }

11

def extract(splits: int = 8, line_threshold: float = 0.8, horizontal_diff_th: float = 0.1, unit_size_diff_th: float = 0.1, barline_min_degree: int = 75) -> Tuple[ndarray, ndarray]:

12

"""

13

Main staff extraction function that detects and analyzes staff lines.

14

15

Parameters:

16

- splits (int): Number of horizontal splits for processing (default: 8)

17

- line_threshold (float): Threshold for staff line detection (default: 0.8)

18

- horizontal_diff_th (float): Threshold for horizontal alignment validation (default: 0.1)

19

- unit_size_diff_th (float): Threshold for unit size consistency (default: 0.1)

20

- barline_min_degree (int): Minimum angle in degrees for barline detection (default: 75)

21

22

Returns:

23

Tuple containing:

24

- staffs (ndarray): Array of Staff instances

25

- zones (ndarray): Array of zone boundaries for each staff group

26

27

Raises:

28

StafflineException: If staff detection fails or results are inconsistent

29

"""

30

31

def extract_part(pred: ndarray, x_offset: int, line_threshold: float = 0.8) -> List[Staff]:

32

"""

33

Extract staff structures from a specific image region.

34

35

Parameters:

36

- pred (ndarray): Binary prediction array for the image region

37

- x_offset (int): Horizontal offset of this region in the full image

38

- line_threshold (float): Detection threshold for staff lines

39

40

Returns:

41

List[Staff]: List of detected Staff instances in this region

42

"""

43

44

def extract_line(pred: ndarray, x_offset: int, line_threshold: float = 0.8) -> Tuple[ndarray, ndarray]:

45

"""

46

Extract individual staff lines from prediction data.

47

48

Parameters:

49

- pred (ndarray): Binary prediction array

50

- x_offset (int): Horizontal offset in the full image

51

- line_threshold (float): Detection threshold

52

53

Returns:

54

Tuple containing line positions and metadata

55

"""

56

```

57

58

### Staff Line Representation

59

60

Individual staff line with position and geometric properties.

61

62

```python { .api }

63

class Line:

64

"""

65

Represents a single staff line with geometric properties.

66

67

Attributes:

68

- points (List[Tuple[int, int]]): List of (x, y) coordinates on the line

69

"""

70

71

def add_point(self, y: int, x: int) -> None:

72

"""

73

Add a coordinate point to this staff line.

74

75

Parameters:

76

- y (int): Y coordinate of the point

77

- x (int): X coordinate of the point

78

"""

79

80

@property

81

def y_center(self) -> float:

82

"""Get the average Y coordinate of all points on this line."""

83

84

@property

85

def y_upper(self) -> int:

86

"""Get the minimum Y coordinate (top) of this line."""

87

88

@property

89

def y_lower(self) -> int:

90

"""Get the maximum Y coordinate (bottom) of this line."""

91

92

@property

93

def x_center(self) -> float:

94

"""Get the average X coordinate of all points on this line."""

95

96

@property

97

def x_left(self) -> int:

98

"""Get the minimum X coordinate (left) of this line."""

99

100

@property

101

def x_right(self) -> int:

102

"""Get the maximum X coordinate (right) of this line."""

103

104

@property

105

def slope(self) -> float:

106

"""Get the estimated slope of this line."""

107

```

108

109

### Complete Staff Representation

110

111

A complete musical staff consisting of five staff lines.

112

113

```python { .api }

114

class Staff:

115

"""

116

Represents a complete musical staff with five staff lines.

117

118

Attributes:

119

- lines (List[Line]): List of the five staff lines (bottom to top)

120

- track (int): Track number for multi-staff systems

121

- group (int): Group number for staff groupings (e.g., piano grand staff)

122

- is_interp (bool): Whether this staff was interpolated from partial data

123

"""

124

125

def add_line(self, line: Line) -> None:

126

"""

127

Add a staff line to this staff.

128

129

Parameters:

130

- line (Line): The staff line to add

131

132

Raises:

133

ValueError: If trying to add more than 5 lines

134

"""

135

136

def duplicate(self, x_offset: int = 0, y_offset: int = 0) -> 'Staff':

137

"""

138

Create a duplicate of this staff with optional offset.

139

140

Parameters:

141

- x_offset (int): Horizontal offset for the duplicate (default: 0)

142

- y_offset (int): Vertical offset for the duplicate (default: 0)

143

144

Returns:

145

Staff: New Staff instance with offset applied

146

"""

147

148

@property

149

def y_center(self) -> float:

150

"""Get the vertical center of this staff (middle of the 5 lines)."""

151

152

@property

153

def y_upper(self) -> int:

154

"""Get the top boundary of this staff."""

155

156

@property

157

def y_lower(self) -> int:

158

"""Get the bottom boundary of this staff."""

159

160

@property

161

def x_center(self) -> float:

162

"""Get the horizontal center of this staff."""

163

164

@property

165

def x_left(self) -> int:

166

"""Get the left boundary of this staff."""

167

168

@property

169

def x_right(self) -> int:

170

"""Get the right boundary of this staff."""

171

172

@property

173

def unit_size(self) -> float:

174

"""

175

Get the unit size (average distance between staff lines).

176

177

This is the fundamental measurement unit for all musical elements.

178

All other measurements (note sizes, stem lengths, etc.) are

179

proportional to this unit size.

180

"""

181

182

@property

183

def incomplete(self) -> bool:

184

"""Check if this staff has fewer than 5 lines."""

185

186

@property

187

def slope(self) -> float:

188

"""Get the average slope of all lines in this staff."""

189

```

190

191

### Staff Line Position Enumeration

192

193

Enumeration for staff line positions within a staff.

194

195

```python { .api }

196

class LineLabel(enum.Enum):

197

"""

198

Enumeration for staff line positions.

199

200

Values represent the five staff lines from bottom to top.

201

"""

202

FIRST = 0 # Bottom staff line

203

SECOND = 1 # Second staff line from bottom

204

THIRD = 2 # Middle staff line

205

FOURTH = 3 # Fourth staff line from bottom

206

FIFTH = 4 # Top staff line

207

```

208

209

## Usage Examples

210

211

### Basic Staff Detection

212

213

```python

214

from oemer.staffline_extraction import extract

215

from oemer.layers import register_layer, get_layer

216

import numpy as np

217

import cv2

218

219

# Load and prepare image

220

image = cv2.imread("sheet_music.jpg")

221

register_layer("original_image", image)

222

223

# Simulate staff predictions (in real usage, this comes from neural network)

224

h, w = image.shape[:2]

225

staff_pred = np.random.randint(0, 2, (h, w), dtype=np.uint8)

226

register_layer("staff_pred", staff_pred)

227

228

# Extract staff lines

229

try:

230

staffs, zones = extract(

231

splits=8, # Process in 8 horizontal sections

232

line_threshold=0.8, # High confidence threshold

233

horizontal_diff_th=0.1, # Strict alignment requirement

234

unit_size_diff_th=0.1 # Consistent spacing requirement

235

)

236

237

print(f"Detected {len(staffs)} staff structures")

238

print(f"Staff zones: {len(zones)}")

239

240

# Examine each staff

241

for i, staff in enumerate(staffs):

242

print(f"\nStaff {i}:")

243

print(f" Lines: {len(staff.lines)}")

244

print(f" Unit size: {staff.unit_size:.2f} pixels")

245

print(f" Track: {staff.track}")

246

print(f" Group: {staff.group}")

247

print(f" Center: ({staff.x_center:.1f}, {staff.y_center:.1f})")

248

print(f" Bounds: x=[{staff.x_left}, {staff.x_right}], y=[{staff.y_upper}, {staff.y_lower}]")

249

print(f" Slope: {staff.slope:.6f}")

250

print(f" Complete: {not staff.incomplete}")

251

252

except Exception as e:

253

print(f"Staff extraction failed: {e}")

254

```

255

256

### Staff Analysis and Validation

257

258

```python

259

from oemer.staffline_extraction import extract, Staff, Line

260

import numpy as np

261

262

def analyze_staff_quality(staffs: List[Staff]) -> dict:

263

"""Analyze the quality and consistency of detected staffs."""

264

265

analysis = {

266

'total_staffs': len(staffs),

267

'complete_staffs': 0,

268

'incomplete_staffs': 0,

269

'unit_sizes': [],

270

'slopes': [],

271

'tracks': set(),

272

'groups': set()

273

}

274

275

for staff in staffs:

276

if staff.incomplete:

277

analysis['incomplete_staffs'] += 1

278

print(f"Warning: Incomplete staff with {len(staff.lines)} lines")

279

else:

280

analysis['complete_staffs'] += 1

281

282

analysis['unit_sizes'].append(staff.unit_size)

283

analysis['slopes'].append(staff.slope)

284

analysis['tracks'].add(staff.track)

285

analysis['groups'].add(staff.group)

286

287

# Calculate statistics

288

if analysis['unit_sizes']:

289

analysis['avg_unit_size'] = np.mean(analysis['unit_sizes'])

290

analysis['unit_size_std'] = np.std(analysis['unit_sizes'])

291

analysis['avg_slope'] = np.mean(analysis['slopes'])

292

analysis['slope_std'] = np.std(analysis['slopes'])

293

294

return analysis

295

296

# Run staff detection and analysis

297

staffs, zones = extract()

298

analysis = analyze_staff_quality(staffs)

299

300

print("Staff Analysis Results:")

301

print(f"Total staffs: {analysis['total_staffs']}")

302

print(f"Complete staffs: {analysis['complete_staffs']}")

303

print(f"Incomplete staffs: {analysis['incomplete_staffs']}")

304

print(f"Average unit size: {analysis.get('avg_unit_size', 0):.2f} ± {analysis.get('unit_size_std', 0):.2f}")

305

print(f"Average slope: {analysis.get('avg_slope', 0):.6f} ± {analysis.get('slope_std', 0):.6f}")

306

print(f"Tracks detected: {sorted(analysis['tracks'])}")

307

print(f"Groups detected: {sorted(analysis['groups'])}")

308

```

309

310

### Manual Staff Construction

311

312

```python

313

from oemer.staffline_extraction import Staff, Line, LineLabel

314

315

def create_staff_manually(line_positions: List[List[Tuple[int, int]]]) -> Staff:

316

"""Create a staff manually from line coordinate data."""

317

318

staff = Staff()

319

320

for i, points in enumerate(line_positions):

321

if i >= 5: # Maximum 5 lines per staff

322

break

323

324

line = Line()

325

for y, x in points:

326

line.add_point(y, x)

327

328

staff.add_line(line)

329

330

return staff

331

332

# Example: Create a staff with known line positions

333

line_data = [

334

[(100, 50), (100, 100), (100, 150), (100, 200)], # Bottom line

335

[(110, 50), (110, 100), (110, 150), (110, 200)], # Second line

336

[(120, 50), (120, 100), (120, 150), (120, 200)], # Middle line

337

[(130, 50), (130, 100), (130, 150), (130, 200)], # Fourth line

338

[(140, 50), (140, 100), (140, 150), (140, 200)] # Top line

339

]

340

341

custom_staff = create_staff_manually(line_data)

342

print(f"Custom staff unit size: {custom_staff.unit_size}")

343

print(f"Custom staff center: ({custom_staff.x_center}, {custom_staff.y_center})")

344

```

345

346

### Multi-Staff System Processing

347

348

```python

349

from oemer.staffline_extraction import extract

350

import matplotlib.pyplot as plt

351

352

def visualize_staff_system(staffs: List[Staff], zones: List[range]) -> None:

353

"""Visualize detected staff system with tracks and groups."""

354

355

fig, ax = plt.subplots(figsize=(15, 10))

356

357

# Color map for different groups

358

colors = ['red', 'blue', 'green', 'orange', 'purple', 'brown']

359

360

for staff in staffs:

361

color = colors[staff.group % len(colors)]

362

363

# Draw staff lines

364

for line in staff.lines:

365

x_coords = [p[1] for p in line.points] if hasattr(line, 'points') else [staff.x_left, staff.x_right]

366

y_coords = [p[0] for p in line.points] if hasattr(line, 'points') else [line.y_center, line.y_center]

367

ax.plot(x_coords, y_coords, color=color, linewidth=2, alpha=0.7)

368

369

# Add staff information

370

ax.text(staff.x_left - 50, staff.y_center,

371

f'T{staff.track}G{staff.group}',

372

fontsize=10, color=color, weight='bold')

373

374

# Draw zone boundaries

375

for i, zone in enumerate(zones):

376

ax.axhspan(zone.start, zone.stop, alpha=0.1, color='gray')

377

ax.text(10, (zone.start + zone.stop) / 2, f'Zone {i}',

378

rotation=90, fontsize=8)

379

380

ax.set_title('Detected Staff System')

381

ax.set_xlabel('X Position')

382

ax.set_ylabel('Y Position')

383

ax.invert_yaxis() # Invert Y-axis to match image coordinates

384

ax.grid(True, alpha=0.3)

385

386

plt.tight_layout()

387

plt.savefig('staff_system_visualization.png', dpi=150)

388

plt.show()

389

390

# Detect and visualize staff system

391

staffs, zones = extract()

392

visualize_staff_system(staffs, zones)

393

```

394

395

## Staff Detection Algorithm

396

397

### Multi-Stage Processing

398

399

1. **Image Splitting**: Divide image into horizontal sections for robust detection

400

2. **Peak Detection**: Find horizontal accumulation peaks corresponding to staff lines

401

3. **Line Grouping**: Group individual lines into complete 5-line staffs

402

4. **Validation**: Check consistency of unit sizes and alignment

403

5. **Track Assignment**: Assign track numbers for multi-staff systems

404

6. **Group Assignment**: Group related staffs (e.g., piano grand staff)

405

406

### Key Measurements

407

408

- **Unit Size**: Average distance between adjacent staff lines

409

- **Slope**: Average slope across all lines in a staff

410

- **Alignment**: Horizontal consistency of staff line positions

411

- **Spacing**: Vertical consistency of line spacing

412

413

### Quality Metrics

414

415

- Staff lines should be approximately parallel

416

- Unit size should be consistent within ±10% across all staffs

417

- Complete staffs should have exactly 5 lines

418

- Track and group assignments should follow musical conventions

419

420

The staff detection system provides the foundational coordinate system for all subsequent musical element recognition, making its accuracy critical for the overall OMR pipeline performance.