1 //
2 // Copyright (c) 2010-2021 Antmicro
3 //
4 // This file is licensed under the MIT License.
5 // Full license text is available in 'licenses/MIT.txt'.
6 //
7 
8 using System;
9 using System.IO;
10 using Antmicro.Renode.Core;
11 using Antmicro.Renode.Utilities;
12 using Antmicro.Renode.Backends.Display;
13 using Antmicro.Renode.Exceptions;
14 using BitMiracle.LibJpeg.Classic;
15 
16 using Antmicro.Migrant;
17 using Antmicro.Migrant.Hooks;
18 
19 namespace Antmicro.Renode.HostInterfaces.Camera
20 {
21     public static class HostCameraExtensions
22     {
AddExternalCamera(this Emulation emulation, string device, string name = R)23         public static void AddExternalCamera(this Emulation emulation, string device, string name = "camera")
24         {
25             var camera = new HostCamera(device);
26 
27             emulation.HostMachine.AddHostMachineElement(camera, name);
28         }
29     }
30 
31     public class HostCamera : IHostMachineElement
32     {
HostCamera(string device)33         public HostCamera(string device)
34         {
35 #if !PLATFORM_LINUX
36             throw new RecoverableException("Host camera integration is currently available on Linux only");
37 #else
38             this.device = device;
39             InitCamera();
40 #endif
41         }
42 
GrabFrame()43         public byte[] GrabFrame()
44         {
45             var frame = VideoCapturer.GrabSingleFrame();
46             if(ForcedScaleDownFactor != 1 || Quality != -1 || cropToSize != null)
47             {
48                 var decompressed = DecompressJpgToRaw(frame);
49                 frame = CompressRawToJpeg(decompressed.Data, decompressed.Width, decompressed.Height, ForcedScaleDownFactor, Quality, cropToSize);
50             }
51 
52             lastFrame = frame;
53             return lastFrame;
54         }
55 
GetLastFrame(bool grab = false)56         public RawImageData GetLastFrame(bool grab = false)
57         {
58             if(grab)
59             {
60                 GrabFrame();
61             }
62 
63             if(lastFrame == null)
64             {
65                 return new RawImageData(new byte[0], 0, 0);
66             }
67 
68             var decompressed = DecompressJpgToRaw(lastFrame);
69             var converter = PixelManipulationTools.GetConverter(PixelFormat.RGB888, ELFSharp.ELF.Endianess.BigEndian, RawImageData.PixelFormat, ELFSharp.ELF.Endianess.BigEndian);
70             var result = new byte[decompressed.Width * decompressed.Height * RawImageData.PixelFormat.GetColorDepth()];
71             converter.Convert(decompressed.Data, ref result);
72 
73             return new RawImageData(result, decompressed.Width, decompressed.Height);
74         }
75 
SaveLastFrame(string path, bool grab = false)76         public void SaveLastFrame(string path, bool grab = false)
77         {
78             if(grab)
79             {
80                 GrabFrame();
81             }
82 
83             if(lastFrame == null)
84             {
85                 throw new RecoverableException("There is no frame to save. Please grab some and try again");
86             }
87 
88             try
89             {
90                 File.WriteAllBytes(path, lastFrame);
91             }
92             catch(Exception e)
93             {
94                 throw new RecoverableException($"There was a problem when saving the last frame: {e.Message}");
95             }
96         }
97 
SetImageSize(int width, int height)98         public void SetImageSize(int width, int height)
99         {
100             var result = VideoCapturer.SetImageSize(width, height);
101             if(result == null)
102             {
103                 throw new RecoverableException("There was an error when setting image size. See log for details");
104             }
105 
106             if(result.Item1 != width || result.Item2 != height)
107             {
108                 // image returned from the video capturer will not match the expected size precisely,
109                 // so we'll need to recompress and crop it manually
110                 cropToSize = Tuple.Create(width, height);
111             }
112             else
113             {
114                 // image returned from the video capturer will be of the expected size,
115                 // so there is no need for manual cropping
116                 cropToSize = null;
117             }
118         }
119 
120         // this is to manually override the quality;
121         // can be used to reduce the size of the returned JPEG image
122         public int Quality { get; set; } = -1;
123 
124         // this is to manually scale the image down;
125         // can be used to reduce the size of the returned JPEG image
126         public int ForcedScaleDownFactor { get; set; } = 1;
127 
128         [PostDeserialization]
InitCamera()129         private void InitCamera()
130         {
131             if(!VideoCapturer.Start(device, this))
132             {
133                 throw new RecoverableException("Couldn't initialize host camera - see logs for details.");
134             }
135         }
136 
137         // the algorithm flow is based on:
138         // https://bitmiracle.github.io/libjpeg.net/help/articles/KB/decompression-details.html
DecompressJpgToRaw(byte[] image)139         private DecompressionResult DecompressJpgToRaw(byte[] image)
140         {
141             var cinfo = new jpeg_decompress_struct(new jpeg_error_mgr());
142 
143             using(var memoryStream = new MemoryStream(image))
144             {
145                 cinfo.jpeg_stdio_src(memoryStream);
146                 cinfo.jpeg_read_header(true);
147 
148                 cinfo.Out_color_space = J_COLOR_SPACE.JCS_RGB;
149 
150                 cinfo.jpeg_start_decompress();
151 
152                 // there are 3 components: R, G, B
153                 var rowStride = 3 * cinfo.Output_width;
154                 var result = new byte[rowStride * cinfo.Output_height];
155                 var resultOffset = 0;
156 
157                 var buffer = new byte[1][];
158                 buffer[0] = new byte[rowStride];
159 
160                 while(cinfo.Output_scanline < cinfo.Output_height)
161                 {
162                     var ct = cinfo.jpeg_read_scanlines(buffer, 1);
163                     if(ct > 0)
164                     {
165                         Array.Copy(buffer[0], 0, result, resultOffset, buffer[0].Length);
166                         resultOffset += buffer[0].Length;
167                     }
168                 }
169 
170                 cinfo.jpeg_finish_decompress();
171                 return new DecompressionResult(result, cinfo.Output_width, cinfo.Output_height);
172             }
173         }
174 
175         // the algorithm flow is based on:
176         // https://bitmiracle.github.io/libjpeg.net/help/articles/KB/compression-details.html
CompressRawToJpeg(byte[] input, int width, int height, int scale, int quality, Tuple<int, int> crop)177         private static byte[] CompressRawToJpeg(byte[] input, int width, int height, int scale, int quality, Tuple<int, int> crop)
178         {
179             jpeg_error_mgr errorManager = new jpeg_error_mgr();
180             jpeg_compress_struct cinfo = new jpeg_compress_struct(errorManager);
181 
182             var memoryStream = new MemoryStream();
183             cinfo.jpeg_stdio_dest(memoryStream);
184 
185             var widthToSkip = 0;
186             var widthToSkipFront = 0;
187             var widthToSkipBack = 0;
188 
189             if(crop != null && width > crop.Item1)
190             {
191                 widthToSkip = (width - crop.Item1);
192                 widthToSkipFront = widthToSkip / 2;
193                 widthToSkipBack = widthToSkip - widthToSkipFront; // to handle odd 'widthToSkip' values
194             }
195 
196             var heightToSkip = 0;
197             var heightToSkipTop = 0;
198 
199             if(crop != null && height > crop.Item2)
200             {
201                 heightToSkip = (height - crop.Item2);
202                 heightToSkipTop = heightToSkip / 2;
203             }
204 
205             cinfo.Image_width = (width - widthToSkip) / scale;
206             cinfo.Image_height = (height - heightToSkip) / scale;
207             cinfo.Input_components = 3;
208             cinfo.In_color_space = J_COLOR_SPACE.JCS_RGB;
209             cinfo.jpeg_set_defaults();
210 
211             if(quality != -1)
212             {
213                 cinfo.jpeg_set_quality(quality, true);
214             }
215 
216             cinfo.jpeg_start_compress(true);
217 
218             int row_stride = cinfo.Image_width * 3; // physical row width in buffer
219             byte[][] rowData = new byte[1][]; // single row
220             rowData[0] = new byte[row_stride];
221 
222             int inputOffset = heightToSkipTop * width;
223 
224             while(cinfo.Next_scanline < cinfo.Image_height)
225             {
226                 // crop pixels at the beginning of the line
227                 inputOffset += 3 * widthToSkipFront;
228 
229                 for(int i = 0; i < rowData[0].Length - 2; i += 3)
230                 {
231                     rowData[0][i] = input[inputOffset];
232                     rowData[0][i + 1] = input[inputOffset + 1];
233                     rowData[0][i + 2] = input[inputOffset + 2];
234 
235                     inputOffset += 3 * scale;
236                 }
237 
238                 // crop pixels at the end of the line
239                 inputOffset += 3 * widthToSkipBack;
240 
241                 // drop some lines due to scaling
242                 inputOffset += 3 * (scale - 1) * width;
243 
244                 cinfo.jpeg_write_scanlines(rowData, 1);
245             }
246 
247             cinfo.jpeg_finish_compress();
248 
249             var result = memoryStream.ToArray();
250             memoryStream.Close();
251             return result;
252         }
253 
254         private byte[] lastFrame;
255         private Tuple<int, int> cropToSize;
256 
257         private readonly string device;
258 
259         private struct DecompressionResult
260         {
DecompressionResultAntmicro.Renode.HostInterfaces.Camera.HostCamera.DecompressionResult261             public DecompressionResult(byte[] data, int width, int height)
262             {
263                 Data = data;
264                 Width = width;
265                 Height = height;
266             }
267 
268             public byte[] Data;
269             public int Width;
270             public int Height;
271         }
272     }
273 }
274