Merge branch 'better-bitmap-resize'

Use "nearest" algorithm for resizing bitmaps.

See #22152.
This commit is contained in:
Vadim Zeitlin
2022-02-27 22:44:50 +00:00
6 changed files with 275 additions and 95 deletions

View File

@@ -837,8 +837,8 @@ public:
This function is just a convenient wrapper for wxImage::Rescale() used
to resize the given @a bmp to the requested size. If you need more
control over resizing, e.g. to specify the quality option different
from ::wxIMAGE_QUALITY_HIGH used by default, please use wxImage
function directly instead.
from ::wxIMAGE_QUALITY_NEAREST used by this function, please use the
wxImage function directly instead.
Both the bitmap itself and size must be valid.

View File

@@ -127,6 +127,12 @@ enum
ID_ROTATE_LEFT = wxID_HIGHEST+1,
ID_ROTATE_RIGHT,
ID_RESIZE,
ID_ZOOM_x2,
ID_ZOOM_DC,
ID_ZOOM_NEAREST,
ID_ZOOM_BILINEAR,
ID_ZOOM_BICUBIC,
ID_ZOOM_BOX_AVERAGE,
ID_PAINT_BG
};
@@ -189,6 +195,7 @@ private:
m_bitmap = bitmap;
m_zoom = 1.;
m_useImageForZoom = false;
wxMenu *menu = new wxMenu;
menu->Append(wxID_SAVEAS);
@@ -197,9 +204,17 @@ private:
"Uncheck this for transparent images");
menu->AppendSeparator();
menu->Append(ID_RESIZE, "&Fit to window\tCtrl-F");
menu->AppendSeparator();
menu->Append(wxID_ZOOM_IN, "Zoom &in\tCtrl-+");
menu->Append(wxID_ZOOM_OUT, "Zoom &out\tCtrl--");
menu->Append(wxID_ZOOM_100, "Reset zoom to &100%\tCtrl-1");
menu->Append(ID_ZOOM_x2, "Double zoom level\tCtrl-2");
menu->AppendSeparator();
menu->AppendRadioItem(ID_ZOOM_DC, "Use wx&DC for zoomin\tShift-Ctrl-D");
menu->AppendRadioItem(ID_ZOOM_NEAREST, "Use rescale nearest\tShift-Ctrl-N");
menu->AppendRadioItem(ID_ZOOM_BILINEAR, "Use rescale bilinear\tShift-Ctrl-L");
menu->AppendRadioItem(ID_ZOOM_BICUBIC, "Use rescale bicubic\tShift-Ctrl-C");
menu->AppendRadioItem(ID_ZOOM_BOX_AVERAGE, "Use rescale box average\tShift-Ctrl-B");
menu->AppendSeparator();
menu->Append(ID_ROTATE_LEFT, "Rotate &left\tCtrl-L");
menu->Append(ID_ROTATE_RIGHT, "Rotate &right\tCtrl-R");
@@ -234,14 +249,27 @@ private:
if ( GetMenuBar()->IsChecked(ID_PAINT_BG) )
dc.Clear();
dc.SetUserScale(m_zoom, m_zoom);
const int width = int(m_zoom * m_bitmap.GetWidth());
const int height = int(m_zoom * m_bitmap.GetHeight());
wxBitmap bitmap;
if ( m_useImageForZoom )
{
bitmap = m_bitmap.ConvertToImage().Scale(width, height,
m_resizeQuality);
}
else
{
dc.SetUserScale(m_zoom, m_zoom);
bitmap = m_bitmap;
}
const wxSize size = GetClientSize();
dc.DrawBitmap
(
m_bitmap,
dc.DeviceToLogicalX((size.x - int(m_zoom * m_bitmap.GetWidth())) / 2),
dc.DeviceToLogicalY((size.y - int(m_zoom * m_bitmap.GetHeight())) / 2),
bitmap,
dc.DeviceToLogicalX((size.x - width) / 2),
dc.DeviceToLogicalY((size.y - height) / 2),
true /* use mask */
);
}
@@ -442,16 +470,68 @@ private:
void OnZoom(wxCommandEvent& event)
{
if ( event.GetId() == wxID_ZOOM_IN )
m_zoom *= 1.2;
else if ( event.GetId() == wxID_ZOOM_OUT )
m_zoom /= 1.2;
else // wxID_ZOOM_100
m_zoom = 1.;
switch ( event.GetId() )
{
case wxID_ZOOM_IN:
m_zoom *= 1.2;
break;
case wxID_ZOOM_OUT:
m_zoom /= 1.2;
break;
case wxID_ZOOM_100:
m_zoom = 1.;
break;
case ID_ZOOM_x2:
m_zoom *= 2.;
break;
default:
wxFAIL_MSG("unknown zoom command");
return;
}
UpdateStatusBar();
}
void OnUseZoom(wxCommandEvent& event)
{
bool useImageForZoom = true;
switch ( event.GetId() )
{
case ID_ZOOM_DC:
useImageForZoom = false;
break;
case ID_ZOOM_NEAREST:
m_resizeQuality = wxIMAGE_QUALITY_NEAREST;
break;
case ID_ZOOM_BILINEAR:
m_resizeQuality = wxIMAGE_QUALITY_BILINEAR;
break;
case ID_ZOOM_BICUBIC:
m_resizeQuality = wxIMAGE_QUALITY_BICUBIC;
break;
case ID_ZOOM_BOX_AVERAGE:
m_resizeQuality = wxIMAGE_QUALITY_BOX_AVERAGE;
break;
default:
wxFAIL_MSG("unknown use for zoom command");
return;
}
m_useImageForZoom = useImageForZoom;
Refresh();
}
void OnRotate(wxCommandEvent& event)
{
double angle = 5;
@@ -518,6 +598,11 @@ private:
wxBitmap m_bitmap;
double m_zoom;
// If false, then wxDC is used for zooming. If true, then m_resizeQuality
// is used with wxImage::Scale() for zooming.
bool m_useImageForZoom;
wxImageResizeQuality m_resizeQuality;
wxDECLARE_EVENT_TABLE();
};
@@ -946,6 +1031,13 @@ wxBEGIN_EVENT_TABLE(MyImageFrame, wxFrame)
EVT_MENU(wxID_ZOOM_IN, MyImageFrame::OnZoom)
EVT_MENU(wxID_ZOOM_OUT, MyImageFrame::OnZoom)
EVT_MENU(wxID_ZOOM_100, MyImageFrame::OnZoom)
EVT_MENU(ID_ZOOM_x2, MyImageFrame::OnZoom)
EVT_MENU(ID_ZOOM_DC, MyImageFrame::OnUseZoom)
EVT_MENU(ID_ZOOM_NEAREST, MyImageFrame::OnUseZoom)
EVT_MENU(ID_ZOOM_BILINEAR, MyImageFrame::OnUseZoom)
EVT_MENU(ID_ZOOM_BICUBIC, MyImageFrame::OnUseZoom)
EVT_MENU(ID_ZOOM_BOX_AVERAGE, MyImageFrame::OnUseZoom)
wxEND_EVENT_TABLE()
//-----------------------------------------------------------------------------

View File

@@ -70,8 +70,11 @@ void wxBitmapHelpers::Rescale(wxBitmap& bmp, const wxSize& sizeNeeded)
wxCHECK_RET( sizeNeeded.IsFullySpecified(), wxS("New size must be given") );
#if wxUSE_IMAGE
// Note that we use "nearest" rescale mode here to preserve sharp edges in
// the icons for which this function is often used. It's also consistent
// with what wxDC::DrawBitmap() does, i.e. the fallback method below.
wxImage img = bmp.ConvertToImage();
img.Rescale(sizeNeeded.x, sizeNeeded.y, wxIMAGE_QUALITY_HIGH);
img.Rescale(sizeNeeded.x, sizeNeeded.y, wxIMAGE_QUALITY_NEAREST);
bmp = wxBitmap(img);
#else // !wxUSE_IMAGE
// Fallback method of scaling the bitmap

View File

@@ -32,7 +32,7 @@
static const char OPTION_LIST = 'l';
static const char OPTION_SINGLE = '1';
static const char OPTION_AVG_COUNT = 'a';
static const char OPTION_RUN_TIME = 't';
static const char OPTION_NUM_RUNS = 'n';
static const char OPTION_NUMERIC_PARAM = 'p';
static const char OPTION_STRING_PARAM = 's';
@@ -64,13 +64,17 @@ public:
const wxString& GetStringParameter() const { return m_strParam; }
private:
// output the results of a single benchmark if successful or just return
// false if anything went wrong
bool RunSingleBenchmark(Bench::Function* func);
// list all registered benchmarks
void ListBenchmarks();
// command lines options/parameters
wxSortedArrayString m_toRun;
long m_numRuns,
m_avgCount,
long m_numRuns, // number of times to run a single benchmark or 0
m_runTime, // minimum time to run a single benchmark if m_numRuns == 0
m_numParam;
wxString m_strParam;
};
@@ -83,14 +87,16 @@ wxIMPLEMENT_APP_CONSOLE(BenchApp);
Bench::Function *Bench::Function::ms_head = NULL;
long Bench::GetNumericParameter()
long Bench::GetNumericParameter(long defVal)
{
return wxGetApp().GetNumericParameter();
const long val = wxGetApp().GetNumericParameter();
return val ? val : defVal;
}
wxString Bench::GetStringParameter()
wxString Bench::GetStringParameter(const wxString& defVal)
{
return wxGetApp().GetStringParameter();
const wxString& val = wxGetApp().GetStringParameter();
return !val.empty() ? val : defVal;
}
// ============================================================================
@@ -99,8 +105,8 @@ wxString Bench::GetStringParameter()
BenchApp::BenchApp()
{
m_avgCount = 10;
m_numRuns = 10000; // just some default (TODO: switch to time-based one)
m_numRuns = 0; // this means to use m_runTime
m_runTime = 500; // default minimum
m_numParam = 0;
}
@@ -132,12 +138,13 @@ void BenchApp::OnInitCmdLine(wxCmdLineParser& parser)
"single",
"run the benchmark once only");
parser.AddOption(OPTION_AVG_COUNT,
"avg-count",
parser.AddOption(OPTION_RUN_TIME,
"run-time",
wxString::Format
(
"number of times to run benchmarking loop (default: %ld)",
m_avgCount
"maximum time to run each benchmark in ms "
"(default: %ld, set to 0 to disable)",
m_runTime
),
wxCMD_LINE_VAL_NUMBER);
parser.AddOption(OPTION_NUM_RUNS,
@@ -145,7 +152,7 @@ void BenchApp::OnInitCmdLine(wxCmdLineParser& parser)
wxString::Format
(
"number of times to run each benchmark in a loop "
"(default: %ld)",
"(default: %ld, 0 means to run until max time passes)",
m_numRuns
),
wxCMD_LINE_VAL_NUMBER);
@@ -188,25 +195,26 @@ bool BenchApp::OnCmdLineParsed(wxCmdLineParser& parser)
return false;
}
bool numRunsSpecified = false;
if ( parser.Found(OPTION_AVG_COUNT, &m_avgCount) )
numRunsSpecified = true;
if ( parser.Found(OPTION_NUM_RUNS, &m_numRuns) )
numRunsSpecified = true;
const bool runTimeSpecified = parser.Found(OPTION_RUN_TIME, &m_runTime);
const bool numRunsSpecified = parser.Found(OPTION_NUM_RUNS, &m_numRuns);
parser.Found(OPTION_NUMERIC_PARAM, &m_numParam);
parser.Found(OPTION_STRING_PARAM, &m_strParam);
if ( parser.Found(OPTION_SINGLE) )
{
if ( numRunsSpecified )
if ( runTimeSpecified || numRunsSpecified )
{
wxFprintf(stderr, "Incompatible options specified.\n");
return false;
}
m_avgCount =
m_numRuns = 1;
}
else if ( numRunsSpecified && !runTimeSpecified )
{
// If only the number of runs is specified, use it only.
m_runTime = 0;
}
// construct sorted array for quick verification of benchmark names
wxSortedArrayString benchmarks;
@@ -235,6 +243,20 @@ bool BenchApp::OnCmdLineParsed(wxCmdLineParser& parser)
int BenchApp::OnRun()
{
int rc = EXIT_SUCCESS;
wxString params;
if ( m_numParam )
params += wxString::Format("N=%ld", m_numParam);
if ( !m_strParam.empty() )
{
if ( !params.empty() )
params += " and ";
params += wxString::Format("s=\"%s\"", m_strParam);
}
if ( !params.empty() )
wxPrintf("Benchmarks are running with non-default %s\n", params);
for ( Bench::Function *func = Bench::Function::GetFirst();
func;
func = func->GetNext() )
@@ -242,68 +264,103 @@ int BenchApp::OnRun()
if ( m_toRun.Index(func->GetName()) == wxNOT_FOUND )
continue;
wxString params;
if ( m_numParam )
params += wxString::Format(" with N=%ld", m_numParam);
if ( !m_strParam.empty() )
if ( !RunSingleBenchmark(func) )
{
if ( !params.empty() )
params += " and";
params += wxString::Format(" with s=\"%s\"", m_strParam);
}
wxPrintf("Benchmarking %s%s: ", func->GetName(), params);
long timeMin = LONG_MAX,
timeMax = 0,
timeTotal = 0;
bool ok = func->Init();
for ( long a = 0; ok && a < m_avgCount; a++ )
{
wxStopWatch sw;
for ( long n = 0; n < m_numRuns && ok; n++ )
{
ok = func->Run();
}
sw.Pause();
const long t = sw.Time();
if ( t < timeMin )
timeMin = t;
if ( t > timeMax )
timeMax = t;
timeTotal += t;
}
func->Done();
if ( !ok )
{
wxPrintf("ERROR\n");
wxFprintf(stderr, "ERROR running %s\n", func->GetName());
rc = EXIT_FAILURE;
}
else
{
wxPrintf("%ldms total, ", timeTotal);
long times = m_avgCount;
if ( m_avgCount > 2 )
{
timeTotal -= timeMin + timeMax;
times -= 2;
}
wxPrintf("%.2f avg (min=%ld, max=%ld)\n",
(float)timeTotal / times, timeMin, timeMax);
}
fflush(stdout);
}
return rc;
}
bool BenchApp::RunSingleBenchmark(Bench::Function* func)
{
if ( !func->Init() )
return false;
wxPrintf("Benchmarking %s: ", func->GetName());
fflush(stdout);
// We use the algorithm for iteratively computing the mean and the
// standard deviation of the sequence of values described in Knuth's
// "The Art of Computer Programming, Volume 2: Seminumerical
// Algorithms", section 4.2.2.
//
// The algorithm defines the sequences M(k) and S(k) as follows:
//
// M(1) = x(1), M(k) = M(k-1) + (x(k) - M(k-1)) / k
// S(1) = 0, S(k) = S(k-1) + (x(k) - M(k-1))*(x(k) - M(k))
//
// where x(k) is the k-th value. Then the mean is simply the last value
// of the first sequence M(N) and the standard deviation is
// sqrt(S(N)/(N-1)).
wxStopWatch swTotal;
if ( !func->Run() )
return false;
double timeMin = DBL_MAX,
timeMax = 0;
double m = swTotal.TimeInMicro().ToDouble();
double s = 0;
long n = 0;
for ( ;; )
{
// One termination condition is reaching the maximum number of runs.
if ( ++n == m_numRuns )
break;
double t;
{
wxStopWatch swThis;
if ( !func->Run() )
return false;
t = swThis.TimeInMicro().ToDouble();
}
if ( t < timeMin )
timeMin = t;
if ( t > timeMax )
timeMax = t;
const double lastM = m;
m += (t - lastM) / n;
s += (t - lastM)*(t - m);
// The other termination condition is that we are running for at least
// m_runTime milliseconds.
if ( m_runTime && swTotal.Time() >= m_runTime )
break;
}
func->Done();
// For a single run there is no standard deviation and min/max don't make
// much sense.
if ( n == 1 )
{
wxPrintf("single run took %.0fus\n", m);
}
else
{
s = sqrt(s / (n - 1));
wxPrintf
(
"%ld runs, %.0fus avg, %.0f std dev (%.0f/%.0f min/max)\n",
n, m, s, timeMin, timeMax
);
}
fflush(stdout);
return true;
}
int BenchApp::OnExit()
{
#if wxUSE_GUI

View File

@@ -83,7 +83,7 @@ private:
Tests may use this parameter in whatever way they see fit, by default it is
1 but can be set to a different value by user from the command line.
*/
long GetNumericParameter();
long GetNumericParameter(long defValue = 1);
/**
Get the string parameter.
@@ -91,7 +91,7 @@ long GetNumericParameter();
Tests may use this parameter in whatever way they see fit, by default it is
empty but can be set to a different value by user from the command line.
*/
wxString GetStringParameter();
wxString GetStringParameter(const wxString& defValue = wxString());
} // namespace Bench

View File

@@ -65,7 +65,7 @@ static const wxImage& GetTestImage()
if ( !s_triedToLoad )
{
s_triedToLoad = true;
s_image.LoadFile("horse.bmp");
s_image.LoadFile(Bench::GetStringParameter("horse.bmp"));
}
return s_image;
@@ -73,20 +73,48 @@ static const wxImage& GetTestImage()
BENCHMARK_FUNC(EnlargeNormal)
{
return GetTestImage().Scale(300, 300, wxIMAGE_QUALITY_NORMAL).IsOk();
const wxImage& image = GetTestImage();
const double factor = Bench::GetNumericParameter(150) / 100.;
return image.Scale(factor*image.GetWidth(), factor*image.GetHeight(),
wxIMAGE_QUALITY_NORMAL).IsOk();
}
BENCHMARK_FUNC(EnlargeBoxAverage)
{
const wxImage& image = GetTestImage();
const double factor = Bench::GetNumericParameter(150) / 100.;
return image.Scale(factor*image.GetWidth(), factor*image.GetHeight(),
wxIMAGE_QUALITY_BOX_AVERAGE).IsOk();
}
BENCHMARK_FUNC(EnlargeHighQuality)
{
return GetTestImage().Scale(300, 300, wxIMAGE_QUALITY_HIGH).IsOk();
const wxImage& image = GetTestImage();
const double factor = Bench::GetNumericParameter(150) / 100.;
return image.Scale(factor*image.GetWidth(), factor*image.GetHeight(),
wxIMAGE_QUALITY_HIGH).IsOk();
}
BENCHMARK_FUNC(ShrinkNormal)
{
return GetTestImage().Scale(50, 50, wxIMAGE_QUALITY_NORMAL).IsOk();
const wxImage& image = GetTestImage();
const double factor = Bench::GetNumericParameter(50) / 100.;
return image.Scale(factor*image.GetWidth(), factor*image.GetHeight(),
wxIMAGE_QUALITY_NORMAL).IsOk();
}
BENCHMARK_FUNC(ShrinkBoxAverage)
{
const wxImage& image = GetTestImage();
const double factor = Bench::GetNumericParameter(50) / 100.;
return image.Scale(factor*image.GetWidth(), factor*image.GetHeight(),
wxIMAGE_QUALITY_BOX_AVERAGE).IsOk();
}
BENCHMARK_FUNC(ShrinkHighQuality)
{
return GetTestImage().Scale(50, 50, wxIMAGE_QUALITY_HIGH).IsOk();
const wxImage& image = GetTestImage();
const double factor = Bench::GetNumericParameter(50) / 100.;
return image.Scale(factor*image.GetWidth(), factor*image.GetHeight(),
wxIMAGE_QUALITY_HIGH).IsOk();
}