16 กุมภาพันธ์ 2556

[Android Code] การถ่ายภาพด้วยกล้อง




ต่อจากคราวที่แล้วนะครับ กับ [Android Code] การติดต่อใช้งานกล้อง
คราวนี้เจ้าของบล็อกก็ขออธิบายต่อจากเรื่องคราวก่อนไว้เลยละกันนะ
คราวนี้เป็นเรื่องการถ่ายภาพนิ่งจากกล้อง แล้วบันทึกลงใน SD Card
โดยคำสั่งที่เคยได้อธิบายไปในบทความเก่า จะไม่ขออธิบายอีกรอบนะ
เพราะว่าต้องการแยกบทความอธิบายทีละส่วนอยู่แล้ว เพราะเยอะมาก


สำหรับการถ่ายภาพก็จะต้องมีการใช้คำสั่งเพื่อให้ทำการถ่ายภาพ
ถ้าผู้ที่หลงเข้ามาอ่านยังจำได้กับตัวแปร mCamera ที่เคยอธิบายไป
Camera mCamera = Camera.open();

ซึ่งจริงๆแล้วคลาส Camera นี้จะมีคำสั่งถ่ายภาพให้อยู่ด้วย คือ
mCamera.takePicture(ShutterCallback shutter, PictureCallback raw , PictureCallback postView, PictureCallback jpeg);
จะเห็นว่าคำสั่ง takePicture จะมี Callback อยู่ด้วยกัน 4 อัน คือ

ShutterCallback shutter - Callback ที่จะทำงานเมื่อ Shutter ทำงาน
โดยที่การทำงานนี้จะอยู่ภายในฟังก์ชันที่ชื่อว่า onShutter

PictureCallback raw - Callback ที่จะทำงานหลังจากถ่ายภาพแล้ว
โดยข้อมูลภาพถ่ายที่ส่งเข้ามาจะอยู่ในฟังก์ชัน onPictureTaken
ซึ่งข้อมูลที่ส่งเข้ามาที่ฟังก์ชันนี้จะเป็นข้อมูลดิบจากการถ่ายภาพ

PictureCallback postView - จะเห็นว่าเป็น PictureCallback เหมือนกัน
แต่ว่าสำหรับ Callback ตัวนี้จะส่งข้อมูลเป็นภาพที่แสดงให้เห็นชั่วคราว
ให้นึกถึงเวลาถ่ายภาพ แล้วจะมีภาพแสดงให้เห็นแปปนึงว่าภาพถ่ายเป็นไง

PictureCallback jpeg - อันนี้เป็น Callback สำหรับส่งข้อมูลภาพที่ได้จริงๆ
โดยข้อมูลที่ส่งมาก็เป็นข้อมูลภาพ JPEG เพื่อให้นำไปสร้างเป็นไฟล์ภาพ

ซึ่งในการใช้งานทั่วไป ก็จะสนใจแค่ shutter กับ jpeg เท่านั้นแหละ
ส่วน raw กับ postView ไม่ต้องสนใจก็ได้ อันนี้ขึ้นอยู่กับความต้องการ
โดยที่ใน Callback ของ jpeg เจ้าของบล็อกก็จะให้ทำการเซฟภาพ


สำหรับในบทความนี้เจ้าของบล็อกจะให้กดหน้าจอเพื่อถ่ายภาพนะ
ไม่ได้มาสร้าง Button แล้วให้กด Button เพื่อถ่ายภาพ ยังไงก็เหมือนกัน
แต่ถึงบอกว่ากดหน้าจอเพื่อถ่ายภาพ จริงๆแล้วก็คือกดที่ Surface View
โดยกำหนด OnClickListner ให้กับ Surface View เพราะ Surface View
มีขนาดเต็มหน้าจอ (เพราะเจ้าของบล็อกกำหนด fill_parent ไว้)
ดังนั้นกดบน Surface View ก็จะเหมือนกับการกดบนหน้าจอเพื่อถ่าย
mPreview.setOnClickListener(new OnClickListener() { public void onClick(View arg0) { mCamera.takePicture(Main.this, null, null, Main.this); } });
ในคำสั่งนี้จะมีการกำหนด Callback ของ shutter กับ jpeg ไปที่ Main.this
ซึ่งก็คือการ Implement Callback เหมือนกับตอน surfaceChanged นั่นเอง


ทีนี้มาดูโค๊ดทั้งหมดกันเลยดีกว่า

Main.java
package app.akexorcist.camerasnap;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.List;

import android.hardware.Camera;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.provider.MediaStore.Images;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import android.app.Activity;
import android.content.ContentValues;
import android.content.Intent;

public class Main extends Activity implements SurfaceHolder.Callback
        , Camera.ShutterCallback, Camera.PictureCallback {
    Camera mCamera;
    SurfaceView mPreview;
    boolean saveState = false;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN 
                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        setContentView(R.layout.main);
        
        mPreview = (SurfaceView)findViewById(R.id.preview);
        mPreview.getHolder().addCallback(this);
        mPreview.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        mPreview.setOnClickListener(new OnClickListener() {
            public void onClick(View arg0) {
                if(!saveState) {
                    saveState = true;
                    mCamera.takePicture(Main.this, null, null, Main.this);
                }
            }            
        });
    }
    
    public void onResume() {
        Log.d("System","onResume");
        super.onResume();
        mCamera = Camera.open();
    }
    
    public void onPause() {
        Log.d("System","onPause");
        super.onPause();
        mCamera.release();
    }

    public void surfaceChanged(SurfaceHolder arg0, int arg1
            , int arg2, int arg3) {
        Log.d("CameraSystem","surfaceChanged");
        Camera.Parameters params = mCamera.getParameters();
        List<Camera.Size> previewSize = params.getSupportedPreviewSizes();
        List<Camera.Size> pictureSize = params.getSupportedPictureSizes();
        params.setPictureSize(pictureSize.get(0).width,pictureSize.get(0).height);
        params.setPreviewSize(previewSize.get(0).width,previewSize.get(0).height);
        params.setJpegQuality(100);
        mCamera.setParameters(params);
        
        try {
            mCamera.setPreviewDisplay(mPreview.getHolder());
            mCamera.startPreview();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void surfaceCreated(SurfaceHolder arg0) { }

    public void surfaceDestroyed(SurfaceHolder arg0) { }

    public void onPictureTaken(byte[] arg0, Camera arg1) { 
        int imageNum = 0;
        Intent imageIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        File imagesFolder = new File(Environment.getExternalStorageDirectory()
                , "DCIM/CameraSnap");
        imagesFolder.mkdirs();
        String fileName = "IMG_" + String.valueOf(imageNum) + ".jpg";
        File output = new File(imagesFolder, fileName);
        
        while (output.exists()){
            imageNum++;
            fileName = "IMG_" + String.valueOf(imageNum) + ".jpg";
            output = new File(imagesFolder, fileName);
        }

        Uri uri = Uri.fromFile(output);
        imageIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
        
        ContentValues image = new ContentValues();
        String dateTaken = DateFormat.getDateTimeInstance()
                .format(Calendar.getInstance().getTime());
        image.put(Images.Media.TITLE, output.toString());
        image.put(Images.Media.DISPLAY_NAME, output.toString());
        image.put(Images.Media.DATE_ADDED, dateTaken);
        image.put(Images.Media.DATE_TAKEN, dateTaken);
        image.put(Images.Media.DATE_MODIFIED, dateTaken);
        image.put(Images.Media.MIME_TYPE, "image/jpg");
        image.put(Images.Media.ORIENTATION, 0);
        String path =  output.getParentFile().toString().toLowerCase();
        String name =  output.getParentFile().getName().toLowerCase();
        image.put(Images.ImageColumns.BUCKET_ID, path.hashCode());
        image.put(Images.ImageColumns.BUCKET_DISPLAY_NAME, name);
        image.put(Images.Media.SIZE, output.length());
        image.put(Images.Media.DATA, output.getAbsolutePath());
        
        OutputStream os;
        
        try {
            os = getContentResolver().openOutputStream(uri);
            os.write(arg0);
            os.flush();
            os.close();
            Toast.makeText(Main.this, fileName, Toast.LENGTH_SHORT).show();
        } catch (FileNotFoundException e) {
        } catch (IOException e) { }

        Log.d("Camera","Restart Preview");    
        mCamera.stopPreview();
        mCamera.startPreview();
        saveState = false;
    }

    public void onShutter() { }
}


สำหรับโค๊ดจะเน้นหนักไปที่ฟังก์ชัน onPictureTaken เป็นหลัก
เจ้าของบล็อกจึงตัดส่วนที่เคยอธิบายไปแล้วออก เพื่อให้ดูง่าย
ถ้าจะดูอธิบายโค๊ดส่วนเก่าๆ ดูที่ [Android Code] การใช้งานกล้อง

1. จะเห็นว่าคราวนี้มีการประกาศตัวแปร Boolean เพิ่มมาตัวนึง
ซึ่งจะใช้สำหรับเช็คสถานะของการถ่ายภาพ เดี๋ยวอธิบายให้อีกที
โดยกำหนดชื่อตัวแปรเป็น saveState พร้อมกับกำหนดเป็น false

2. ต่อจากของเก่า คราวนี้จะกำหนด OnClickListener ให้ mPreview
เพื่อที่ว่าเวลาผู้ใช้กดบนหน้าจอ ก็จะให้ทำการถ่ายภาพนั่นเอง

3. จะเห็นว่ามีการเช็คค่าของตัวแปร saveState ด้วยว่าเป็น false หรือไม่
สำหรับผู้ที่หลงเข้ามาอ่านคนใดสงสัยว่าทำไมต้อง !saveState ด้วย
ก็ให้เข้าใจว่า saveState == false กับ !saveState คืออันเดียวกัน
ถ้า saveState มีค่าเป็น false ก็จะให้เป็น true แล้วทำการถ่ายภาพ
ดังนั้นระหว่างการถ่ายภาพนี้ตัวแปร saveState จะเป็น true แล้ว
ถ้าผู้ใช้กดถ่ายซ้ำระหว่างนี้ก็จะไม่มีผล เพราะ saveState เป็น true

ที่ต้องมีการใช้ saveState ช่วยก็เพื่อป้องกันผู้ใช้กดถ่ายรัวๆ
เพราะถ้ากดถ่ายรัวๆแล้วโปรแกรมยังเซฟภาพถ่ายเดิมไม่เสร็จ
ก็จะเซฟภาพใหม่ซ้อนขึ้นมาอีกเรื่อยๆ จะทำให้เออเรอได้
การใช้ Listener แบบนี้ เมื่อทำงานซ้อนกัน จะไม่ทับของเก่านะ
แต่ว่าจะทำงานแยกขึ้นมาอีกชุด ถ้ากดถ่าย 3 ครั้งติดๆกัน
onClickListener ก็จะทำงานซ้อนขึ้นมา 3 ตัว (หลายๆ Thread)

คำสั่งถ่ายภาพก็จะมีการเรียก Callback ของ shutter กับ jpeg
โดยให้ Callback ไปที่ Main.this จึงต้องประกาศ Implement ด้วย



4. ฟังก์ชัน onPictureTaken ก็มาจาก Implement ว่า Camera.PictureCallback
เมื่อผู้ใช้กดถ่ายภาพ แล้วกล้องถ่ายภาพเสร็จแล้วฟังก์ชันนี้ก็จะทำงาน
โดยจะมีการส่งข้อมูลเข้ามาด้วยกันสองตัวคือ byte[] กับ Camera
โดยที่ byte[] คือข้อมูลของภาพที่อยู่ในรูป Byte Array นั่นแหละ
ส่วน Camera ก็คือคลาสของกล้องที่เรียกใช้งานอยู่ในตอนนั้น


5. สำหรับคำสั่งกลุ่มนี้หลักๆก็คือการกำหนดที่อยู่และชื่อไฟล์ที่จะบันทึก
โดยการกำหนดชื่อไฟล์จะมีการเช็คก่อนว่าชื่อไฟล์ซ้ำกับของเก่าหรือไม่
เพื่อป้องภาพถ่ายไปบันทึกทับไฟล์ที่มีอยู่ก่อนแล้ว (เรื่องนี้สำคัญเลยล่ะ)
โดยที่อยู่ที่จะเก็บไว้ที่ DCIM/CameraSnap ส่วนชื่อไฟล์เริ่มจาก IMG_0.jpg
แล้วทำการเช็คก่อนว่าในโฟลเดอร์ DCIM/CameraSnap มีชื่อไฟล์นี้หรือไม่
ถ้าไม่มีก็ให้ใช้ชือไฟล์เป็น IMG_0.jpg แต่ถ้ามีซ้ำก็ให้เปลี่ยนเป็น IMG_1.jpg
แล้วก็จะเช็คต่อว่ามีชื่อไฟล์นี้ซ้ำหรือไม่ ถ้ามีซ้ำก็เปลี่ยนเป็น IMG_2.jpg
จะเช็คแบบนี้ไปเรื่อยๆ จนกว่าจะได้ชื่อไฟล์ที่ไม่ซ้ำกับที่มีอยู่แล้วนั่นเอง

6. คำสั่งกลุ่มนี้สำหรับกำหนดค่า Description ต่างๆให้กับไฟล์ภาพนั้นๆ
ก็จะกำหนดค่าต่างๆ เช่น ชื่อไฟล์, วันที่บันทึกภาพ หรือประเภทไฟล์เป็นต้น

7. เมื่อเตรียมพร้อมทุกอย่างแล้วก็ทำการบันทึกไฟล์ภาพต่อเลย
โดยจะใช้คลาส OutputStream ที่ทำหน้าที่บันทึกไฟล์ลงในเครื่อง
โดยใช้ค่าต่างๆที่ได้กำหนดไว้ใน 5 กับ 6 แล้วบันทึกลงเครื่อง
พอเสร็จแล้ว ก็แสดงข้อความผ่าน Toast ว่าบันทึกเป็นชื่อไฟล์อะไร

8. หลังจากที่ถ่ายภาพ ภาพที่ถ่ายจะแสดงให้เห็นบนหน้าจอ
จึงต้องปิด Surface View แล้วเปิดใหม่อีกครั้ง เพื่อทำงานต่อได้

9. ฟังก์ชันจากการ Implement ว่า ShutterCallback ที่จะทำงานเมื่อ
Shutter ของกล้องทำงาน ซึ่งเจ้าของบล็อกไม่ได้ใช้งานอะไร
จริงๆ จะไม่กำหนดให้มี ShutterCallback ก็ได้ ใช้คำสั่งแค่นี้พอ
mCamera.takePicture(null, null, null, Main.this);


main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <SurfaceView
        android:id="@+id/preview"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />

</RelativeLayout>


AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="app.akexorcist.camerasnap"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="10"
        android:targetSdkVersion="10" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher" >
        <activity
            android:name="Main"
            android:label="@string/app_name" 
            android:screenOrientation="landscape">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    
</manifest>



สำหรับผู้ที่หลงเข้ามาอ่านผู้ใดต้องการไฟล์ตัวอย่าง
สามารถดาวน์โหลดได้ที่ Camera Snap [Google Drive]


ยังไม่จบนะ มีต่ออีก




เหล่าพันธมิตรแอนดรอยด์

Devahoy Layer Net NuuNeoI The Cheese Factory Somkiat CC Mart Routine Artit-K Arnondora Kamonway Try to be android developer Oatrice Benz Nest Studios Kotchaphan@Medium Jirawatee@Medium Travispea@Medium