2018年9月16日 星期日

OpenCV - Java - 臉部辨識

OpenCV 是一個是一個跨平台的電腦視覺庫,其可讓我們方便的做許多圖像方面的處理,
在這裡我參考了網路上的資料加上修改實測,展示一個簡單的OpenCV範例:

程式語言 : Java
需求:
調用電腦本身的攝影機,在UI上顯示即時截取的畫面,並在辨視出人臉的地方加上方框,並將人臉部份儲存至本地電腦上。

這裡下載OpenCV,我下載的是 3.4.1 的Win pack版本,下載後執行,它會解壓縮生成一個opencv資料,打開它後,
會看到所須的jar檔及dll檔:
opencv-341.jar
並且在x64和x86資料夾內有不同位元的windows dll檔:
opencv_java341.dll

以Eclipse來示範,除了加入opencv0341.jar到lib後,還要如下圖設定Native library location為dll檔所在的資料夾路徑

接下來是程式的部份
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.util.Calendar;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.MatOfByte;
import org.opencv.core.MatOfRect;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.objdetect.CascadeClassifier;
import org.opencv.videoio.VideoCapture;
import org.opencv.videoio.Videoio;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class OpenCV_Test extends Application{

 //是否正在截取攝影機影像
    private boolean isStart = false;
 
    private VideoCapture videoCapture;
    private ScheduledExecutorService scheduledExecutorService;
 
    // JavaFX 元件
    private BorderPane rootNode;
    private VBox vbox;
    private ImageView imageView;
    private Button startStopBtn;
    String startText = "Start";
    String stopText = "Stop";
 
 public static void main(String[] args) {
  Application.launch(args);
 }

 @Override
 public void start(Stage primaryStage) throws Exception {
  
  startStopBtn.setOnAction(new EventHandler<ActionEvent>() {

   @Override
   public void handle(ActionEvent event) {
    if (!isStart) {
     startStopBtn.setText(stopText);
     videoCapture.open(0);
     videoCapture.set(Videoio.CAP_PROP_FRAME_WIDTH, 640);
     videoCapture.set(Videoio.CAP_PROP_FRAME_HEIGHT, 480);
     
     if (videoCapture.isOpened()) {
      isStart = true;
      
      //建立設定截取攝影機的thread
      scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
      scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

       @Override
       public void run() {
        imageView.setImage(captureImage());
       }
       
      }, 0, 16, TimeUnit.MILLISECONDS);
     }else {
      System.err.println("Error: Can not open camera.");
     }
     
    }else {
     startStopBtn.setText(startText);
     isStart = false;
     
     scheduledExecutorService.shutdown();
     try {
      //確保scheduledExecutorService有正確關閉排程
      while(!scheduledExecutorService.awaitTermination(30, TimeUnit.MILLISECONDS)) {
       System.out.println("Not close thread yet");
      }
     } catch (InterruptedException e) {
      System.err.println(e.getMessage());
      e.printStackTrace();
     }
     
     videoCapture.release();
     imageView.setImage(null);
    }
   }
   
  });
  
  //UI畫面
  Scene scene = new Scene(rootNode, 800, 640);
        primaryStage.setTitle("OpenCV Test");
        primaryStage.setScene(scene);
        primaryStage.show();
 }

 //UI畫面設定
 @Override
 public void init() throws Exception {
  super.init();
  
  System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
  videoCapture = new VideoCapture();
  
  rootNode = new BorderPane();
  
  vbox = new VBox();
  vbox.setAlignment(Pos.CENTER);
  rootNode.setCenter(vbox);
  
  imageView = new ImageView();
  imageView.setFitWidth(640);
  imageView.setFitHeight(480);
  imageView.setPreserveRatio(true);
  
  startStopBtn = new Button(startText);
  vbox.getChildren().addAll(imageView, startStopBtn);
  
 }

 //程式關閉時關閉攝影機
 @Override
 public void stop() throws Exception {
  if (videoCapture.isOpened()) {
   videoCapture.release();
  }
  super.stop();
 }
 
 private Image captureImage() {
  Image capturedImage = null;
 
  if (videoCapture.isOpened()) {
   Mat mat = new Mat();
   videoCapture.read(mat);
   if (!mat.empty()) {
    MatOfByte buffer = new MatOfByte();
    //偵測人臉,截取人臉部份存圖,
    //並在要顯示在畫面上的影像用矩形標示人臉部份
    detectDrawAndSaveFace(mat);
          Imgcodecs.imencode(".png", mat, buffer);
          capturedImage = new Image(new ByteArrayInputStream(buffer.toArray()));
   }
  }   
        return capturedImage;
 }
 
 //臉部偵測
    private void detectDrawAndSaveFace(Mat image) {
     CascadeClassifier faceDetector = new CascadeClassifier();
        faceDetector.load("D:\\JavaLib\\opencv\\sources\\data\\haarcascades_cuda\\haarcascade_frontalface_alt2.xml");

        MatOfRect faceDetections = new MatOfRect();
        faceDetector.detectMultiScale(image, faceDetections);
        for (Rect rect : faceDetections.toArray())
        {
         //截detect到人臉的區域,將區域內的圖存檔
            Mat faceImage = new Mat(image, rect);
            String outputDir = "D:\\detectedFaces";
            String outputFilePath = outputDir + "\\detectedFace_" + Calendar.getInstance().getTimeInMillis() + ".png";
            Imgcodecs.imwrite(outputFilePath, faceImage);
            
            //在detect到人臉的區域上畫上矩形邊框,顯示在螢幕上的ImageView上
            Imgproc.rectangle(image, new Point(rect.x, rect.y), new Point(rect.x + rect.width, rect.y + rect.height), new Scalar(0, 255, 0));
        } 
    }
}


Opencv 3.4.1 Win pack 下載分享

參考資料:
  1. Java+opencv3.2.0之人脸检测
  2. [OpenCV] 人臉偵測 (Face Detection)
  3. [OpenCV] 人臉偵測2 (Face Detection)
  4. 利用Java调用OpenCV进行人脸识别OpenCV on Raspberry Pi - Using Java(7)- 使用 OpenCV 截取與顯示影片

2018年9月8日 星期六

Java 圖片旋轉 (Java Image Rotate)

這次要紀錄的是用Java進行圖片旋轉,
我們希望在將圖片旋轉後,能得到能夠剛好容納旋轉後圖片的新片大小,
例如以下的例子,上面的是原圖,下面的是順時鏡旋轉了60度的圖片,
可以看得出來,在此例中,旋轉後的圖其寬高比原圖還要大,原因是為了要容納旋轉後的圖,新圖片的寬高須要重新計算的關係。

在看程式碼之前,我們需要計算旋轉後的新寬高,
首先,例如有一個如下圖所示的,寬為w,高為h的圖片


下圖是旋轉了Θ角後的圖片,並附上了新寬(w')及新高(h')的推導

下圖也是旋轉了θ後的圖片,但是θ為負值,所以sin(θ) 為負值,

上面兩張圖只舉例了θ在-π/2 ~ π/2 的情況,但是在任何角度都是一樣的,都可以推出
w'= h |sin(θ)| + w |cos(θ)|
h' = h |cos(θ)| + w |sin(θ)|

因為Java繪圖會從左上角開始往右下繪圖的關係,在這邊我們稱繪圖區為畫布,
畫布的區域為座標原點右下區域的為置,即第四象限
有了新寬(w')高(h')了以後,我們還需要知道圖片被旋轉了以後,要如何平移才能將旋轉後的圖片放置到畫布中間。

旋轉且平移到畫布中的圖片應如下圖所示:


為了計算方便,我們採取以圖片中心點為旋轉中心的方式來推導,如下圖所示,
δh即δw為圖片以中心點旋轉後要平移的距離,可以看到推導後的結果為
δh = h'/2 - h/2
δw = w'/2 - w/2
經過了上述的推導後,我們得到了以下公式:
w'= h |sin(θ)| + w |cos(θ)|
h' = h |cos(θ)| + w |sin(θ)|
δh = h'/2 - h/2
δw = w'/2 - w/2

於是我們知道了旋轉圖片的程式邏輯:
1. 計算出新圖片的寬(w')高(h')
2.將圖片以中心點(w/2, h/2)為旋轉中心旋轉 θ
3.將圖片進行δw, δh 的平移使其位於第四象限的畫布中

最後,依照上述邏輯寫成的Java程式碼如下:
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;

public class ImageRotateTest {
 public static void main(String[] args) throws IOException {
  String inputFilePath = "D:\\inputImage.png";
  String outputFilePath = "D:\\outputRotatedImage.png";
  
  File inputImage = new File(inputFilePath);
  File outputImage = new File(outputFilePath);
  
  BufferedImage newBufferedImage = rotateImage(ImageIO.read(inputImage), 60);
  
  String outputImageExtension = outputFilePath.substring(outputFilePath.lastIndexOf(".") + 1);
  ImageIO.write(newBufferedImage, outputImageExtension, outputImage);
 }

 public static BufferedImage rotateImage(BufferedImage bufferedimage, int degree) {
  int w = bufferedimage.getWidth();
  int h = bufferedimage.getHeight();
  int type = bufferedimage.getColorModel().getTransparency();
  double radiusDegree = Math.toRadians(degree);

  double newH = Math.abs(h * Math.cos(radiusDegree)) + Math.abs(w * Math.sin(radiusDegree));
  double newW = Math.abs(h * Math.sin(radiusDegree)) + Math.abs(w * Math.cos(radiusDegree));

  double deltaX = (newW - w) / 2;
  double deltaY = (newH - h) / 2;
  
  BufferedImage img = new BufferedImage((int) newW, (int) newH, type);
  Graphics2D graphics2d = img.createGraphics();
  graphics2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);

  // 以下為矩陣相乘, [translate] * [rotate] * [image的x,y]
  // 所以行為為,先rotate,再translate
  graphics2d.translate(deltaX, deltaY);
  graphics2d.rotate(radiusDegree, w / 2, h / 2);  //以中心點旋轉
  graphics2d.drawImage(bufferedimage, 0, 0, null);

  graphics2d.dispose();

  return img;
 }
}


要注意的是Java進行圖形變換 (旋轉、平移、縮放等) 的方法矩陣變換,而連乘的矩陣是右邊的先做運算再輪到左邊,所先先旋轉再平移,在程式上的順序是
先寫 Graphics2D.translate() 再 Graphics2D.rotate()

原始碼下載:
ImageRotateTest.7z
旋轉圖片推導

參考資料:
  1. Black area using java 2D graphics rotation?
  2. 浅谈矩阵变换——Matrix
P.S. 
改使用ImageIcon的程式 (速度比較快)
ImageIcon imageIcon = new ImageIcon(fromSrc);
  File toImage = new File(toSrc);

        int w= imageIcon.getIconWidth();
        int h= imageIcon.getIconHeight();
        int imageType= BufferedImage.TYPE_INT_RGB;
        
        double radiusDegree = Math.toRadians(degree);
        //calculate new width/height of rotated image
        double newW = Math.abs(h * Math.sin(radiusDegree)) + Math.abs(w * Math.cos(radiusDegree));
        double newH = Math.abs(h * Math.cos(radiusDegree)) + Math.abs(w * Math.sin(radiusDegree));
        //calculate new width/height offset of rotated image
        double deltaX = (newW - w) / 2;
        double deltaY = (newH - h) / 2;
        
        BufferedImage img = new BufferedImage((int) newW, (int) newH, imageType);
        Graphics2D graphics2d= img.createGraphics();
        graphics2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        
        //Image Transoform Matrix : �A [translate] * [rotate] * [x,y of Image]
        //rotate --> translate
        graphics2d.translate(deltaX, deltaY);
        graphics2d.rotate(radiusDegree, w/2, h/2);
        
        graphics2d.drawImage(imageIcon.getImage(), 0, 0, null);
        graphics2d.dispose();
        
        String outputfileExtension = toSrc.substring(toSrc.lastIndexOf(".") + 1);
        ImageIO.write(img, outputfileExtension, toImage);