def _build_graph(
        self,
        initial_shape: tuple[int, ...],
        custom_loss: "Tensor" | None = None,
        rendering_function: "Callable" | None = None,
    ) -> tuple["Tensor", ...]:
        """
        Build the TensorFlow graph for the attack.

        :param initial_shape: Image/texture shape.
        :param custom_loss: Custom loss function from users.
        :param rendering_function: A rendering function to use textures as input.
        :return: A tuple of tensors.
        """
        import tensorflow as tf

        # Create a placeholder to pass input image/texture
        initial_input = tf.placeholder(dtype=tf.float32, shape=initial_shape, name="initial_input")

        # Create adversarial image
        project_texture_op = None

        if self.texture_as_input:
            # Create a placeholder to pass input texture mask
            mask_input = tf.placeholder(dtype=tf.float32, shape=initial_shape, name="mask_input")

            # Create texture variable
            if self.use_spectral:
                initial_value = np.zeros(
                    (2, initial_shape[0], initial_shape[3], initial_shape[1], int(np.ceil(initial_shape[2] / 2) + 1))
                )

                current_texture_variable = tf.Variable(
                    initial_value=initial_value, dtype=tf.float32, name="current_texture_variable"
                )

                current_texture = current_texture_variable
                current_texture = tf.complex(current_texture[0], current_texture[1])
                current_texture = tf.map_fn(tf.spectral.irfft2d, current_texture, dtype=tf.float32)
                current_texture = tf.transpose(current_texture, (0, 2, 3, 1))

            else:
                initial_value = np.zeros((initial_shape[0], initial_shape[1], initial_shape[2], initial_shape[3]))

                current_texture_variable = tf.Variable(
                    initial_value=initial_value, dtype=tf.float32, name="current_texture_variable"
                )

                current_texture = current_texture_variable

            # Invert texture for projection
            project_texture = initial_input * (1.0 - mask_input) + current_texture * mask_input

            if self.soft_clip:
                project_texture = tf.nn.sigmoid(project_texture)
            else:
                project_texture = tf.clip_by_value(project_texture, 0.0, 1.0)

            if self.use_spectral:
                project_texture = tf.transpose(project_texture, (0, 3, 1, 2))
                project_texture = tf.map_fn(tf.spectral.rfft2d, project_texture, dtype=tf.complex64)
                project_texture = tf.stack([tf.real(project_texture), tf.imag(project_texture)])

            # Update texture variable
            project_texture_op = tf.assign(current_texture_variable, project_texture, name="project_texture_op")

            # Create a placeholder to pass the background
            background_phd = tf.placeholder(
                dtype=tf.float32, shape=initial_input.shape.as_list(), name="background_phd"
            )

            # Create a placeholder to pass the image frame
            image_frame_phd = tf.placeholder(
                dtype=tf.float32, shape=[initial_shape[0], None, None, 4], name="image_frame_phd"
            )

            # Create adversarial image
            if rendering_function is not None:
                current_image = rendering_function(background_phd, image_frame_phd, current_texture)
            else:  # pragma: no cover
                ValueError("Callable rendering_function is None.")

        else:
            # Create image variable
            current_image_variable = tf.Variable(
                initial_value=np.zeros(initial_input.shape.as_list()), dtype=tf.float32, name="current_image_variable"
            )

            # Create a placeholder to pass random transformation
            random_transformation_phd = tf.placeholder(
                dtype=tf.float32, shape=initial_input.shape.as_list(), name="random_transformation_phd"
            )

            # Update current image
            current_image = current_image_variable + random_transformation_phd
            current_image = (tf.tanh(current_image) + 1) / 2
            current_image = tf.identity(current_image, name="current_image")

        # Assign current image to the input of the object detector
        current_image_assign_to_input_image_op = tf.assign(
            ref=self.estimator.input_images, value=current_image, name="current_image_assign_to_input_image_op"
        )

        # Create attack loss
        if self.texture_as_input:
            total_loss = self._create_attack_loss(
                initial_input=initial_input, current_value=current_texture, custom_loss=custom_loss
            )
        else:
            total_loss = self._create_attack_loss(
                initial_input=initial_input, current_value=current_image, custom_loss=custom_loss
            )

        # Create optimizer
        optimizer = self._create_optimizer()

        # Create gradients
        if self.texture_as_input:
            gradients = optimizer.compute_gradients(total_loss, var_list=[current_texture_variable])[0][0]
        else:
            gradients = optimizer.compute_gradients(total_loss, var_list=[current_image_variable])[0][0]

        # Create variables to store gradients
        if self.texture_as_input:
            sum_gradients = tf.Variable(
                initial_value=np.zeros(current_texture_variable.shape.as_list()),
                trainable=False,
                name="sum_gradients",
                dtype=tf.float32,
                collections=[tf.GraphKeys.GLOBAL_VARIABLES, tf.GraphKeys.LOCAL_VARIABLES],
            )

        else:
            sum_gradients = tf.Variable(
                initial_value=np.zeros(current_image_variable.shape.as_list()),
                trainable=False,
                name="sum_gradients",
                dtype=tf.float32,
                collections=[tf.GraphKeys.GLOBAL_VARIABLES, tf.GraphKeys.LOCAL_VARIABLES],
            )

        num_gradients = tf.Variable(
            initial_value=0.0,
            trainable=False,
            name="count_gradients",
            collections=[tf.GraphKeys.GLOBAL_VARIABLES, tf.GraphKeys.LOCAL_VARIABLES],
        )

        # Accumulate gradients
        accumulated_sum_gradients = tf.assign_add(sum_gradients, gradients)
        accumulated_num_gradients = tf.assign_add(num_gradients, 1.0)

        # Final gradients
        final_gradients = tf.div(
            accumulated_sum_gradients, tf.maximum(accumulated_num_gradients, 1.0), name="final_gradients"
        )

        if self.sign_gradients:
            final_gradients = tf.sign(final_gradients)

        # Create accumulated gradients operator
        accumulated_gradients_op = tf.group(
            [accumulated_sum_gradients, accumulated_num_gradients], name="accumulated_gradients_op"
        )

        # Create final attack optimization operator and return
        if self.texture_as_input:
            # Create final attack optimization operator
            final_attack_optimization_op = optimizer.apply_gradients(
                grads_and_vars=[(final_gradients, current_texture_variable)], name="final_attack_optimization_op"
            )

            return (
                project_texture_op,
                current_image_assign_to_input_image_op,
                accumulated_gradients_op,
                final_attack_optimization_op,
                current_texture_variable,
                current_texture,
            )

        # Create final attack optimization operator
        final_attack_optimization_op = optimizer.apply_gradients(
            grads_and_vars=[(final_gradients, current_image_variable)], name="final_attack_optimization_op"
        )

        return (
            project_texture_op,
            current_image_assign_to_input_image_op,
            accumulated_gradients_op,
            final_attack_optimization_op,
            current_image_variable,
            current_image,
        )

        Build the TensorFlow graph for the attack.

        :param initial_shape: Image/texture shape.
        :param custom_loss: Custom loss function from users.
        :param rendering_function: A rendering function to use textures as input.
        :return: A tuple of tensors.
        
art/attacks/evasion/shapeshifter.py