Browse Source

[Feat] Support serving pedestrian attribute recognition pipeline (#2437)

* Extract common functions

* Add pedestrian attribute recognition serving app
Lin Manhui 1 year ago
parent
commit
42ed9a5e7a

+ 94 - 334
docs/pipeline_usage/tutorials/cv_pipelines/pedestrian_attribute_recognition.en.md

@@ -252,40 +252,40 @@ Below are the API reference and multi-language service invocation examples:
 
 <p>For all operations provided by the service:</p>
 <ul>
-<li>The response body and the request body of POST requests are both JSON data (JSON objects).</li>
-<li>When the request is successfully processed, the response status code is <code>200</code>, and the attributes of the response body are as follows:</li>
+<li>Both the response body and the request body for POST requests are JSON data (JSON objects).</li>
+<li>When the request is processed successfully, the response status code is <code>200</code>, and the response body properties are as follows:</li>
 </ul>
 <table>
 <thead>
 <tr>
 <th>Name</th>
 <th>Type</th>
-<th>Meaning</th>
+<th>Description</th>
 </tr>
 </thead>
 <tbody>
 <tr>
 <td><code>errorCode</code></td>
 <td><code>integer</code></td>
-<td>Error code. Fixed to <code>0</code>.</td>
+<td>Error code. Fixed as <code>0</code>.</td>
 </tr>
 <tr>
 <td><code>errorMsg</code></td>
 <td><code>string</code></td>
-<td>Error description. Fixed to <code>"Success"</code>.</td>
+<td>Error description. Fixed as <code>"Success"</code>.</td>
 </tr>
 </tbody>
 </table>
-<p>The response body may also have a <code>result</code> attribute of type <code>object</code>, which stores the operation result information.</p>
+<p>The response body may also have a <code>result</code> property of type <code>object</code>, which stores the operation result information.</p>
 <ul>
-<li>When the request is not successfully processed, the attributes of the response body are as follows:</li>
+<li>When the request is not processed successfully, the response body properties are as follows:</li>
 </ul>
 <table>
 <thead>
 <tr>
 <th>Name</th>
 <th>Type</th>
-<th>Meaning</th>
+<th>Description</th>
 </tr>
 </thead>
 <tbody>
@@ -301,21 +301,21 @@ Below are the API reference and multi-language service invocation examples:
 </tr>
 </tbody>
 </table>
-<p>The operations provided by the service are as follows:</p>
+<p>Operations provided by the service are as follows:</p>
 <ul>
 <li><b><code>infer</code></b></li>
 </ul>
-<p>Obtain OCR results for an image.</p>
-<p><code>POST /ocr</code></p>
+<p>Get pedestrian attribute recognition results.</p>
+<p><code>POST /pedestrian-attribute-recognition</code></p>
 <ul>
-<li>The attributes of the request body are as follows:</li>
+<li>The request body properties are as follows:</li>
 </ul>
 <table>
 <thead>
 <tr>
 <th>Name</th>
 <th>Type</th>
-<th>Meaning</th>
+<th>Description</th>
 <th>Required</th>
 </tr>
 </thead>
@@ -323,18 +323,88 @@ Below are the API reference and multi-language service invocation examples:
 <tr>
 <td><code>image</code></td>
 <td><code>string</code></td>
-<td>The URL of an accessible image file or the Base64 encoded result of the image file content.</td>
+<td>The URL of an image file accessible by the service or the Base64 encoded result of the image file content.</td>
 <td>Yes</td>
 </tr>
+</tbody>
+</table>
+<ul>
+<li>When the request is processed successfully, the <code>result</code> of the response body has the following properties:</li>
+</ul>
+<table>
+<thead>
+<tr>
+<th>Name</th>
+<th>Type</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td><code>pedestrians</code></td>
+<td><code>array</code></td>
+<td>Information about the pedestrian's location and attributes.</td>
+</tr>
+<tr>
+<td><code>image</code></td>
+<td><code>string</code></td>
+<td>The pedestrian attribute recognition result image. The image is in JPEG format and encoded using Base64.</td>
+</tr>
+</tbody>
+</table>
+<p>Each element in <code>pedestrians</code> is an <code>object</code> with the following properties:</p>
+<table>
+<thead>
+<tr>
+<th>Name</th>
+<th>Type</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td><code>bbox</code></td>
+<td><code>array</code></td>
+<td>The location of the pedestrian. The elements in the array are the x-coordinate of the top-left corner, the y-coordinate of the top-left corner, the x-coordinate of the bottom-right corner, and the y-coordinate of the bottom-right corner of the bounding box, respectively.</td>
+</tr>
+<tr>
+<td><code>attributes</code></td>
+<td><code>array</code></td>
+<td>The pedestrian attributes.</td>
+</tr>
 <tr>
-<td><code>inferenceParams</code></td>
-<td><code>object</code></td>
-<td>Inference parameters.</td>
-<td>No</td>
+<td><code>score</code></td>
+<td><code>number</code></td>
+<td>The detection score.</td>
 </tr>
 </tbody>
 </table>
-<p>The attributes of```markdown</p>
+<p>Each element in <code>attributes</code> is an <code>object</code> with the following properties:</p>
+<table>
+<thead>
+<tr>
+<th>Name</th>
+<th>Type</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td><code>label</code></td>
+<td><code>string</code></td>
+<td>The label of the attribute.</td>
+</tr>
+<tr>
+<td><code>score</code></td>
+<td><code>number</code></td>
+<td>The classification score.</td>
+</tr>
+</tbody>
+</table>
+</details>
+
+<details><summary>Multi-Language Service Invocation Examples</summary>
+
 <details>
 <summary>Python</summary>
 
@@ -342,337 +412,27 @@ Below are the API reference and multi-language service invocation examples:
 <pre><code class="language-python">import base64
 import requests
 
-API_URL = &quot;http://localhost:8080/ocr&quot; # Service URL
+API_URL = &quot;http://localhost:8080/pedestrian-attribute-recognition&quot;
 image_path = &quot;./demo.jpg&quot;
 output_image_path = &quot;./out.jpg&quot;
 
-# Encode the local image to Base64
 with open(image_path, &quot;rb&quot;) as file:
     image_bytes = file.read()
     image_data = base64.b64encode(image_bytes).decode(&quot;ascii&quot;)
 
-payload = {&quot;image&quot;: image_data}  # Base64 encoded file content or image URL
+payload = {&quot;image&quot;: image_data}
 
-# Call the API
 response = requests.post(API_URL, json=payload)
 
-# Process the response data
 assert response.status_code == 200
 result = response.json()[&quot;result&quot;]
 with open(output_image_path, &quot;wb&quot;) as file:
     file.write(base64.b64decode(result[&quot;image&quot;]))
 print(f&quot;Output image saved at {output_image_path}&quot;)
-print(&quot;\nDetected texts:&quot;)
-print(result[&quot;texts&quot;])
+print(&quot;\nDetected pedestrians:&quot;)
+print(result[&quot;pedestrians&quot;])
 </code></pre></details>
-
-<details><summary>C++</summary>
-
-<pre><code class="language-cpp">#include &lt;iostream&gt;
-#include &quot;cpp-httplib/httplib.h&quot; // https://github.com/Huiyicc/cpp-httplib
-#include &quot;nlohmann/json.hpp&quot; // https://github.com/nlohmann/json
-#include &quot;base64.hpp&quot; // https://github.com/tobiaslocker/base64
-
-int main() {
-    httplib::Client client(&quot;localhost:8080&quot;);
-    const std::string imagePath = &quot;./demo.jpg&quot;;
-    const std::string outputImagePath = &quot;./out.jpg&quot;;
-
-    httplib::Headers headers = {
-        {&quot;Content-Type&quot;, &quot;application/json&quot;}
-    };
-
-    // Encode the local image to Base64
-    std::ifstream file(imagePath, std::ios::binary | std::ios::ate);
-    std::streamsize size = file.tellg();
-    file.seekg(0, std::ios::beg);
-
-    std::vector&lt;char&gt; buffer(size);
-    if (!file.read(buffer.data(), size)) {
-        std::cerr &lt;&lt; &quot;Error reading file.&quot; &lt;&lt; std::endl;
-        return 1;
-    }
-    std::string bufferStr(reinterpret_cast&lt;const char*&gt;(buffer.data()), buffer.size());
-    std::string encodedImage = base64::to_base64(bufferStr);
-
-    nlohmann::json jsonObj;
-    jsonObj[&quot;image&quot;] = encodedImage;
-    std::string body = jsonObj.dump();
-
-    // Call the API
-    auto response = client.Post(&quot;/ocr&quot;, headers, body, &quot;application/json&quot;);
-    // Process the response data
-    if (response &amp;&amp; response-&gt;status == 200) {
-        nlohmann::json jsonResponse = nlohmann::json::parse(response-&gt;body);
-        auto result = jsonResponse[&quot;result&quot;];
-
-        encodedImage = result[&quot;image&quot;];
-        std::string decodedString = base64::from_base64(encodedImage);
-        std::vector&lt;unsigned char&gt; decodedImage(decodedString.begin(), decodedString.end());
-        std::ofstream outputImage(outputImagePath, std::ios::binary | std::ios::out);
-        if (outputImage.is_open()) {
-            outputImage.write(reinterpret_cast&lt;char*&gt;(decodedImage.data()), decodedImage.size());
-            outputImage.close();
-            std::cout &lt;&lt; &quot;Output image saved at &quot; &lt;&lt; outputImagePath &lt;&lt; std::endl;
-        } else {
-            std::cerr &lt;&lt; &quot;Unable to open file for writing: &quot; &lt;&lt; outputImagePath &lt;&lt; std::endl;
-        }
-
-        auto texts = result[&quot;texts&quot;];
-        std::cout &lt;&lt; &quot;\nDetected texts:&quot; &lt;&lt; std::endl;
-        for (const auto&amp; text : texts) {
-            std::cout &lt;&lt; text &lt;&lt; std::endl;
-        }
-    } else {
-        std::cout &lt;&lt; &quot;Failed to send HTTP request.&quot; &lt;&lt; std::endl;
-        return 1;
-    }
-
-    return 0;
-}
-
-</code></pre></details>
-``````markdown
-# Tutorial on Artificial Intelligence and Computer Vision
-
-This tutorial, intended for numerous developers, covers the basics and applications of AI and Computer Vision.
-
-<details><summary>Java</summary>
-
-<pre><code class="language-java">import okhttp3.*;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.Base64;
-
-public class Main {
-    public static void main(String[] args) throws IOException {
-        String API_URL = &quot;http://localhost:8080/ocr&quot;; // Service URL
-        String imagePath = &quot;./demo.jpg&quot;; // Local image path
-        String outputImagePath = &quot;./out.jpg&quot;; // Output image path
-
-        // Encode the local image to Base64
-        File file = new File(imagePath);
-        byte[] fileContent = java.nio.file.Files.readAllBytes(file.toPath());
-        String imageData = Base64.getEncoder().encodeToString(fileContent);
-
-        ObjectMapper objectMapper = new ObjectMapper();
-        ObjectNode params = objectMapper.createObjectNode();
-        params.put(&quot;image&quot;, imageData); // Base64-encoded file content or image URL
-
-        // Create an OkHttpClient instance
-        OkHttpClient client = new OkHttpClient();
-        MediaType JSON = MediaType.get(&quot;application/json; charset=utf-8&quot;);
-        RequestBody body = RequestBody.create(params.toString(), JSON);
-        Request request = new Request.Builder()
-                .url(API_URL)
-                .post(body)
-                .build();
-
-        // Call the API and process the response
-        try (Response response = client.newCall(request).execute()) {
-            if (response.isSuccessful()) {
-                String responseBody = response.body().string();
-                JsonNode resultNode = objectMapper.readTree(responseBody);
-                JsonNode result = resultNode.get(&quot;result&quot;);
-                String base64Image = result.get(&quot;image&quot;).asText();
-                JsonNode texts = result.get(&quot;texts&quot;);
-
-                byte[] imageBytes = Base64.getDecoder().decode(base64Image);
-                try (FileOutputStream fos = new FileOutputStream(outputImagePath)) {
-                    fos.write(imageBytes);
-                }
-                System.out.println(&quot;Output image saved at &quot; + outputImagePath);
-                System.out.println(&quot;\nDetected texts: &quot; + texts.toString());
-            } else {
-                System.err.println(&quot;Request failed with code: &quot; + response.code());
-            }
-        }
-    }
-}
-</code></pre></details>
-
-<details><summary>Go</summary>
-
-<pre><code class="language-go">package main
-
-import (
-    &quot;bytes&quot;
-    &quot;encoding/base64&quot;
-    &quot;encoding/json&quot;
-    &quot;fmt&quot;
-    &quot;io/ioutil&quot;
-    &quot;net/http&quot;
-)
-
-func main() {
-    API_URL := &quot;http://localhost:8080/ocr&quot;
-    imagePath := &quot;./demo.jpg&quot;
-    outputImagePath := &quot;./out.jpg&quot;
-
-    // Encode the local image to Base64
-    imageBytes, err := ioutil.ReadFile(imagePath)
-    if err != nil {
-        fmt.Println(&quot;Error reading image file:&quot;, err)
-        return
-    }
-    imageData := base64.StdEncoding.EncodeToString(imageBytes)
-
-    payload := map[string]string{&quot;image&quot;: imageData} // Base64-encoded file content or image URL
-    payloadBytes, err := json.Marshal(payload)
-    if err != nil {
-        fmt.Println(&quot;Error marshaling payload:&quot;, err)
-        return
-    }
-
-    // Call the API
-    client := &amp;http.Client{}
-    req, err := http.NewRequest(&quot;POST&quot;, API_URL, bytes.NewBuffer(payloadBytes))
-    if err != nil {
-        fmt.Println(&quot;Error creating request:&quot;, err)
-        return
-    }
-
-    res, err := client.Do(req)
-    if err != nil {
-        fmt.Println(&quot;Error sending request:&quot;, err)
-        return
-    }
-    defer res.Body.Close()
-
-    // Process the response
-    body, err := ioutil.ReadAll(res.Body)
-    if err != nil {
-        fmt.Println(&quot;Error reading response body:&quot;, err)
-        return
-    }```markdown
-# An English Tutorial on Artificial Intelligence and Computer Vision
-
-This tutorial document is intended for numerous developers and covers content related to artificial intelligence and computer vision.
-
-&lt;details&gt;
-&lt;summary&gt;C#&lt;/summary&gt;
-
-```csharp
-using System;
-using System.IO;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Text;
-using System.Threading.Tasks;
-using Newtonsoft.Json.Linq;
-
-class Program
-{
-static readonly string API_URL = &quot;http://localhost:8080/ocr&quot;;
-static readonly string imagePath = &quot;./demo.jpg&quot;;
-static readonly string outputImagePath = &quot;./out.jpg&quot;;
-
-static async Task Main(string[] args)
-{
-var httpClient = new HttpClient();
-
-// Encode the local image to Base64
-byte[] imageBytes = File.ReadAllBytes(imagePath);
-string image_data = Convert.ToBase64String(imageBytes);
-
-var payload = new JObject{ { &quot;image&quot;, image_data } }; // Base64 encoded file content or image URL
-var content = new StringContent(payload.ToString(), Encoding.UTF8, &quot;application/json&quot;);
-
-// Call the API
-HttpResponseMessage response = await httpClient.PostAsync(API_URL, content);
-response.EnsureSuccessStatusCode();
-
-// Process the API response
-string responseBody = await response.Content.ReadAsStringAsync();
-JObject jsonResponse = JObject.Parse(responseBody);
-
-string base64Image = jsonResponse[&quot;result&quot;][&quot;image&quot;].ToString();
-byte[] outputImageBytes = Convert.FromBase64String(base64Image);
-
-File.WriteAllBytes(outputImagePath, outputImageBytes);
-Console.WriteLine($&quot;Output image saved at {outputImagePath}&quot;);
-Console.WriteLine(&quot;\nDetected texts:&quot;);
-Console.WriteLine(jsonResponse[&quot;result&quot;][&quot;texts&quot;].ToString());
-}
-}
-</code></pre></details>
-
-<details><summary>Node.js</summary>
-
-<pre><code class="language-js">const axios = require('axios');
-const fs = require('fs');
-
-const API_URL = 'http://localhost:8080/ocr';
-const imagePath = './demo.jpg';
-const outputImagePath = &quot;./out.jpg&quot;;
-
-let config = {
-   method: 'POST',
-   maxBodyLength: Infinity,
-   url: API_URL,
-   data: JSON.stringify({
-    'image': encodeImageToBase64(imagePath)  // Base64 encoded file content or image URL
-  })
-};
-
-// Encode the local image to Base64
-function encodeImageToBase64(filePath) {
-  const bitmap = fs.readFileSync(filePath);
-  return Buffer.from(bitmap).toString('base64');
-}
-
-// Call the API
-axios.request(config)
-.then((response) =&gt; {
-    // Process the API response
-    const result = response.data[&quot;result&quot;];
-    const imageBuffer = Buffer.from(result[&quot;image&quot;], 'base64');
-    fs.writeFile(outputImagePath, imageBuffer, (err) =&gt; {
-      if (err) throw err;
-      console.log(`Output image saved at ${outputImagePath}`);
-    });
-    console.log(&quot;\nDetected texts:&quot;);
-    console.log(result[&quot;texts&quot;]);
-})
-.catch((error) =&gt; {
-  console.log(error);
-});
-</code></pre></details>
-
-<details>
-<summary>PHP</summary>
-
-```php
-<?php
-
-$API_URL = "http://localhost:8080/ocr"; // Service URL
-$image_path = "./demo.jpg";
-$output_image_path = "./out.jpg";
-
-// Encode the local image to Base64
-$image_data = base64_encode(file_get_contents($image_path));
-$payload = array("image" => $image_data); // Base64 encoded file content or image URL
-
-// Call the API
-$ch = curl_init($API_URL);
-curl_setopt($ch, CURLOPT_POST, true);
-curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
-curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
-$response = curl_exec($ch);
-curl_close($ch);
-
-// Process the API response
-$result = json_decode($response, true)["result"];
-file_put_contents($output
-```
-
-<details>
-<details>
+</details>
 <br/>
 
 📱 <b>Edge Deployment</b>: Edge deployment is a method where computing and data processing functions are placed on the user's device itself, allowing the device to process data directly without relying on remote servers. PaddleX supports deploying models on edge devices such as Android. For detailed edge deployment procedures, please refer to the [PaddleX Edge Deployment Guide](../../../pipeline_deploy/edge_deploy.en.md).

+ 32 - 413
docs/pipeline_usage/tutorials/cv_pipelines/pedestrian_attribute_recognition.md

@@ -305,8 +305,8 @@ for res in output:
 <ul>
 <li><b><code>infer</code></b></li>
 </ul>
-<p>获取图像OCR结果。</p>
-<p><code>POST /ocr</code></p>
+<p>获取行人属性识别结果。</p>
+<p><code>POST /pedestrian-attribute-recognition</code></p>
 <ul>
 <li>请求体的属性如下:</li>
 </ul>
@@ -326,36 +326,33 @@ for res in output:
 <td>服务可访问的图像文件的URL或图像文件内容的Base64编码结果。</td>
 <td>是</td>
 </tr>
-<tr>
-<td><code>inferenceParams</code></td>
-<td><code>object</code></td>
-<td>推理参数。</td>
-<td>否</td>
-</tr>
 </tbody>
 </table>
-<p><code>inferenceParams</code>的属性如下:</p>
+<ul>
+<li>请求处理成功时,响应体的<code>result</code>具有如下属性:</li>
+</ul>
 <table>
 <thead>
 <tr>
 <th>名称</th>
 <th>类型</th>
 <th>含义</th>
-<th>是否必填</th>
 </tr>
 </thead>
 <tbody>
 <tr>
-<td><code>maxLongSide</code></td>
-<td><code>integer</code></td>
-<td>推理时,若文本检测模型的输入图像较长边的长度大于<code>maxLongSide</code>,则将对图像进行缩放,使其较长边的长度等于<code>maxLongSide</code>。</td>
-<td>否</td>
+<td><code>pedestrians</code></td>
+<td><code>array</code></td>
+<td>行人的位置及属性等信息。</td>
+</tr>
+<tr>
+<td><code>image</code></td>
+<td><code>string</code></td>
+<td>行人属性识别结果图。图像为JPEG格式,使用Base64编码。</td>
 </tr>
 </tbody>
 </table>
-<ul>
-<li>请求处理成功时,响应体的<code>result</code>具有如下属性:</li>
-</ul>
+<p><code>pedestrians</code>中的每个元素为一个<code>object</code>,具有如下属性:</p>
 <table>
 <thead>
 <tr>
@@ -366,18 +363,23 @@ for res in output:
 </thead>
 <tbody>
 <tr>
-<td><code>texts</code></td>
+<td><code>bbox</code></td>
 <td><code>array</code></td>
-<td>文本位置、内容和得分。</td>
+<td>行人位置。数组中元素依次为边界框左上角x坐标、左上角y坐标、右下角x坐标以及右下角y坐标。</td>
 </tr>
 <tr>
-<td><code>image</code></td>
-<td><code>string</code></td>
-<td>OCR结果图,其中标注检测到的文本位置。图像为JPEG格式,使用Base64编码。</td>
+<td><code>attributes</code></td>
+<td><code>array</code></td>
+<td>行人属性。</td>
+</tr>
+<tr>
+<td><code>score</code></td>
+<td><code>number</code></td>
+<td>检测得分。</td>
 </tr>
 </tbody>
 </table>
-<p><code>texts</code>中的每个元素为一个<code>object</code>,具有如下属性:</p>
+<p><code>attributes</code>中的每个元素为一个<code>object</code>,具有如下属性:</p>
 <table>
 <thead>
 <tr>
@@ -388,73 +390,18 @@ for res in output:
 </thead>
 <tbody>
 <tr>
-<td><code>poly</code></td>
-<td><code>array</code></td>
-<td>文本位置。数组中元素依次为包围文本的多边形的顶点坐标。</td>
-</tr>
-<tr>
-<td><code>text</code></td>
+<td><code>label</code></td>
 <td><code>string</code></td>
-<td>文本内容。</td>
+<td>属性标签。</td>
 </tr>
 <tr>
 <td><code>score</code></td>
 <td><code>number</code></td>
-<td>文本识别得分。</td>
+<td>分类得分。</td>
 </tr>
 </tbody>
 </table>
-<p><code>result</code>示例如下:</p>
-<pre><code class="language-json">{
-&quot;texts&quot;: [
-{
-&quot;poly&quot;: [
-[
-444,
-244
-],
-[
-705,
-244
-],
-[
-705,
-311
-],
-[
-444,
-311
-]
-],
-&quot;text&quot;: &quot;北京南站&quot;,
-&quot;score&quot;: 0.9
-},
-{
-&quot;poly&quot;: [
-[
-992,
-248
-],
-[
-1263,
-251
-],
-[
-1263,
-318
-],
-[
-992,
-315
-]
-],
-&quot;text&quot;: &quot;天津站&quot;,
-&quot;score&quot;: 0.5
-}
-],
-&quot;image&quot;: &quot;xxxxxx&quot;
-}
-</code></pre></details>
+</details>
 
 <details><summary>多语言调用服务示例</summary>
 
@@ -465,7 +412,7 @@ for res in output:
 <pre><code class="language-python">import base64
 import requests
 
-API_URL = &quot;http://localhost:8080/ocr&quot; # 服务URL
+API_URL = &quot;http://localhost:8080/pedestrian-attribute-recognition&quot; # 服务URL
 image_path = &quot;./demo.jpg&quot;
 output_image_path = &quot;./out.jpg&quot;
 
@@ -485,336 +432,8 @@ result = response.json()[&quot;result&quot;]
 with open(output_image_path, &quot;wb&quot;) as file:
     file.write(base64.b64decode(result[&quot;image&quot;]))
 print(f&quot;Output image saved at {output_image_path}&quot;)
-print(&quot;\nDetected texts:&quot;)
-print(result[&quot;texts&quot;])
-</code></pre></details>
-
-<details><summary>C++</summary>
-
-<pre><code class="language-cpp">#include &lt;iostream&gt;
-#include &quot;cpp-httplib/httplib.h&quot; // https://github.com/Huiyicc/cpp-httplib
-#include &quot;nlohmann/json.hpp&quot; // https://github.com/nlohmann/json
-#include &quot;base64.hpp&quot; // https://github.com/tobiaslocker/base64
-
-int main() {
-    httplib::Client client(&quot;localhost:8080&quot;);
-    const std::string imagePath = &quot;./demo.jpg&quot;;
-    const std::string outputImagePath = &quot;./out.jpg&quot;;
-
-    httplib::Headers headers = {
-        {&quot;Content-Type&quot;, &quot;application/json&quot;}
-    };
-
-    // 对本地图像进行Base64编码
-    std::ifstream file(imagePath, std::ios::binary | std::ios::ate);
-    std::streamsize size = file.tellg();
-    file.seekg(0, std::ios::beg);
-
-    std::vector&lt;char&gt; buffer(size);
-    if (!file.read(buffer.data(), size)) {
-        std::cerr &lt;&lt; &quot;Error reading file.&quot; &lt;&lt; std::endl;
-        return 1;
-    }
-    std::string bufferStr(reinterpret_cast&lt;const char*&gt;(buffer.data()), buffer.size());
-    std::string encodedImage = base64::to_base64(bufferStr);
-
-    nlohmann::json jsonObj;
-    jsonObj[&quot;image&quot;] = encodedImage;
-    std::string body = jsonObj.dump();
-
-    // 调用API
-    auto response = client.Post(&quot;/ocr&quot;, headers, body, &quot;application/json&quot;);
-    // 处理接口返回数据
-    if (response &amp;&amp; response-&gt;status == 200) {
-        nlohmann::json jsonResponse = nlohmann::json::parse(response-&gt;body);
-        auto result = jsonResponse[&quot;result&quot;];
-
-        encodedImage = result[&quot;image&quot;];
-        std::string decodedString = base64::from_base64(encodedImage);
-        std::vector&lt;unsigned char&gt; decodedImage(decodedString.begin(), decodedString.end());
-        std::ofstream outputImage(outPutImagePath, std::ios::binary | std::ios::out);
-        if (outputImage.is_open()) {
-            outputImage.write(reinterpret_cast&lt;char*&gt;(decodedImage.data()), decodedImage.size());
-            outputImage.close();
-            std::cout &lt;&lt; &quot;Output image saved at &quot; &lt;&lt; outPutImagePath &lt;&lt; std::endl;
-        } else {
-            std::cerr &lt;&lt; &quot;Unable to open file for writing: &quot; &lt;&lt; outPutImagePath &lt;&lt; std::endl;
-        }
-
-        auto texts = result[&quot;texts&quot;];
-        std::cout &lt;&lt; &quot;\nDetected texts:&quot; &lt;&lt; std::endl;
-        for (const auto&amp; text : texts) {
-            std::cout &lt;&lt; text &lt;&lt; std::endl;
-        }
-    } else {
-        std::cout &lt;&lt; &quot;Failed to send HTTP request.&quot; &lt;&lt; std::endl;
-        return 1;
-    }
-
-    return 0;
-}
-</code></pre></details>
-
-<details><summary>Java</summary>
-
-<pre><code class="language-java">import okhttp3.*;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.Base64;
-
-public class Main {
-    public static void main(String[] args) throws IOException {
-        String API_URL = &quot;http://localhost:8080/ocr&quot;; // 服务URL
-        String imagePath = &quot;./demo.jpg&quot;; // 本地图像
-        String outputImagePath = &quot;./out.jpg&quot;; // 输出图像
-
-        // 对本地图像进行Base64编码
-        File file = new File(imagePath);
-        byte[] fileContent = java.nio.file.Files.readAllBytes(file.toPath());
-        String imageData = Base64.getEncoder().encodeToString(fileContent);
-
-        ObjectMapper objectMapper = new ObjectMapper();
-        ObjectNode params = objectMapper.createObjectNode();
-        params.put(&quot;image&quot;, imageData); // Base64编码的文件内容或者图像URL
-
-        // 创建 OkHttpClient 实例
-        OkHttpClient client = new OkHttpClient();
-        MediaType JSON = MediaType.Companion.get(&quot;application/json; charset=utf-8&quot;);
-        RequestBody body = RequestBody.Companion.create(params.toString(), JSON);
-        Request request = new Request.Builder()
-                .url(API_URL)
-                .post(body)
-                .build();
-
-        // 调用API并处理接口返回数据
-        try (Response response = client.newCall(request).execute()) {
-            if (response.isSuccessful()) {
-                String responseBody = response.body().string();
-                JsonNode resultNode = objectMapper.readTree(responseBody);
-                JsonNode result = resultNode.get(&quot;result&quot;);
-                String base64Image = result.get(&quot;image&quot;).asText();
-                JsonNode texts = result.get(&quot;texts&quot;);
-
-                byte[] imageBytes = Base64.getDecoder().decode(base64Image);
-                try (FileOutputStream fos = new FileOutputStream(outputImagePath)) {
-                    fos.write(imageBytes);
-                }
-                System.out.println(&quot;Output image saved at &quot; + outputImagePath);
-                System.out.println(&quot;\nDetected texts: &quot; + texts.toString());
-            } else {
-                System.err.println(&quot;Request failed with code: &quot; + response.code());
-            }
-        }
-    }
-}
-</code></pre></details>
-
-<details><summary>Go</summary>
-
-<pre><code class="language-go">package main
-
-import (
-    &quot;bytes&quot;
-    &quot;encoding/base64&quot;
-    &quot;encoding/json&quot;
-    &quot;fmt&quot;
-    &quot;io/ioutil&quot;
-    &quot;net/http&quot;
-)
-
-func main() {
-    API_URL := &quot;http://localhost:8080/ocr&quot;
-    imagePath := &quot;./demo.jpg&quot;
-    outputImagePath := &quot;./out.jpg&quot;
-
-    // 对本地图像进行Base64编码
-    imageBytes, err := ioutil.ReadFile(imagePath)
-    if err != nil {
-        fmt.Println(&quot;Error reading image file:&quot;, err)
-        return
-    }
-    imageData := base64.StdEncoding.EncodeToString(imageBytes)
-
-    payload := map[string]string{&quot;image&quot;: imageData} // Base64编码的文件内容或者图像URL
-    payloadBytes, err := json.Marshal(payload)
-    if err != nil {
-        fmt.Println(&quot;Error marshaling payload:&quot;, err)
-        return
-    }
-
-    // 调用API
-    client := &amp;http.Client{}
-    req, err := http.NewRequest(&quot;POST&quot;, API_URL, bytes.NewBuffer(payloadBytes))
-    if err != nil {
-        fmt.Println(&quot;Error creating request:&quot;, err)
-        return
-    }
-
-    res, err := client.Do(req)
-    if err != nil {
-        fmt.Println(&quot;Error sending request:&quot;, err)
-        return
-    }
-    defer res.Body.Close()
-
-    // 处理接口返回数据
-    body, err := ioutil.ReadAll(res.Body)
-    if err != nil {
-        fmt.Println(&quot;Error reading response body:&quot;, err)
-        return
-    }
-    type Response struct {
-        Result struct {
-            Image      string   `json:&quot;image&quot;`
-            Texts []map[string]interface{} `json:&quot;texts&quot;`
-        } `json:&quot;result&quot;`
-    }
-    var respData Response
-    err = json.Unmarshal([]byte(string(body)), &amp;respData)
-    if err != nil {
-        fmt.Println(&quot;Error unmarshaling response body:&quot;, err)
-        return
-    }
-
-    outputImageData, err := base64.StdEncoding.DecodeString(respData.Result.Image)
-    if err != nil {
-        fmt.Println(&quot;Error decoding base64 image data:&quot;, err)
-        return
-    }
-    err = ioutil.WriteFile(outputImagePath, outputImageData, 0644)
-    if err != nil {
-        fmt.Println(&quot;Error writing image to file:&quot;, err)
-        return
-    }
-    fmt.Printf(&quot;Image saved at %s.jpg\n&quot;, outputImagePath)
-    fmt.Println(&quot;\nDetected texts:&quot;)
-    for _, text := range respData.Result.Texts {
-        fmt.Println(text)
-    }
-}
-</code></pre></details>
-
-<details><summary>C#</summary>
-
-<pre><code class="language-csharp">using System;
-using System.IO;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Text;
-using System.Threading.Tasks;
-using Newtonsoft.Json.Linq;
-
-class Program
-{
-    static readonly string API_URL = &quot;http://localhost:8080/ocr&quot;;
-    static readonly string imagePath = &quot;./demo.jpg&quot;;
-    static readonly string outputImagePath = &quot;./out.jpg&quot;;
-
-    static async Task Main(string[] args)
-    {
-        var httpClient = new HttpClient();
-
-        // 对本地图像进行Base64编码
-        byte[] imageBytes = File.ReadAllBytes(imagePath);
-        string image_data = Convert.ToBase64String(imageBytes);
-
-        var payload = new JObject{ { &quot;image&quot;, image_data } }; // Base64编码的文件内容或者图像URL
-        var content = new StringContent(payload.ToString(), Encoding.UTF8, &quot;application/json&quot;);
-
-        // 调用API
-        HttpResponseMessage response = await httpClient.PostAsync(API_URL, content);
-        response.EnsureSuccessStatusCode();
-
-        // 处理接口返回数据
-        string responseBody = await response.Content.ReadAsStringAsync();
-        JObject jsonResponse = JObject.Parse(responseBody);
-
-        string base64Image = jsonResponse[&quot;result&quot;][&quot;image&quot;].ToString();
-        byte[] outputImageBytes = Convert.FromBase64String(base64Image);
-
-        File.WriteAllBytes(outputImagePath, outputImageBytes);
-        Console.WriteLine($&quot;Output image saved at {outputImagePath}&quot;);
-        Console.WriteLine(&quot;\nDetected texts:&quot;);
-        Console.WriteLine(jsonResponse[&quot;result&quot;][&quot;texts&quot;].ToString());
-    }
-}
-</code></pre></details>
-
-<details><summary>Node.js</summary>
-
-<pre><code class="language-js">const axios = require('axios');
-const fs = require('fs');
-
-const API_URL = 'http://localhost:8080/ocr'
-const imagePath = './demo.jpg'
-const outputImagePath = &quot;./out.jpg&quot;;
-
-let config = {
-   method: 'POST',
-   maxBodyLength: Infinity,
-   url: API_URL,
-   data: JSON.stringify({
-    'image': encodeImageToBase64(imagePath)  // Base64编码的文件内容或者图像URL
-  })
-};
-
-// 对本地图像进行Base64编码
-function encodeImageToBase64(filePath) {
-  const bitmap = fs.readFileSync(filePath);
-  return Buffer.from(bitmap).toString('base64');
-}
-
-// 调用API
-axios.request(config)
-.then((response) =&gt; {
-    // 处理接口返回数据
-    const result = response.data[&quot;result&quot;];
-    const imageBuffer = Buffer.from(result[&quot;image&quot;], 'base64');
-    fs.writeFile(outputImagePath, imageBuffer, (err) =&gt; {
-      if (err) throw err;
-      console.log(`Output image saved at ${outputImagePath}`);
-    });
-    console.log(&quot;\nDetected texts:&quot;);
-    console.log(result[&quot;texts&quot;]);
-})
-.catch((error) =&gt; {
-  console.log(error);
-});
-</code></pre></details>
-
-<details><summary>PHP</summary>
-
-<pre><code class="language-php">&lt;?php
-
-$API_URL = &quot;http://localhost:8080/ocr&quot;; // 服务URL
-$image_path = &quot;./demo.jpg&quot;;
-$output_image_path = &quot;./out.jpg&quot;;
-
-// 对本地图像进行Base64编码
-$image_data = base64_encode(file_get_contents($image_path));
-$payload = array(&quot;image&quot; =&gt; $image_data); // Base64编码的文件内容或者图像URL
-
-// 调用API
-$ch = curl_init($API_URL);
-curl_setopt($ch, CURLOPT_POST, true);
-curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
-curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
-$response = curl_exec($ch);
-curl_close($ch);
-
-// 处理接口返回数据
-$result = json_decode($response, true)[&quot;result&quot;];
-file_put_contents($output_image_path, base64_decode($result[&quot;image&quot;]));
-echo &quot;Output image saved at &quot; . $output_image_path . &quot;\n&quot;;
-echo &quot;\nDetected texts:\n&quot;;
-print_r($result[&quot;texts&quot;]);
-
-?&gt;
+print(&quot;\nDetected pedestrians:&quot;)
+print(result[&quot;pedestrians&quot;])
 </code></pre></details>
 </details>
 <br/>

+ 4 - 1
paddlex/inference/pipelines/__init__.py

@@ -39,7 +39,10 @@ from .seal_recognition import SealOCRPipeline
 from .ppchatocrv3 import PPChatOCRPipeline
 from .layout_parsing import LayoutParsingPipeline
 from .pp_shitu_v2 import ShiTuV2Pipeline
-from .attribute_recognition import AttributeRecPipeline
+from .attribute_recognition import (
+    PedestrianAttributeRecPipeline,
+    VehicleAttributeRecPipeline,
+)
 
 
 def load_pipeline_config(pipeline: str) -> Dict[str, Any]:

+ 8 - 2
paddlex/inference/pipelines/attribute_recognition.py

@@ -24,8 +24,6 @@ from .base import BasePipeline
 class AttributeRecPipeline(BasePipeline):
     """Attribute Rec Pipeline"""
 
-    entities = ["pedestrian_attribute_recognition", "vehicle_attribute_recognition"]
-
     def __init__(
         self,
         det_model,
@@ -84,3 +82,11 @@ class AttributeRecPipeline(BasePipeline):
                 }
             )
         return AttributeRecResult(single_img_res)
+
+
+class PedestrianAttributeRecPipeline(AttributeRecPipeline):
+    entities = "pedestrian_attribute_recognition"
+
+
+class VehicleAttributeRecPipeline(AttributeRecPipeline):
+    entities = "vehicle_attribute_recognition"

+ 10 - 0
paddlex/inference/pipelines/serving/_pipeline_apps/__init__.py

@@ -16,6 +16,7 @@ from typing import Any, Dict
 
 from fastapi import FastAPI
 
+from ...attribute_recognition import PedestrianAttributeRecPipeline
 from ...base import BasePipeline
 from ...formula_recognition import FormulaRecognitionPipeline
 from ...layout_parsing import LayoutParsingPipeline
@@ -48,6 +49,9 @@ from .multi_label_image_classification import (
 )
 from .object_detection import create_pipeline_app as create_object_detection_app
 from .ocr import create_pipeline_app as create_ocr_app
+from .pedestrian_attribute_recognition import (
+    create_pipeline_app as create_pedestrian_attribute_recognition_app,
+)
 from .ppchatocrv3 import create_pipeline_app as create_ppchatocrv3_app
 from .seal_recognition import create_pipeline_app as create_seal_recognition_app
 from .semantic_segmentation import (
@@ -158,6 +162,12 @@ def create_pipeline_app(
                 "Expected `pipeline` to be an instance of `LayoutParsingPipeline`."
             )
         return create_layout_parsing_app(pipeline, app_config)
+    elif pipeline_name == "pedestrian_attribute_recognition":
+        if not isinstance(pipeline, PedestrianAttributeRecPipeline):
+            raise TypeError(
+                "Expected `pipeline` to be an instance of `PedestrianAttributeRecPipeline`."
+            )
+        return create_pedestrian_attribute_recognition_app(pipeline, app_config)
     else:
         if BasePipeline.get(pipeline_name):
             raise ValueError(

+ 5 - 73
paddlex/inference/pipelines/serving/_pipeline_apps/layout_parsing.py

@@ -13,17 +13,14 @@
 # limitations under the License.
 
 import os
-import re
-import uuid
 from typing import Final, List, Literal, Optional, Tuple
-from urllib.parse import parse_qs, urlparse
 
 import cv2
 import numpy as np
 from fastapi import FastAPI, HTTPException
 from numpy.typing import ArrayLike
 from pydantic import BaseModel, Field
-from typing_extensions import Annotated, TypeAlias, assert_never
+from typing_extensions import Annotated, TypeAlias
 
 from .....utils import logging
 from ...layout_parsing import LayoutParsingPipeline
@@ -71,71 +68,6 @@ class InferResult(BaseModel):
     layoutParsingResults: List[LayoutParsingResult]
 
 
-def _generate_request_id() -> str:
-    return str(uuid.uuid4())
-
-
-def _infer_file_type(url: str) -> FileType:
-    # Is it more reliable to guess the file type based on the response headers?
-    SUPPORTED_IMG_EXTS: Final[List[str]] = [".jpg", ".jpeg", ".png"]
-
-    url_parts = urlparse(url)
-    ext = os.path.splitext(url_parts.path)[1]
-    # HACK: The support for BOS URLs with query params is implementation-based,
-    # not interface-based.
-    is_bos_url = (
-        re.fullmatch(r"(?:bj|bd|su|gz|cd|hkg|fwh|fsh)\.bcebos\.com", url_parts.netloc)
-        is not None
-    )
-    if is_bos_url and url_parts.query:
-        params = parse_qs(url_parts.query)
-        if (
-            "responseContentDisposition" not in params
-            or len(params["responseContentDisposition"]) != 1
-        ):
-            raise ValueError("`responseContentDisposition` not found")
-        match_ = re.match(
-            r"attachment;filename=(.*)", params["responseContentDisposition"][0]
-        )
-        if not match_ or not match_.groups()[0] is not None:
-            raise ValueError(
-                "Failed to extract the filename from `responseContentDisposition`"
-            )
-        ext = os.path.splitext(match_.groups()[0])[1]
-    ext = ext.lower()
-    if ext == ".pdf":
-        return 0
-    elif ext in SUPPORTED_IMG_EXTS:
-        return 1
-    else:
-        raise ValueError("Unsupported file type")
-
-
-def _bytes_to_arrays(
-    file_bytes: bytes,
-    file_type: FileType,
-    *,
-    max_img_size: Tuple[int, int],
-    max_num_imgs: int,
-) -> List[np.ndarray]:
-    if file_type == 0:
-        images = serving_utils.read_pdf(
-            file_bytes, resize=True, max_num_imgs=max_num_imgs
-        )
-    elif file_type == 1:
-        images = [serving_utils.image_bytes_to_array(file_bytes)]
-    else:
-        assert_never(file_type)
-    h, w = images[0].shape[0:2]
-    if w > max_img_size[1] or h > max_img_size[0]:
-        if w / h > max_img_size[0] / max_img_size[1]:
-            factor = max_img_size[0] / w
-        else:
-            factor = max_img_size[1] / h
-        images = [cv2.resize(img, (int(factor * w), int(factor * h))) for img in images]
-    return images
-
-
 def _postprocess_image(
     img: ArrayLike,
     request_id: str,
@@ -180,12 +112,12 @@ def create_pipeline_app(
         pipeline = ctx.pipeline
         aiohttp_session = ctx.aiohttp_session
 
-        request_id = _generate_request_id()
+        request_id = serving_utils.generate_request_id()
 
         if request.fileType is None:
             if serving_utils.is_url(request.file):
                 try:
-                    file_type = _infer_file_type(request.file)
+                    file_type = serving_utils.infer_file_type(request.file)
                 except Exception as e:
                     logging.exception(e)
                     raise HTTPException(
@@ -195,7 +127,7 @@ def create_pipeline_app(
             else:
                 raise HTTPException(status_code=422, detail="Unknown file type")
         else:
-            file_type = request.fileType
+            file_type = "PDF" if request.fileType == 0 else "IMAGE"
 
         if request.inferenceParams:
             max_long_side = request.inferenceParams.maxLongSide
@@ -210,7 +142,7 @@ def create_pipeline_app(
                 request.file, aiohttp_session
             )
             images = await serving_utils.call_async(
-                _bytes_to_arrays,
+                serving_utils.file_to_images,
                 file_bytes,
                 file_type,
                 max_img_size=ctx.extra["max_img_size"],

+ 100 - 0
paddlex/inference/pipelines/serving/_pipeline_apps/pedestrian_attribute_recognition.py

@@ -0,0 +1,100 @@
+# copyright (c) 2024 PaddlePaddle Authors. All Rights Reserve.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import List
+
+from fastapi import FastAPI, HTTPException
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated, TypeAlias
+
+from .....utils import logging
+from ...attribute_recognition import PedestrianAttributeRecPipeline
+from .. import utils as serving_utils
+from ..app import AppConfig, create_app
+from ..models import Response, ResultResponse
+
+
+class InferRequest(BaseModel):
+    image: str
+
+
+BoundingBox: TypeAlias = Annotated[List[float], Field(min_length=4, max_length=4)]
+
+
+class Attribute(BaseModel):
+    label: str
+    score: float
+
+
+class Pedestrian(BaseModel):
+    bbox: BoundingBox
+    attributes: List[Attribute]
+    score: float
+
+
+class InferResult(BaseModel):
+    pedestrians: List[Pedestrian]
+    image: str
+
+
+def create_pipeline_app(
+    pipeline: PedestrianAttributeRecPipeline, app_config: AppConfig
+) -> FastAPI:
+    app, ctx = create_app(
+        pipeline=pipeline, app_config=app_config, app_aiohttp_session=True
+    )
+
+    @app.post(
+        "/pedestrian-attribute-recognition",
+        operation_id="infer",
+        responses={422: {"model": Response}},
+    )
+    async def _infer(request: InferRequest) -> ResultResponse[InferResult]:
+        pipeline = ctx.pipeline
+        aiohttp_session = ctx.aiohttp_session
+
+        try:
+            file_bytes = await serving_utils.get_raw_bytes(
+                request.image, aiohttp_session
+            )
+            image = serving_utils.image_bytes_to_array(file_bytes)
+
+            result = (await pipeline.infer(image))[0]
+
+            pedestrians: List[Pedestrian] = []
+            for obj in result["boxes"]:
+                pedestrians.append(
+                    Pedestrian(
+                        bbox=obj["coordinate"],
+                        attributes=[
+                            Attribute(label=l, score=s)
+                            for l, s in zip(obj["labels"], obj["cls_scores"])
+                        ],
+                        score=obj["det_score"],
+                    )
+                )
+            output_image_base64 = serving_utils.image_to_base64(result.img)
+
+            return ResultResponse(
+                logId=serving_utils.generate_log_id(),
+                errorCode=0,
+                errorMsg="Success",
+                result=InferResult(pedestrians=pedestrians, image=output_image_base64),
+            )
+
+        except Exception as e:
+            logging.exception(e)
+            raise HTTPException(status_code=500, detail="Internal server error")
+
+    return app

+ 4 - 72
paddlex/inference/pipelines/serving/_pipeline_apps/ppchatocrv3.py

@@ -14,10 +14,7 @@
 
 import asyncio
 import os
-import re
-import uuid
 from typing import Awaitable, Final, List, Literal, Optional, Tuple, Union
-from urllib.parse import parse_qs, urlparse
 
 import cv2
 import numpy as np
@@ -155,46 +152,6 @@ class ChatResult(BaseModel):
     prompts: Optional[Prompts] = None
 
 
-def _generate_request_id() -> str:
-    return str(uuid.uuid4())
-
-
-def _infer_file_type(url: str) -> FileType:
-    # Is it more reliable to guess the file type based on the response headers?
-    SUPPORTED_IMG_EXTS: Final[List[str]] = [".jpg", ".jpeg", ".png"]
-
-    url_parts = urlparse(url)
-    ext = os.path.splitext(url_parts.path)[1]
-    # HACK: The support for BOS URLs with query params is implementation-based,
-    # not interface-based.
-    is_bos_url = (
-        re.fullmatch(r"(?:bj|bd|su|gz|cd|hkg|fwh|fsh)\.bcebos\.com", url_parts.netloc)
-        is not None
-    )
-    if is_bos_url and url_parts.query:
-        params = parse_qs(url_parts.query)
-        if (
-            "responseContentDisposition" not in params
-            or len(params["responseContentDisposition"]) != 1
-        ):
-            raise ValueError("`responseContentDisposition` not found")
-        match_ = re.match(
-            r"attachment;filename=(.*)", params["responseContentDisposition"][0]
-        )
-        if not match_ or not match_.groups()[0] is not None:
-            raise ValueError(
-                "Failed to extract the filename from `responseContentDisposition`"
-            )
-        ext = os.path.splitext(match_.groups()[0])[1]
-    ext = ext.lower()
-    if ext == ".pdf":
-        return 0
-    elif ext in SUPPORTED_IMG_EXTS:
-        return 1
-    else:
-        raise ValueError("Unsupported file type")
-
-
 def _llm_params_to_dict(llm_params: LLMParams) -> dict:
     if llm_params.apiType == "qianfan":
         return {
@@ -208,31 +165,6 @@ def _llm_params_to_dict(llm_params: LLMParams) -> dict:
         assert_never(llm_params.apiType)
 
 
-def _bytes_to_arrays(
-    file_bytes: bytes,
-    file_type: FileType,
-    *,
-    max_img_size: Tuple[int, int],
-    max_num_imgs: int,
-) -> List[np.ndarray]:
-    if file_type == 0:
-        images = serving_utils.read_pdf(
-            file_bytes, resize=True, max_num_imgs=max_num_imgs
-        )
-    elif file_type == 1:
-        images = [serving_utils.image_bytes_to_array(file_bytes)]
-    else:
-        assert_never(file_type)
-    h, w = images[0].shape[0:2]
-    if w > max_img_size[1] or h > max_img_size[0]:
-        if w / h > max_img_size[0] / max_img_size[1]:
-            factor = max_img_size[0] / w
-        else:
-            factor = max_img_size[1] / h
-        images = [cv2.resize(img, (int(factor * w), int(factor * h))) for img in images]
-    return images
-
-
 def _postprocess_image(
     img: ArrayLike,
     request_id: str,
@@ -274,12 +206,12 @@ def create_pipeline_app(pipeline: PPChatOCRPipeline, app_config: AppConfig) -> F
         pipeline = ctx.pipeline
         aiohttp_session = ctx.aiohttp_session
 
-        request_id = _generate_request_id()
+        request_id = serving_utils.generate_request_id()
 
         if request.fileType is None:
             if serving_utils.is_url(request.file):
                 try:
-                    file_type = _infer_file_type(request.file)
+                    file_type = serving_utils.infer_file_type(request.file)
                 except Exception as e:
                     logging.exception(e)
                     raise HTTPException(
@@ -289,7 +221,7 @@ def create_pipeline_app(pipeline: PPChatOCRPipeline, app_config: AppConfig) -> F
             else:
                 raise HTTPException(status_code=422, detail="Unknown file type")
         else:
-            file_type = request.fileType
+            file_type = "PDF" if request.fileType == 0 else "IMAGE"
 
         if request.inferenceParams:
             max_long_side = request.inferenceParams.maxLongSide
@@ -304,7 +236,7 @@ def create_pipeline_app(pipeline: PPChatOCRPipeline, app_config: AppConfig) -> F
                 request.file, aiohttp_session
             )
             images = await serving_utils.call_async(
-                _bytes_to_arrays,
+                serving_utils.file_to_images,
                 file_bytes,
                 file_type,
                 max_img_size=ctx.extra["max_img_size"],

+ 70 - 3
paddlex/inference/pipelines/serving/utils.py

@@ -15,10 +15,12 @@
 import asyncio
 import base64
 import io
+import os
+import re
 import uuid
 from functools import partial
-from typing import Awaitable, Callable, List, Optional, TypeVar
-from urllib.parse import urlparse
+from typing import Awaitable, Callable, List, Literal, Optional, TypeVar, Final, Tuple
+from urllib.parse import parse_qs, urlparse
 
 import aiohttp
 import cv2
@@ -27,7 +29,9 @@ import numpy as np
 import pandas as pd
 import yarl
 from PIL import Image
-from typing_extensions import ParamSpec
+from typing_extensions import ParamSpec, assert_never
+
+FileType = Literal["IMAGE", "PDF"]
 
 _P = ParamSpec("_P")
 _R = TypeVar("_R")
@@ -37,6 +41,10 @@ def generate_log_id() -> str:
     return str(uuid.uuid4())
 
 
+def generate_request_id() -> str:
+    return str(uuid.uuid4())
+
+
 def is_url(s: str) -> bool:
     if not (s.startswith("http://") or s.startswith("https://")):
         # Quick rejection
@@ -45,6 +53,42 @@ def is_url(s: str) -> bool:
     return all([result.scheme, result.netloc]) and result.scheme in ("http", "https")
 
 
+def infer_file_type(url: str) -> FileType:
+    # Is it more reliable to guess the file type based on the response headers?
+    SUPPORTED_IMG_EXTS: Final[List[str]] = [".jpg", ".jpeg", ".png"]
+
+    url_parts = urlparse(url)
+    ext = os.path.splitext(url_parts.path)[1]
+    # HACK: The support for BOS URLs with query params is implementation-based,
+    # not interface-based.
+    is_bos_url = (
+        re.fullmatch(r"(?:bj|bd|su|gz|cd|hkg|fwh|fsh)\.bcebos\.com", url_parts.netloc)
+        is not None
+    )
+    if is_bos_url and url_parts.query:
+        params = parse_qs(url_parts.query)
+        if (
+            "responseContentDisposition" not in params
+            or len(params["responseContentDisposition"]) != 1
+        ):
+            raise ValueError("`responseContentDisposition` not found")
+        match_ = re.match(
+            r"attachment;filename=(.*)", params["responseContentDisposition"][0]
+        )
+        if not match_ or not match_.groups()[0] is not None:
+            raise ValueError(
+                "Failed to extract the filename from `responseContentDisposition`"
+            )
+        ext = os.path.splitext(match_.groups()[0])[1]
+    ext = ext.lower()
+    if ext == ".pdf":
+        return "PDF"
+    elif ext in SUPPORTED_IMG_EXTS:
+        return "IMAGE"
+    else:
+        raise ValueError("Unsupported file type")
+
+
 async def get_raw_bytes(file: str, session: aiohttp.ClientSession) -> bytes:
     if is_url(file):
         async with session.get(yarl.URL(file, encoded=True)) as resp:
@@ -102,6 +146,29 @@ def read_pdf(
     return images
 
 
+def file_to_images(
+    file_bytes: bytes,
+    file_type: Literal["IMAGE", "PDF"],
+    *,
+    max_img_size: Tuple[int, int],
+    max_num_imgs: int,
+) -> List[np.ndarray]:
+    if file_type == "IMAGE":
+        images = [image_bytes_to_array(file_bytes)]
+    elif file_type == "PDF":
+        images = read_pdf(file_bytes, resize=True, max_num_imgs=max_num_imgs)
+    else:
+        assert_never(file_type)
+    h, w = images[0].shape[0:2]
+    if w > max_img_size[1] or h > max_img_size[0]:
+        if w / h > max_img_size[0] / max_img_size[1]:
+            factor = max_img_size[0] / w
+        else:
+            factor = max_img_size[1] / h
+        images = [cv2.resize(img, (int(factor * w), int(factor * h))) for img in images]
+    return images
+
+
 def call_async(
     func: Callable[_P, _R], /, *args: _P.args, **kwargs: _P.kwargs
 ) -> Awaitable[_R]: