Processing中PImage類和loadImage()、createImage()函式的相關解析

EYE 發表於 2021-07-01

聊一聊Processing中PImage類和loadImage()、createImage()函式。因為要借P5做多媒體創意展示,圖片是一個很重要的媒體。有必要就圖片的獲取和展放作總結。

首先

有一點需要先提出來,PGraphics是繼承自PImage的,看原始碼:

public class PGraphics extends PImage implements PConstants {
	...
}

因此,理論上所有的繪製函式其實它的繪製物件都是PImage,都在這張圖紙上呈現內容,即預設的PGraphics g,可參考筆者另一篇:
https://www.cnblogs.com/sharpeye/p/13734132.html
這就給我們一個參考,意思是PGraphicsPImage時常要考慮兩者互相轉換的問題。實際執行時也是如此。

其次

PImage類中設有混合疊加的方法、圖片檔案的IO方法,即儲存讀取方法等,比較常用的是loadPixels()save()filter()等,當然還有許多內部成員變數,比如format width height pixels等等。其中format指的是顏色通道格式,比如有RGB ARGB ALPHA等。save()是可以儲存帶有alpha通道的影像的。
PImage類要使用必須要new例項物件,一般的,無外乎是使用loadImage()createImage()這兩個函式來獲得這一物件。如果要讀取一張現有的影像資訊,那麼就load
根據官網說明,loadImage()有兩引數可供填寫,即:loadImage(filename, extension)
filename指的是本地檔案路徑或者url檔案路徑。本地檔案路徑可以是絕對地址也可以是相對地址。
extension指的是指定檔案字尾,即檔案型別匹配。如果於真實檔案型別不匹配也能讀進記憶體不過通道資料未必能讀取,也就是alpha層的問題。檔案型別共有幾類 ".tga", ".jpg", ".png", ".jpeg", ".gif"等,".tif"無法讀取,P5自己的.tiff儲存檔案是可以的。請見下文例子:

PImage img1;
PImage img2;
PImage webImg;
void setup() {
  size(500,500);
  img1 = loadImage("mypic.png");	//讀取相對路徑下的檔案,即pde根目錄下的檔案,如果有data資料夾,則在此資料夾下尋找
  img2 = loadImage("d://mypic.png");//讀取絕對路徑下的檔案

  String url = "https://processing.org/img/processing-web.png";//讀取web網際網路上的檔案,也可讀取區域網下的檔案
  webImg = loadImage(url, "png");//選定讀取的檔案型別為png
}

void draw() {
  background(0);

  image(img1, 0, 0);
  image(img2, 200, 0);
  image(webImg, 0, 200);
}

會注意到,讀取本地的檔案速度非常理想,但是網際網路上的檔案會根據網路情況產生不少等待時間,給它看成是Processing的假死狀態,這是不希望看到的情況,如何來避免呢?有個函式官方給我們了----requstImage()
這個函式就可以避免假死狀態,或者稱之為非阻塞式讀取,而傳統讀取是阻塞式的。實質上查閱原始碼會看到它新建了一個thread執行緒,因此,可以避免佔用主執行緒而耽誤往後的語句執行任務。程式碼如下:


  /**
   * ( begin auto-generated from requestImage.xml )
   *
   * This function load images on a separate thread so that your sketch does
   * not freeze while images load during <b>setup()</b>. While the image is
   * loading, its width and height will be 0. If an error occurs while
   * loading the image, its width and height will be set to -1. You'll know
   * when the image has loaded properly because its width and height will be
   * greater than 0. Asynchronous image loading (particularly when
   * downloading from a server) can dramatically improve performance.<br />
   * <br/> <b>extension</b> parameter is used to determine the image type in
   * cases where the image filename does not end with a proper extension.
   * Specify the extension as the second parameter to <b>requestImage()</b>.
   *
   * ( end auto-generated )
   *
   * @webref image:loading_displaying
   * @param filename name of the file to load, can be .gif, .jpg, .tga, or a handful of other image types depending on your platform
   * @param extension the type of image to load, for example "png", "gif", "jpg"
   * @see PImage
   * @see PApplet#loadImage(String, String)
   */
  public PImage requestImage(String filename, String extension) {
    // Make sure saving to this file completes before trying to load it
    // Has to be called on main thread, because P2D and P3D need GL functions
    if (g != null) {
      g.awaitAsyncSaveCompletion(filename);
    }
    PImage vessel = createImage(0, 0, ARGB);

    // if the image loading thread pool hasn't been created, create it
    if (requestImagePool == null) {
      ThreadFactory factory = new ThreadFactory() {
        public Thread newThread(Runnable r) {
          return new Thread(r, REQUEST_IMAGE_THREAD_PREFIX);
        }
      };
      requestImagePool = Executors.newFixedThreadPool(4, factory);
    }
    requestImagePool.execute(() -> {
      PImage actual = loadImage(filename, extension);

      // An error message should have already printed
      if (actual == null) {
        vessel.width = -1;
        vessel.height = -1;

      } else {
        vessel.width = actual.width;
        vessel.height = actual.height;
        vessel.format = actual.format;
        vessel.pixels = actual.pixels;

        vessel.pixelWidth = actual.width;
        vessel.pixelHeight = actual.height;
        vessel.pixelDensity = 1;
      }
    });
    return vessel;
  }

能發現官方設立了一個執行緒池介面類ExecutorService,而後新建執行緒池,每次讀取圖片都新建一個執行緒入執行緒池,統一管理,並用ExecutorService例項的物件來execute執行,從而獨立於主執行緒來執行任務。

再次

光有讀取還不完善,來看看自己生成圖片物件createImage()。很清楚,三個引數

Parameters	
w	int: width in pixels	圖片物件寬度尺寸
h	int: height in pixels	圖片物件高度尺寸
format	int: Either RGB, ARGB, ALPHA (grayscale alpha channel)	圖片型別

官方介紹到,要注意使用規範性,如下:

PImage image = createImage(600,400,RGB);	//正確的例項PImage寫法	其中圖片通道型別有RGB, ARGB, ALPHA
// *錯誤* PImage image = new PImage(600,400,RGB);		//錯誤的寫法!!

實際上參照原始碼,就是new PImage的方法例項:

public PImage createImage(int w, int h, int format) {
  PImage image = new PImage(w, h, format);
  image.parent = this;  // make save() work
  return image;
}

值得思考的是:
一、 PImage物件改變其畫素無外乎要修改其pixels陣列值,而不能再圖片上直接繪製圖形,如果要繪製則必須轉換成PGraphics;
二、 如果要複製PImage以例項多個物件,可以使用copy()或者clone(),比如:

PImage op;
PImage op2;

try  {
    op = (PImage)img2.clone();	使用clone方法 注意轉換型別
  }
  catch(Exception e) {
  }
  
op2 = img2.copy():	使用copy方法

三、 繪製在PGraphics上的方法有三種,一種是image(),一種是set(),還有一種是手動以遍歷畫素的形式來繪製渲染。如下:

  image(img1, 100, 0);
  image(img2, 200, 0);
  image(webImg, 0, 200);   //使用image方法
  
  set(0,0,img2);   //使用set方法

  loadPixels();
  img1.loadPixels();
  for (int x = 0; x < img1.width; x++) {
    for (int y = 0; y < img1.height; y++) {
      color c = img1.pixels[y*img1.width + x];
      if(alpha(c) == 0)   continue;   //alpha通道檢測
      pixels[y*width + x] = c ;   //相當於 g.pixels[]...   使用了pixels陣列直接賦值
    }
  }
  updatePixels();

//************************************//   另一種手動填充畫素 set
  img1.loadPixels();
  for (int x = 0; x < img1.width; x++) {
    for (int y = 0; y < img1.height; y++) {
      color c = img1.pixels[y*img1.width + x];
      //if(alpha(c) == 0)   continue;  使用set方法  alpha通道檢測可跳過
      set(x,y,c);   //使用set方法    注意使用set方法跟updatePixels方法有衝突,這裡去掉其呼叫
    }
  }
//************************************//

最後

當然,在實際使用中,會用到大量技巧,比如image()set()兩函式渲染圖片效率有出入,一般可以用set()來提高渲染效率,但是無法進行矩陣變換等運算。再如,PImage中的save()可以儲存帶通道的圖片。這次就簡單做一總結,我們往後再細聊,感謝閱讀!!