text: from PIL import Image
  from io import BytesIO
  import matplotlib.pyplot as plt
  import numpy as np
  
  import torch
  import torch.optim as optim
  import requests
  from torchvision import transforms, models
  
  
  def load_image(img_path, max_size=400, shape=None):
      """Load image (max size <= 400)"""
      image = Image.open(img_path).convert('RGB')
      
      if max(image.size) > max_size:
          size = max_size
      else:
          size = max(image.size)
      
      if shape is not None:
          size = shape
          
      in_transform = transforms.Compose([
                          transforms.Resize(size),
                          transforms.ToTensor(),
                          transforms.Normalize((0.485, 0.456, 0.406), 
                                               (0.229, 0.224, 0.225))])
  
      image = in_transform(image)[:3,:,:].unsqueeze(0)
      
      return image
  
  def im_convert(tensor):
      """ Display a tensor as an image. """
      image = tensor.to("cpu").clone().detach()
      image = image.numpy().squeeze()
      image = image.transpose(1,2,0)
      image = image * np.array((0.229, 0.224, 0.225)) + np.array((0.485, 0.456, 0.406))
      image = image.clip(0, 1)
  
      return image
  
  def get_features(image, model, layers=None):
      if layers is None:
          layers = {'0': 'conv1_1',
                    '5': 'conv2_1', 
                    '10': 'conv3_1', 
                    '19': 'conv4_1',
                    '21': 'conv4_2',  ## content representation
                    '28': 'conv5_1'}
          
      features = {}
      x = image
  
      for name, layer in model._modules.items():
          x = layer(x)
          if name in layers:
              features[layers[name]] = x
              
      return features
  
  def gram_matrix(tensor):
      """ Calculate the Gram Matrix of a given tensor"""
      # get the batch_size, depth, height, and width of the Tensor
      _, d, h, w = tensor.size()
      
      # calculate the gram matrix
      tensor = tensor.view(d, h * w)
      gram = torch.mm(tensor, tensor.t())
      
      return gram
  
  
  
  
  vgg = models.vgg19(pretrained=True).features
  
  for param in vgg.parameters():
      param.requires_grad_(False)
  
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  
  vgg.to(device)
  
  print(vgg)
  
  
  # load content and style image
  content = load_image('yash.jpg').to(device)
  style = load_image('style.jpg', shape=content.shape[-2:]).to(device)
  
  
  # get content and style features
  content_features = get_features(content, vgg)
  style_features = get_features(style, vgg)
  
  
  # calculate the gram matrices for each layer of our style representation
  style_grams = {layer: gram_matrix(style_features[layer]) for layer in style_features}
  
  # create a third "target" image
  target = content.clone().requires_grad_(True).to(device)
  
  
  # weights for each style layer
  style_weights = {'conv1_1': 1.0,
                   'conv2_1': 0.6,
                   'conv3_1': 0.4,
                   'conv4_1': 0.3,
                   'conv5_1': 0.1}
  
  content_weight = 1  # alpha
  style_weight = 1e6  # beta
  
  show_every = 400
  
  optimizer = optim.Adam([target], lr=0.003)
  epochs = 5000  # decide how many iterations to update your image (5000)
  
  for ii in range(1, epochs+1):
      # get the features from your target image
      target_features = get_features(target, vgg)
      
      # the content loss
      content_loss = torch.mean((target_features['conv4_2'] - content_features['conv4_2'])**2)
      
      # the style loss
      # initialize the style loss to 0
      style_loss = 0
      # then add to it for each layer's gram matrix loss
      for layer in style_weights:
          # get the "target" style representation for the layer
          target_feature = target_features[layer]
          target_gram = gram_matrix(target_feature)
          _, d, h, w = target_feature.shape
          # get the "style" style representation
          style_gram = style_grams[layer]
          # the style loss for one layer, weighted appropriately
          layer_style_loss = style_weights[layer] * torch.mean((target_gram - style_gram)**2)
          # add to the style loss
          style_loss += layer_style_loss / (d * h * w)
          
      # calculate the *total* loss
      total_loss = content_weight * content_loss + style_weight * style_loss
      
      # update your target image
      optimizer.zero_grad()
      total_loss.backward()
      optimizer.step()
      
      # display intermediate images and print the loss
      if  ii % show_every == 0:
          print('Total loss: ', total_loss.item())
          #plt.imshow(im_convert(target))
          #plt.show()
  
  
  
  
  # display the image
  plt.figure()
  plt.subplot(1, 3, 1)
  plt.imshow(im_convert(content))
  
  plt.subplot(1, 3, 2)
  plt.imshow(im_convert(style))
  
  plt.subplot(1, 3, 3)
  plt.imshow(im_convert(target))
  
  plt.show()
  
  