Here is a function that I use that works. I use the PHP Api to get an user context object and do the rest through curl.
$opContext - from the PHP API
$user_id - from d2l
$filename - image filename on server
$filepath - path to file on server (I have faculty and students in different places)
$filetype - for the mimetype
static function set_user_image($opContext,$user_id,$filename,$filepath,$filetype){
$fp = fopen($filepath.$filename, 'r');
$contents = fread($fp, filesize($filepath.$filename));
fclose($fp);
$random_hash = "xxBOUNDARYxx";
$request ="--".$random_hash."\r\nContent-Type: application/json\r\n\r\n{\"Text\":\"Some comment\", \"HTML\":null}\r\n\r\n--".
$random_hash."\r\nContent-Disposition: form-data; name=\"profileimage\"; filename="."\"$filename\""."\r\nContent-Type: image/$filetype\r\n\r\n".
$contents."\r\n\r\n--".$random_hash;
$length=strlen($request);
$url = $opContext->createAuthenticatedUri("/d2l/api/lp/1.1/profile/user/$user_id/image","POST");
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,$url);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array("HTTP/1.1", "Content-Type: multipart/form-data; boundary=xxBOUNDARYxx","Content-Length:".$length));
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
curl_setopt($ch, CURLOPT_POST,true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $request);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,true);
$response = curl_exec($ch);
}