【问题标题】:Image compression with minimal loss of quality图像压缩,质量损失最小
【发布时间】:2018-08-24 19:40:06
【问题描述】:

值得澄清的是,对于编程语言 Swift 及其所有组件,我已经了解了三天! 三天我尝试解决这个问题,我没有找到一个单一的解决方案,我翻遍了很多网站!我需要确保上传到服务器时的图像很有趣,不超过 150 kb,但同时要保证质量好。 在搜索过程中,我发现了一种算法,在网站和 GitHub 上,他们写道,它是 WhatsApp messenger 图像压缩的副本,但在 iphone 8 上它不能很好地压缩图像。举个例子,iphone 8上拍照的图片在传输到Messenger WhatsApp时压缩到100kb,而图片质量非常好,长宽都超过1000px。如果你使用这个算法,那么在长度为:800px、宽度为:600px、压缩比为 0.01 的情况下,图像的重量超过 200 kb。而且质量很糟糕。 这是算法:

extension UIImage {
func compressImage() -> UIImage? {
    // Reducing file size to a 10th
    var actualHeight: CGFloat = self.size.height
    var actualWidth: CGFloat = self.size.width
    let maxHeight: CGFloat = 800.0
    let maxWidth: CGFloat = 600.0
    var imgRatio: CGFloat = actualWidth/actualHeight
    let maxRatio: CGFloat = maxWidth/maxHeight
    var compressionQuality: CGFloat = 0.01

    if actualHeight > maxHeight || actualWidth > maxWidth {
        if imgRatio < maxRatio {
            //adjust width according to maxHeight
            imgRatio = maxHeight / actualHeight
            actualWidth = imgRatio * actualWidth
            actualHeight = maxHeight
        } else if imgRatio > maxRatio {
            //adjust height according to maxWidth
            imgRatio = maxWidth / actualWidth
            actualHeight = imgRatio * actualHeight
            actualWidth = maxWidth
        } else {
            actualHeight = maxHeight
            actualWidth = maxWidth
            //compressionQuality = 1
        }
    }
    let rect = CGRect(x: 0.0, y: 0.0, width: actualWidth, height: actualHeight)
    UIGraphicsBeginImageContext(rect.size)
    self.draw(in: rect)
    guard let img = UIGraphicsGetImageFromCurrentImageContext() else {
        return nil
    }
    UIGraphicsEndImageContext()
    guard let imageData = UIImageJPEGRepresentation(img, compressionQuality) else {
        return nil
    }
    return UIImage(data: imageData)
}

}

由于它不适用于这个算法,我开始寻找 Swift 的图像压缩库,但不幸的是我没有找到任何东西!然后我尝试使用LZFSE压缩算法,但据我了解,在另一台设备上没有解压缩器(假设使用Android OS),图像将不会显示,此外,他在其中一行代码中抱怨nil-data . 代码如下:

 public enum CompressionAlgorithm {
  case lz4   // speed is critical
  case lz4a  // space is critical
  case zlib  // reasonable speed and space
  case lzfse // better speed and space
}

private enum CompressionOperation {
  case compression, decompression
}

private func perform(_ operation: CompressionOperation,
                     on input: Data,
                     using algorithm: CompressionAlgorithm,
                     workingBufferSize: Int = 2000) -> Data?  {
  var output = Data()

  // set the algorithm
  let streamAlgorithm: compression_algorithm
  switch algorithm {
    case .lz4:   streamAlgorithm = COMPRESSION_LZ4
    case .lz4a:  streamAlgorithm = COMPRESSION_LZMA
    case .zlib:  streamAlgorithm = COMPRESSION_ZLIB
    case .lzfse: streamAlgorithm = COMPRESSION_LZFSE
  }

  // set the stream operation, and flags
  let streamOperation: compression_stream_operation
  let flags: Int32
  switch operation {
    case .compression:
      streamOperation = COMPRESSION_STREAM_ENCODE
      flags = Int32(COMPRESSION_STREAM_FINALIZE.rawValue)
    case .decompression:
      streamOperation = COMPRESSION_STREAM_DECODE
      flags = 0
  }

  // create a stream
  var streamPointer = UnsafeMutablePointer<compression_stream>
    .allocate(capacity: 1)
  defer {
    streamPointer.deallocate(capacity: 1)
  }

  // initialize the stream
  var stream = streamPointer.pointee
  var status = compression_stream_init(&stream, streamOperation, streamAlgorithm)
  guard status != COMPRESSION_STATUS_ERROR else {
    return nil
  }
  defer {
    compression_stream_destroy(&stream)
  }

  // set up a destination buffer
  let dstSize = workingBufferSize
  let dstPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: dstSize)
  defer {
    dstPointer.deallocate(capacity: dstSize)
  }

  // process the input
  return input.withUnsafeBytes { (srcPointer: UnsafePointer<UInt8>) in

    stream.src_ptr = srcPointer
    stream.src_size = input.count
    stream.dst_ptr = dstPointer
    stream.dst_size = dstSize

    while status == COMPRESSION_STATUS_OK {
      // process the stream
      status = compression_stream_process(&stream, flags)

      // collect bytes from the stream and reset
      switch status {

      case COMPRESSION_STATUS_OK:
        output.append(dstPointer, count: dstSize)
        stream.dst_ptr = dstPointer
        stream.dst_size = dstSize

      case COMPRESSION_STATUS_ERROR:
        return nil

      case COMPRESSION_STATUS_END:
        output.append(dstPointer, count: stream.dst_ptr - dstPointer)

      default:
        fatalError()
      }
    }
    return output
  }
}

// Compressed keeps the compressed data and the algorithm
// together as one unit, so you never forget how the data was
// compressed.  
public struct Compressed {
  public let data: Data
  public let algorithm: CompressionAlgorithm

  public init(data: Data, algorithm: CompressionAlgorithm) {
    self.data = data
    self.algorithm = algorithm
  }

  // Compress the input with the specified algorithm. Returns nil if it fails.
  public static func compress(input: Data,
                              with algorithm: CompressionAlgorithm) -> Compressed? {
    guard let data = perform(.compression, on: input, using: algorithm) else {
      return nil
    }
    return Compressed(data: data, algorithm: algorithm)
  }

  // Factory method to return uncompressed data. Returns nil if it cannot be decompressed.
  public func makeDecompressed() -> Data? {
    return perform(.decompression, on: data, using: algorithm)
  }
}

// For discoverability, add a compressed method to Data
extension Data {
  // Factory method to make compressed data or nil if it fails.
  public func makeCompressed(with algorithm: CompressionAlgorithm) -> Compressed? {
    return Compressed.compress(input: self, with: algorithm)
  }
}

在此行中,应用此算法后:

UIImage(data: imageCompressData)//к переменной imageCompressData был применен код сверху!

接下来,我无意中发现了这段代码:

func resizeImageUsingVImage(image:UIImage, size:CGSize) -> UIImage? {
    let cgImage = image.cgImage!
    var format = vImage_CGImageFormat(bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: nil, bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.first.rawValue), version: 0, decode: nil, renderingIntent: CGColorRenderingIntent.defaultIntent)
    var sourceBuffer = vImage_Buffer()
    defer {
        free(sourceBuffer.data)
    }
    var error = vImageBuffer_InitWithCGImage(&sourceBuffer, &format, nil, cgImage, numericCast(kvImageNoFlags))
    guard error == kvImageNoError else { return nil }
    // create a destination buffer
    let scale = image.scale
    let destWidth = Int(size.width)
    let destHeight = Int(size.height)
    let bytesPerPixel = image.cgImage!.bitsPerPixel/8
    let destBytesPerRow = destWidth * bytesPerPixel
    let destData = UnsafeMutablePointer<UInt8>.allocate(capacity: destHeight * destBytesPerRow)
    defer {
        destData.deallocate(capacity: destHeight * destBytesPerRow)
    }
    var destBuffer = vImage_Buffer(data: destData, height: vImagePixelCount(destHeight), width: vImagePixelCount(destWidth), rowBytes: destBytesPerRow)
    // scale the image
    error = vImageScale_ARGB8888(&sourceBuffer, &destBuffer, nil, numericCast(kvImageHighQualityResampling))
    guard error == kvImageNoError else { return nil }
    // create a CGImage from vImage_Buffer
    var destCGImage = vImageCreateCGImageFromBuffer(&destBuffer, &format, nil, nil, numericCast(kvImageNoFlags), &error)?.takeRetainedValue()
    guard error == kvImageNoError else { return nil }
    // create a UIImage
    let resizedImage = destCGImage.flatMap { UIImage(cgImage: $0, scale: 0.0, orientation: image.imageOrientation) }
    destCGImage = nil
    return resizedImage
}

更详细地查看代码并研究了 vImage 和 Accelerate 库的文档后,我意识到这个非常强大的图像处理库,如果我有足够的知识,也许我可以创建一个好的图像压缩器,但是我的知识是不够的,所以所有的希望都寄托在你身上。

【问题讨论】:

  • 压缩质量 0.01 的质量低得离谱。你不会得到可用的结果。我通常使用大约 0.6 或更好的质量。尝试将质量设置提高到更合理的值,然后查看您获得的文件大小。

标签: swift xcode image compression


【解决方案1】:

在第一个样本中,0.01 压缩质量意味着质量将是原始图像的 1%。

尝试将值更改为 0.8 以获得 80% 的质量,看看它是否会变得更好。

【讨论】:

    【解决方案2】:

    由于您真正想要做的是确保您上传了最高质量的图片,最大图片大小 (800x600) 和数据字节大小 (150k) 我将它分为两​​个单独的步骤:

    // size down the image to fit in 800x600
    let sizedImage = image.aspectFit(toSize:CGSize(width:800, height:600))
    
    // get the jpeg image with a max size of 150k bytes
    let imageData = sizedImage.getJPEGData(withMaxSize:150_000)
    

    当然,如果没有 UIImage.aspectFit 的定义,所有这些都无济于事:

    func *(lhs:CGSize, rhs:CGFloat) -> CGSize {
        return CGSize(width: lhs.width * rhs, height: lhs.height * rhs)
    }
    
    extension CGSize {
        func aspectFit(toSize:CGSize) -> CGSize {
            return self * min(toSize.width / width, toSize.height / height, 1.0)
        }
    }
    
    extension UIImage {
        func aspectFit(toSize:CGSize) -> UIImage? {
            let newSize = size.aspectFit(toSize:toSize)
    
            UIGraphicsBeginImageContext(newSize)
            defer {
                UIGraphicsEndImageContext()
            }
    
            draw(in: CGRect(origin: CGPoint.zero, size: newSize))
    
            return UIGraphicsGetImageFromCurrentImageContext()
        }
    }
    

    UIImage.getJPEGData:

    extension UIImage {
        func getJPEGData(withMaxSize max:Int) -> Data? {
            for quality in stride(from: 1.0 as CGFloat, to: 0.05, by: 0.05) {
                if let data = UIImageJPEGRepresentation(self, quality), data.count < max {
                    return data
                }
            }
    
            return nil
        }
    }
    

    这是一种“更智能”的方法,它执行二进制搜索以获得低于 150k 的最佳图像质量,但对于大多数用途而言,它确实可能有点矫枉过正:

    func binarySearch<Type:Comparable, Result>(lhs:Type, rhs:Type, next:(Type, Type)->Type, predicate:(Type)->Result?) -> (Type, Result)? {
        let middle = next(lhs, rhs)
    
        if(middle == lhs) {
            return predicate(lhs).map { ( middle, $0) }
        }
    
        if(predicate(middle) != nil) {
            return binarySearch(lhs: middle, rhs: rhs, next: next, predicate: predicate)
        }
        else {
            return binarySearch(lhs: lhs, rhs: middle, next: next, predicate: predicate)
        }
    }
    
    extension UIImage {
        func jpegData(withMaxSize max:Int) -> Data? {
            if let data = UIImageJPEGRepresentation(self, 1.00), data.count < max {
                return data
            }
    
            guard let (quality, data) = binarySearch(lhs: 1, rhs: 100, next: { ($0 + $1) / 2 }, predicate: { (quality:Int)->Data? in
                guard let data = UIImageJPEGRepresentation(self, CGFloat(quality)/100) else {
                    return nil
                }
    
                return data.count <= max ? data : nil
            }) else {
                return nil
            }
    
            print("quality = \(quality)")
    
            return data
        }
    }
    

    【讨论】:

    • 不知道有什么原因不会。
    • 对我不起作用,质量很差,在 WhatsApp 上不是这样
    • 那我不知道。我在几张以 6kx4k 开始的图像上使用了相同的过程(以及这个确切的代码),效果非常好。我所知道的唯一真正致命的情况是某些图像会非常笨拙地缩放(通常是所需大小的 1-2 倍),这可能会给您留下不好的伪影,而 jpeg 压缩只会使情况变得更糟。如果图像包含几乎垂直/水平的边缘/线条,例如建筑物,情况会更糟。
    • 我有一个循环:while imageSizeKB&gt;150 { let resizedImage=resizingImage.resized (withPercentage: 0.9) let imageData=UIImageJPEGRepresentation (resizedImage!, 0.5) resizingImage=resizedImage! imageSizeKB=Double((imageData? .count)!) print ("imageSizeKb =", imageSizeKB) print ("resizingImage.width =", resizingImage.size.width, ", resizingImage.height =",resizingImage.size.height) } 工作正常,日志大小:1000px, h:>1000px
    • Но в этом коде, размер >1500кб, вы знаете почему: let resizedImage = newImage?.resizedTo1MB() let dataResizedImage = UIImagePNGRepresentation(resizedImage!) print("dataResizedImage = ", Double((dataResizedImage?.count)!) / 1000.0)
    猜你喜欢
    • 2012-06-22
    • 2014-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多