반응형

IP 카메라, NVR 등을 연결할 때, 네트워크 IP가 달라 원하는 기기에 직접 접근할 수 없는 경우가 있다. IP 주소를 바꿀 수 없는 기기라면 매번 네트워크 IP를 다시 지정하긴 귀찮으므로 하나의 랜 카드에 두 개의 네트워크 IP를 지정해 보자.

 

랜 카드 이더넷 속성 - 인터넷 프로토콜 버전 4(TCP/IPv4) - 속성을 클릭한다.

 

원하는 고정 IP 주소를 지정한다. DNS 서버는 설정하지 않는다.

현재 사용하는 네트워크 IP는 192.168.0이다. 호스트 IP는 원하는 주소(100)를 지정한다.

DNS 서버를 설정하지 않았기 때문에 url로 인터넷 사이트에 접속할 수 없게 된다.

그리고 '고급(V)...' 버튼을 클릭한다.

 

IP 주소에서 추가 버튼을 클릭하고 원하는 네트워크 주소(192.168.1)와 호스트 주소(82)를 지정한다.

 

이제 다른 네트워크 주소(192.168.1)를 사용하는 NVR(100)에 접속할 수 있다.

 

 

하지만 다른 인터넷 사이트에 접속하기가 어려우므로 가능하다면 NVR의 네트워크 IP(192.168.1)를 내가 사용하는 네트워크 IP(192.168.0)로 바꾸는게 편하다.

 

NVR 네트워크 설정에서 DHCP를 사용하도록 설정한다.

네트워크 IP가 192.168.0으로 바뀐다.

 

 

반응형
Posted by J-sean
:

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

반응형

FFmpeg로 IP Camera에서 스트리밍 되는 영상을 저장해 보자.

 

ffmpeg -rtsp_transport tcp -i rtsp://admin:123456@192.168.0.56:554/stream1 -b:a 4k -t 10 -y output.mp4

 

-rtsp_transport = RTSP 전송 프로토콜 설정.

FFmpeg Protocols Documentation

 

-b:a = audio bitrate. 너무 높으면 아래와 같은 경고가 출력된다. audio sampling frequency(-ar)를 높이거나 bitrate(-b:a or -ab)를 낮춰야 한다.

[aac @ 000001b64b5b7c00] Too many bits 8832.000000 > 6144 per frame requested, clamping to max

 

-t = duration

 

※ 참고

HikVision Camera RTSP Stream

HikVision Camera RTSP with Authentication
rtsp://<username>:<password>@<IP address of device>:<RTSP port>/Streaming/channels/<channel number><stream number>
NOTE: <stream number> represents main stream (01), or the sub stream (02)
Example:
rtsp://192.168.0.100:554/Streaming/channels/101 – get the main stream of the 1st channel
rtsp://192.168.0.100:554/Streaming/channels/102 – get the sub stream of the 1st channel

 

https://stackoverflow.com/questions/56423581/save-rtsp-stream-continuously-into-multi-mp4-files-with-specific-length-10-minu

https://butteryoon.github.io/dev/2019/04/10/using_ffmpeg.html

 

 

FFmpeg Formats Documentation

4.68 segment , stream-segment, ssegment 참고

 

반응형
Posted by J-sean
:
반응형

 

ONVIF 프로토콜을 지원하는 IP 카메라를 사용해 보자.

(아래 내용은 저렴한 IP Camera를 사용한 예이다. HikVision Camera도 같은 코드로 사용 가능 하지만 저렴한 카메라와 다르게 에러나 노이즈가 거의 없다)

 

※ 참고

HikVision Camera RTSP Stream

HikVision FAQ

 

import cv2

#capture = cv2.VideoCapture(0, cv2.CAP_DSHOW)
# 노트북에 연결된 카메라는 대부분 cv2.CAP_DSHOW 옵션이 필요 없지만
# 데스크탑에 USB로 연결된 카메라 사용시에는 cv2.CAP_DSHOW 옵션이 필요 할 수 있다.

#cap = cv2.VideoCapture('rtsp://admin:@192.168.0.44')
# NVR 사용

cap = cv2.VideoCapture('rtsp://admin:123456@192.168.0.161:554/stream0')
# NVR 없이 카메라만 연결 Sub Stream
# 형식: rtsp://[ID]:[PW]@[IP주소]:[PORT번호]/[기타설정]

#cap = cv2.VideoCapture('rtsp://admin:123456@192.168.0.161:554/stream1')
# NVR 없이 카메라만 연결 Main Stream

# 카메라 Configure - Stream manager - Video Setting - Encoding Format 에서
# H265 로 설정하면 아래와 같은 에러 메세지가 발생한다.
# [hevc @ 0000024133eada80] PPS id out of range: 0
# H264 로 설정하면 괜찮다. 특정 카메라에서만 이런 현상이 나타날 수도 있다.

if not cap.isOpened():
    print("Not opened")
    exit()

while cv2.waitKey(1) < 0:
    ret, frame = cap.read()
    if not ret:
        print("False returned")
        exit()
    cv2.imshow("video", frame)

cap.release()
cv2.destroyAllWindows()

 

HikVision Camera RTSP Stream Setting

RTSP without Authentication (NVR/DVR/IPC/Encoder)
rtsp://<IP address of device>:<RTSP port>/Streaming/channels/<channel number><stream number>
RTSP with Authentication
rtsp://<username>:<password>@<IP address of device>:<RTSP port>/Streaming/channels/<channel number><stream number>
NOTE: <stream number> represents main stream (01), or the sub stream (02)
Example:
rtsp://192.168.0.100:554/Streaming/channels/101 – get the main stream of the 1st channel
rtsp://192.168.0.100:554/Streaming/channels/102 – get the sub stream of the 1st channel

 

만약 Sub Stream(Stream0)이 아닌 Main Stream(Stream1)을 사용하면 영상은 큰 문제 없이 계속 출력되지만 아래와 같은 에러 메세지가 계속 나타난다. (Sub Stream에서도 종종 에러가 발생하기도 했다)

 

 

이런 에러 때문인지는 모르겠지만 OpenCV에서 rtsp를 이용해 출력한 영상은 카메라에서 자체 지원하는 웹뷰 등을 이용해 확인한 영상보다 노이즈가 심하다.

 

import queue
import threading
import cv2

q=queue.Queue()
stop = False

def Receive():
    print("start Reveiving")
    
    cap = cv2.VideoCapture('rtsp://admin:123456@192.168.0.161:554/stream0')
    if not cap.isOpened():
        print("Not opened")
        exit()
    
    global stop
    ret = True
    while ret and not stop:
        ret, frame = cap.read()
        lock.acquire()
        if not ret:
            print("False returned")
            continue
        q.put(frame)
        lock.release()
    
    cap.release()
    cv2.destroyAllWindows()

def Display():
     print("Start Displaying")

     global stop
     while cv2.waitKey(1) < 0:
         lock.acquire()
         if not q.empty():
             frame=q.get()
             cv2.imshow("stream", frame)
         lock.release()
     stop = True

if __name__ == '__main__':
    lock = threading.Lock()
    
    t1 = threading.Thread(target = Receive)
    t2 = threading.Thread(target = Display)
    t1.start()
   t2.start()

 

영상을 받아오는 부분과 출력하는 부분을 다른 스레드로 구분하고 각각의 스레드에서 영상에 접근할때 충돌을 방지하기 위해 Primitive Lock을 사용해 봤지만 별 효과는 없다.

오히려 Main Stream(Stream1)에서 Sub Stream(Stream0)으로 바꿔 영상의 크기를 줄이는게 에러 확률을 크게 낮추었다.

 

 

카메라 Video Setting 에서 위와 같이 바꾸는 것도 에러 확률을 크게 낮추었다.

● Resolution: 1920X1080 => 1280X720

● Quality: Good => Worst 

Frame Rate, Max Bitrate는 별 영향이 없었다.

 

Packet Loss가 있어서 그런건지도 모르겠다.

stackoverflow

 

반응형
Posted by J-sean
:
반응형

C#에서 OpenCV Mat 데이터를 네트워크로 송수신 할 수 있도록 준비하는 과정을 시뮬레이션 해 보자.

아래 링크의 글에서 비트맵이 아닌 OpenCV Mat 데이터 송수신 과정이라 보면 된다.

2021.12.25 - [C#] - C# TCP/IP Image transfer - 이미지(파일) 전송 3

 

폼에 PictureBox, Button을 적당히 배치한다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
 
using OpenCvSharp;
using System.IO;
 
namespace OpenCV
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
 
            pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage;
        }
 
        private void button1_Click(object sender, EventArgs e)
        {
            // 클라이언트 시뮬레이션
            // OpenCV Matrix를 생성하고 바이트 배열로 변환한다.
            Mat clientImage = new Mat("Barbara.jpg");
            byte[] data = clientImage.ToBytes(".jpg"); // ".jpg", ".png", ".bmp"
            //MemoryStream clientMemoryStream = clientImage.ToMemoryStream();
            //byte[] data = clientMemoryStream.ToArray();
 
            // 네트워크 시뮬레이션
            // ...
            // 클라이언트에서 OpenCV Matrix 바이트 배열(data)을 서버로 전송
 
            // 서버 시뮬레이션
            // 클라이언트에서 받은 바이트 배열(data)을 메모리 스트림으로
            // 변환 후 다시 비트맵으로 변환한다.
            MemoryStream serverMemoryStream = new MemoryStream(data);
            Bitmap bitmap = new Bitmap(serverMemoryStream);
            //Mat serverImage = OpenCvSharp.Extensions.BitmapConverter.ToMat(bitmap);
            //Cv2.ImShow("Server Image", serverImage);
            pictureBox1.Image = bitmap;
        }
    }
}
 

 

클라이언트에서 Mat.ToBytes()가 핵심이다. (메모리 스트림으로 변환할 필요가 없다)

소스를 입력하고 빌드한다.

 

실행하면 버튼만 나타난다.

 

버튼을 클릭하면 이미지가 표시된다.

 

반응형
Posted by J-sean
:
반응형

2021.12.23 - [C#] - C# TCP/IP Image transfer - 이미지(파일) 전송 1

2021.12.24 - [C#] - C# TCP/IP Image transfer - 이미지(파일) 전송 2

 

위 두 링크에서 만든 프로그램을 개선해 클라이언트의 화면을 서버로 연속 전송, 영상으로 재생해 보자.

간단한 화면공유(Screen Share) 프로그램을 만드는 것이다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
 
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.IO;
 
namespace Server
{
    public partial class Form1 : Form
    {
        TcpListener listener;
        TcpClient client;
        NetworkStream networkStream;
        MemoryStream memoryStream;
        Bitmap bitmap;
        IPHostEntry ipHostEntry;
        Thread thread;
 
        string serverIP;
        int serverPort;
        byte[] data;
        byte[] dataSizeFromClient;
        int receivedDataSize;
        int expectedDataSize;
 
        public Form1()
        {
            InitializeComponent();
 
            pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage;
 
            serverPort = 7000;
            // 호스트 이름으로 검색되는 첫 번째 IP4 주소 확인
            string hostName = Dns.GetHostName();
            ipHostEntry = Dns.GetHostEntry(hostName);
            foreach (IPAddress address in ipHostEntry.AddressList)
            {
                if (address.AddressFamily == AddressFamily.InterNetwork)
                {
                    serverIP = address.ToString();
                    break;
                }
            }
            listBox1.Items.Add("Server IP: " + serverIP);
 
            data = new byte[1048576 * 10]; // 1MB * 10
                                           // 클라이언트로 부터 전송되는 PNG 파일은 상황에 따라 4MB를 넘기도 한다.
            dataSizeFromClient = new byte[sizeof(int)];
        }
 
        private void ThreadProc()
        {
            listener = new TcpListener(IPAddress.Any, serverPort);
            //listener = new TcpListener(IPAddress.Parse("127.0.0.1"), serverPort);
            listener.Start();
 
            client = listener.AcceptTcpClient();
            listBox1.Items.Add("Client IP: " + client.Client.RemoteEndPoint.ToString().Split(':')[0]);
            networkStream = client.GetStream();
 
            while (true)
            {
                try
                {
                    receivedDataSize = 0;
 
                    if (networkStream.CanRead)
                    {
                        // 클라이언트로 부터 받아야 할 데이터 사이즈 정보 확인.
                        networkStream.Read(dataSizeFromClient, 0, dataSizeFromClient.Length);
                        expectedDataSize = BitConverter.ToInt32(dataSizeFromClient, 0);
                        //listBox1.Items.Add("Expected data size: " + (expectedDataSize / 1024).ToString() + "KB");
 
                        // 데이터 수신.                        
                        do
                        {
                            // 클라이언트로 부터 받아야 할 데이터 사이즈 만큼만 정확히 받는다.
                            receivedDataSize += networkStream.Read(data, receivedDataSize, expectedDataSize - receivedDataSize);
                            // Reads data from the NetworkStream and stores it to a byte array.
                        } while (expectedDataSize != receivedDataSize);
                        //while (networkStream.DataAvailable);
                        // NetworkStream.DataAvailable 은 네트워크 상황에 따라 정확하지 않을 가능성이 크다.
                        
                        //listBox1.Items.Add("Data size: " + (receivedDataSize / 1024).ToString() + "KB");
                        // 클라이언트로 부터 받은 데이터 사이즈가 버퍼 사이즈(10MB) 보다 크다면 버퍼 사이즈를 늘려야 한다.
                        // 그렇지 않으면 NetworkStream.Read()에서 ArgumentOutOfRangeException 예외 발생.
                    }
 
                    //listBox1.Items.Add("Data received: " + (receivedDataSize / 1024).ToString() + "KB");
                    memoryStream = new MemoryStream(data, 0, receivedDataSize);
                    // Initializes a new non-resizable instance of the MemoryStream class
                    // based on the specified region (index) of a byte array.            
                    bitmap = new Bitmap(memoryStream);
                    pictureBox1.Image = bitmap;
                }
                catch (Exception e)
                {
                    listBox1.Items.Add(e.Message);
                    listBox1.Items.Add(e.StackTrace);
                    break;
                }
            }
 
            listener.Stop();
            client.Close();
            networkStream.Close();
            memoryStream.Close();
        }
 
 
        private void button1_Click(object sender, EventArgs e)
        {
            // 클라이언트 접속 대기를 위한 스레드 생성(스레드는 1개만 생성한다)
            if (thread == null || !thread.IsAlive)
            {
                thread = new Thread(new ThreadStart(ThreadProc));
                thread.Start();
                listBox1.Items.Add("Waiting for a client...");
            }
        }
 
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (thread != null && thread.IsAlive)
            {
                // TcpListener를 정지 시키지 않고 클라이언트 연결 대기시 프로그램을
                // 종료하려 하면 스레드가 종료되지 않아 프로그램이 종료되지 않는다.
                listener.Stop();
 
                // 클라이언트와 연결된 스레드를 종료하지 않으면 프로그램을 종료해도
                // 백그라운드에서 계속 실행된다.                
                thread.Abort();
            }
        }
    }
}
 

 

서버 코드를 입력하고 빌드한다.

Server.zip
0.00MB

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
 
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.IO;
 
namespace Client
{
    public partial class Form1 : Form
    {
        TcpClient client;
        NetworkStream networkStream;
        MemoryStream memoryStream;
        Bitmap screen;
        Thread thread;
 
        string serverIP;
        int serverPort;
        byte[] data;
        byte[] dataSizeForServer;
 
        public Form1()
        {
            InitializeComponent();
 
            pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage;
            textBox1.Text = "192.168.0.100";
        }
 
        private void ThreadProc()
        {
            serverIP = textBox1.Text;
            serverPort = 7000;
            try
            {
                client = new TcpClient(serverIP, serverPort);
                networkStream = client.GetStream();
                listBox1.Items.Add("Connected to: " + client.Client.RemoteEndPoint.ToString().Split(':')[0]);
            }
            catch (Exception e)
            {
                listBox1.Items.Add(e.Message);
                listBox1.Items.Add(e.StackTrace);
 
                return;
            }
 
            try
            {
                while (true)
                {
                    screen = GetScreen();
                    memoryStream = new MemoryStream();
                    screen.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
                    data = memoryStream.ToArray();
                    pictureBox1.Image = screen;
 
                    if (networkStream.CanWrite)
                    {
                        // 보낼 데이터 사이즈를 서버에 미리 공유
                        dataSizeForServer = BitConverter.GetBytes(data.Length);
                        networkStream.Write(dataSizeForServer, 0, dataSizeForServer.Length);
 
                        // 데이터 송신
                        networkStream.Write(data, 0, data.Length);
                        //listBox1.Items.Add("Data sent: " + (data.Length / 1024).ToString() + "KB");
                    }
 
                    // Thread.Sleep()을 삭제하면 더 부드러운 화면을 볼 수 있다.
                    // 하지만 CPU 점유율이 크게 높아진다. 적절히 조정하자.
                    Thread.Sleep(100);
                }
            }
            catch (Exception e)
            {
                listBox1.Items.Add(e.Message);
                listBox1.Items.Add(e.StackTrace);
            }
 
            client.Close();
            networkStream.Close();
            memoryStream.Close();
        }
 
        private void button1_Click(object sender, EventArgs e)
        {
            // 클라이언트 접속 대기를 위한 스레드 생성(스레드는 1개만 생성한다)
            if (thread == null || !thread.IsAlive)
            {
                thread = new Thread(new ThreadStart(ThreadProc));
                thread.Start();
                listBox1.Items.Add("Connecting to the server...");
            }
        }
 
        private Bitmap GetScreen()
        {
            Bitmap bitmap = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
            Graphics g = Graphics.FromImage(bitmap);
            g.CopyFromScreen(0000, bitmap.Size);
            g.Dispose();
 
            return bitmap;
        }
 
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (thread != null && thread.IsAlive)
            {
                // 스레드를 종료하지 않으면 백그라운드에서 계속 실행된다.
                thread.Abort();
            }
        }
    }
}
 

 

클라이언트 코드를 입력하고 빌드한다.

Client.zip
0.01MB

 

한 컴퓨터에서 서버, 클라이언트를 모두 실행한 화면

 

 

NetworkStream.DataAvailable 프로퍼티는 네트워크 상황에 따라 부정확한 경우가 많다. 그러므로 서버에서는 클라이언트로 부터 받을 데이터의 사이즈를 미리 확인하고 그 사이즈만큼 정확히 받는게 중요하다.

작은 크기의 데이터는 사이즈 정보 교환없이 한번에 송수신해도 별 문제는 없다. 하지만 이 프로그램처럼 수백 KB의 데이터를 주고 받는 경우 한번에 송수신이 되지 않을 가능성이 크다.

 

클라이언트에서는 서버로 데이터를 전송하기 전, 보낼 데이터 사이즈를 미리 공유해 서버에서 수신 준비할 수 있도록 한다.

MemoryStream으로 저장할 데이터 포맷을 ImageFormat.Jpeg로 바꾸면 ImageFormat.Png에 비해 훨씬 작은 사이즈의 데이터가 만들어지고 영상의 FPS를 향상시킬 수 있다. (PNG는 비손실 압축, JPEG는 손실 압축)

 

반응형
Posted by J-sean
: