Point Cloud Library (PCL)  1.12.0-dev
organized_pointcloud_compression.hpp
1 /*
2  * Software License Agreement (BSD License)
3  *
4  * Point Cloud Library (PCL) - www.pointclouds.org
5  * Copyright (c) 2009-2012, Willow Garage, Inc.
6  *
7  * All rights reserved.
8  *
9  * Redistribution and use in source and binary forms, with or without
10  * modification, are permitted provided that the following conditions
11  * are met:
12  *
13  * * Redistributions of source code must retain the above copyright
14  * notice, this list of conditions and the following disclaimer.
15  * * Redistributions in binary form must reproduce the above
16  * copyright notice, this list of conditions and the following
17  * disclaimer in the documentation and/or other materials provided
18  * with the distribution.
19  * * Neither the name of Willow Garage, Inc. nor the names of its
20  * contributors may be used to endorse or promote products derived
21  * from this software without specific prior written permission.
22  *
23  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
26  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
27  * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
28  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
29  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
30  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
32  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
33  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34  * POSSIBILITY OF SUCH DAMAGE.
35  *
36  */
37 
38 #ifndef ORGANIZED_COMPRESSION_HPP
39 #define ORGANIZED_COMPRESSION_HPP
40 
41 #include <pcl/compression/organized_pointcloud_compression.h>
42 
43 #include <pcl/pcl_macros.h>
44 #include <pcl/point_cloud.h>
45 
46 #include <pcl/compression/libpng_wrapper.h>
47 #include <pcl/compression/organized_pointcloud_conversion.h>
48 
49 #include <vector>
50 #include <cassert>
51 
52 namespace pcl
53 {
54  namespace io
55  {
56  //////////////////////////////////////////////////////////////////////////////////////////////
57  template<typename PointT> void
59  std::ostream& compressedDataOut_arg,
60  bool doColorEncoding,
61  bool convertToMono,
62  bool bShowStatistics_arg,
63  int pngLevel_arg)
64  {
65  std::uint32_t cloud_width = cloud_arg->width;
66  std::uint32_t cloud_height = cloud_arg->height;
67 
68  float maxDepth, focalLength, disparityShift, disparityScale;
69 
70  // no disparity scaling/shifting required during decoding
71  disparityScale = 1.0f;
72  disparityShift = 0.0f;
73 
74  analyzeOrganizedCloud (cloud_arg, maxDepth, focalLength);
75 
76  // encode header identifier
77  compressedDataOut_arg.write (reinterpret_cast<const char*> (frameHeaderIdentifier_), strlen (frameHeaderIdentifier_));
78  // encode point cloud width
79  compressedDataOut_arg.write (reinterpret_cast<const char*> (&cloud_width), sizeof (cloud_width));
80  // encode frame type height
81  compressedDataOut_arg.write (reinterpret_cast<const char*> (&cloud_height), sizeof (cloud_height));
82  // encode frame max depth
83  compressedDataOut_arg.write (reinterpret_cast<const char*> (&maxDepth), sizeof (maxDepth));
84  // encode frame focal length
85  compressedDataOut_arg.write (reinterpret_cast<const char*> (&focalLength), sizeof (focalLength));
86  // encode frame disparity scale
87  compressedDataOut_arg.write (reinterpret_cast<const char*> (&disparityScale), sizeof (disparityScale));
88  // encode frame disparity shift
89  compressedDataOut_arg.write (reinterpret_cast<const char*> (&disparityShift), sizeof (disparityShift));
90 
91  // disparity and rgb image data
92  std::vector<std::uint16_t> disparityData;
93  std::vector<std::uint8_t> colorData;
94 
95  // compressed disparity and rgb image data
96  std::vector<std::uint8_t> compressedDisparity;
97  std::vector<std::uint8_t> compressedColor;
98 
99  std::uint32_t compressedDisparitySize = 0;
100  std::uint32_t compressedColorSize = 0;
101 
102  // Convert point cloud to disparity and rgb image
103  OrganizedConversion<PointT>::convert (*cloud_arg, focalLength, disparityShift, disparityScale, convertToMono, disparityData, colorData);
104 
105  // Compress disparity information
106  encodeMonoImageToPNG (disparityData, cloud_width, cloud_height, compressedDisparity, pngLevel_arg);
107 
108  compressedDisparitySize = static_cast<std::uint32_t>(compressedDisparity.size());
109  // Encode size of compressed disparity image data
110  compressedDataOut_arg.write (reinterpret_cast<const char*> (&compressedDisparitySize), sizeof (compressedDisparitySize));
111  // Output compressed disparity to ostream
112  compressedDataOut_arg.write (reinterpret_cast<const char*> (&compressedDisparity[0]), compressedDisparity.size () * sizeof(std::uint8_t));
113 
114  // Compress color information
115  if (CompressionPointTraits<PointT>::hasColor && doColorEncoding)
116  {
117  if (convertToMono)
118  {
119  encodeMonoImageToPNG (colorData, cloud_width, cloud_height, compressedColor, 1 /*Z_BEST_SPEED*/);
120  } else
121  {
122  encodeRGBImageToPNG (colorData, cloud_width, cloud_height, compressedColor, 1 /*Z_BEST_SPEED*/);
123  }
124  }
125 
126  compressedColorSize = static_cast<std::uint32_t>(compressedColor.size ());
127  // Encode size of compressed Color image data
128  compressedDataOut_arg.write (reinterpret_cast<const char*> (&compressedColorSize), sizeof (compressedColorSize));
129  // Output compressed disparity to ostream
130  compressedDataOut_arg.write (reinterpret_cast<const char*> (&compressedColor[0]), compressedColor.size () * sizeof(std::uint8_t));
131 
132  if (bShowStatistics_arg)
133  {
134  std::uint64_t pointCount = cloud_width * cloud_height;
135  float bytesPerPoint = static_cast<float> (compressedDisparitySize+compressedColorSize) / static_cast<float> (pointCount);
136 
137  PCL_INFO("*** POINTCLOUD ENCODING ***\n");
138  PCL_INFO("Number of encoded points: %ld\n", pointCount);
139  PCL_INFO("Size of uncompressed point cloud: %.2f kBytes\n", (static_cast<float> (pointCount) * CompressionPointTraits<PointT>::bytesPerPoint) / 1024.0f);
140  PCL_INFO("Size of compressed point cloud: %.2f kBytes\n", static_cast<float> (compressedDisparitySize+compressedColorSize) / 1024.0f);
141  PCL_INFO("Total bytes per point: %.4f bytes\n", static_cast<float> (bytesPerPoint));
142  PCL_INFO("Total compression percentage: %.4f%%\n", (bytesPerPoint) / (CompressionPointTraits<PointT>::bytesPerPoint) * 100.0f);
143  PCL_INFO("Compression ratio: %.2f\n\n", static_cast<float> (CompressionPointTraits<PointT>::bytesPerPoint) / bytesPerPoint);
144  }
145 
146  // flush output stream
147  compressedDataOut_arg.flush();
148  }
149 
150  //////////////////////////////////////////////////////////////////////////////////////////////
151  template<typename PointT> void
153  std::vector<std::uint8_t>& colorImage_arg,
154  std::uint32_t width_arg,
155  std::uint32_t height_arg,
156  std::ostream& compressedDataOut_arg,
157  bool doColorEncoding,
158  bool convertToMono,
159  bool bShowStatistics_arg,
160  int pngLevel_arg,
161  float focalLength_arg,
162  float disparityShift_arg,
163  float disparityScale_arg)
164  {
165  float maxDepth = -1;
166 
167  std::size_t cloud_size = width_arg*height_arg;
168  assert (disparityMap_arg.size()==cloud_size);
169  if (!colorImage_arg.empty ())
170  {
171  assert (colorImage_arg.size()==cloud_size*3);
172  }
173 
174  // encode header identifier
175  compressedDataOut_arg.write (reinterpret_cast<const char*> (frameHeaderIdentifier_), strlen (frameHeaderIdentifier_));
176  // encode point cloud width
177  compressedDataOut_arg.write (reinterpret_cast<const char*> (&width_arg), sizeof (width_arg));
178  // encode frame type height
179  compressedDataOut_arg.write (reinterpret_cast<const char*> (&height_arg), sizeof (height_arg));
180  // encode frame max depth
181  compressedDataOut_arg.write (reinterpret_cast<const char*> (&maxDepth), sizeof (maxDepth));
182  // encode frame focal length
183  compressedDataOut_arg.write (reinterpret_cast<const char*> (&focalLength_arg), sizeof (focalLength_arg));
184  // encode frame disparity scale
185  compressedDataOut_arg.write (reinterpret_cast<const char*> (&disparityScale_arg), sizeof (disparityScale_arg));
186  // encode frame disparity shift
187  compressedDataOut_arg.write (reinterpret_cast<const char*> (&disparityShift_arg), sizeof (disparityShift_arg));
188 
189  // compressed disparity and rgb image data
190  std::vector<std::uint8_t> compressedDisparity;
191  std::vector<std::uint8_t> compressedColor;
192 
193  std::uint32_t compressedDisparitySize = 0;
194  std::uint32_t compressedColorSize = 0;
195 
196  // Remove color information of invalid points
197  std::uint16_t* depth_ptr = &disparityMap_arg[0];
198  std::uint8_t* color_ptr = &colorImage_arg[0];
199 
200  for (std::size_t i = 0; i < cloud_size; ++i, ++depth_ptr, color_ptr += sizeof(std::uint8_t) * 3)
201  {
202  if (!(*depth_ptr) || (*depth_ptr==0x7FF))
203  memset(color_ptr, 0, sizeof(std::uint8_t)*3);
204  }
205 
206  // Compress disparity information
207  encodeMonoImageToPNG (disparityMap_arg, width_arg, height_arg, compressedDisparity, pngLevel_arg);
208 
209  compressedDisparitySize = static_cast<std::uint32_t>(compressedDisparity.size());
210  // Encode size of compressed disparity image data
211  compressedDataOut_arg.write (reinterpret_cast<const char*> (&compressedDisparitySize), sizeof (compressedDisparitySize));
212  // Output compressed disparity to ostream
213  compressedDataOut_arg.write (reinterpret_cast<const char*> (&compressedDisparity[0]), compressedDisparity.size () * sizeof(std::uint8_t));
214 
215  // Compress color information
216  if (!colorImage_arg.empty () && doColorEncoding)
217  {
218  if (convertToMono)
219  {
220  std::vector<std::uint8_t> monoImage;
221  std::size_t size = width_arg*height_arg;
222 
223  monoImage.reserve(size);
224 
225  // grayscale conversion
226  for (std::size_t i = 0; i < size; ++i)
227  {
228  std::uint8_t grayvalue = static_cast<std::uint8_t>(0.2989 * static_cast<float>(colorImage_arg[i*3+0]) +
229  0.5870 * static_cast<float>(colorImage_arg[i*3+1]) +
230  0.1140 * static_cast<float>(colorImage_arg[i*3+2]));
231  monoImage.push_back(grayvalue);
232  }
233  encodeMonoImageToPNG (monoImage, width_arg, height_arg, compressedColor, 1 /*Z_BEST_SPEED*/);
234 
235  } else
236  {
237  encodeRGBImageToPNG (colorImage_arg, width_arg, height_arg, compressedColor, 1 /*Z_BEST_SPEED*/);
238  }
239 
240  }
241 
242  compressedColorSize = static_cast<std::uint32_t>(compressedColor.size ());
243  // Encode size of compressed Color image data
244  compressedDataOut_arg.write (reinterpret_cast<const char*> (&compressedColorSize), sizeof (compressedColorSize));
245  // Output compressed disparity to ostream
246  compressedDataOut_arg.write (reinterpret_cast<const char*> (&compressedColor[0]), compressedColor.size () * sizeof(std::uint8_t));
247 
248  if (bShowStatistics_arg)
249  {
250  std::uint64_t pointCount = width_arg * height_arg;
251  float bytesPerPoint = static_cast<float> (compressedDisparitySize+compressedColorSize) / static_cast<float> (pointCount);
252 
253  PCL_INFO("*** POINTCLOUD ENCODING ***\n");
254  PCL_INFO("Number of encoded points: %ld\n", pointCount);
255  PCL_INFO("Size of uncompressed disparity map+color image: %.2f kBytes\n", (static_cast<float> (pointCount) * (sizeof(std::uint8_t)*3+sizeof(std::uint16_t))) / 1024.0f);
256  PCL_INFO("Size of compressed point cloud: %.2f kBytes\n", static_cast<float> (compressedDisparitySize+compressedColorSize) / 1024.0f);
257  PCL_INFO("Total bytes per point: %.4f bytes\n", static_cast<float> (bytesPerPoint));
258  PCL_INFO("Total compression percentage: %.4f%%\n", (bytesPerPoint) / (sizeof(std::uint8_t)*3+sizeof(std::uint16_t)) * 100.0f);
259  PCL_INFO("Compression ratio: %.2f\n\n", static_cast<float> (CompressionPointTraits<PointT>::bytesPerPoint) / bytesPerPoint);
260  }
261 
262  // flush output stream
263  compressedDataOut_arg.flush();
264  }
265 
266  //////////////////////////////////////////////////////////////////////////////////////////////
267  template<typename PointT> bool
268  OrganizedPointCloudCompression<PointT>::decodePointCloud (std::istream& compressedDataIn_arg,
269  PointCloudPtr &cloud_arg,
270  bool bShowStatistics_arg)
271  {
272  std::uint32_t cloud_width;
273  std::uint32_t cloud_height;
274  float maxDepth;
275  float focalLength;
276  float disparityShift = 0.0f;
277  float disparityScale;
278 
279  // disparity and rgb image data
280  std::vector<std::uint16_t> disparityData;
281  std::vector<std::uint8_t> colorData;
282 
283  // compressed disparity and rgb image data
284  std::vector<std::uint8_t> compressedDisparity;
285  std::vector<std::uint8_t> compressedColor;
286 
287  std::uint32_t compressedDisparitySize;
288  std::uint32_t compressedColorSize;
289 
290  // PNG decoded parameters
291  unsigned int png_channels = 1;
292 
293  // sync to frame header
294  unsigned int headerIdPos = 0;
295  bool valid_stream = true;
296  while (valid_stream && (headerIdPos < strlen (frameHeaderIdentifier_)))
297  {
298  char readChar;
299  compressedDataIn_arg.read (static_cast<char*> (&readChar), sizeof (readChar));
300  if (compressedDataIn_arg.gcount()!= sizeof (readChar))
301  valid_stream = false;
302  if (readChar != frameHeaderIdentifier_[headerIdPos++])
303  headerIdPos = (frameHeaderIdentifier_[0] == readChar) ? 1 : 0;
304 
305  valid_stream &= compressedDataIn_arg.good ();
306  }
307 
308  if (valid_stream) {
309 
310  //////////////
311  // reading frame header
312  compressedDataIn_arg.read (reinterpret_cast<char*> (&cloud_width), sizeof (cloud_width));
313  compressedDataIn_arg.read (reinterpret_cast<char*> (&cloud_height), sizeof (cloud_height));
314  compressedDataIn_arg.read (reinterpret_cast<char*> (&maxDepth), sizeof (maxDepth));
315  compressedDataIn_arg.read (reinterpret_cast<char*> (&focalLength), sizeof (focalLength));
316  compressedDataIn_arg.read (reinterpret_cast<char*> (&disparityScale), sizeof (disparityScale));
317  compressedDataIn_arg.read (reinterpret_cast<char*> (&disparityShift), sizeof (disparityShift));
318 
319  // reading compressed disparity data
320  compressedDataIn_arg.read (reinterpret_cast<char*> (&compressedDisparitySize), sizeof (compressedDisparitySize));
321  compressedDisparity.resize (compressedDisparitySize);
322  compressedDataIn_arg.read (reinterpret_cast<char*> (&compressedDisparity[0]), compressedDisparitySize * sizeof(std::uint8_t));
323 
324  // reading compressed rgb data
325  compressedDataIn_arg.read (reinterpret_cast<char*> (&compressedColorSize), sizeof (compressedColorSize));
326  compressedColor.resize (compressedColorSize);
327  compressedDataIn_arg.read (reinterpret_cast<char*> (&compressedColor[0]), compressedColorSize * sizeof(std::uint8_t));
328 
329  std::size_t png_width = 0;
330  std::size_t png_height = 0;
331 
332  // decode PNG compressed disparity data
333  decodePNGToImage (compressedDisparity, disparityData, png_width, png_height, png_channels);
334 
335  // decode PNG compressed rgb data
336  decodePNGToImage (compressedColor, colorData, png_width, png_height, png_channels);
337  }
338 
339  if (disparityShift==0.0f)
340  {
341  // reconstruct point cloud
343  colorData,
344  (png_channels == 1),
345  cloud_width,
346  cloud_height,
347  focalLength,
348  disparityShift,
349  disparityScale,
350  *cloud_arg);
351  } else
352  {
353 
354  // we need to decode a raw shift image
355  std::size_t size = disparityData.size();
356  std::vector<float> depthData;
357  depthData.resize(size);
358 
359  // initialize shift-to-depth converter
360  if (!sd_converter_.isInitialized())
361  sd_converter_.generateLookupTable();
362 
363  // convert shift to depth image
364  for (std::size_t i=0; i<size; ++i)
365  depthData[i] = sd_converter_.shiftToDepth(disparityData[i]);
366 
367  // reconstruct point cloud
369  colorData,
370  static_cast<bool>(png_channels==1),
371  cloud_width,
372  cloud_height,
373  focalLength,
374  *cloud_arg);
375  }
376 
377  if (bShowStatistics_arg)
378  {
379  std::uint64_t pointCount = cloud_width * cloud_height;
380  float bytesPerPoint = static_cast<float> (compressedDisparitySize+compressedColorSize) / static_cast<float> (pointCount);
381 
382  PCL_INFO("*** POINTCLOUD DECODING ***\n");
383  PCL_INFO("Number of encoded points: %ld\n", pointCount);
384  PCL_INFO("Size of uncompressed point cloud: %.2f kBytes\n", (static_cast<float> (pointCount) * CompressionPointTraits<PointT>::bytesPerPoint) / 1024.0f);
385  PCL_INFO("Size of compressed point cloud: %.2f kBytes\n", static_cast<float> (compressedDisparitySize+compressedColorSize) / 1024.0f);
386  PCL_INFO("Total bytes per point: %.4f bytes\n", static_cast<float> (bytesPerPoint));
387  PCL_INFO("Total compression percentage: %.4f%%\n", (bytesPerPoint) / (CompressionPointTraits<PointT>::bytesPerPoint) * 100.0f);
388  PCL_INFO("Compression ratio: %.2f\n\n", static_cast<float> (CompressionPointTraits<PointT>::bytesPerPoint) / bytesPerPoint);
389  }
390 
391  return valid_stream;
392  }
393 
394  //////////////////////////////////////////////////////////////////////////////////////////////
395  template<typename PointT> void
397  float& maxDepth_arg,
398  float& focalLength_arg) const
399  {
400  std::size_t width = cloud_arg->width;
401  std::size_t height = cloud_arg->height;
402 
403  // Center of organized point cloud
404  int centerX = static_cast<int> (width / 2);
405  int centerY = static_cast<int> (height / 2);
406 
407  // Ensure we have an organized point cloud
408  assert((width>1) && (height>1));
409  assert(width*height == cloud_arg->size());
410 
411  float maxDepth = 0;
412  float focalLength = 0;
413 
414  std::size_t it = 0;
415  for (int y = -centerY; y < centerY; ++y )
416  for (int x = -centerX; x < centerX; ++x )
417  {
418  const PointT& point = (*cloud_arg)[it++];
419 
420  if (pcl::isFinite (point))
421  {
422  if (maxDepth < point.z)
423  {
424  // Update maximum depth
425  maxDepth = point.z;
426 
427  // Calculate focal length
428  focalLength = 2.0f / (point.x / (static_cast<float> (x) * point.z) + point.y / (static_cast<float> (y) * point.z));
429  }
430  }
431  }
432 
433  // Update return values
434  maxDepth_arg = maxDepth;
435  focalLength_arg = focalLength;
436  }
437 
438  }
439 }
440 
441 #endif
442 
pcl_macros.h
Defines all the PCL and non-PCL macros used.
pcl
Definition: convolution.h:46
pcl::io::CompressionPointTraits
Definition: organized_pointcloud_conversion.h:55
pcl::isFinite
bool isFinite(const PointT &pt)
Tests if the 3D components of a point are all finite param[in] pt point to be tested return true if f...
Definition: point_tests.h:55
pcl::io::OrganizedPointCloudCompression::PointCloudConstPtr
typename PointCloud::ConstPtr PointCloudConstPtr
Definition: organized_pointcloud_compression.h:60
pcl::io::encodeRGBImageToPNG
PCL_EXPORTS void encodeRGBImageToPNG(std::vector< std::uint8_t > &image_arg, std::size_t width_arg, std::size_t height_arg, std::vector< std::uint8_t > &pngData_arg, int png_level_arg=-1)
Encodes 8-bit RGB image to PNG format.
pcl::PointXYZRGB
A point structure representing Euclidean xyz coordinates, and the RGB color.
Definition: point_types.hpp:674
pcl::io::decodePNGToImage
PCL_EXPORTS void decodePNGToImage(std::vector< std::uint8_t > &pngData_arg, std::vector< std::uint8_t > &imageData_arg, std::size_t &width_arg, std::size_t &heigh_argt, unsigned int &channels_arg)
Decode compressed PNG to 8-bit image.
pcl::io::OrganizedPointCloudCompression::decodePointCloud
bool decodePointCloud(std::istream &compressedDataIn_arg, PointCloudPtr &cloud_arg, bool bShowStatistics_arg=true)
Decode point cloud from input stream.
Definition: organized_pointcloud_compression.hpp:268
pcl::io::OrganizedPointCloudCompression::encodePointCloud
void encodePointCloud(const PointCloudConstPtr &cloud_arg, std::ostream &compressedDataOut_arg, bool doColorEncoding=false, bool convertToMono=false, bool bShowStatistics_arg=true, int pngLevel_arg=-1)
Encode point cloud to output stream.
Definition: organized_pointcloud_compression.hpp:58
pcl::io::OrganizedPointCloudCompression::encodeRawDisparityMapWithColorImage
void encodeRawDisparityMapWithColorImage(std::vector< std::uint16_t > &disparityMap_arg, std::vector< std::uint8_t > &colorImage_arg, std::uint32_t width_arg, std::uint32_t height_arg, std::ostream &compressedDataOut_arg, bool doColorEncoding=false, bool convertToMono=false, bool bShowStatistics_arg=true, int pngLevel_arg=-1, float focalLength_arg=525.0f, float disparityShift_arg=174.825f, float disparityScale_arg=-0.161175f)
Encode raw disparity map and color image.
Definition: organized_pointcloud_compression.hpp:152
pcl::io::encodeMonoImageToPNG
PCL_EXPORTS void encodeMonoImageToPNG(std::vector< std::uint8_t > &image_arg, std::size_t width_arg, std::size_t height_arg, std::vector< std::uint8_t > &pngData_arg, int png_level_arg=-1)
Encodes 8-bit mono image to PNG format.
pcl::io::OrganizedPointCloudCompression::PointCloudPtr
typename PointCloud::Ptr PointCloudPtr
Definition: organized_pointcloud_compression.h:59
pcl::io::OrganizedPointCloudCompression::analyzeOrganizedCloud
void analyzeOrganizedCloud(PointCloudConstPtr cloud_arg, float &maxDepth_arg, float &focalLength_arg) const
Analyze input point cloud and calculate the maximum depth and focal length.
Definition: organized_pointcloud_compression.hpp:396
pcl::io::OrganizedConversion
Definition: organized_pointcloud_conversion.h:79