In this post I want to describe how to programmatically change Modern Page Banner Image
The code below is written on JavaScript using JSOM (JavaScript Client Object Model) but it can be easily converted to C# (CSOM) and PowerShell as well.
Let's say you've created some site using any provisioning mechanism (for example, PnP Provisioning Engine). And you also created multiple modern pages on the site and want to change the banner image for all of them.
After some investigation I found a way to achieve that: list items in Site Pages library have a field named LayoutWebpartsContent. If you look at the value of this field you'll see something like:

<div>
  <div data-sp-controldata="%7B%22id%22&amp;#58;%22cbe7b0a9-3504-44dd-a3a3-0e5cacd07788%22,%22instanceId%22&amp;#58;%22cbe7b0a9-3504-44dd-a3a3-0e5cacd07788%22,%22title%22&amp;#58;%22Title%20Region%22,%22description%22&amp;#58;%22Title%20Region%20Description%22,%22serverProcessedContent%22&amp;#58;%7B%22htmlStrings%22&amp;#58;%7B%7D,%22searchablePlainTexts%22&amp;#58;%7B%7D,%22imageSources%22&amp;#58;%7B7D,%22links%22&amp;#58;%7B%7D%7D,%22dataVersion%22&amp;#58;%221.0%22,%22properties%22&amp;#58;%7B%22title%22&amp;#58;%22Logo%20test%22,%22imageSourceType%22&amp;#58;4,%22translateX%22&amp;#58;50,%22translateY%22&amp;#58;50%7D%7D" data-sp-canvascontrol=""></div>
</div>
So it's an HTML markup that contains interesting attribute data-sp-controldata. It looks like some escaped JSON.
Let's unescape it and also replace &#58; with :. The result looks like that:
{
    "id": "cbe7b0a9-3504-44dd-a3a3-0e5cacd07788",
    "instanceId": "cbe7b0a9-3504-44dd-a3a3-0e5cacd07788",
    "title": "Title Region",
    "description": "Title Region Description",
    "serverProcessedContent": {
        "htmlStrings": {},
        "searchablePlainTexts": {},
        "imageSources": {},
        "links": {}
    },
    "dataVersion": "1.0",
    "properties": {
        "title": "Logo test",
        "imageSourceType": 4,
        "translateX": 50,
        "translateY": 50
    }
}
If you proceed the same steps for the page that contains some banner image, you'll see a bit different result:
{
    "id": "cbe7b0a9-3504-44dd-a3a3-0e5cacd07788",
    "instanceId": "cbe7b0a9-3504-44dd-a3a3-0e5cacd07788",
    "title": "Title Region",
    "description": "Title Region Description",
    "serverProcessedContent": {
        "htmlStrings": {},
        "searchablePlainTexts": {},
        "imageSources": {
            "imageSource": "/sites/contoso/SiteAssets/SitePages/banner.png"
        },
        "links": {}
    },
    "dataVersion": "1.0",
    "properties": {
        "title": "Logo test",
        "imageSourceType": 2,
        "translateX": 50,
        "translateY": 50
    }
}
The differences here are in imageSource and imageSourceType properties (Actually you can also see different values for tranlateX and translateY if you changed focal point in the UI): imageSource contains server relative URL of the image that is used as a banner and imageSourceType contains 2 instead of 4.
This information is enough to add (or replace) your own banner image on any page.
First, you need to add the picture to some location on your tenant - SharePoint or OneDrive.
Second, change imageSource and imageSourceType properties as shown below:
var ctx = SP.ClientContext.get_current(); // SP context
var web = ctx.get_web(); // current web
var list = web.get_lists().getByTitle('Site Pages'); // pages library
var items = list.getItems(SP.CamlQuery.createAllItemsQuery()); // all items
ctx.load(items); // loading items from server...
ctx.executeQueryAsync(function() {
    var item = items.get_item(0); // here I'm working with single item by index. But you can iterate through all pages here
    var layoutWebpartsContent = item.get_item('LayoutWebpartsContent'); // getting content
    console.log(layoutWebpartsContent); // let's display the content
    
    var dataAttrContent = /data-sp-controldata="([^"]+)"/gmi.exec(layoutWebpartsContent); // getting data-sp-controldata content
    
    if (dataAttrContent.length) { 
        // we found the attribute.
        // Let's unescape and parse it to JSON
        // the content of the attribute is a 'group' in RegExp result. It will be located as second entry (with index 1)
        var unescaped = unescape(dataAttrContent[1]) // unescape
        var content = JSON.parse(unescaped.replace(/&amp;#58;/gmi, ':')); // replace &amp;#58; with :

        //
        // changing imageSource
        //
        if (!content.serverProcessedContent) {
            content.serverProcessedContent = {};
        }
        if (!content.serverProcessedContent.imageSources) {
            content.serverProcessedContent.imageSources = {};
        }
        content.serverProcessedContent.imageSources.imageSource = '/sites/contoso/SiteAssets/SitePages/banner.png';

        //
        // Changing imageSourceType
        //
        if (!content.properties) {
            content.properties = {};
        }
        content.properties.imageSourceType = 2;

        //
        // escaping back and updating item
        //
        debugger;
        var newContent = JSON.stringify(content);
        newContent = escape(newContent); // escaping
        newContent = newContent.replace(/%3A/gmi, ':').replace(/%2C/gmi, ','); // we need to replace %3A (:) with &amp;#58; and %2C (,) with ,
        layoutWebpartsContent = layoutWebpartsContent.replace(dataAttrContent[1], newContent);
        item.set_item('LayoutWebpartsContent', layoutWebpartsContent);
        item.update();
        ctx.executeQueryAsync(function() {
            console.log('success');
        }, function() {
            console.log('fail');
        });
    }
});
That's it.
Now your page should have the banner you wanted.
Have fun!