def _perturb(
        self,
        x: np.ndarray,
        y: np.ndarray,
        mask: np.ndarray,
        obj_width: float,
        focal: float,
        clip_min: float,
        clip_max: float,
        pts: np.ndarray | None,
    ) -> np.ndarray:
        """
        Internal attack function for one example.

        :param x: An array with one original input to be attacked.
        :param y: The target label.
        :param mask: An array with a mask to be applied to the adversarial perturbations. Shape needs to be
                     broadcastable to the shape of x. Any features for which the mask is zero will not be adversarially
                     perturbed.
        :param obj_width: Estimated width of object in inches for perspective transform.
        :param focal: Estimated focal length in ft for perspective transform.
        :param clip_min: Minimum value of an example.
        :param clip_max: Maximum value of an example.
        :param pts: Optional. A set of points that will set the crop size in the perspective transform.
        :return: An adversarial example.
        """
        import torch

        x = (x.copy() - clip_min) / (clip_max - clip_min)

        mask = mask / np.max(mask)
        mask = np.where(mask > 0.5, 1.0, 0.0)

        x_copy = x.copy()
        mask_copy = mask.copy()
        y_onehot = y.copy()
        y = np.argmax(y, axis=0)

        # Load victim img and starting mask.
        img = torch.tensor(x_copy, requires_grad=True, device=self.estimator.device)
        mask_tensor = torch.tensor(mask_copy, requires_grad=True, device=self.estimator.device).to(img.dtype)
        y_onehot_tensor = torch.tensor(y_onehot, requires_grad=True, device=self.estimator.device)

        # Load transforms.
        xforms = get_transform_params(
            self.num_xforms,
            self.rotation_range,
            self.dist_range,
            self.gamma_range,
            self.crop_percent_range,
            self.off_x_range,
            self.off_y_range,
            self.blur_kernels,
            obj_width,
            focal,
        )

        target_label = y
        # Attack
        rounds = 0
        transform_robustness = -1.0
        prev_attack = img.detach().clone()
        while True:
            adv_img = img.detach().clone()
            adv_img = torch.where(mask_tensor > 0.5, prev_attack, adv_img)
            rand_start = (
                torch.FloatTensor(*img.size())
                .uniform_(self.rand_start_epsilon_range[0], self.rand_start_epsilon_range[1])
                .to(adv_img.device)
            )
            adv_img = (adv_img + mask_tensor * rand_start).detach()
            adv_img = torch.clamp(adv_img, 0, 1).to(img.dtype)
            adv_img.requires_grad = True
            final_avg_grad = torch.zeros(img.size())

            # Do EOT adversarial attack with current mask.
            loop_length = self.steps if rounds > 0 else self.first_steps
            for _ in range(loop_length):
                avg_grad = torch.zeros(adv_img.size()).to(self.estimator.device)
                for xform in xforms:
                    xform_img = transform_wb(
                        img.clone().unsqueeze(0),
                        adv_img.unsqueeze(0),
                        mask_tensor,
                        xform,
                        self.net_size,
                        clip_min,
                        clip_max,
                        pts,
                    )

                    logits, _ = self.estimator._predict_framework(xform_img.to(self.estimator.device), y_onehot_tensor)
                    if self.use_logits:
                        loss = torch.nn.functional.cross_entropy(
                            input=logits,
                            target=torch.tensor(target_label).unsqueeze(0).to(logits.device),
                            reduction="mean",
                        )
                    else:
                        loss = torch.nn.functional.nll_loss(
                            input=logits, target=torch.tensor(target_label).unsqueeze(0), reduction="mean"
                        )

                    grad = torch.autograd.grad(loss, adv_img)[0]
                    avg_grad += grad
                avg_grad /= len(xforms)

                avg_grad_sign = avg_grad.clone()
                avg_grad_sign = torch.sign(avg_grad_sign)
                avg_grad_sign[torch.isnan(avg_grad_sign)] = 0
                adv_img = adv_img - mask_tensor * self.step_size * avg_grad_sign

                adv_img = adv_img.clamp(0, 1)
                transform_robustness = self._eval(
                    img.detach().clone(),
                    adv_img,
                    mask_tensor,
                    target_label,
                    y_onehot_tensor,
                    xforms,
                    clip_min,
                    clip_max,
                    pts,
                )
                final_avg_grad = avg_grad
                if transform_robustness >= self.min_tr:
                    break

            if transform_robustness < self.min_tr:
                break

            prev_attack = adv_img.detach().clone()

            # Remove low-impact pixel-patches or pixels in mask.
            pert = adv_img - img
            final_avg_grad[torch.isnan(final_avg_grad)] = 0
            final_avg_grad = mask_tensor * final_avg_grad * pert
            pixelwise_avg_grads = torch.sum(torch.abs(final_avg_grad), dim=0)

            for _ in range(self.num_patches_to_remove):
                # Find minimum gradient patch and remove it.
                min_patch_grad = None
                min_patch_grad_idx = None
                for i in np.arange(
                    0, pixelwise_avg_grads.shape[0] - self.patch_removal_size + 0.0001, self.patch_removal_interval
                ):
                    for j in np.arange(
                        0, pixelwise_avg_grads.shape[1] - self.patch_removal_size + 0.0001, self.patch_removal_interval
                    ):
                        patch_grad = pixelwise_avg_grads[
                            int(round(i)) : int(round(i + self.patch_removal_size)),
                            int(round(j)) : int(round(j + self.patch_removal_size)),
                        ].sum()

                        if (
                            mask_tensor[
                                0,
                                int(round(i)) : int(round(i + self.patch_removal_size)),
                                int(round(j)) : int(round(j + self.patch_removal_size)),
                            ].sum()
                            > 0
                        ):
                            patch_grad = (
                                patch_grad
                                / mask_tensor[
                                    0,
                                    int(round(i)) : int(round(i + self.patch_removal_size)),
                                    int(round(j)) : int(round(j + self.patch_removal_size)),
                                ].sum()
                            )  # TODO1
                            if min_patch_grad is None or patch_grad.item() < min_patch_grad:
                                min_patch_grad = patch_grad.item()
                                min_patch_grad_idx = (i, j)
                if min_patch_grad_idx is None:
                    break
                i, j = min_patch_grad_idx
                mask_tensor[
                    0,
                    int(round(i)) : int(round(i + self.patch_removal_size)),
                    int(round(j)) : int(round(j + self.patch_removal_size)),
                ] = 0
                if img.size()[0] == 3:
                    mask_tensor[
                        1,
                        int(round(i)) : int(round(i + self.patch_removal_size)),
                        int(round(j)) : int(round(j + self.patch_removal_size)),
                    ] = 0
                    mask_tensor[
                        2,
                        int(round(i)) : int(round(i + self.patch_removal_size)),
                        int(round(j)) : int(round(j + self.patch_removal_size)),
                    ] = 0

            rounds += 1

        out = prev_attack.detach().clone().cpu().numpy()
        adversarial = np.clip(out.copy() * (clip_max - clip_min) + clip_min, clip_min, clip_max)
        return adversarial

        Internal attack function for one example.

        :param x: An array with one original input to be attacked.
        :param y: The target label.
        :param mask: An array with a mask to be applied to the adversarial perturbations. Shape needs to be
                     broadcastable to the shape of x. Any features for which the mask is zero will not be adversarially
                     perturbed.
        :param obj_width: Estimated width of object in inches for perspective transform.
        :param focal: Estimated focal length in ft for perspective transform.
        :param clip_min: Minimum value of an example.
        :param clip_max: Maximum value of an example.
        :param pts: Optional. A set of points that will set the crop size in the perspective transform.
        :return: An adversarial example.
        
art/attacks/evasion/graphite/graphite_whitebox_pytorch.py