Back in December 2015 my modded camera had the first Ha-enhanced light. As I keep most raws, I dag up those in order to test whether my processing abilities have evolved. One aspect is the healing algorithm I wrote to handle the dense star fields – ie: remove all highlights and their immediate context and fill the holes from the remaining image data. Canon 1100D mod, 20×110 sec, ISO 1600, Canon 200mm f/2.8 lens dialed to f/4.5, EQ3 mount, unguided. More than fairly dark sky of a place near Dangau Mare, Cluj county, Romania.
Finally, the process
The code I used to process this time
<!--
script to fill the blanks of input.jpg
by extrapolating the context of the blank,
saving the output as output*.jpg
-->
<div align="center">
<?php
report("=============starting the script=============");
$i = imagecreatefromstring(file_get_contents('input.jpg'));
report("created the input image");
//settings
$GLOBALS['minlumi'] = 35;
$GLOBALS['maxlumi'] = 140;
$GLOBALS['max_damage'] = 250;
$GLOBALS['csillaglumi'] = 150;
$GLOBALS["kill_highlight_radius"] = 2; //2 or 3
$stages_to_save = 25;
set_time_limit(60000);//majdnem 24 ora :P
ini_set('memory_limit', '2048M'); //2 giga, szoval legyszives
ignore_user_abort(true);
//========================================================================
function report($s){
$s = $s.'<br />';
$s = str_replace("<br /><br />", "<br />", $s);
$s = date('Y.m.d. H:i:s ').$s;
echo $s;
$s = str_replace("<br />", "\r\n", $s)."\r\n";
$s = str_replace("\r\n\r\n", "\r\n", $s);
file_put_contents('log.txt', $s, FILE_APPEND | LOCK_EX);
}
//========================================================================
function numberaskilo($i){
while (strlen($i) < 3){
$i = "0".$i;
}
return $i;
}
//========================================================================
function int2rgb($i){
if (is_array($i)){
return $i;
}
$r = $i >> 16;
$g = ($i >> 8) %256;
$b = $i % 256;
return array($r, $g, $b);
}
//========================================================================
function rgb2int($r, $g=false, $b=false){
if (is_array($r)){
$b = $r[2];
$g = $r[1];
$r = $r[0];
}
return $r * 256*256 + $g*256 + $b;
}
//========================================================================
function luminanceof($i_or_rgb){
if (!is_array($i_or_rgb)){
$i_or_rgb = int2rgb($i_or_rgb);
}
return floor(($i_or_rgb[0] + $i_or_rgb[1]*2 + $i_or_rgb[2]) / 4);
}
//========================================================================
function imagesetpixel_($i, $tx, $ty, $p){
if (($tx >= 0) && ($tx < imagesx($i)) && ($ty >= 0) && ($ty < imagesy($i))){
imagesetpixel($i, $tx, $ty, $p);
};
}
//========================================================================
function imagecolorat_($i, $tx, $ty){
if (($tx >= 0) && ($tx < imagesx($i)) && ($ty >= 0) && ($ty < imagesy($i))){
return imagecolorat($i, $tx, $ty);
};
return 0;
}
//========================================================================
function maxcolororluminance($pix){
return luminanceof($pix);
////
$pix = int2rgb($pix);
$pix[] = floor(($pix[0] + $pix[1] + $pix[1] + $pix[2]) / 4);
return max($pix);
}
//========================================================================
function heal_pixel_if_possible($i, $x, $y){
$xy = $x.'_'.$y;
$pix = imagecolorat($i, $x, $y);
if (maxcolororluminance($pix) >= $GLOBALS['minlumi']){
return false;
};
$u = array();
for ($tx = -1; $tx < 2; $tx++){
for ($ty = -1; $ty < 2; $ty++){
if (($tx==0)&&($ty==0)){
// _self
}else{
$p = imagecolorat_($i, $tx+$x, $ty+$y);
$l = maxcolororluminance($p);
if (($l < $GLOBALS['maxlumi']) && ($l >= $GLOBALS['minlumi'])){
$u[] = int2rgb($p);
}
};
}
}
if (count($u) >= $GLOBALS['mincontext']){
$uj = array(0,0,0);
foreach ($u as $j){
for ($q=0; $q<3; $q++){
$uj[$q] = $uj[$q] + $j[$q];
};
}
for ($q=0; $q<3; $q++){
$uj[$q] = floor($uj[$q] / count($u));
};
imagesetpixel($i, $x, $y, rgb2int($uj));
return true;
}
}
//========================================================================
function handle_this_pixel(&$i, &$x, &$y, &$healed, &$save_stage_at_x_heals, &$st){
if (!file_exists("run.txt")){
imagejpeg($i, "output_8_premature.jpg", 95);
die();
}
if (heal_pixel_if_possible($i, $x, $y)){
$healed++;
if ($healed % $save_stage_at_x_heals == 0){
imagejpeg($i, 'output_3_stage_'.numberaskilo($st).'.jpg', 95);
$st++;
}
};
};
//========================================================================
function kill_starmap(&$i, &$star_map, $radius = -1){
if ($radius == -1){
$radius = $GLOBALS["kill_highlight_radius"];
};
foreach ($star_map as $star){
$min = -$radius;
$max = $radius;
for ($rx = $min; $rx <= $max; $rx++){
for ($ry = $min; $ry <= $max; $ry++){
if (
(($rx == $min) OR ($rx == $max))
AND
(($ry == $min) OR ($ry == $max))
){
//this is a corner, but lets make the hole a bit round
}else{
imagesetpixel_($i, $star[0]+$rx, $star[1]+$ry, 0);
}
}
}
}
}
//========================================================================
function kill_off_the_highlights(&$i){
report("killing off the highlights");
$star_map = array();
for ($x = 0; $x < imagesx($i); $x++){
for ($y = 0; $y < imagesy($i); $y++){
$pix = imagecolorat($i, $x, $y);
if (maxcolororluminance($pix) > $GLOBALS['csillaglumi']){
$star_map[] = array($x, $y);
};
}
}
kill_starmap($i, $star_map);
imagejpeg($i, "input_2_nohighlights.jpg", 100);
report('done killing off the highlights');
};
//========================================================================
function kill_off_remaining_starlike_areas(&$i){
////ha minden iranyba ugyanaz vagy kisebb, akkor jelolt
$lumidelta = 25;
$max_coldelta = 45;
$radius = 3;
$kill = array();
$oszto = 10;
$progress = round(imagesx($i) / $oszto);
$ko = 0;
for ($x =0; $x < imagesx($i); $x++){
if (($x+1) % $progress == 0){
$ko++;
file_put_contents("input_4_no_smallerstars_".numberaskilo($ko)."_of_".numberaskilo($oszto).".txt", 100);
}
if (!file_exists('run.txt')){
die("premature exit");
}
for ($y = 0; $y < imagesy($i); $y++){
$p = imagecolorat($i, $x, $y);
if ($p > 0){
$rgb = int2rgb($p);
$atlag = floor(($rgb[0] + $rgb[1]*2 + $rgb[2])/4);
$coldelta = 0;
for ($j =0; $j<=2; $j++){
$coldelta = max($coldelta, abs($rgb[$j] - $atlag));
}
//based on hue, this could be a star, check its context
if ($coldelta < $max_coldelta){
$context = array();
$context_is_lighter = false;
for ($tx = -$radius; $tx <= $radius; $tx++){
for ($ty = -$radius; $ty <= $radius; $ty++){
if ((abs($tx)==$radius)AND(abs($ty)==$radius)){
//round the corners
}else{
$k = imagecolorat_($i, $x+$tx, $y+$ty);
$k_rgb = int2rgb($k);
$k_atlag = floor(($k_rgb[0] + $k_rgb[1]*2 + $k_rgb[2])/4);
if ($k_atlag > $atlag){
$context_is_lighter = true;
}
$context[] = $k;
}
}
}
$fainter = 0;
$black = 0;
if (!$context_is_lighter){
$context_looks_good = 0;
foreach ($context as $c) if (($c!=0)OR(true)) {
if ($c ==0 ){
$black++;
}
$c_rgb = int2rgb($c);
$c_atlag = floor(($c_rgb[0] + $c_rgb[1]*2 + $c_rgb[2])/4);
$c_coldelta = 0;
for ($j =0; $j<=2; $j++){
$c_coldelta = max($c_coldelta, abs($c_rgb[$j] - $c_atlag));
}
$is_fainter = ($c_atlag < $atlag-$lumidelta);
if ($is_fainter){
$fainter++;
}
if (((abs($c_coldelta - $coldelta) < 15) AND (abs($c_atlag - $atlag) < 5)) OR ($is_fainter)){
$context_looks_good++;
}
}
if ((abs(count($context) - $context_looks_good) < max(2, count($context) / 4)) AND ($fainter > 0) AND ($black < count($context) / 3)){
$kill[] = array($x, $y);
}
};
}//pixel is grey enough
}//pixel not black
}
}
kill_starmap($i, $kill, $radius);
imagejpeg($i, "input_6_no_smallerstars.jpg", 100);
}
//========================================================================
function try_loading_a_cache_if_exists(&$i){
$icv = 'input_9_carved.jpg';
//remove this cache, start from scratch
if (file_exists($icv)){
unlink($icv);
}
if (!file_exists($icv)){
report('image input loaded, no cache<br />');
kill_off_the_highlights($i);
kill_off_remaining_starlike_areas($i);
report('saving the carved version<br />');
imagejpeg($i, $icv, 100);
foreach (glob("*.jpg") as $filename){
echo '<img src="'.$filename.'?r='.mt_rand().'" />';
}
//die();
}else{
report('image outpIt loaded from cache<br />');
imagedestroy($i);
$i = imagecreatefromstring(file_get_contents($icv));
}
}
function filter_lighterthanoriginal(&$i){
//apply a "lighter than" filter, assuming the "to be replaced" areas were black
$o = imagecreatefromstring(file_get_contents("input.jpg"));
for ($x = 0; $x< imagesx($o); $x++){
for ($y =0; $y< imagesy($o); $y++){
$pixo = imagecolorat($o, $x, $y);
$pixi = imagecolorat($i, $x, $y);
if (luminanceof($pixi) > luminanceof($pixo)){
imagesetpixel($o, $x, $y, $pixi);
}
};
};
imagejpeg($o, 'output_z_final.jpg', 100);
}
//================================== main ==================================
try_loading_a_cache_if_exists($i);
//count the pixels to heal
report('starting to count the pixels that need to be healed');
$pixels_to_heal = 0;
$skip_y = array();
for ($y = imagesy($i)-1; $y > -1; $y--){
$th = 0;
for ($x = imagesx($i)-1; $x > -1; $x--){
$pix = imagecolorat($i, $x, $y);
if (maxcolororluminance($pix) < $GLOBALS['minlumi']){
$pixels_to_heal++;
$th++;
}
}
if ($th==0){
$skip_y[] = $y;
report('nothing to heal in line '.$y.', adding to ignore list');
}
}
report('counted '.$pixels_to_heal.' pixels to heal, and save in '.$stages_to_save.' stages.<br />');
report(count($skip_y).' lines already being ignored');
$save_stage_at_x_heals = floor($pixels_to_heal / max($stages_to_save, 1))+1;
//iterate and heal
$healed = 0;
$maxx = imagesx($i);
$maxy = imagesy($i);
//$maxx = 300;
//$maxy = 300;
//$iterations = 10;
$st = 0;
$lumi_delta = 255 - $GLOBALS["maxlumi"];
$healed_in_this_round = 0;
$GLOBALS['maxlumi_orig'] = $GLOBALS["maxlumi"];
$skip_y_orig = $skip_y;
$st2 = 0;
$range_sx = array();
for ($sx = 0; $sx < imagesx($i); $sx = $sx + $GLOBALS['max_damage']){
$range_sx[] = $sx;
};
shuffle($range_sx);
$range_sy = array();
for ($sy = 0; $sy < imagesy($i); $sy = $sy + $GLOBALS['max_damage']){
$range_sy[] = $sy;
};
shuffle($range_sy);
//for random pairs of areas
$pairs = array();
foreach ($range_sy as $sy){
foreach ($range_sx as $sx){
$pairs[] = array($sx, $sy);
}
};
shuffle($pairs);
for ($main_iteration = 0; $main_iteration < 2; $main_iteration++){
foreach ($pairs as $pair){
$sx = $pair[0];
$sy = $pair[1];
$GLOBALS['mincontext'] = 5; //start high then fall down
$GLOBALS["maxlumi"] = $GLOBALS["maxlumi_orig"];
$startx = $sx;
$starty = $sy;
$endy = min(imagesy($i), $starty + $GLOBALS['max_damage']);
$endx = min(imagesx($i), $startx + $GLOBALS['max_damage']);
$skip_y = $skip_y_orig;
$to_heal_in_iteration = 1;//dummy
$max_iterations = 50; // 10 still leaves holes
for ($iterations = 0; $iterations < $max_iterations; $iterations++) if ($to_heal_in_iteration > 0){
$h1 = $healed;
report("starting to heal [$startx, $starty, $endx, $endy]. Already healed = ".$healed.' of '.$pixels_to_heal.', '.
$healed_in_this_round.' in the last round. Mincontext now = '.$GLOBALS['mincontext'].'<br />');
//checking lines that are ready
$to_heal_in_iteration = 0;
for ($y =$starty; $y<$endy; $y++) if (!in_array($y, $skip_y)){
$to_heal = 0;
for ($x = $startx; $x<$endx; $x++){
$pix = imagecolorat($i, $x, $y);
if (maxcolororluminance($pix) < $GLOBALS['minlumi']){
$to_heal++;
$to_heal_in_iteration++;
}
}
if ($to_heal == 0){
$skip_y[] = $y;
report('added line '.$y.' to those that are ready and can be ignored<br />');
}
};
$xrange = range($startx, $endx-1);
shuffle($xrange);
$yrange = range($starty, $endy-1);
shuffle($yrange);
$method = $iterations; //egyszer igy, egyszer ugy
// don't rerun empty lines
report('starting to handle pixels');
if ($method % 2 == 1){
report('method 1 running<br />');
foreach($yrange as $y) if (!in_array($y, $skip_y)){
foreach($xrange as $x){
handle_this_pixel($i, $x, $y, $healed, $save_stage_at_x_heals, $st);
}
}
}else{
report('method 2 running<br />');
foreach($xrange as $x){
foreach($yrange as $y) if (!in_array($y, $skip_y)){
handle_this_pixel($i, $x, $y, $healed, $save_stage_at_x_heals, $st);
}
}
}
report('done');
//checking if the process' tolerance is low enough
$healed_in_this_round = $healed - $h1;
if ($healed_in_this_round < 100){
if ($GLOBALS['mincontext'] == 3){
report('few pixels healed, mincontext already small so increasing lumi tolerance<br />');
$GLOBALS['maxlumi'] = min(255, floor($GLOBALS['maxlumi']+$lumi_delta / 10));
};
if ($GLOBALS['mincontext'] > 3){
report('few pixels healed so decreased mincontext from '.$GLOBALS['mincontext'].'<br />');
$GLOBALS['mincontext']--;
};
}
/* if ($iterations > 1){
////not doing this with the smaller areas
if ($GLOBALS['mincontext'] > 4){
report('decreased mincontext from '.$GLOBALS['mincontext'].'<br />');
$GLOBALS['mincontext']--;
}
}
*/
if ($iterations > 3){
if ($GLOBALS['mincontext'] > 3){
report('decreased mincontext from '.$GLOBALS['mincontext'].'<br />');
$GLOBALS['mincontext']--;
}
}
};
$st2++;
imagejpeg($i, 'outpot_2_asregion_'.numberaskilo($st2).'.jpg');
};//for pairs
};//main_iteration
imagejpeg($i, 'output_8_before_lighterthan.jpg', 100);
filter_lighterthanoriginal($i);
// output a blinking interface to reveal the differences
echo '<img id = "img1" src="input.jpg" width="800" />';
echo '<img id = "img2" src="output_z_final.jpg?'.mt_rand().'" width = "800" style="display:none"/>';
?>
</div>
<script>
setInterval(function (){
var a = document.getElementById("img1");
var b = document.getElementById("img2");
if (a.style.display!="none"){
a.style.display = "none";
b.style.display = "";
}else{
a.style.display = "";
b.style.display = "none";
}
}, 1000);
</script>

















