#include "services/bge_preview_service.hpp" #include "tile_compile/image/background_extraction.hpp" #include #include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; using tile_compile::Matrix2Df; namespace tile_compile::web { namespace { constexpr int kMaxEdge = 1600; struct InputProxy { Matrix2Df r, g, b; std::vector mask; std::string source; std::string signature; }; struct CachedResult { std::unordered_map> images; nlohmann::json diagnostics; }; std::mutex cache_mutex; std::unordered_map input_cache; std::unordered_map result_cache; std::string fits_error(int status) { char text[FLEN_STATUS]{}; fits_get_errstatus(status, text); return text; } std::vector read_plane(const fs::path& path, long plane, long& width, long& height, long& planes) { fitsfile* file = nullptr; int status = 0; if (fits_open_file(&file, path.string().c_str(), READONLY, &status)) throw std::runtime_error("Cannot open FITS " + path.string() + ": " + fits_error(status)); int naxis = 0; long axes[3]{1,1,1}; if (fits_get_img_dim(file, &naxis, &status) || fits_get_img_size(file, 3, axes, &status) || naxis < 2) { fits_close_file(file, &status); throw std::runtime_error("Invalid FITS image " + path.string()); } width = axes[0]; height = axes[1]; planes = naxis >= 3 ? axes[2] : 1; if (plane > planes) { fits_close_file(file, &status); throw std::runtime_error("Missing FITS plane"); } std::vector out(static_cast(width * height)); long first[3]{1,1,plane}; int any = 0; if (fits_read_pix(file, TFLOAT, first, width * height, nullptr, out.data(), &any, &status)) { const auto message = fits_error(status); fits_close_file(file, &status); throw std::runtime_error(message); } fits_close_file(file, &status); return out; } Matrix2Df matrix_from(const std::vector& values, int rows, int cols) { Matrix2Df out(rows, cols); for (int y=0;y(y*cols+x)]; return out; } Matrix2Df resize_matrix(const Matrix2Df& in, int rows, int cols, int interpolation) { cv::Mat source(in.rows(), in.cols(), CV_32F); for (int y=0;y(y,x)=in(y,x); cv::Mat target; cv::resize(source,target,cv::Size(cols,rows),0,0,interpolation); Matrix2Df out(rows,cols); for (int y=0;y(y,x); return out; } std::string signature(const fs::path& image, const fs::path& mask) { return image.string()+":"+std::to_string(fs::file_size(image))+":"+ std::to_string(static_cast( fs::last_write_time(image).time_since_epoch().count()))+":"+ mask.string()+":"+std::to_string(fs::file_size(mask))+":"+ std::to_string(static_cast( fs::last_write_time(mask).time_since_epoch().count())); } InputProxy load_input(const fs::path& run_dir) { const fs::path outputs=run_dir/"outputs"; fs::path image=outputs/"stacked_rgb_solve.fits"; if (!fs::exists(image)) image=outputs/"stacked_rgb.fits"; const fs::path mask_path=outputs/"canvas_mask.fits"; if (!fs::exists(image)) throw std::runtime_error("No pre-BGE RGB artifact found"); if (!fs::exists(mask_path)) throw std::runtime_error("canvas_mask.fits is required"); const std::string sig=signature(image,mask_path); const std::string key=fs::weakly_canonical(run_dir).string(); { std::lock_guard lock(cache_mutex); auto it=input_cache.find(key); if(it!=input_cache.end()&&it->second.signature==sig)return it->second; } long w,h,p; auto rv=read_plane(image,1,w,h,p); if(p<3) throw std::runtime_error("Pre-BGE RGB artifact has fewer than three planes"); long gw,gh,gp,bw,bh,bp; auto gv=read_plane(image,2,gw,gh,gp); auto bv=read_plane(image,3,bw,bh,bp); if(gw!=w||gh!=h||bw!=w||bh!=h) throw std::runtime_error("Pre-BGE RGB dimensions mismatch"); long mw,mh,mp; auto mv=read_plane(mask_path,1,mw,mh,mp); if(mw!=w||mh!=h) throw std::runtime_error("Canvas mask dimensions mismatch"); InputProxy proxy{matrix_from(rv,h,w),matrix_from(gv,gh,gw),matrix_from(bv,bh,bw),{},image.filename().string(),sig}; proxy.mask.resize(mv.size()); for(size_t i=0;i0.5f?1u:0u; const int edge=std::max(h,w); if(edge>kMaxEdge){ const double s=static_cast(kMaxEdge)/edge; const int rows=std::max(1,static_cast(std::lround(h*s))); const int cols=std::max(1,static_cast(std::lround(w*s))); proxy.r=resize_matrix(proxy.r,rows,cols,cv::INTER_AREA); proxy.g=resize_matrix(proxy.g,rows,cols,cv::INTER_AREA); proxy.b=resize_matrix(proxy.b,rows,cols,cv::INTER_AREA); cv::Mat m(h,w,CV_8U,proxy.mask.data()), mr; cv::resize(m,mr,cv::Size(cols,rows),0,0,cv::INTER_NEAREST); proxy.mask.assign(mr.data,mr.data+static_cast(rows*cols)); } std::lock_guard lock(cache_mutex); if(input_cache.size()>=4)input_cache.erase(input_cache.begin()); input_cache[key]=proxy; return proxy; } templateT val(const nlohmann::json& p,const char* key,T fallback){return p.contains(key)?p.at(key).get():fallback;} image::BGEConfig make_config(const nlohmann::json& p,const InputProxy& input,const nlohmann::json& polygons){ image::BGEConfig cfg; cfg.enabled=true; cfg.method="autobge"; cfg.autobge.num_sample_points=val(p,"num_sample_points",0); cfg.autobge.poly_degree=val(p,"poly_degree",2); cfg.autobge.rbf_smooth=val(p,"rbf_smooth",2.f); cfg.autobge.downsample_scale=val(p,"downsample_scale",4); cfg.autobge.patch_size=val(p,"patch_size",35); cfg.autobge.patch_estimator=val(p,"patch_estimator","sigma_clipped_median"); cfg.autobge.stretch_mode=val(p,"stretch_mode","linear"); cfg.autobge.stretch_target_median=val(p,"stretch_target_median",.25f); cfg.autobge.border_margin=val(p,"border_margin",10); cfg.autobge.bright_exclusion_fraction=val(p,"bright_exclusion_fraction",.2f); cfg.autobge.gradient_descent_max_iters=val(p,"gradient_descent_max_iters",100); cfg.autobge.random_seed=val(p,"random_seed",42); cfg.autobge.normalize_between_stages=val(p,"normalize_between_stages",true); cfg.autobge.apply_guards=val(p,"apply_guards",true); cfg.autobge.mono_mode=val(p,"mono_mode","rgb_duplicate"); std::vector bad; auto range=[&](const char*n,double v,double lo,double hi){if(!std::isfinite(v)||vhi){std::ostringstream s;s<(),yi=poly[i][1].get(),xj=poly[j][0].get(),yj=poly[j][1].get(); if(xi<0||xi>1||yi<0||yi>1)throw std::invalid_argument("Exclusion polygon coordinates must be in [0,1]"); if(((yi>py)!=(yj>py))&&px<(xj-xi)*(py-yi)/((yj-yi)+1e-20)+xi)inside=!inside;} if(inside)cfg.sampling_valid_mask[static_cast(y*input.r.cols()+x)]=0u;}} } return cfg; } std::pair display_range(const InputProxy& in){std::vector v;v.reserve(in.mask.size()/8+1);for(int y=0;y(y*in.r.cols()+x);if(in.mask[i]&&i%8==0){v.push_back(in.r(y,x));v.push_back(in.g(y,x));v.push_back(in.b(y,x));}}if(v.empty())return{0,1};std::sort(v.begin(),v.end());return{v[v.size()/1000],v[std::min(v.size()-1,v.size()*999/1000)]};} std::vector encode(const Matrix2Df&r,const Matrix2Df&g,const Matrix2Df&b,float lo,float hi){cv::Mat out(r.rows(),r.cols(),CV_8UC3);const float d=std::max(hi-lo,1e-9f);for(int y=0;y(y,x);p[2]=cv::saturate_cast((r(y,x)-lo)/d*255);p[1]=cv::saturate_cast((g(y,x)-lo)/d*255);p[0]=cv::saturate_cast((b(y,x)-lo)/d*255);}std::vector png;if(!cv::imencode(".png",out,png))throw std::runtime_error("PNG encoding failed");return png;} } // namespace BgePreviewResult create_bge_preview(const fs::path& run_dir,const nlohmann::json& params,const nlohmann::json& polygons,const nlohmann::json& manual_sample_points,const std::string& view){ BgePreviewResult out; try{ InputProxy input=load_input(run_dir); const std::string cache_key=input.signature+params.dump()+polygons.dump()+manual_sample_points.dump(); {std::lock_guardlock(cache_mutex);auto it=result_cache.find(cache_key);if(it!=result_cache.end()){out.ok=true;out.status=200;out.diagnostics=it->second.diagnostics;if(view!="diagnostics"){auto im=it->second.images.find(view);if(im==it->second.images.end())throw std::invalid_argument("Unknown BGE preview view");out.png=im->second;}return out;}} auto cfg=make_config(params,input,polygons);if(manual_sample_points.is_array()){for(const auto& pt:manual_sample_points){if(!pt.is_array()||pt.size()<2)continue;if(!pt[0].is_number()||!pt[1].is_number())continue;const double nx=pt[0].get(), ny=pt[1].get();if(nx<0.0||nx>1.0||ny<0.0||ny>1.0)continue;cfg.autobge.user_sample_points.push_back({static_cast(nx),static_cast(ny)});}} auto models=image::build_autobge_models(input.r,input.g,input.b,cfg); if(!models.success)throw std::runtime_error("AutoBGE could not build channel models"); Matrix2Df cr=input.r,cg=input.g,cb=input.b; image::BGEDiagnostics diag; diag.attempted=true;diag.bge_method="autobge";diag.method="autobge";diag.channels=models.channel_diagnostics; const bool applied=image::finalize_bge_from_channel_models(cr,cg,cb,models.channel_models,models.channel_diagnostics,cfg,&diag); Matrix2Df br=models.channel_models[0].model,bg=models.channel_models[1].model,bb=models.channel_models[2].model; auto range=display_range(input); std::vector bv;for(int y=0;ylock(cache_mutex);if(result_cache.size()>=4)result_cache.erase(result_cache.begin());result_cache[cache_key]=cached;} out.ok=true;out.status=200;out.diagnostics=cached.diagnostics;if(view!="diagnostics"){auto im=cached.images.find(view);if(im==cached.images.end())throw std::invalid_argument("Unknown BGE preview view");out.png=im->second;} }catch(const std::invalid_argument&e){out.status=400;out.error=e.what();}catch(const std::exception&e){out.status=400;out.error=e.what();}return out; } } // namespace tile_compile::web