สร้างเว็บไซต์ที่เรียบง่ายซึ่งจําแนกประเภทรูปภาพ

1. ข้อควรทราบก่อนที่จะเริ่มต้น

ใน Codelab นี้ คุณรู้วิธีเรียกใช้การจําแนกประเภทรูปภาพจากเว็บไซต์โดยใช้การแสดง TensorFlow กับ REST และ gRPC

สิ่งที่ต้องมีก่อน

  • ความรู้เบื้องต้นเกี่ยวกับการพัฒนาเว็บ เช่น HTML และ JavaScript
  • ความรู้เบื้องต้นเกี่ยวกับแมชชีนเลิร์นนิงด้วย TensorFlow เช่น การฝึกอบรมและการทําให้ใช้งานได้
  • ความรู้เบื้องต้นเกี่ยวกับเทอร์มินัลและ Docker

สิ่งที่คุณจะได้เรียนรู้

  • วิธีค้นหาโมเดลการจัดประเภทรูปภาพล่วงหน้าใน TensorFlow Hub
  • วิธีสร้างเว็บไซต์แบบง่ายและคาดการณ์ด้วยโมเดลการจัดประเภทรูปภาพที่ดาวน์โหลดผ่าน TensorFlow Serving (REST และ gRPC)
  • วิธีแสดงผลการตรวจจับใน UI

สิ่งที่ต้องมี

2. ตั้งค่า

วิธีดาวน์โหลดโค้ดสําหรับ Codelab นี้

  1. ไปที่ที่เก็บ GitHub นี้
  2. คลิก Code > Download ZIP เพื่อดาวน์โหลดโค้ดทั้งหมดสําหรับ Codelab นี้

a72f2bb4caa9a96.png

  1. แตกไฟล์ ZIP ที่ดาวน์โหลดเพื่อคลายโฟลเดอร์รูท codelabs ด้วยทรัพยากรทั้งหมดที่คุณต้องการ

Codelab นี้ต้องการเฉพาะไฟล์ในไดเรกทอรีย่อย TFServing/ImageClassificationWeb ในที่เก็บ ซึ่งมี 2 โฟลเดอร์ดังนี้

  • โฟลเดอร์ starter มีโค้ดเริ่มต้นที่คุณสร้างสําหรับ Codelab นี้
  • โฟลเดอร์ finished มีโค้ดที่สมบูรณ์สําหรับแอปตัวอย่างที่สมบูรณ์

3. ติดตั้งการอ้างอิง

วิธีติดตั้งทรัพยากร Dependency

  • ไปที่โฟลเดอร์ starter แล้วติดตั้งเทอร์มินัล NPM ที่จําเป็นในเทอร์มินัล
npm install

4. เรียกใช้เว็บไซต์เริ่มต้น

ใช้เว็บเซิร์ฟเวอร์สําหรับ Chrome เพื่อโหลดไฟล์ TFServing/ImageClassificationWeb/starter/dist/index.html โดยทําดังนี้

  1. ป้อน Chrome://apps/ ในแถบที่อยู่ของ Chrome จากนั้นค้นหา เว็บเซิร์ฟเวอร์สําหรับ Chrome ในรายการแอป
  2. เปิดเว็บเซิร์ฟเวอร์สําหรับ Chrome แล้วเลือกโฟลเดอร์ TFServing/ImageClassificationWeb/starter/dist/
  3. คลิกปุ่มสลับเว็บเซิร์ฟเวอร์เพื่อเปิดใช้ แล้วไปที่ http://localhost:8887/ ในเบราว์เซอร์

f7b43cd44ebf1f1b.png

เรียกใช้และสํารวจเว็บไซต์

คุณควรเห็นเว็บไซต์ในตอนนี้ UI มีความเรียบง่าย: จะมีรูปภาพแมวที่คุณต้องการแยกประเภทและผู้ใช้สามารถส่งข้อมูลไปยังแบ็กเอนด์ด้วย REST หรือ gRPC แบ็กเอนด์จะแยกประเภทรูปภาพในรูปภาพและส่งคืนผลการค้นหาการแยกประเภทไปยังเว็บไซต์ ซึ่งแสดงผลการค้นหา

837d97a27c59a0b3.png

หากคุณคลิกแยกประเภท ไม่มีอะไรเกิดขึ้นเนื่องจากยังสื่อสารกับแบ็กเอนด์ไม่ได้

5. ทําให้โมเดลการจัดประเภทรูปภาพใช้งานได้ด้วยการแสดงผล TensorFlow

การแยกประเภทรูปภาพเป็นงาน ML ที่พบบ่อยซึ่งจัดประเภทรูปภาพเป็นหมวดหมู่ที่กําหนดไว้ล่วงหน้าโดยอิงตามเนื้อหาหลักของรูปภาพ ตัวอย่างการจัดประเภทดอกไม้มีดังนี้

a6da16b4a7665db0.png

มีโมเดลการจัดประเภทรูปภาพไว้ล่วงหน้าแล้วใน TensorFlow Hub คุณใช้โมเดล Inception v3 ยอดนิยมสําหรับ Codelab นี้

หากต้องการทําให้โมเดลการจัดประเภทรูปภาพใช้งานได้ด้วยการแสดงผล TensorFlow ให้ทําดังนี้

  1. ดาวน์โหลดไฟล์โมเดล Inception v3
  2. ยกเลิกการบีบอัดไฟล์ .tar.gz โดยใช้เครื่องมือขยายข้อมูลที่บีบอัด เช่น 7-Zip
  3. สร้างโฟลเดอร์ inception_v3 แล้วสร้างโฟลเดอร์ย่อย 123 ในโฟลเดอร์นั้น
  4. วางโฟลเดอร์ variables และไฟล์ saved_model.pb ที่แยกไว้ในโฟลเดอร์ย่อย 123

คุณจะอ้างอิงโฟลเดอร์ inception_v3 ว่าเป็นโฟลเดอร์ SavedModel ก็ได้ 123 เป็นหมายเลขเวอร์ชันตัวอย่าง คุณเลือกหมายเลขอื่นได้หากต้องการ

โครงสร้างโฟลเดอร์ควรมีลักษณะเช่นนี้

21a8675ac8d31907.png

เริ่มแสดงโฆษณา TensorFlow

  • จากนั้นเทอร์มินัล เริ่มแสดง TensorFlow ด้วย Docker แต่แทนที่ PATH/TO/SAVEDMODEL ด้วยเส้นทางสัมบูรณ์ของโฟลเดอร์ inception_v3 ในคอมพิวเตอร์
docker pull tensorflow/serving

docker run -it --rm -p 8500:8500 -p 8501:8501 -v "PATH/TO/SAVEDMODEL:/models/inception" -e MODEL_NAME=inception tensorflow/serving

Docker จะดาวน์โหลดอิมเมจการแสดงผล TensorFlow โดยอัตโนมัติก่อน ซึ่งจะใช้เวลา 1 นาที หลังจากนั้น TensorFlow Serving ควรจะเริ่มแสดง บันทึกควรมีลักษณะเหมือนข้อมูลโค้ดนี้

2022-02-25 06:01:12.513231: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:206] Restoring SavedModel bundle.
2022-02-25 06:01:12.585012: I external/org_tensorflow/tensorflow/core/platform/profile_utils/cpu_utils.cc:114] CPU Frequency: 3000000000 Hz
2022-02-25 06:01:13.395083: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:190] Running initialization op on SavedModel bundle at path: /models/inception/123
2022-02-25 06:01:13.837562: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:277] SavedModel load for tags { serve }; Status: success: OK. Took 1928700 microseconds.
2022-02-25 06:01:13.877848: I tensorflow_serving/servables/tensorflow/saved_model_warmup_util.cc:59] No warmup data file found at /models/inception/123/assets.extra/tf_serving_warmup_requests
2022-02-25 06:01:13.929844: I tensorflow_serving/core/loader_harness.cc:87] Successfully loaded servable version {name: inception version: 123}
2022-02-25 06:01:13.985848: I tensorflow_serving/model_servers/server_core.cc:486] Finished adding/updating models
2022-02-25 06:01:13.985987: I tensorflow_serving/model_servers/server.cc:367] Profiler service is enabled
2022-02-25 06:01:13.988994: I tensorflow_serving/model_servers/server.cc:393] Running gRPC ModelServer at 0.0.0.0:8500 ...
[warn] getaddrinfo: address family for nodename not supported
2022-02-25 06:01:14.033872: I tensorflow_serving/model_servers/server.cc:414] Exporting HTTP/REST API at:localhost:8501 ...
[evhttp_server.cc : 245] NET_LOG: Entering the event loop ...

6. ตั้งค่าพร็อกซี Envoy

ปัจจุบัน TensorFlow Serving ไม่ตั้งค่าส่วนหัว Access-Control-Allow-Origin ดังนั้นเบราว์เซอร์จะบล็อกคําขอจาก JavaScript ฟรอนท์เอนด์เป็น TensorFlow Serving ด้วยเหตุผลด้านความปลอดภัย วิธีแก้ปัญหานี้คือใช้พร็อกซี เช่น Envoy เพื่อพร็อกซีคําขอจาก JavaScript ไปยังแบ็กเอนด์ TensorFlow Serving

เริ่ม Envoy

  • ในเทอร์มินัล ให้ดาวน์โหลดอิมเมจ Envoy และเริ่ม Envoy ด้วย Docker แต่แทนที่ตัวยึดตําแหน่ง PATH/TO/ENVOY-CUSTOM.YAML ด้วยเส้นทางสัมบูรณ์ของไฟล์ envoy-custom.yaml ในโฟลเดอร์ starter
docker pull envoyproxy/envoy-dev:fd3e8370ddb7a96634c192d1461516e6de1d1797

docker run --add-host host.docker.internal:host-gateway --rm -it -p 9901:9901 -p 8000:8000 -p 8080:8080 -v PATH/TO/ENVOY-CUSTOM.YAML:/envoy-custom.yaml envoyproxy/envoy-dev:fd3e8370ddb7a96634c192d1461516e6de1d1797 -c /envoy-custom.yaml

Docker จะดาวน์โหลดอิมเมจ Envoy โดยอัตโนมัติก่อน หลังจากนั้น Envoy ควรเริ่มทํางาน บันทึกควรมีลักษณะเหมือนข้อมูลโค้ดนี้

[2022-03-02 07:51:48.563][1][info][main] [source/server/server.cc:436]   response trailer map: 152 bytes: grpc-message,grpc-status
[2022-03-02 07:51:48.681][1][info][main] [source/server/server.cc:772] runtime: {}
[2022-03-02 07:51:48.682][1][info][admin] [source/server/admin/admin.cc:134] admin address: 0.0.0.0:9901
[2022-03-02 07:51:48.683][1][info][config] [source/server/configuration_impl.cc:127] loading tracing configuration
[2022-03-02 07:51:48.683][1][info][config] [source/server/configuration_impl.cc:87] loading 0 static secret(s)
[2022-03-02 07:51:48.683][1][info][config] [source/server/configuration_impl.cc:93] loading 2 cluster(s)
[2022-03-02 07:51:48.687][1][info][config] [source/server/configuration_impl.cc:97] loading 2 listener(s)
[2022-03-02 07:51:48.694][1][info][config] [source/server/configuration_impl.cc:109] loading stats configuration
[2022-03-02 07:51:48.696][1][info][main] [source/server/server.cc:868] starting main dispatch loop
[2022-03-02 07:51:48.881][1][info][runtime] [source/common/runtime/runtime_impl.cc:446] RTDS has finished initialization
[2022-03-02 07:51:48.881][1][info][upstream] [source/common/upstream/cluster_manager_impl.cc:207] cm init: all clusters initialized
[2022-03-02 07:51:48.881][1][info][main] [source/server/server.cc:849] all clusters initialized. initializing init manager
[2022-03-02 07:51:48.881][1][info][config] [source/server/listener_manager_impl.cc:784] all dependencies initialized. starting workers
[2022-03-02 07:51:48.902][1][warning][main] [source/server/server.cc:747] there is no configured limit to the number of allowed active connections. Set a limit via the runtime key overload.global_downstream_max_connections

7. เชื่อมต่อเว็บไซต์กับ TensorFlow ผ่าน REST

แบ็กเอนด์พร้อมแล้วเพื่อให้คุณส่งคําขอไคลเอ็นต์ไปยัง TensorFlow Serving เพื่อจัดประเภทรูปภาพได้ คุณส่งคําขอไปยังการแสดง TensorFlow ได้ 2 วิธีดังนี้

  • REST
  • gRPC

ส่งคําขอและรับการตอบกลับผ่าน REST

การส่งและรับคําขอผ่าน REST มี 3 ขั้นตอนง่ายๆ ดังนี้

  1. สร้างคําขอ REST
  2. ส่งคําขอ REST ไปยังการแสดงผล TensorFlow
  3. ดึงผลลัพธ์ที่คาดการณ์ไว้จากการตอบกลับของ REST และแสดงผลลัพธ์

คุณจะทําขั้นตอนเหล่านี้ได้ในไฟล์ src/index.js

สร้างคําขอ REST

ขณะนี้ฟังก์ชัน classify_img() ไม่ได้ส่งคําขอ REST ไปยังการแสดงผล TensorFlow คุณต้องใช้สาขา REST นี้เพื่อสร้างคําขอ REST ก่อน

if (radioButtons[0].checked) {
    console.log('Using REST');
    // TODO: Add code to send a REST request to TensorFlow Serving.

} 

การแสดงผล TensorFlow ต้องการคําขอ POST ที่มี Tensor รูปภาพสําหรับโมเดล Inception v3 ที่คุณใช้ ดังนั้นคุณต้องดึงค่า RGB จากแต่ละพิกเซลของรูปภาพไปยังอาร์เรย์ แล้วรวมอาร์เรย์ไว้ใน JSON ซึ่งเป็นเพย์โหลดของคําขอ

  • เพิ่มโค้ดนี้ลงในสาขา REST
//Create the REST request.
let imgTensor = new Array();
let pixelArray = new Array();
context.drawImage(img, 0, 0);
for(let i=0; i<inputImgHeight; i++) {
    pixelArray[i] = new Array();
    for (let j=0; j<inputImgWidth; j++) {
        pixelArray[i][j] = new Array();
        pixelArray[i][j].push(context.getImageData(i, j, 1, 1).data[0]/255); 
        pixelArray[i][j].push(context.getImageData(i, j, 1, 1).data[1]/255); 
        pixelArray[i][j].push(context.getImageData(i, j, 1, 1).data[2]/255); 
    }
}
imgTensor.push(pixelArray);

const RESTURL = 'http://localhost:8000/v1/models/inception:predict';        
let xhr = new XMLHttpRequest();
xhr.open('POST', RESTURL);
xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8;');
let data = JSON.stringify({
    instances: imgTensor
});    
xhr.onload = () => {

}
xhr.onerror = () => {
    console.log('REST request error');
}

ส่งคําขอ REST ไปยังการแสดงโฆษณา TensorFlow

ตอนนี้คุณส่งคําขอได้แล้ว

  • เพิ่มโค้ดนี้หลังโค้ดด้านบนในสาขา REST
// Send the REST request.
xhr.send(data);

ประมวลผลการตอบกลับ REST จาก TensorFlow Serving

โมเดล Inception v3 จะแสดงผลอาร์เรย์ของความน่าจะเป็นที่รูปภาพอยู่ในหมวดหมู่ที่กําหนดไว้ล่วงหน้า เมื่อการคาดการณ์ประสบความสําเร็จ คุณควรโหลดหมวดหมู่ที่น่าจะเป็นมากที่สุดใน UI

คุณใช้ Listener onload() เพื่อจัดการการตอบกลับ

xhr.onload = () => {

}
  • เพิ่มโค้ดนี้ลงใน Listener onload():
// Process the REST response.
const response = JSON.parse(xhr.responseText);
const maxIndex = argmax(response['predictions'][0])
document.getElementById('category').textContent = 'Predicted category: ' + maxIndex;

ตอนนี้ Listener จะดึงความน่าจะเป็นที่คาดการณ์จากการตอบสนอง ระบุหมวดหมู่ที่น่าจะเป็นของออบเจ็กต์มากที่สุด และแสดงผลลัพธ์ใน UI

เรียกใช้

  1. ไปที่โฟลเดอร์ starter แล้วใช้ Webpack เพื่อรวมไฟล์ JavaScript ทั้งหมดเป็นไฟล์เดียวที่คุณฝังในไฟล์ dist/index.html ได้
npm install -g npx
npm install --save-dev webpack
npx webpack
  1. รีเฟรช http://localhost:8887/ ในเบราว์เซอร์ แล้วคลิก REST > Classify

เว็บไซต์แสดง 286 เป็นหมวดหมู่ที่คาดการณ์ ซึ่งจะแมปกับป้ายกํากับ Egyptian Cat ในชุดข้อมูล ImageNet

c865a93b9b58335d.png

8. เชื่อมต่อเว็บไซต์กับ TensorFlow Serving ผ่าน gRPC

นอกจาก REST แล้ว TensorFlow Serving ยังรองรับ gRPC อีกด้วย

b6f4449c2c850b0e.png

gRPC เป็นเฟรมเวิร์ก Call Procedure Call (RPC) แบบโอเพนซอร์สที่มีประสิทธิภาพสูงและใช้งานได้กับทุกสภาพแวดล้อม เครื่องมือนี้จะเชื่อมบริการอย่างมีประสิทธิภาพทั่วทั้งศูนย์ข้อมูลต่างๆ ที่รองรับความสามารถในการจัดสรรภาระงาน การติดตาม การตรวจสอบประสิทธิภาพการทํางาน และการตรวจสอบสิทธิ์แบบเสียบปลั๊กได้ พบว่า gRPC มีประสิทธิภาพมากกว่า REST ในทางปฏิบัติ

ส่งคําขอและรับการตอบกลับด้วย gRPC

มี 4 ขั้นตอนง่ายๆ ดังนี้

  1. ไม่บังคับ: สร้างโค้ดไคลเอ็นต์ gRPC
  2. สร้างคําขอ gRPC
  3. ส่งคําขอ gRPC ไปยัง TensorFlow Serving
  4. ดึงผลลัพธ์ที่คาดการณ์ไว้จากการตอบกลับของ gRPC แล้วแสดงใน UI

คุณทําตามขั้นตอนเหล่านี้ในไฟล์ src/index.js ได้

ไม่บังคับ: สร้างโค้ดไคลเอ็นต์ gRPC

หากต้องการใช้ gRPC กับการให้บริการ TensorFlow คุณต้องทําตามเวิร์กโฟลว์ gRPC ดูข้อมูลเพิ่มเติมเกี่ยวกับรายละเอียดได้ที่เอกสารประกอบ gRPC

a9d0e5cb543467b4.png

TensorFlow Serving และ TensorFlow จะกําหนดไฟล์ .proto ให้คุณ เมื่อคํานึงถึง TensorFlow และ TensorFlow Serving 2.8 จึงต้องมีไฟล์ .proto รายการต่อไปนี้

tensorflow/core/example/example.proto
tensorflow/core/example/feature.proto
tensorflow/core/protobuf/struct.proto
tensorflow/core/protobuf/saved_object_graph.proto
tensorflow/core/protobuf/saver.proto
tensorflow/core/protobuf/trackable_object_graph.proto
tensorflow/core/protobuf/meta_graph.proto
tensorflow/core/framework/node_def.proto
tensorflow/core/framework/attr_value.proto
tensorflow/core/framework/function.proto
tensorflow/core/framework/types.proto
tensorflow/core/framework/tensor_shape.proto
tensorflow/core/framework/full_type.proto
tensorflow/core/framework/versions.proto
tensorflow/core/framework/op_def.proto
tensorflow/core/framework/graph.proto
tensorflow/core/framework/tensor.proto
tensorflow/core/framework/resource_handle.proto
tensorflow/core/framework/variable.proto

tensorflow_serving/apis/inference.proto
tensorflow_serving/apis/classification.proto
tensorflow_serving/apis/predict.proto
tensorflow_serving/apis/regression.proto
tensorflow_serving/apis/get_model_metadata.proto
tensorflow_serving/apis/input.proto
tensorflow_serving/apis/prediction_service.proto
tensorflow_serving/apis/model.proto
  • จากนั้นไปที่โฟลเดอร์ starter/src/proto/ แล้วสร้างเทอร์มินัลไปที่เทอร์มินัล
bash generate_grpc_stub_js.sh

สร้างคําขอ gRPC

คุณสร้างคําขอ gRPC ในสาขา gRPC ได้เช่นเดียวกับคําขอ REST.

if (connectionMode[picker.selectedRow(inComponent: 0)] == "REST") {

}
else {
    print("Using gRPC")
    // TODO: Add code to send a gRPC request to TensorFlow Serving.
    
}
  • เพิ่มโค้ดนี้ลงในสาขา gRPC
// Create the gRPC request.
const PredictModule = require('./proto/generated/tensorflow_serving/apis/predict_pb.js');
const PredictionServiceClientModule = require('./proto/generated/tensorflow_serving/apis/prediction_service_grpc_web_pb.js');
const ModelModule = require('./proto/generated/tensorflow_serving/apis/model_pb.js');
const TensorModule = require('./proto/generated/tensorflow/core/framework/tensor_pb.js');

const GPRCURL = 'http://localhost:8080';
const stub = new PredictionServiceClientModule.PredictionServiceClient(GPRCURL);

const modelSpec = new ModelModule.ModelSpec();
modelSpec.setName('inception');

const tensorProto = new TensorModule.TensorProto();
const tensorShapeProto = new TensorModule.TensorShapeProto();

const batchDim = (new TensorModule.TensorShapeProto.Dim()).setSize(1);
const heightDim = (new TensorModule.TensorShapeProto.Dim()).setSize(inputImgHeight);
const widthDim = (new TensorModule.TensorShapeProto.Dim()).setSize(inputImgWidth);
const channelDim = (new TensorModule.TensorShapeProto.Dim()).setSize(3);

tensorShapeProto.setDimList([batchDim, heightDim, widthDim, channelDim]);

tensorProto.setDtype(proto.tensorflow.DataType.DT_FLOAT);
tensorProto.setTensorShape(tensorShapeProto);
context.drawImage(img, 0, 0);
for(let i=0; i<inputImgHeight; i++) {
    for (let j=0; j<inputImgWidth; j++) {
        tensorProto.addFloatVal(context.getImageData(i, j, 1, 1).data[0]/255); 
        tensorProto.addFloatVal(context.getImageData(i, j, 1, 1).data[1]/255); 
        tensorProto.addFloatVal(context.getImageData(i, j, 1, 1).data[2]/255); 
    }
}

const predictionServiceRequest = new PredictModule.PredictRequest();
predictionServiceRequest.setModelSpec(modelSpec);
predictionServiceRequest.getInputsMap().set('inputs', tensorProto);

ส่งคําขอ gRPC ไปยังการแสดงผล TensorFlow

ตอนนี้คุณส่งคําขอได้แล้ว

  • เพิ่มโค้ดนี้หลังรหัสในสาขา gRPC ในข้อมูลโค้ดก่อนหน้า
// Send the gRPC request.
stub.predict(predictionServiceRequest, {}, function(err, response) {
    // TODO: Add code to process the response.
});

ประมวลผลการตอบสนอง gRPC จาก TensorFlow Serving

สุดท้าย ให้คุณใช้ฟังก์ชันเรียกกลับด้านบนเพื่อจัดการกับการตอบกลับ

  • เพิ่มโค้ดนี้ลงในเนื้อหาของฟังก์ชันในข้อมูลโค้ดก่อนหน้า
// Process the gRPC response.
if (err) {
    console.log(err.code);
    console.log(err.message);
} 
else {
    const maxIndex = argmax(response.getOutputsMap().get('logits').getFloatValList());
    document.getElementById('category').textContent = 'Predicted category: ' + maxIndex;
}

ตอนนี้ Listener จะดึงความน่าจะเป็นที่คาดการณ์จากการตอบสนอง ระบุหมวดหมู่ที่น่าจะเป็นของออบเจ็กต์มากที่สุด และแสดงผลลัพธ์ใน UI

เรียกใช้

  1. ในเทอร์มินัล ให้ใช้ Webpack เพื่อรวมไฟล์ JavaScript ทั้งหมดเป็นไฟล์เดียวที่คุณฝังในไฟล์ index.html ได้
npx webpack
  1. รีเฟรช http://localhost:8887/ ในเบราว์เซอร์
  2. คลิก gRPC > จัดประเภท

เว็บไซต์แสดงหมวดหมู่ที่คาดของ 286 ซึ่งแมปกับป้ายกํากับ Egyptian Cat ในชุดข้อมูล ImageNet

9. ยินดีด้วย

คุณใช้ TensorFlow Serving เพื่อเพิ่มความสามารถในการแยกประเภทรูปภาพลงในเว็บไซต์แล้ว

ดูข้อมูลเพิ่มเติม