Halikonjoen vnha silta, Halikko


KUVAN TUONTI NÄYTÖLLE

KUVAN TUONTI NÄYTÖLLE LAADUKKAANA - FUNKTIOT

Seuraavassa on muutama omissa sovelluksissani käyttämääni funktiota, jotka pitävät kuvan laadun hyvänä. Useat Xcode-kontrollit tekevät kuvista epäteräviä, jos antaa niiden skaalata kuvia.

Näihin funktioihin kannattaa lisätä sovellukseen sopivalla tavalla ilmoituksia miksi homma epäonnistui. Rivien säästämiseksi tässä on nyt epäonnistumispaikoissa vain esimerkiksi '{ return nil }'

Jos tavallisen kameran tekemän kuvan haluaa näytölle pienempikokoisena, kuten normaalitilanne on, niin seuraavassa on funktio, joka pienentää ja terävöittää kuvan. Ilman terävöitystä kuva usein näyttää yllättävän epäterävältä. Jäljempänä oleva kokoaKuva6() tekee riittävän terävän kuvan silloin jos lopputuloksen tarvitsee olla thumbnail-kokoa esim. pienempi kuin 200x150 pikseliä.

Tämä seuraava funktio hyödyntää coreImage-kirjastoa, eikä se ota automaattisesti huomioon kuvan laittamista oikeaan asentoon (Orientation) kuten muut alempana olevat funktiot. Siksi tässä on erikseen Orientation-tiedon hakeva funktio, joka toimii ainakin iPhonella ja iPadilla otetuilla valokuvilla.

Huomaa, että coreImage tekee osan työstään hyödyntäen näytönohjainta, ja sitä varten taustalla on käytössä "Metal API". Kun sovellusta testaa ja ajaa Xcodessa, niin Metal API -kutsujen ennakkotarkastus on käytössä, kun taas Xcode:n ulkopuolella se ei ole käytössä ja suorituskin on nopeampaa. Mojave-käyttöjärjestelmässä ennakkotarkastuksessa on bugi ja tulee vaikeasti tulkittavia virheitä, joiden ilmoituksissa on termejä "MTLDebugComputeCommandEncoder... failed assertion", ja "Thread 1: signal SIGABRT". Tästä selviää ottamalla ennakkotarkastuksen pois käytöstä: valitse Xcoden menusta:
Product -> Scheme -> Edit Scheme -> Options -> Run -> Metal API Validation Disabled


Funktio skaalaaTeravoita() soveltuu parhaiten tilanteeseen, jossa tuodaan näytölle yksittäinen kuva. Funktion sisällä muodostettavat CIContext() ja CIFilter() ovat raskaita ja jos ikkunassa esimerkiksi haluaa suurentaa ja pienentään kuvaa ikkunaa suurentamalla ja pienentämällä, niin kannattaa osa funktion muuttujista ottaa ulos luokkamuuttujiksi.

Käyttöesimerkki:
//let tila = CGSize(width: ww, height: hh)  // tila johon kuva sijoitetaan
//let uusima = skaalaaTeravoita(kuvapolkuz, koko: tila)
//imageviewOma.setFrameSize(uusima!.size)
//imageviewOma.image = uusima!

class func skaalaaTeravoita(_ polkuz: String, koko: CGSize) -> NSImage?
{
   let urli = URL(fileURLWithPath: polkuz)  // koko polku
   let ciimage0 = CIImage(contentsOf: urli)  // nil tarkistettava
   if ciimage0 == nil   { return nil }
   let orient = Meka.haeOrient(polkuz)   // haetaan kuvasta tieto Orientation
  
   let context = CIContext(options: [.useSoftwareRenderer : false])
   let ciimage = ciimage0!.oriented(orient)  // käännetään kuva oikein päin
   let alkuw = ciimage.extent.width   // CGFloat
   let alkuh = ciimage.extent.height
   let alkuswh = alkuw / alkuh  // alkuperäiskuvan sivusuhde
  
   let w = koko.width   // tähän tilaan pitää mahtua  (CGFloat)
   let h = koko.height
   let suhde = w / h      // halutun pikkukuvan sivusuhde
   let uusiw = (alkuswh <= suhde) ?  h * alkuswh : w
   let uusih = (alkuswh <= suhde) ?  h : w / alkuswh

   // filtterit ketjutetaan (output on seuraavan input)
   // suoritus tapahtuu optimaalisesti GPU:ssa
   let scaleFilter = CIFilter(name:"CILanczosScaleTransform")!
   scaleFilter.setValue(ciimage, forKey: kCIInputImageKey)
   scaleFilter.setValue(Double(uusiw / w), forKey: kCIInputScaleKey)
   scaleFilter.setValue(suhde, forKey: kCIInputAspectRatioKey)

   let sharpFilter = CIFilter(name: "CIUnsharpMask")!
   sharpFilter.setValue(scaleFilter.outputImage!, forKey: kCIInputImageKey)
   sharpFilter.setValue(2.0, forKey: "inputIntensity")
   sharpFilter.setValue(1.0, forKey: "inputRadius")
   let ouCIIma2 = sharpFilter.outputImage
     
   let ouCGIma2 = context.createCGImage(ouCIIma2!, from: ouCIIma2!.extent)
   let uutila = CGSize(width: uusiw, height: uusih)
   let nsima = NSImage(cgImage: ouCGIma2!, size: uutila)
   return nsima
}


Seuraava funktio hakee kuvatiedoston metatiedoista Orientation -tiedon.
Outputtina löytynyt Orientation, ja jos ei löydy, niin outputtina .up (normaaliasento)
Testattu kameroilla iPhone ja iPad.
Käyttöesimerkki:

// let ori = haeOrient(kuvapolkuz)
// let ciimage = ciimage0!.oriented(orient)  // käännetään kuva oikein päin

class func haeOrient(_ polkuz: String) -> CGImagePropertyOrientation
{
   if polkuz.isEmpty { return CGImagePropertyOrientation.up }
   let fm = FileManager()
   if !fm.fileExists(atPath: polkuz) { return CGImagePropertyOrientation.up }
     
   let urli = URL(fileURLWithPath: polkuz)  // koko polku
   // Tämä kurkistaa levyllä metatietoja, eikä lataa kuvaa muistiin:
   let cgref = CGImageSourceCreateWithURL(urli as CFURL, nil)
   if cgref == nil   { return CGImagePropertyOrientation.up }
     
   let imaprop2: NSDictionary = CGImageSourceCopyPropertiesAtIndex(cgref!, 0, nil)!
   let suunta: Int? = imaprop2["Orientation"] as! Int?
   let ori = (suunta != nil) ? suunta! : EIOO
   var orient: CGImagePropertyOrientation = .right
   switch ori    // orientation-muunnos macOS-ohjelmakirjastoja varten
   {
   case 1:
      orient = CGImagePropertyOrientation.up
   case 6:
      orient = CGImagePropertyOrientation.right
   case 3:
      orient = CGImagePropertyOrientation.down
   case 8:
      orient = CGImagePropertyOrientation.left
   default:
      orient = CGImagePropertyOrientation.up
   }
   return orient
}



/*
Tämä funktio tuottaa aina odotusten kokoisen kuvan esimerkiksi NSImageView:lle, ja tulos on laadukas. Inputtina joko muistissa oleva kuvatiedostorakenne (blobx), tai kuvatiedosto (tayspolkuz). Funktio käyttää cgImage-funktioita. Kutsussa annetaan haluttu uuden kuvan koko.
Ei tehdä kuvasta suurennosta, eli jos haluttu kuva on alkuperäistä suurempi, funktio antaa alkuperäisen kuvan (koodia on helppo täydentää).

Thumbnail-kuvan tekeminen tietokantaan vietäväksi:
let ima = kokoaKuva6(tayspolkuz, blobx: nil, wid: 102, hei: 80)
let blob = ima!.tiffRepresentation
let blob2 = NSBitmapImageRep(data: blob!)!.representation(using: .jpeg, properties: [NSBitmapImageRep.PropertyKey.compressionFactor : 0.6])

*/
//------------------------------------
class func kokoaKuva6(_ polkuz: String, blobx: Data?, wid w: Int, hei h: Int) -> NSImage?
{
   if polkuz.isEmpty && blobx == nil  { return nil }
   if !polkuz.isEmpty && blobx != nil  { return nil }

   var ww = 0  // kuvan 'polkuz' mitat pixeliä
   var hh = 0
 
   var imasorsa: CGImageSource? = nil   // kuvalähde
   if !polkuz.isEmpty
   {
      let fm = FileManager()
      if !fm.fileExists(atPath: polkuz)  { return nil }
      let failiUrl = URL(fileURLWithPath: polkuz, isDirectory: false)
      imasorsa = CGImageSourceCreateWithURL(failiUrl.absoluteURL as CFURL, nil)
   }
   if blobx != nil
   {
      imasorsa = CGImageSourceCreateWithData(blobx! as CFData, nil)
   }
   if imasorsa == nil  { return nil }
 
   let imaprop = CGImageSourceCopyPropertiesAtIndex(imasorsa!, 0, nil)
   if imaprop == nil  { return nil }
   let imaprop2: NSDictionary = imaprop!
   ww = imaprop2["PixelWidth"] as! Int
   hh = imaprop2["PixelHeight"] as! Int
   if ww == 0 || hh == 0  { return nil }
 
   var wux = 0  // lopputuloskuvan suurin pikselimitta
   if (ww <= w && hh <= h) ||  blobx != nil  // onko tiedoston kuva haluttua pienempi
   {
      wux = (ww > hh) ? ww : hh   // annetaan vain alkuperäinen kuva
   }
   else   // skaalataan alkuperäiskuva pienemmäksi
   {
      let suw = CGFloat(w) / CGFloat(ww)
      let suh = CGFloat(h) / CGFloat(hh)
      let suhde = min(suw, suh)
      let wx = (ww > hh) ? ww : hh
      wux = Int(round(suhde * CGFloat(wx)))
   }
 
   let thumbnailOptions =
   [
      String(kCGImageSourceCreateThumbnailFromImageAlways): true,
      String(kCGImageSourceCreateThumbnailWithTransform): true,
      String(kCGImageSourceThumbnailMaxPixelSize): wux
   ] as [String : Any]
 
   if let thumbnailRef = CGImageSourceCreateThumbnailAtIndex(imasorsa!, 0, thumbnailOptions as CFDictionary?)
   {
      return NSImage(cgImage: thumbnailRef, size: NSSize.zero)
   }
   return nil
}

/*
Tämä funktio tuottaa aina odotusten kokoisen kuvan esimerkiksi NSImageView:lle, mutta tulos on joskus hieman heikompi kuin edellä johtuen ilmeisesti kirjastofunktioiden pakkaus- tms. oletuksista. Tätä voi käyttää esimerkiksi silloin, jos katselukuvan laadulla ei ole suurta merkitystä. Inputtina joko muistissa oleva kuvatiedostorakenne (blobx), tai kuvatiedosto (tayspolkuz). Funktio lukee NSImageRept-rakenteet ja tekee NSImagen jossa vain yksi NSImageRept. Vaikka lähtötiedostossakin olisi vain yksi, niin tuloksena oleva NSImage näkyy oikein NSImageView:llä, vaikka funktiolla NSImage.init(contentsOfFile: tayspolkuz) tehty ei näkyisi.
*/
//------------------------------------
class func kokoaKuva2(_ blobx: Data?, polkuz: String) -> NSImage?
{
   if polkuz.isEmpty && blobx == nil  { return nil }
   if !polkuz.isEmpty && blobx != nil  { return nil }

   let reptit: [NSImageRep]
   if !polkuz.isEmpty
   {
      let fm = FileManager()
      if !fm.fileExists(atPath: polkuz)  { return nil }
      reptit = NSBitmapImageRep.imageReps(withContentsOfFile: polkuz)!
   }
   else
   {
      let ima = NSImage.init(data: blobx!)
      if ima == nil  { return nil }
      reptit = ima!.representations
   }
 
   var w = 0
   var h = 0
   let oli = reptit.count
   if oli == 0  { return nil }
   var ivali = 0
 
   // etsitään suurinta kuvaa vastaava imagerep ja se otetaan
   for i in 0..<oli
   {
      if reptit[i].pixelsWide > w
      {
         w = reptit[i].pixelsWide
         h = reptit[i].pixelsHigh
         ivali = i
      }
   }
   let imarepuusi = reptit[ivali]  // otetaan vain suurimman kuvan imagerep
   let imax = NSImage.init(size: CGSize(width: w, height: h))
   imax.addRepresentation(imarepuusi)
 
   return imax
}


/*
Tämän funktion inputtina on NSImage ja siitä halutaan skaalata toisenkokoinen (suurempi tai
pienempi) kuva (NSImage), jonka koko annetaan funktion parametrina. Tulos on laadukas.
Funktio käyttää cgImage-funktioita.
*/
//------------------------------------
class func kuvanKokomuutos(_ alkuima: NSImage?,  w: CGFloat, h: CGFloat) -> NSImage?
{
   if alkuima == nil || w < 0.1 || h < 0.1  { return nil }
   let alkukoko = alkuima!.size
   let suhde = w / h // halutun pikkukuvan sivusuhde
      // alkuperäiskuvan sivusuhde:
   let son = alkukoko.width / alkukoko.height
   let uusiw = (son <= suhde) ?  h * son : w
   let uusih = (son <= suhde) ?  h : w / son
   let uuskoko = CGSize(width: floor(uusiw), height: floor(uusih))
   var fromrect = CGRect(x: 0, y: 0, width: alkukoko.width, height: alkukoko.height)
    // Cast the NSImage to a CGImage, and high quality:
   NSGraphicsContext.current()?.imageInterpolation = NSImageInterpolation.high
   let imageRef = alkuima!.cgImage(forProposedRect: &fromrect, context: NSGraphicsContext.current, hints: nil)
   return NSImage(cgImage: imageRef!, size: uuskoko)
}





--------------------------------
(sivua muokattu 14.11.2019)