gaotingquan před 1 rokem
rodič
revize
aef3f21891

+ 1 - 518
paddlex/inference/components/transforms/image/__init__.py

@@ -12,521 +12,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import os
-import math
-
-from pathlib import Path
-from copy import deepcopy
-
-import numpy as np
-import cv2
-
-from .....utils.download import download
-from .....utils.cache import CACHE_DIR
-from ....utils.io import ImageReader, ImageWriter
-from ...base import BaseComponent
-from . import funcs as F
-
-__all__ = [
-    "ReadImage",
-    "Flip",
-    "Crop",
-    "Resize",
-    "ResizeByLong",
-    "ResizeByShort",
-    "Pad",
-    "Normalize",
-    "ToCHWImage",
-]
-
-
-def _check_image_size(input_):
-    """check image size"""
-    if not (
-        isinstance(input_, (list, tuple))
-        and len(input_) == 2
-        and isinstance(input_[0], int)
-        and isinstance(input_[1], int)
-    ):
-        raise TypeError(f"{input_} cannot represent a valid image size.")
-
-
-class ReadImage(BaseComponent):
-    """Load image from the file."""
-
-    INPUT_KEYS = ["img"]
-    OUTPUT_KEYS = ["img", "img_size", "ori_img", "ori_img_size"]
-    DEAULT_INPUTS = {"img": "img"}
-    DEAULT_OUTPUTS = {
-        "img": "img",
-        "img_path": "img_path",
-        "img_size": "img_size",
-        "ori_img": "ori_img",
-        "ori_img_size": "ori_img_size",
-    }
-
-    _FLAGS_DICT = {
-        "BGR": cv2.IMREAD_COLOR,
-        "RGB": cv2.IMREAD_COLOR,
-        "GRAY": cv2.IMREAD_GRAYSCALE,
-    }
-    SUFFIX = ["jpg", "png", "jpeg", "JPEG", "JPG", "bmp"]
-
-    def __init__(self, batch_size=1, format="BGR"):
-        """
-        Initialize the instance.
-
-        Args:
-            format (str, optional): Target color format to convert the image to.
-                Choices are 'BGR', 'RGB', and 'GRAY'. Default: 'BGR'.
-        """
-        super().__init__()
-        self.batch_size = batch_size
-        self.format = format
-        flags = self._FLAGS_DICT[self.format]
-        self._reader = ImageReader(backend="opencv", flags=flags)
-        self._writer = ImageWriter(backend="opencv")
-
-    def apply(self, img):
-        """apply"""
-        if not isinstance(img, str):
-            img_path = (Path(CACHE_DIR) / "predict_input" / "tmp_img.jpg").as_posix()
-            self._writer.write(img_path, img)
-            yield [
-                {
-                    "img_path": img_path,
-                    "img": img,
-                    "img_size": [img.shape[1], img.shape[0]],
-                    "ori_img": deepcopy(img),
-                    "ori_img_size": deepcopy([img.shape[1], img.shape[0]]),
-                }
-            ]
-        else:
-            img_path = img
-            # XXX: auto download for url
-            img_path = self._download_from_url(img_path)
-            image_list = self._get_image_list(img_path)
-            batch = []
-            for img_path in image_list:
-                img = self._read_img(img_path)
-                batch.append(img)
-                if len(batch) >= self.batch_size:
-                    yield batch
-                    batch = []
-            if len(batch) > 0:
-                yield batch
-
-    def _read_img(self, img_path):
-        blob = self._reader.read(img_path)
-        if blob is None:
-            raise Exception("Image read Error")
-
-        if self.format == "RGB":
-            if blob.ndim != 3:
-                raise RuntimeError("Array is not 3-dimensional.")
-            # BGR to RGB
-            blob = blob[..., ::-1]
-        return {
-            "img_path": img_path,
-            "img": blob,
-            "img_size": [blob.shape[1], blob.shape[0]],
-            "ori_img": deepcopy(blob),
-            "ori_img_size": deepcopy([blob.shape[1], blob.shape[0]]),
-        }
-
-    def _download_from_url(self, in_path):
-        if in_path.startswith("http"):
-            file_name = Path(in_path).name
-            save_path = Path(CACHE_DIR) / "predict_input" / file_name
-            download(in_path, save_path, overwrite=True)
-            return save_path.as_posix()
-        return in_path
-
-    def _get_image_list(self, img_file):
-        imgs_lists = []
-        if img_file is None or not os.path.exists(img_file):
-            raise Exception(f"Not found any img file in path: {img_file}")
-
-        if os.path.isfile(img_file) and img_file.split(".")[-1] in self.SUFFIX:
-            imgs_lists.append(img_file)
-        elif os.path.isdir(img_file):
-            for root, dirs, files in os.walk(img_file):
-                for single_file in files:
-                    if single_file.split(".")[-1] in self.SUFFIX:
-                        imgs_lists.append(os.path.join(root, single_file))
-        if len(imgs_lists) == 0:
-            raise Exception("not found any img file in {}".format(img_file))
-        imgs_lists = sorted(imgs_lists)
-        return imgs_lists
-
-
-class GetImageInfo(BaseComponent):
-    """Get Image Info"""
-
-    INPUT_KEYS = "img"
-    OUTPUT_KEYS = "img_size"
-    DEAULT_INPUTS = {"img": "img"}
-    DEAULT_OUTPUTS = {"img_size": "img_size"}
-
-    def __init__(self):
-        super().__init__()
-
-    def apply(self, img):
-        """apply"""
-        return {"img_size": [img.shape[1], img.shape[0]]}
-
-
-class Flip(BaseComponent):
-    """Flip the image vertically or horizontally."""
-
-    INPUT_KEYS = "img"
-    OUTPUT_KEYS = "img"
-    DEAULT_INPUTS = {"img": "img"}
-    DEAULT_OUTPUTS = {"img": "img"}
-
-    def __init__(self, mode="H"):
-        """
-        Initialize the instance.
-
-        Args:
-            mode (str, optional): 'H' for horizontal flipping and 'V' for vertical
-                flipping. Default: 'H'.
-        """
-        super().__init__()
-        if mode not in ("H", "V"):
-            raise ValueError("`mode` should be 'H' or 'V'.")
-        self.mode = mode
-
-    def apply(self, img):
-        """apply"""
-        if self.mode == "H":
-            img = F.flip_h(img)
-        elif self.mode == "V":
-            img = F.flip_v(img)
-        return {"img": img}
-
-
-class Crop(BaseComponent):
-    """Crop region from the image."""
-
-    INPUT_KEYS = "img"
-    OUTPUT_KEYS = ["img", "img_size"]
-    DEAULT_INPUTS = {"img": "img"}
-    DEAULT_OUTPUTS = {"img": "img", "img_size": "img_size"}
-
-    def __init__(self, crop_size, mode="C"):
-        """
-        Initialize the instance.
-
-        Args:
-            crop_size (list|tuple|int): Width and height of the region to crop.
-            mode (str, optional): 'C' for cropping the center part and 'TL' for
-                cropping the top left part. Default: 'C'.
-        """
-        super().__init__()
-        if isinstance(crop_size, int):
-            crop_size = [crop_size, crop_size]
-        _check_image_size(crop_size)
-
-        self.crop_size = crop_size
-
-        if mode not in ("C", "TL"):
-            raise ValueError("Unsupported interpolation method")
-        self.mode = mode
-
-    def apply(self, img):
-        """apply"""
-        h, w = img.shape[:2]
-        cw, ch = self.crop_size
-        if self.mode == "C":
-            x1 = max(0, (w - cw) // 2)
-            y1 = max(0, (h - ch) // 2)
-        elif self.mode == "TL":
-            x1, y1 = 0, 0
-        x2 = min(w, x1 + cw)
-        y2 = min(h, y1 + ch)
-        coords = (x1, y1, x2, y2)
-        if coords == (0, 0, w, h):
-            raise ValueError(
-                f"Input image ({w}, {h}) smaller than the target size ({cw}, {ch})."
-            )
-        img = F.slice(img, coords=coords)
-        return {"img": img, "img_size": [img.shape[1], img.shape[0]]}
-
-
-class _BaseResize(BaseComponent):
-    _INTERP_DICT = {
-        "NEAREST": cv2.INTER_NEAREST,
-        "LINEAR": cv2.INTER_LINEAR,
-        "CUBIC": cv2.INTER_CUBIC,
-        "AREA": cv2.INTER_AREA,
-        "LANCZOS4": cv2.INTER_LANCZOS4,
-    }
-
-    def __init__(self, size_divisor, interp):
-        super().__init__()
-
-        if size_divisor is not None:
-            assert isinstance(
-                size_divisor, int
-            ), "`size_divisor` should be None or int."
-        self.size_divisor = size_divisor
-
-        try:
-            interp = self._INTERP_DICT[interp]
-        except KeyError:
-            raise ValueError(
-                "`interp` should be one of {}.".format(self._INTERP_DICT.keys())
-            )
-        self.interp = interp
-
-    @staticmethod
-    def _rescale_size(img_size, target_size):
-        """rescale size"""
-        scale = min(max(target_size) / max(img_size), min(target_size) / min(img_size))
-        rescaled_size = [round(i * scale) for i in img_size]
-        return rescaled_size, scale
-
-
-class Resize(_BaseResize):
-    """Resize the image."""
-
-    INPUT_KEYS = "img"
-    OUTPUT_KEYS = ["img", "img_size", "scale_factors"]
-    DEAULT_INPUTS = {"img": "img"}
-    DEAULT_OUTPUTS = {
-        "img": "img",
-        "img_size": "img_size",
-        "scale_factors": "scale_factors",
-    }
-
-    def __init__(
-        self, target_size, keep_ratio=False, size_divisor=None, interp="LINEAR"
-    ):
-        """
-        Initialize the instance.
-
-        Args:
-            target_size (list|tuple|int): Target width and height.
-            keep_ratio (bool, optional): Whether to keep the aspect ratio of resized
-                image. Default: False.
-            size_divisor (int|None, optional): Divisor of resized image size.
-                Default: None.
-            interp (str, optional): Interpolation method. Choices are 'NEAREST',
-                'LINEAR', 'CUBIC', 'AREA', and 'LANCZOS4'. Default: 'LINEAR'.
-        """
-        super().__init__(size_divisor=size_divisor, interp=interp)
-
-        if isinstance(target_size, int):
-            target_size = [target_size, target_size]
-        _check_image_size(target_size)
-        self.target_size = target_size
-
-        self.keep_ratio = keep_ratio
-
-    def apply(self, img):
-        """apply"""
-        target_size = self.target_size
-        original_size = img.shape[:2]
-
-        if self.keep_ratio:
-            h, w = img.shape[0:2]
-            target_size, _ = self._rescale_size((w, h), self.target_size)
-
-        if self.size_divisor:
-            target_size = [
-                math.ceil(i / self.size_divisor) * self.size_divisor
-                for i in target_size
-            ]
-
-        img_scale_w, img_scale_h = [
-            target_size[1] / original_size[1],
-            target_size[0] / original_size[0],
-        ]
-        img = F.resize(img, target_size, interp=self.interp)
-        return {
-            "img": img,
-            "img_size": [img.shape[1], img.shape[0]],
-            "scale_factors": [img_scale_w, img_scale_h],
-        }
-
-
-class ResizeByLong(_BaseResize):
-    """
-    Proportionally resize the image by specifying the target length of the
-    longest side.
-    """
-
-    INPUT_KEYS = "img"
-    OUTPUT_KEYS = ["img", "img_size"]
-    DEAULT_INPUTS = {"img": "img"}
-    DEAULT_OUTPUTS = {"img": "img", "img_size": "img_size"}
-
-    def __init__(self, target_long_edge, size_divisor=None, interp="LINEAR"):
-        """
-        Initialize the instance.
-
-        Args:
-            target_long_edge (int): Target length of the longest side of image.
-            size_divisor (int|None, optional): Divisor of resized image size.
-                Default: None.
-            interp (str, optional): Interpolation method. Choices are 'NEAREST',
-                'LINEAR', 'CUBIC', 'AREA', and 'LANCZOS4'. Default: 'LINEAR'.
-        """
-        super().__init__(size_divisor=size_divisor, interp=interp)
-        self.target_long_edge = target_long_edge
-
-    def apply(self, img):
-        """apply"""
-        h, w = img.shape[:2]
-        scale = self.target_long_edge / max(h, w)
-        h_resize = round(h * scale)
-        w_resize = round(w * scale)
-        if self.size_divisor is not None:
-            h_resize = math.ceil(h_resize / self.size_divisor) * self.size_divisor
-            w_resize = math.ceil(w_resize / self.size_divisor) * self.size_divisor
-
-        img = F.resize(img, (w_resize, h_resize), interp=self.interp)
-        return {"img": img, "img_size": [img.shape[1], img.shape[0]]}
-
-
-class ResizeByShort(_BaseResize):
-    """
-    Proportionally resize the image by specifying the target length of the
-    shortest side.
-    """
-
-    INPUT_KEYS = "img"
-    OUTPUT_KEYS = ["img", "img_size"]
-    DEAULT_INPUTS = {"img": "img"}
-    DEAULT_OUTPUTS = {"img": "img", "img_size": "img_size"}
-
-    def __init__(self, target_short_edge, size_divisor=None, interp="LINEAR"):
-        """
-        Initialize the instance.
-
-        Args:
-            target_short_edge (int): Target length of the shortest side of image.
-            size_divisor (int|None, optional): Divisor of resized image size.
-                Default: None.
-            interp (str, optional): Interpolation method. Choices are 'NEAREST',
-                'LINEAR', 'CUBIC', 'AREA', and 'LANCZOS4'. Default: 'LINEAR'.
-        """
-        super().__init__(size_divisor=size_divisor, interp=interp)
-        self.target_short_edge = target_short_edge
-
-    def apply(self, img):
-        """apply"""
-        h, w = img.shape[:2]
-        scale = self.target_short_edge / min(h, w)
-        h_resize = round(h * scale)
-        w_resize = round(w * scale)
-        if self.size_divisor is not None:
-            h_resize = math.ceil(h_resize / self.size_divisor) * self.size_divisor
-            w_resize = math.ceil(w_resize / self.size_divisor) * self.size_divisor
-
-        img = F.resize(img, (w_resize, h_resize), interp=self.interp)
-        return {"img": img, "img_size": [img.shape[1], img.shape[0]]}
-
-
-class Pad(BaseComponent):
-    """Pad the image."""
-
-    INPUT_KEYS = "img"
-    OUTPUT_KEYS = ["img", "img_size"]
-    DEAULT_INPUTS = {"img": "img"}
-    DEAULT_OUTPUTS = {"img": "img", "img_size": "img_size"}
-
-    def __init__(self, target_size, val=127.5):
-        """
-        Initialize the instance.
-
-        Args:
-            target_size (list|tuple|int): Target width and height of the image after
-                padding.
-            val (float, optional): Value to fill the padded area. Default: 127.5.
-        """
-        super().__init__()
-
-        if isinstance(target_size, int):
-            target_size = [target_size, target_size]
-        _check_image_size(target_size)
-        self.target_size = target_size
-
-        self.val = val
-
-    def apply(self, img):
-        """apply"""
-        h, w = img.shape[:2]
-        tw, th = self.target_size
-        ph = th - h
-        pw = tw - w
-
-        if ph < 0 or pw < 0:
-            raise ValueError(
-                f"Input image ({w}, {h}) smaller than the target size ({tw}, {th})."
-            )
-        else:
-            img = F.pad(img, pad=(0, ph, 0, pw), val=self.val)
-        return {"img": img, "img_size": [img.shape[1], img.shape[0]]}
-
-
-class Normalize(BaseComponent):
-    """Normalize the image."""
-
-    INPUT_KEYS = "img"
-    OUTPUT_KEYS = "img"
-    DEAULT_INPUTS = {"img": "img"}
-    DEAULT_OUTPUTS = {"img": "img"}
-
-    def __init__(self, scale=1.0 / 255, mean=0.5, std=0.5, preserve_dtype=False):
-        """
-        Initialize the instance.
-
-        Args:
-            scale (float, optional): Scaling factor to apply to the image before
-                applying normalization. Default: 1/255.
-            mean (float|tuple|list, optional): Means for each channel of the image.
-                Default: 0.5.
-            std (float|tuple|list, optional): Standard deviations for each channel
-                of the image. Default: 0.5.
-            preserve_dtype (bool, optional): Whether to preserve the original dtype
-                of the image.
-        """
-        super().__init__()
-
-        self.scale = np.float32(scale)
-        if isinstance(mean, float):
-            mean = [mean]
-        self.mean = np.asarray(mean).astype("float32")
-        if isinstance(std, float):
-            std = [std]
-        self.std = np.asarray(std).astype("float32")
-        self.preserve_dtype = preserve_dtype
-
-    def apply(self, img):
-        """apply"""
-        old_type = img.dtype
-        # XXX: If `old_type` has higher precision than float32,
-        # we will lose some precision.
-        img = img.astype("float32", copy=False)
-        img *= self.scale
-        img -= self.mean
-        img /= self.std
-        if self.preserve_dtype:
-            img = img.astype(old_type, copy=False)
-        return {"img": img}
-
-
-class ToCHWImage(BaseComponent):
-    """Reorder the dimensions of the image from HWC to CHW."""
-
-    INPUT_KEYS = "img"
-    OUTPUT_KEYS = "img"
-    DEAULT_INPUTS = {"img": "img"}
-    DEAULT_OUTPUTS = {"img": "img"}
-
-    def apply(self, img):
-        """apply"""
-        img = img.transpose((2, 0, 1))
-        return {"img": img}
+from .common import *

+ 532 - 0
paddlex/inference/components/transforms/image/common.py

@@ -0,0 +1,532 @@
+# 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.
+
+import os
+import math
+
+from pathlib import Path
+from copy import deepcopy
+
+import numpy as np
+import cv2
+
+from .....utils.download import download
+from .....utils.cache import CACHE_DIR
+from ....utils.io import ImageReader, ImageWriter
+from ...base import BaseComponent
+from . import funcs as F
+
+__all__ = [
+    "ReadImage",
+    "Flip",
+    "Crop",
+    "Resize",
+    "ResizeByLong",
+    "ResizeByShort",
+    "Pad",
+    "Normalize",
+    "ToCHWImage",
+]
+
+
+def _check_image_size(input_):
+    """check image size"""
+    if not (
+        isinstance(input_, (list, tuple))
+        and len(input_) == 2
+        and isinstance(input_[0], int)
+        and isinstance(input_[1], int)
+    ):
+        raise TypeError(f"{input_} cannot represent a valid image size.")
+
+
+class ReadImage(BaseComponent):
+    """Load image from the file."""
+
+    INPUT_KEYS = ["img"]
+    OUTPUT_KEYS = ["img", "img_size", "ori_img", "ori_img_size"]
+    DEAULT_INPUTS = {"img": "img"}
+    DEAULT_OUTPUTS = {
+        "img": "img",
+        "img_path": "img_path",
+        "img_size": "img_size",
+        "ori_img": "ori_img",
+        "ori_img_size": "ori_img_size",
+    }
+
+    _FLAGS_DICT = {
+        "BGR": cv2.IMREAD_COLOR,
+        "RGB": cv2.IMREAD_COLOR,
+        "GRAY": cv2.IMREAD_GRAYSCALE,
+    }
+    SUFFIX = ["jpg", "png", "jpeg", "JPEG", "JPG", "bmp"]
+
+    def __init__(self, batch_size=1, format="BGR"):
+        """
+        Initialize the instance.
+
+        Args:
+            format (str, optional): Target color format to convert the image to.
+                Choices are 'BGR', 'RGB', and 'GRAY'. Default: 'BGR'.
+        """
+        super().__init__()
+        self.batch_size = batch_size
+        self.format = format
+        flags = self._FLAGS_DICT[self.format]
+        self._reader = ImageReader(backend="opencv", flags=flags)
+        self._writer = ImageWriter(backend="opencv")
+
+    def apply(self, img):
+        """apply"""
+        if not isinstance(img, str):
+            img_path = (Path(CACHE_DIR) / "predict_input" / "tmp_img.jpg").as_posix()
+            self._writer.write(img_path, img)
+            yield [
+                {
+                    "img_path": img_path,
+                    "img": img,
+                    "img_size": [img.shape[1], img.shape[0]],
+                    "ori_img": deepcopy(img),
+                    "ori_img_size": deepcopy([img.shape[1], img.shape[0]]),
+                }
+            ]
+        else:
+            img_path = img
+            # XXX: auto download for url
+            img_path = self._download_from_url(img_path)
+            image_list = self._get_image_list(img_path)
+            batch = []
+            for img_path in image_list:
+                img = self._read_img(img_path)
+                batch.append(img)
+                if len(batch) >= self.batch_size:
+                    yield batch
+                    batch = []
+            if len(batch) > 0:
+                yield batch
+
+    def _read_img(self, img_path):
+        blob = self._reader.read(img_path)
+        if blob is None:
+            raise Exception("Image read Error")
+
+        if self.format == "RGB":
+            if blob.ndim != 3:
+                raise RuntimeError("Array is not 3-dimensional.")
+            # BGR to RGB
+            blob = blob[..., ::-1]
+        return {
+            "img_path": img_path,
+            "img": blob,
+            "img_size": [blob.shape[1], blob.shape[0]],
+            "ori_img": deepcopy(blob),
+            "ori_img_size": deepcopy([blob.shape[1], blob.shape[0]]),
+        }
+
+    def _download_from_url(self, in_path):
+        if in_path.startswith("http"):
+            file_name = Path(in_path).name
+            save_path = Path(CACHE_DIR) / "predict_input" / file_name
+            download(in_path, save_path, overwrite=True)
+            return save_path.as_posix()
+        return in_path
+
+    def _get_image_list(self, img_file):
+        imgs_lists = []
+        if img_file is None or not os.path.exists(img_file):
+            raise Exception(f"Not found any img file in path: {img_file}")
+
+        if os.path.isfile(img_file) and img_file.split(".")[-1] in self.SUFFIX:
+            imgs_lists.append(img_file)
+        elif os.path.isdir(img_file):
+            for root, dirs, files in os.walk(img_file):
+                for single_file in files:
+                    if single_file.split(".")[-1] in self.SUFFIX:
+                        imgs_lists.append(os.path.join(root, single_file))
+        if len(imgs_lists) == 0:
+            raise Exception("not found any img file in {}".format(img_file))
+        imgs_lists = sorted(imgs_lists)
+        return imgs_lists
+
+
+class GetImageInfo(BaseComponent):
+    """Get Image Info"""
+
+    INPUT_KEYS = "img"
+    OUTPUT_KEYS = "img_size"
+    DEAULT_INPUTS = {"img": "img"}
+    DEAULT_OUTPUTS = {"img_size": "img_size"}
+
+    def __init__(self):
+        super().__init__()
+
+    def apply(self, img):
+        """apply"""
+        return {"img_size": [img.shape[1], img.shape[0]]}
+
+
+class Flip(BaseComponent):
+    """Flip the image vertically or horizontally."""
+
+    INPUT_KEYS = "img"
+    OUTPUT_KEYS = "img"
+    DEAULT_INPUTS = {"img": "img"}
+    DEAULT_OUTPUTS = {"img": "img"}
+
+    def __init__(self, mode="H"):
+        """
+        Initialize the instance.
+
+        Args:
+            mode (str, optional): 'H' for horizontal flipping and 'V' for vertical
+                flipping. Default: 'H'.
+        """
+        super().__init__()
+        if mode not in ("H", "V"):
+            raise ValueError("`mode` should be 'H' or 'V'.")
+        self.mode = mode
+
+    def apply(self, img):
+        """apply"""
+        if self.mode == "H":
+            img = F.flip_h(img)
+        elif self.mode == "V":
+            img = F.flip_v(img)
+        return {"img": img}
+
+
+class Crop(BaseComponent):
+    """Crop region from the image."""
+
+    INPUT_KEYS = "img"
+    OUTPUT_KEYS = ["img", "img_size"]
+    DEAULT_INPUTS = {"img": "img"}
+    DEAULT_OUTPUTS = {"img": "img", "img_size": "img_size"}
+
+    def __init__(self, crop_size, mode="C"):
+        """
+        Initialize the instance.
+
+        Args:
+            crop_size (list|tuple|int): Width and height of the region to crop.
+            mode (str, optional): 'C' for cropping the center part and 'TL' for
+                cropping the top left part. Default: 'C'.
+        """
+        super().__init__()
+        if isinstance(crop_size, int):
+            crop_size = [crop_size, crop_size]
+        _check_image_size(crop_size)
+
+        self.crop_size = crop_size
+
+        if mode not in ("C", "TL"):
+            raise ValueError("Unsupported interpolation method")
+        self.mode = mode
+
+    def apply(self, img):
+        """apply"""
+        h, w = img.shape[:2]
+        cw, ch = self.crop_size
+        if self.mode == "C":
+            x1 = max(0, (w - cw) // 2)
+            y1 = max(0, (h - ch) // 2)
+        elif self.mode == "TL":
+            x1, y1 = 0, 0
+        x2 = min(w, x1 + cw)
+        y2 = min(h, y1 + ch)
+        coords = (x1, y1, x2, y2)
+        if coords == (0, 0, w, h):
+            raise ValueError(
+                f"Input image ({w}, {h}) smaller than the target size ({cw}, {ch})."
+            )
+        img = F.slice(img, coords=coords)
+        return {"img": img, "img_size": [img.shape[1], img.shape[0]]}
+
+
+class _BaseResize(BaseComponent):
+    _INTERP_DICT = {
+        "NEAREST": cv2.INTER_NEAREST,
+        "LINEAR": cv2.INTER_LINEAR,
+        "CUBIC": cv2.INTER_CUBIC,
+        "AREA": cv2.INTER_AREA,
+        "LANCZOS4": cv2.INTER_LANCZOS4,
+    }
+
+    def __init__(self, size_divisor, interp):
+        super().__init__()
+
+        if size_divisor is not None:
+            assert isinstance(
+                size_divisor, int
+            ), "`size_divisor` should be None or int."
+        self.size_divisor = size_divisor
+
+        try:
+            interp = self._INTERP_DICT[interp]
+        except KeyError:
+            raise ValueError(
+                "`interp` should be one of {}.".format(self._INTERP_DICT.keys())
+            )
+        self.interp = interp
+
+    @staticmethod
+    def _rescale_size(img_size, target_size):
+        """rescale size"""
+        scale = min(max(target_size) / max(img_size), min(target_size) / min(img_size))
+        rescaled_size = [round(i * scale) for i in img_size]
+        return rescaled_size, scale
+
+
+class Resize(_BaseResize):
+    """Resize the image."""
+
+    INPUT_KEYS = "img"
+    OUTPUT_KEYS = ["img", "img_size", "scale_factors"]
+    DEAULT_INPUTS = {"img": "img"}
+    DEAULT_OUTPUTS = {
+        "img": "img",
+        "img_size": "img_size",
+        "scale_factors": "scale_factors",
+    }
+
+    def __init__(
+        self, target_size, keep_ratio=False, size_divisor=None, interp="LINEAR"
+    ):
+        """
+        Initialize the instance.
+
+        Args:
+            target_size (list|tuple|int): Target width and height.
+            keep_ratio (bool, optional): Whether to keep the aspect ratio of resized
+                image. Default: False.
+            size_divisor (int|None, optional): Divisor of resized image size.
+                Default: None.
+            interp (str, optional): Interpolation method. Choices are 'NEAREST',
+                'LINEAR', 'CUBIC', 'AREA', and 'LANCZOS4'. Default: 'LINEAR'.
+        """
+        super().__init__(size_divisor=size_divisor, interp=interp)
+
+        if isinstance(target_size, int):
+            target_size = [target_size, target_size]
+        _check_image_size(target_size)
+        self.target_size = target_size
+
+        self.keep_ratio = keep_ratio
+
+    def apply(self, img):
+        """apply"""
+        target_size = self.target_size
+        original_size = img.shape[:2]
+
+        if self.keep_ratio:
+            h, w = img.shape[0:2]
+            target_size, _ = self._rescale_size((w, h), self.target_size)
+
+        if self.size_divisor:
+            target_size = [
+                math.ceil(i / self.size_divisor) * self.size_divisor
+                for i in target_size
+            ]
+
+        img_scale_w, img_scale_h = [
+            target_size[1] / original_size[1],
+            target_size[0] / original_size[0],
+        ]
+        img = F.resize(img, target_size, interp=self.interp)
+        return {
+            "img": img,
+            "img_size": [img.shape[1], img.shape[0]],
+            "scale_factors": [img_scale_w, img_scale_h],
+        }
+
+
+class ResizeByLong(_BaseResize):
+    """
+    Proportionally resize the image by specifying the target length of the
+    longest side.
+    """
+
+    INPUT_KEYS = "img"
+    OUTPUT_KEYS = ["img", "img_size"]
+    DEAULT_INPUTS = {"img": "img"}
+    DEAULT_OUTPUTS = {"img": "img", "img_size": "img_size"}
+
+    def __init__(self, target_long_edge, size_divisor=None, interp="LINEAR"):
+        """
+        Initialize the instance.
+
+        Args:
+            target_long_edge (int): Target length of the longest side of image.
+            size_divisor (int|None, optional): Divisor of resized image size.
+                Default: None.
+            interp (str, optional): Interpolation method. Choices are 'NEAREST',
+                'LINEAR', 'CUBIC', 'AREA', and 'LANCZOS4'. Default: 'LINEAR'.
+        """
+        super().__init__(size_divisor=size_divisor, interp=interp)
+        self.target_long_edge = target_long_edge
+
+    def apply(self, img):
+        """apply"""
+        h, w = img.shape[:2]
+        scale = self.target_long_edge / max(h, w)
+        h_resize = round(h * scale)
+        w_resize = round(w * scale)
+        if self.size_divisor is not None:
+            h_resize = math.ceil(h_resize / self.size_divisor) * self.size_divisor
+            w_resize = math.ceil(w_resize / self.size_divisor) * self.size_divisor
+
+        img = F.resize(img, (w_resize, h_resize), interp=self.interp)
+        return {"img": img, "img_size": [img.shape[1], img.shape[0]]}
+
+
+class ResizeByShort(_BaseResize):
+    """
+    Proportionally resize the image by specifying the target length of the
+    shortest side.
+    """
+
+    INPUT_KEYS = "img"
+    OUTPUT_KEYS = ["img", "img_size"]
+    DEAULT_INPUTS = {"img": "img"}
+    DEAULT_OUTPUTS = {"img": "img", "img_size": "img_size"}
+
+    def __init__(self, target_short_edge, size_divisor=None, interp="LINEAR"):
+        """
+        Initialize the instance.
+
+        Args:
+            target_short_edge (int): Target length of the shortest side of image.
+            size_divisor (int|None, optional): Divisor of resized image size.
+                Default: None.
+            interp (str, optional): Interpolation method. Choices are 'NEAREST',
+                'LINEAR', 'CUBIC', 'AREA', and 'LANCZOS4'. Default: 'LINEAR'.
+        """
+        super().__init__(size_divisor=size_divisor, interp=interp)
+        self.target_short_edge = target_short_edge
+
+    def apply(self, img):
+        """apply"""
+        h, w = img.shape[:2]
+        scale = self.target_short_edge / min(h, w)
+        h_resize = round(h * scale)
+        w_resize = round(w * scale)
+        if self.size_divisor is not None:
+            h_resize = math.ceil(h_resize / self.size_divisor) * self.size_divisor
+            w_resize = math.ceil(w_resize / self.size_divisor) * self.size_divisor
+
+        img = F.resize(img, (w_resize, h_resize), interp=self.interp)
+        return {"img": img, "img_size": [img.shape[1], img.shape[0]]}
+
+
+class Pad(BaseComponent):
+    """Pad the image."""
+
+    INPUT_KEYS = "img"
+    OUTPUT_KEYS = ["img", "img_size"]
+    DEAULT_INPUTS = {"img": "img"}
+    DEAULT_OUTPUTS = {"img": "img", "img_size": "img_size"}
+
+    def __init__(self, target_size, val=127.5):
+        """
+        Initialize the instance.
+
+        Args:
+            target_size (list|tuple|int): Target width and height of the image after
+                padding.
+            val (float, optional): Value to fill the padded area. Default: 127.5.
+        """
+        super().__init__()
+
+        if isinstance(target_size, int):
+            target_size = [target_size, target_size]
+        _check_image_size(target_size)
+        self.target_size = target_size
+
+        self.val = val
+
+    def apply(self, img):
+        """apply"""
+        h, w = img.shape[:2]
+        tw, th = self.target_size
+        ph = th - h
+        pw = tw - w
+
+        if ph < 0 or pw < 0:
+            raise ValueError(
+                f"Input image ({w}, {h}) smaller than the target size ({tw}, {th})."
+            )
+        else:
+            img = F.pad(img, pad=(0, ph, 0, pw), val=self.val)
+        return {"img": img, "img_size": [img.shape[1], img.shape[0]]}
+
+
+class Normalize(BaseComponent):
+    """Normalize the image."""
+
+    INPUT_KEYS = "img"
+    OUTPUT_KEYS = "img"
+    DEAULT_INPUTS = {"img": "img"}
+    DEAULT_OUTPUTS = {"img": "img"}
+
+    def __init__(self, scale=1.0 / 255, mean=0.5, std=0.5, preserve_dtype=False):
+        """
+        Initialize the instance.
+
+        Args:
+            scale (float, optional): Scaling factor to apply to the image before
+                applying normalization. Default: 1/255.
+            mean (float|tuple|list, optional): Means for each channel of the image.
+                Default: 0.5.
+            std (float|tuple|list, optional): Standard deviations for each channel
+                of the image. Default: 0.5.
+            preserve_dtype (bool, optional): Whether to preserve the original dtype
+                of the image.
+        """
+        super().__init__()
+
+        self.scale = np.float32(scale)
+        if isinstance(mean, float):
+            mean = [mean]
+        self.mean = np.asarray(mean).astype("float32")
+        if isinstance(std, float):
+            std = [std]
+        self.std = np.asarray(std).astype("float32")
+        self.preserve_dtype = preserve_dtype
+
+    def apply(self, img):
+        """apply"""
+        old_type = img.dtype
+        # XXX: If `old_type` has higher precision than float32,
+        # we will lose some precision.
+        img = img.astype("float32", copy=False)
+        img *= self.scale
+        img -= self.mean
+        img /= self.std
+        if self.preserve_dtype:
+            img = img.astype(old_type, copy=False)
+        return {"img": img}
+
+
+class ToCHWImage(BaseComponent):
+    """Reorder the dimensions of the image from HWC to CHW."""
+
+    INPUT_KEYS = "img"
+    OUTPUT_KEYS = "img"
+    DEAULT_INPUTS = {"img": "img"}
+    DEAULT_OUTPUTS = {"img": "img"}
+
+    def apply(self, img):
+        """apply"""
+        img = img.transpose((2, 0, 1))
+        return {"img": img}

+ 1 - 1
paddlex/inference/components/transforms/ts/__init__.py

@@ -12,4 +12,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from .ts_common import *
+from .common import *

+ 33 - 9
paddlex/inference/components/transforms/ts/ts_common.py → paddlex/inference/components/transforms/ts/common.py

@@ -23,7 +23,7 @@ from .....utils.cache import CACHE_DIR
 from ....utils.io.readers import TSReader
 from ....utils.io.writers import TSWriter
 from ...base import BaseComponent
-from .ts_functions import load_from_dataframe, time_feature
+from .funcs import load_from_dataframe, time_feature
 
 
 __all__ = [
@@ -36,6 +36,8 @@ __all__ = [
     "BuildPadMask",
     "ArraytoTS",
     "TSDeNormalize",
+    "GetAnomaly",
+    "GetCls",
 ]
 
 
@@ -311,17 +313,18 @@ class ArraytoTS(BaseComponent):
 
 class GetAnomaly(BaseComponent):
 
-    INPUT_KEYS = ["ori_ts", "pred_ts"]
-    OUTPUT_KEYS = ["pred_ts"]
-    DEAULT_INPUTS = {"ori_ts": "ori_ts", "pred_ts": "pred_ts"}
-    DEAULT_OUTPUTS = {"pred_ts": "pred_ts"}
+    INPUT_KEYS = ["ori_ts", "pred"]
+    OUTPUT_KEYS = ["anomaly"]
+    DEAULT_INPUTS = {"ori_ts": "ori_ts", "pred": "pred"}
+    DEAULT_OUTPUTS = {"anomaly": "anomaly"}
 
     def __init__(self, model_threshold, info_params):
         super().__init__()
         self.model_threshold = model_threshold
         self.info_params = info_params
 
-    def apply(self, ori_ts, pred_ts):
+    def apply(self, ori_ts, pred):
+        pred = pred[0]
         if ori_ts.get("past_target", None) is not None:
             ts = ori_ts["past_target"]
         elif ori_ts.get("observed_cov_numeric", None) is not None:
@@ -338,14 +341,35 @@ class GetAnomaly(BaseComponent):
             else self.info_params["feature_cols"]
         )
 
-        anomaly_score = np.mean(np.square(pred_ts - np.array(ts)), axis=-1)
+        anomaly_score = np.mean(np.square(pred - np.array(ts)), axis=-1)
         anomaly_label = (anomaly_score >= self.model_threshold) + 0
 
         past_target_index = ts.index
         past_target_index.name = self.info_params["time_col"]
         anomaly_label = pd.DataFrame(
-            np.reshape(anomaly_label, newshape=[pred_ts.shape[0], -1]),
+            np.reshape(anomaly_label, newshape=[pred.shape[0], -1]),
             index=past_target_index,
             columns=["label"],
         )
-        return {"pred_ts": anomaly_label}
+        return {"anomaly": anomaly_label}
+
+
+class GetCls(BaseComponent):
+
+    INPUT_KEYS = ["pred"]
+    OUTPUT_KEYS = ["classification"]
+    DEAULT_INPUTS = {"pred": "pred"}
+    DEAULT_OUTPUTS = {"classification": "classification"}
+
+    def __init__(self):
+        super().__init__()
+
+    def apply(self, pred):
+        pred_ts = pred[0]
+        pred_ts -= np.max(pred_ts, axis=-1, keepdims=True)
+        pred_ts = np.exp(pred_ts) / np.sum(np.exp(pred_ts), axis=-1, keepdims=True)
+        classid = np.argmax(pred_ts, axis=-1)
+        pred_score = pred_ts[classid]
+        result = pd.DataFrame.from_dict({"classid": [classid], "score": [pred_score]})
+        result.index.name = "sample"
+        return {"classification": result}

+ 0 - 0
paddlex/inference/components/transforms/ts/ts_functions.py → paddlex/inference/components/transforms/ts/funcs.py


+ 2 - 1
paddlex/inference/models/__init__.py

@@ -14,7 +14,7 @@
 
 
 from pathlib import Path
-from .official_models import official_models
+from ..utils.official_models import official_models
 
 from .base import BasePredictor, BasicPredictor
 from .image_classification import ClasPredictor
@@ -26,6 +26,7 @@ from .instance_segmentation import InstanceSegPredictor
 from .semantic_segmentation import SegPredictor
 from .general_recognition import ShiTuRecPredictor
 from .ts_fc import TSFcPredictor
+from .ts_ad import TSAdPredictor
 from .ts_cls import TSClsPredictor
 from .image_unwarping import WarpPredictor
 

+ 8 - 17
paddlex/inference/models/ts.py → paddlex/inference/models/ts_ad.py

@@ -14,15 +14,14 @@
 
 import os
 
-from ...utils.func_register import FuncRegister
-from ...modules.ts_forecast.model_list import MODELS
+from ...modules.ts_anomaly_detection.model_list import MODELS
 from ..components import *
-from ..results import TSResult
+from ..results import TSAdResult
 from ..utils.process_hook import batchable_method
 from .base import BasicPredictor
 
 
-class TSPredictor(BasicPredictor):
+class TSAdPredictor(BasicPredictor):
 
     entities = MODELS
 
@@ -69,20 +68,12 @@ class TSPredictor(BasicPredictor):
     def _build_postprocess(self):
         if not self.config.get("info_params", None):
             raise Exception("info_params is not found in config file")
-
         ops = {}
-        ops["ArraytoTS"] = ArraytoTS(self.config["info_params"])
-        if self.config.get("scale", None):
-            scaler_file_path = os.path.join(self.model_dir, "scaler.pkl")
-            if not os.path.exists(scaler_file_path):
-                raise Exception(f"Cannot find scaler file: {scaler_file_path}")
-            ops["TSDeNormalize"] = TSDeNormalize(
-                scaler_file_path, self.config["info_params"]
-            )
+        ops["GetAnomaly"] = GetAnomaly(
+            self.config["model_threshold"], self.config["info_params"]
+        )
         return ops
 
     @batchable_method
-    def _pack_res(self, data):
-        return {
-            "result": TSResult({"ts_path": data["ts_path"], "forecast": data["pred"]})
-        }
+    def _pack_res(self, single):
+        return TSAdResult({"ts_path": single["ts_path"], "anomaly": single["anomaly"]})

+ 4 - 2
paddlex/inference/models/ts_cls.py

@@ -34,7 +34,7 @@ class TSClsPredictor(BasicPredictor):
             model_prefix=self.MODEL_FILE_PREFIX,
             option=self.pp_option,
         )
-        return {**preprocess, "predictor": predictor}
+        return {**preprocess, "predictor": predictor, "GetCls": GetCls()}
 
     def _build_preprocess(self):
         if not self.config.get("info_params", None):
@@ -57,4 +57,6 @@ class TSClsPredictor(BasicPredictor):
         return ops
 
     def _pack_res(self, single):
-        return TSClsResult({"ts_path": single["ts_path"], "forecast": single["pred"]})
+        return TSClsResult(
+            {"ts_path": single["ts_path"], "classification": single["classification"]}
+        )

+ 5 - 0
paddlex/inference/pipelines/single_model_pipeline.py

@@ -22,6 +22,11 @@ class SingleModelPipeline(BasePipeline):
         "object_detection",
         "instance_segmentation",
         "semantic_segmentation",
+        "ts_fc",
+        "ts_ad",
+        "ts_cls",
+        "multi_label_image_classification",
+        "anomaly_detection",
     ]
 
     def __init__(self, model, batch_size=1, device="gpu", predictor_kwargs=None):

+ 1 - 1
paddlex/inference/results/__init__.py

@@ -21,5 +21,5 @@ from .ocr import OCRResult
 from .det import DetResult
 from .seg import SegResult
 from .instance_seg import InstanceSegResult
-from .ts import TSFcResult, TSClsResult
+from .ts import TSFcResult, TSAdResult, TSClsResult
 from .warp import DocTrResult

+ 0 - 1
paddlex/inference/results/base.py

@@ -18,7 +18,6 @@ import numpy as np
 import json
 
 from ...utils import logging
-import numpy as np
 from ..utils.io import JsonWriter, ImageReader, ImageWriter
 
 

+ 18 - 15
paddlex/inference/results/ts.py

@@ -16,6 +16,7 @@ from pathlib import Path
 import numpy as np
 import pandas as pd
 
+from ...utils import logging
 from ..utils.io import TSWriter
 from .base import BaseResult
 
@@ -31,30 +32,32 @@ class TSFcResult(BaseResult):
         if not save_path.endswith(".csv"):
             save_path = Path(save_path) / f"{Path(self['ts_path']).stem}.csv"
         self._writer.write(save_path, self["forecast"])
+        logging.info(f"The result has been saved in {save_path}.")
 
 
 class TSClsResult(BaseResult):
 
     def __init__(self, data):
-        super().__init__(
-            {"ts_path": data["ts_path"], "classification": self.process_data(data)}
-        )
+        super().__init__(data)
         self._writer = TSWriter(backend="pandas")
 
-    def process_data(self, data):
-        """apply"""
-        pred_ts = data["forecast"][0]
-        pred_ts -= np.max(pred_ts, axis=-1, keepdims=True)
-        pred_ts = np.exp(pred_ts) / np.sum(np.exp(pred_ts), axis=-1, keepdims=True)
-        classid = np.argmax(pred_ts, axis=-1)
-        pred_score = pred_ts[classid]
-        result = {"classid": [classid], "score": [pred_score]}
-        result = pd.DataFrame.from_dict(result)
-        result.index.name = "sample"
-        return result
-
     def save_to_csv(self, save_path):
         """write ts"""
         if not save_path.endswith(".csv"):
             save_path = Path(save_path) / f"{Path(self['ts_path']).stem}.csv"
         self._writer.write(save_path, self["classification"])
+        logging.info(f"The result has been saved in {save_path}.")
+
+
+class TSAdResult(BaseResult):
+
+    def __init__(self, data):
+        super().__init__(data)
+        self._writer = TSWriter(backend="pandas")
+
+    def save_to_csv(self, save_path):
+        """write ts"""
+        if not save_path.endswith(".csv"):
+            save_path = Path(save_path) / f"{Path(self['ts_path']).stem}.csv"
+        self._writer.write(save_path, self["anomaly"])
+        logging.info(f"The result has been saved in {save_path}.")

+ 0 - 0
paddlex/inference/models/official_models.py → paddlex/inference/utils/official_models.py