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