티스토리 뷰
Claude Sonnet 4.5가 알려주는 nx witness server multi camera case plugin 동작 과정
developer0hye 2025. 12. 1. 13:18NX Witness Multiple Camera 처리 가이드
개요
NX Witness는 여러 카메라를 동시에 관리하고 처리할 수 있는 비디오 관리 시스템(VMS)입니다. 이 문서는 multiple camera가 연결될 때 발생하는 일들과 처리 과정을 설명합니다.
1. 카메라 연결 시 발생하는 일들
1.1 카메라 디스커버리 (Camera Discovery)
[카메라 1 연결] → [네트워크 스캔] → [ONVIF/RTSP 프로토콜 감지]
[카메라 2 연결] → [네트워크 스캔] → [ONVIF/RTSP 프로토콜 감지]
[카메라 3 연결] → [네트워크 스캔] → [ONVIF/RTSP 프로토콜 감지]처리 과정:
- NX Witness 서버가 주기적으로 네트워크를 스캔
- 새로운 카메라 감지 시 ONVIF/RTSP 등의 표준 프로토콜로 통신 시도
- 카메라 정보 (모델명, 해상도, 지원 코덱 등) 수집
- 서버의 카메라 데이터베이스에 등록
1.2 카메라 인증 및 설정
각 카메라는 독립적으로 인증 과정을 거칩니다:
카메라 1: [인증] → [스트림 설정] → [녹화 설정]
카메라 2: [인증] → [스트림 설정] → [녹화 설정]
카메라 3: [인증] → [스트림 설정] → [녹화 설정]2. 비디오 스트림 처리
2.1 스트림 타입
NX Witness는 각 카메라에서 두 가지 스트림을 처리할 수 있습니다:
Primary Stream (고해상도)
- 1080p, 4K 등 고화질
- 녹화용
- 높은 대역폭 사용
Secondary Stream (저해상도)
- 720p, 480p 등 저화질
- 라이브 모니터링용
- 낮은 대역폭 사용
2.2 Multiple Camera 스트림 관리
┌─────────────────────────────────────────┐
│ NX Witness Server Core │
├─────────────────────────────────────────┤
│ Stream Manager │
│ ├─ Camera 1 Primary Stream (Thread 1) │
│ ├─ Camera 1 Secondary Stream (Thread 2)│
│ ├─ Camera 2 Primary Stream (Thread 3) │
│ ├─ Camera 2 Secondary Stream (Thread 4)│
│ ├─ Camera 3 Primary Stream (Thread 5) │
│ └─ Camera 3 Secondary Stream (Thread 6)│
└─────────────────────────────────────────┘처리 방식:
- 각 카메라 스트림은 독립적인 스레드에서 처리
- 스트림 버퍼링을 통한 네트워크 지연 보상
- 프레임 드롭 감지 및 재연결 메커니즘
3. 플러그인 처리 (Analytics Plugin)
3.1 스트림 식별 방법
플러그인이 현재 처리 중인 스트림/카메라를 구분하는 방법:
3.1.1 카메라 식별자 (Device ID)
NX Witness는 각 카메라에 고유한 식별자를 할당합니다:
Camera 식별 정보:
{
"deviceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", // UUID
"deviceName": "Front Door Camera",
"physicalId": "192.168.1.101:80",
"model": "AXIS P1435-LE",
"manufacturer": "AXIS"
}플러그인이 받는 정보:
- 프레임을 처리할 때마다 해당 프레임의 출처 카메라 정보가 함께 전달됩니다
- Device ID를 통해 어떤 카메라의 프레임인지 정확히 식별 가능
- 같은 카메라의 프레임들은 항상 동일한 Device ID를 가집니다
3.1.2 스트림 타입 구분
프레임 메타데이터 예시:
{
"deviceId": "a1b2c3d4-...",
"streamType": "primary", // 또는 "secondary"
"timestamp": 1701423045123,
"width": 1920,
"height": 1080,
"format": "NV12"
}Primary vs Secondary 구분:
streamType필드로 고해상도/저해상도 스트림 구분- 같은 카메라라도 두 개의 다른 스트림 처리 가능
- 각 스트림마다 독립적인 플러그인 인스턴스 생성될 수 있음
3.1.3 실제 처리 흐름
1. 플러그인 인스턴스 생성 시:
┌────────────────────────────────────────┐
│ createInstance(deviceId, streamType) │
│ │
│ → 인스턴스에 저장: │
│ this.deviceId = deviceId │
│ this.streamType = streamType │
│ this.cameraName = "Camera 1" │
└────────────────────────────────────────┘
2. 프레임 처리 시:
┌────────────────────────────────────────┐
│ processFrame(frame) │
│ │
│ frame.deviceId 확인: │
│ → "a1b2c3d4-..." (Camera 1) │
│ │
│ this.deviceId와 비교: │
│ → 일치함! 내가 담당하는 카메라 │
│ │
│ 처리 진행... │
└────────────────────────────────────────┘3.1.4 Multiple Camera 시나리오 예시
3대의 카메라가 연결된 경우:
시스템 상태:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─────────────────────────────────────────────────────────┐
│ NX Witness Server │
├─────────────────────────────────────────────────────────┤
│ │
│ Plugin Instance #1 │
│ deviceId: "aaaa-1111-..." │
│ deviceName: "Front Door" │
│ streamType: "primary" │
│ status: Processing frame #12345 │
│ │
│ Plugin Instance #2 │
│ deviceId: "bbbb-2222-..." │
│ deviceName: "Parking Lot" │
│ streamType: "primary" │
│ status: Processing frame #12340 │
│ │
│ Plugin Instance #3 │
│ deviceId: "cccc-3333-..." │
│ deviceName: "Back Entrance" │
│ streamType: "primary" │
│ status: Processing frame #12338 │
│ │
└─────────────────────────────────────────────────────────┘각 인스턴스가 프레임을 받을 때:
[시간: 10:30:45.000]
Instance #1이 프레임 수신:
frame.deviceId = "aaaa-1111-..."
frame.deviceName = "Front Door"
frame.timestamp = 1701423045000
→ Instance #1: "내 deviceId와 일치! 처리 시작"
→ Instance #2: (이 프레임 안 받음)
→ Instance #3: (이 프레임 안 받음)
Instance #2가 프레임 수신:
frame.deviceId = "bbbb-2222-..."
frame.deviceName = "Parking Lot"
frame.timestamp = 1701423045000
→ Instance #1: (이 프레임 안 받음)
→ Instance #2: "내 deviceId와 일치! 처리 시작"
→ Instance #3: (이 프레임 안 받음)
Instance #3가 프레임 수신:
frame.deviceId = "cccc-3333-..."
frame.deviceName = "Back Entrance"
frame.timestamp = 1701423045000
→ Instance #1: (이 프레임 안 받음)
→ Instance #2: (이 프레임 안 받음)
→ Instance #3: "내 deviceId와 일치! 처리 시작"3.1.5 플러그인 내부에서 카메라 구분하기
로깅 예시:
[Instance #1] [aaaa-1111] [Front Door] Processing frame 12345
[Instance #1] [aaaa-1111] [Front Door] Detected 2 objects
[Instance #1] [aaaa-1111] [Front Door] Processing time: 45ms
[Instance #2] [bbbb-2222] [Parking Lot] Processing frame 12340
[Instance #2] [bbbb-2222] [Parking Lot] Detected 5 objects
[Instance #2] [bbbb-2222] [Parking Lot] Processing time: 52ms
[Instance #3] [cccc-3333] [Back Entrance] Processing frame 12338
[Instance #3] [cccc-3333] [Back Entrance] Detected 1 object
[Instance #3] [cccc-3333] [Back Entrance] Processing time: 38ms카메라별 설정 관리:
플러그인이 카메라별로 다른 설정 유지:
Instance #1 (Front Door):
- 감지 영역: 전체 화면
- 감지 감도: 높음 (보안 중요)
- 알림: 즉시
Instance #2 (Parking Lot):
- 감지 영역: 주차 구역만
- 감지 감도: 중간
- 알림: 5분 딜레이
Instance #3 (Back Entrance):
- 감지 영역: 출입문 앞
- 감지 감도: 낮음
- 알림: 업무시간만3.1.6 왜 각 인스턴스마다 정보를 알아야 하는가?
1. 독립적인 상태 관리
Instance #1 (Front Door) 상태:
- 마지막 감지 시간: 10:25:30
- 연속 감지 횟수: 3
- 배경 모델: [특정 카메라 뷰에 맞게 학습됨]
Instance #2 (Parking Lot) 상태:
- 마지막 감지 시간: 10:20:15
- 연속 감지 횟수: 0
- 배경 모델: [완전히 다른 카메라 뷰에 맞게 학습됨]2. 결과 보고 시 출처 명확화
이벤트 전송:
{
"eventType": "person_detected",
"deviceId": "aaaa-1111-...", ← 어느 카메라인지
"deviceName": "Front Door", ← 사람이 읽기 쉬운 이름
"timestamp": 1701423045000,
"objects": [...]
}
→ NX Witness UI에서 "Front Door 카메라에서 사람 감지"로 표시3. 디버깅 및 문제 해결
문제 발생 시:
"Instance #2 (Parking Lot)에서만 처리 속도 느림"
→ 해당 카메라의 해상도가 다른가?
→ 해당 장소의 객체 수가 많은가?
→ 해당 인스턴스만 재시작 가능3.2 플러그인 인스턴스 생성
Multiple camera 환경에서 플러그인은 다음과 같이 동작합니다:
Camera 1 연결
↓
[Plugin Engine Instance 1 생성]
↓
Camera 1 프레임 → Instance 1 처리 → 결과 반환
Camera 2 연결
↓
[Plugin Engine Instance 2 생성]
↓
Camera 2 프레임 → Instance 2 처리 → 결과 반환
Camera 3 연결
↓
[Plugin Engine Instance 3 생성]
↓
Camera 3 프레임 → Instance 3 처리 → 결과 반환중요 개념:
- 각 카메라마다 독립적인 플러그인 인스턴스 생성
- 각 인스턴스는 자체 상태와 메모리 공간 보유
- 인스턴스 간 격리를 통한 안정성 확보
3.2 플러그인 처리 파이프라인
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Camera 1 │ │ Camera 2 │ │ Camera 3 │
│ Stream │ │ Stream │ │ Stream │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
↓ ↓ ↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Frame Buffer │ │ Frame Buffer │ │ Frame Buffer │
│ Queue 1 │ │ Queue 2 │ │ Queue 3 │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
↓ ↓ ↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Plugin │ │ Plugin │ │ Plugin │
│ Instance 1 │ │ Instance 2 │ │ Instance 3 │
│ (Detect) │ │ (Detect) │ │ (Detect) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
↓ ↓ ↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Metadata │ │ Metadata │ │ Metadata │
│ (Objects, │ │ (Objects, │ │ (Objects, │
│ Events) │ │ Events) │ │ Events) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└─────────────────────┴─────────────────────┘
↓
┌──────────────────────┐
│ NX Witness UI │
│ (Display Results) │
└──────────────────────┘4. 리소스 관리
4.1 CPU 및 메모리 관리
Multiple camera 처리 시 리소스 할당:
시스템 리소스 (예: 8 Core CPU, 16GB RAM)
│
├─ NX Witness Server Core (2 Cores, 4GB RAM)
│ ├─ Stream Processing
│ ├─ Recording Management
│ └─ Network I/O
│
├─ Camera 1 Plugin (1 Core, 2GB RAM)
├─ Camera 2 Plugin (1 Core, 2GB RAM)
├─ Camera 3 Plugin (1 Core, 2GB RAM)
│
└─ OS & Other Services (3 Cores, 6GB RAM)4.2 프레임 처리 우선순위
동시 처리 전략:
병렬 처리 (Parallel Processing)
- 여러 CPU 코어를 활용하여 각 카메라를 동시 처리
- 각 플러그인 인스턴스는 독립적인 스레드에서 실행
프레임 스킵 (Frame Skipping)
- 리소스 부족 시 일부 프레임 건너뛰기
- 예: 30fps 스트림에서 15fps만 분석
적응형 품질 조정 (Adaptive Quality)
- 부하에 따라 분석 해상도 자동 조정
- 여러 카메라 활성 시 → 낮은 해상도로 처리
5. 동기화 및 타임스탬프
5.1 시간 동기화
Multiple camera의 정확한 시간 동기화는 중요합니다:
[NTP Server] → [NX Witness Server] → [All Cameras]
↓
[Unified Timestamp]
↓
┌─────────────┼─────────────┐
↓ ↓ ↓
Camera 1 Camera 2 Camera 3
Frame t=0 Frame t=0 Frame t=0
Frame t=33ms Frame t=33ms Frame t=33ms
Frame t=66ms Frame t=66ms Frame t=66ms중요성:
- 이벤트 상관관계 분석
- 여러 카메라 뷰 동기화된 재생
- 정확한 녹화 타임라인
5.2 프레임 메타데이터
각 프레임은 다음 정보를 포함:
{
"camera_id": "camera-001",
"timestamp": "2025-12-01T10:30:45.123Z",
"frame_number": 12345,
"resolution": "1920x1080",
"codec": "H.264",
"analytics": {
"objects_detected": 3,
"processing_time_ms": 45
}
}
6. 이벤트 및 알림 관리
6.1 Multiple Camera 이벤트 처리
Camera 1: 사람 감지 (10:30:45) ─┐
│
Camera 2: 차량 감지 (10:30:46) ─┼─→ [Event Aggregator]
│ ↓
Camera 3: 동작 감지 (10:30:47) ─┘ [Rule Engine]
↓
┌──────┴──────┐
│ │
[알림 전송] [녹화 시작]6.2 이벤트 우선순위
긴급 이벤트 (즉시 알림)
- 침입 감지
- 화재 감지
- 비상 상황
일반 이벤트 (대기열 처리)
- 사람/차량 감지
- 라인 크로싱
- 배회 감지
정보성 이벤트 (로그만 기록)
- 카메라 연결/해제
- 시스템 상태 변경
7. 실제 처리 흐름 예시
7.1 3개 카메라 동시 처리 시나리오
Time: T=0
────────────────────────────────────────────
Camera 1: Frame 1 도착 → 버퍼에 추가 → 처리 대기
Camera 2: Frame 1 도착 → 버퍼에 추가 → 처리 대기
Camera 3: Frame 1 도착 → 버퍼에 추가 → 처리 대기
Time: T=10ms
────────────────────────────────────────────
Camera 1: Plugin 처리 중 (45% 완료)
Camera 2: Plugin 처리 중 (30% 완료)
Camera 3: Plugin 처리 중 (60% 완료)
Time: T=45ms
────────────────────────────────────────────
Camera 1: 처리 완료 → 결과 전송 → Frame 2 처리 시작
Camera 2: 처리 완료 → 결과 전송 → Frame 2 처리 시작
Camera 3: 처리 완료 → 결과 전송 → Frame 2 처리 시작
Time: T=90ms
────────────────────────────────────────────
Camera 1: Frame 2 처리 완료 → Frame 3 처리 시작
Camera 2: Frame 2 처리 완료 → Frame 3 처리 시작
Camera 3: Frame 2 처리 완료 → Frame 3 처리 시작7.2 부하 상황 처리
시나리오: 10개 카메라, 리소스 부족
상황 감지:
- CPU 사용률 95% 이상
- 프레임 처리 지연 발생
- 버퍼 오버플로우 경고
자동 조정:
1. 낮은 우선순위 카메라 프레임 스킵 (30fps → 15fps)
2. 분석 해상도 감소 (1080p → 720p)
3. 일부 플러그인 기능 비활성화
4. 관리자에게 알림 전송
결과:
- CPU 사용률 75%로 감소
- 모든 카메라 계속 처리 (품질 조정)
- 시스템 안정성 유지8. 네트워크 고려사항
8.1 대역폭 계산
예시: 3개 카메라 설정
Camera 1: 1920x1080 @ 30fps, H.264
→ Primary: ~8 Mbps
→ Secondary: ~2 Mbps
Camera 2: 1920x1080 @ 30fps, H.264
→ Primary: ~8 Mbps
→ Secondary: ~2 Mbps
Camera 3: 2560x1440 @ 30fps, H.265
→ Primary: ~10 Mbps
→ Secondary: ~2 Mbps
총 대역폭 필요량: ~32 Mbps
권장 네트워크: 100 Mbps 이상8.2 네트워크 장애 처리
정상 상태:
Camera 1 ─┐
Camera 2 ─┼─→ [Switch] ─→ [NX Server]
Camera 3 ─┘
장애 발생:
Camera 1 ─┐
Camera 2 ─X (네트워크 끊김)
Camera 3 ─┘
서버 동작:
1. Camera 2 재연결 시도 (자동, 5초 간격)
2. Camera 1, 3는 정상 처리 계속
3. Camera 2 오프라인 상태 기록
4. 재연결 시 자동으로 처리 재개9. 스케일링 전략
9.1 수직 스케일링 (Vertical Scaling)
서버 성능 업그레이드:
기본 설정: 업그레이드:
4 CPU Cores → 16 CPU Cores
8 GB RAM → 32 GB RAM
1 Gbps NIC → 10 Gbps NIC
처리 가능 카메라:
Before: 4-6 cameras
After: 16-20 cameras9.2 수평 스케일링 (Horizontal Scaling)
여러 서버 사용:
┌─────────────────┐ ┌─────────────────┐
│ Server 1 │ │ Server 2 │
│ Camera 1-10 │ │ Camera 11-20 │
└────────┬────────┘ └────────┬────────┘
│ │
└────────┬───────────────┘
↓
┌──────────────────┐
│ Master Server │
│ (Coordination) │
└──────────────────┘10. 모니터링 및 디버깅
10.1 시스템 메트릭
각 카메라별 모니터링:
Camera 1 Status:
✓ Connection: Active
✓ FPS: 30.0 (stable)
✓ Bitrate: 8.2 Mbps
✓ Plugin Processing: 42ms avg
✓ Frame Drop: 0.1%
Camera 2 Status:
⚠ Connection: Unstable
⚠ FPS: 25.3 (dropping)
⚠ Bitrate: 6.8 Mbps (low)
✓ Plugin Processing: 38ms avg
⚠ Frame Drop: 5.2%
Camera 3 Status:
✓ Connection: Active
✓ FPS: 30.0 (stable)
✓ Bitrate: 10.1 Mbps
⚠ Plugin Processing: 95ms avg (slow)
✓ Frame Drop: 0.2%10.2 로그 구조
[2025-12-01 10:30:45.123] [Camera-1] [INFO] Frame received: 1920x1080
[2025-12-01 10:30:45.156] [Camera-1] [DEBUG] Plugin processing started
[2025-12-01 10:30:45.198] [Camera-1] [INFO] Objects detected: 2
[2025-12-01 10:30:45.201] [Camera-1] [DEBUG] Plugin processing completed: 45ms
[2025-12-01 10:30:45.125] [Camera-2] [INFO] Frame received: 1920x1080
[2025-12-01 10:30:45.160] [Camera-2] [DEBUG] Plugin processing started
[2025-12-01 10:30:45.205] [Camera-2] [INFO] Objects detected: 0
[2025-12-01 10:30:45.208] [Camera-2] [DEBUG] Plugin processing completed: 48ms
[2025-12-01 10:30:45.130] [Camera-3] [WARNING] High processing time: 95ms
[2025-12-01 10:30:45.135] [Camera-3] [INFO] Frame received: 2560x144011. 베스트 프랙티스
11.1 설계 원칙
독립성 (Independence)
- 각 카메라는 독립적으로 동작
- 한 카메라의 장애가 다른 카메라에 영향 없음
확장성 (Scalability)
- 카메라 추가 시 선형적 리소스 증가
- 플러그인 인스턴스 독립적 관리
내결함성 (Fault Tolerance)
- 자동 재연결 메커니즘
- 부분 장애 시에도 시스템 계속 동작
11.2 성능 최적화
1. 프레임 전처리
- 해상도 조정은 한 번만 수행
- 결과를 캐시하여 재사용
2. 배치 처리
- 가능한 경우 여러 프레임을 배치로 처리
- GPU 사용 시 효율성 증대
3. 스마트 스케줄링
- 우선순위 기반 프레임 처리
- 중요 카메라에 더 많은 리소스 할당
4. 메모리 관리
- 프레임 버퍼 크기 제한
- 사용 후 즉시 메모리 해제12. 트러블슈팅 가이드
12.1 일반적인 문제
문제 1: 일부 카메라 프레임 드롭
증상:
- 특정 카메라에서 프레임 손실 발생
- FPS가 설정값보다 낮음
원인:
- 네트워크 대역폭 부족
- 플러그인 처리 시간 과다
- 서버 리소스 부족
해결:
1. 네트워크 대역폭 확인
2. Secondary 스트림 사용
3. 프레임 스킵 설정 조정
4. 플러그인 최적화문제 2: 플러그인 처리 지연
증상:
- 플러그인 처리 시간 지속적 증가
- 버퍼 오버플로우 발생
원인:
- CPU 리소스 부족
- 비효율적인 알고리즘
- 메모리 부족
해결:
1. 처리할 카메라 수 감소
2. 분석 해상도 낮추기
3. GPU 가속 사용
4. 서버 업그레이드12.2 디버깅 체크리스트
□ 모든 카메라 네트워크 연결 확인
□ 각 카메라 스트림 품질 확인
□ CPU/메모리 사용률 모니터링
□ 네트워크 대역폭 측정
□ 플러그인 로그 확인
□ 프레임 처리 시간 측정
□ 버퍼 상태 확인
□ 디스크 I/O 성능 점검13. 요약
Multiple Camera 처리의 핵심 개념
- 각 카메라는 독립적인 스트림과 플러그인 인스턴스를 가집니다
- 병렬 처리를 통해 여러 카메라를 동시에 처리합니다
- 리소스 부족 시 자동으로 품질과 성능을 조정합니다
- 시간 동기화를 통해 모든 카메라의 이벤트를 정확히 추적합니다
- 네트워크와 시스템 리소스를 효율적으로 관리해야 합니다
성공적인 Multiple Camera 시스템을 위한 요구사항
✓ 충분한 네트워크 대역폭 (카메라당 10-15 Mbps)
✓ 적절한 서버 사양 (카메라 4-6개당 CPU 코어 1개)
✓ 효율적인 플러그인 구현 (처리 시간 < 프레임 간격)
✓ 모니터링 및 알림 시스템
✓ 장애 대응 및 복구 메커니즘참고 자료
- NX Witness 공식 문서: https://www.networkoptix.com/
- ONVIF 프로토콜: https://www.onvif.org/
- RTSP 스트리밍: RFC 2326
- H.264/H.265 비디오 코덱 표준
이 문서는 NX Witness multiple camera 처리에 대한 일반적인 이해를 돕기 위한 교육 자료입니다.
실제 구현 세부사항은 NX Witness 버전과 설정에 따라 다를 수 있습니다.
'Computer Vision' 카테고리의 다른 글
| OpenCV 빌드 정보 조회하는 함수 cv2.getBuildInformation() (0) | 2025.11.12 |
|---|---|
| OpenCV Mat 은 정녕 Color Space를 확인할 수 있는 방법이 없는 것인가... (0) | 2024.02.25 |
| 챗 지피티와 함께하는 색 공간(Color Space) 3D 시각화(Feat. YCbCr or YUV) (1) | 2024.02.07 |
- Total
- Today
- Yesterday
- FairMOT
- 자료구조
- C++ Deploy
- 백트래킹
- 가장 긴 증가하는 부분 수열
- 순열
- 백준 11053
- 백준 1766
- 단축키
- 백준
- 파이참
- MOT
- ㅂ
- Lowest Common Ancestor
- 백준 11437
- 위상 정렬 알고리즘
- cosine
- PyCharm
- 이분탐색
- 조합
- 문제집
- LCA
- 인공지능을 위한 선형대수
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
