Сжатие изображения с минимальной потерей качества - PullRequest
0 голосов
/ 24 августа 2018

Стоит уточнить, что с языком программирования Swift и со всеми его компонентами я знаком уже три дня! Три дня пытаюсь решить эту проблему, ни одного решения не нашел, перерыл много сайтов! Мне нужно убедиться, что изображения при загрузке на сервер веселились не более 150 кб, но в то же время, чтобы качество было хорошим. Во время поиска я нашел алгоритм, на сайтах и ​​на GitHub они писали, что это была копия сжатия изображения в мессенджере WhatsApp, но на iphone 8 он не очень хорошо сжимает изображение. Если вы приведете пример, сжатие сфотографированного изображения на iphone 8 при передаче в мессенджер WhatsApp сжимается до 100 кб., При этом качество изображения очень хорошее, а длина и ширина больше 1000 пикселей. И если вы используете этот алгоритм, то при длине: 800px и ширине: 600px и степени сжатия 0,01 изображение весит более 200 кб. и качество ужасное. Вот алгоритм:

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) изображение не будет отображаться, кроме того, он пожаловался на nil-данные в одной из строк кода. , Вот код:

 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, я понял, что эта очень мощная библиотека обработки изображений, если у меня хватит знаний, может быть, я смогу создать хороший компрессор изображений, но мой знаний недостаточно, поэтому вся надежда на Вас.

Ответы [ 2 ]

0 голосов
/ 25 августа 2018

Поскольку то, что вы действительно хотите сделать, это убедиться, что у вас загружено изображение высочайшего качества с максимальным размером изображения (800x600) и размером байта данных (150 КБ), я бы разделил его на два отдельных этапа:

// 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
    }
}

ЭтоНемного «умнее» подхода, который выполняет бинарный поиск для получения наилучшего качества изображения с разрешением менее 150 тыс., но в большинстве случаев это, вероятно, излишне:

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
    }
}
0 голосов
/ 24 августа 2018

В первом примере качество сжатия 0,01 означает, что качество будет составлять 1% от исходного изображения.

Попробуйте изменить значение на 0,8 для качества 80% и посмотрите, улучшится ли оно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...